feat: persist draft messages and UI refinements
- Add dedicated draft-store with AsyncStorage persistence for drafts that survive app refresh/crash - Reduce markdown bold weight from bold to medium (500) - Various theme and component style updates - Server bootstrap and config improvements 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -1,12 +1,42 @@
|
||||
import express, { type Express } from "express";
|
||||
import express from "express";
|
||||
import basicAuth from "express-basic-auth";
|
||||
import { createServer as createHTTPServer, type Server as HTTPServer } from "http";
|
||||
import { createReadStream } from "fs";
|
||||
import { createServer as createHTTPServer } from "http";
|
||||
import { createReadStream, unlinkSync, existsSync } from "fs";
|
||||
import { stat } from "fs/promises";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
type ListenTarget =
|
||||
| { type: "tcp"; host: string; port: number }
|
||||
| { type: "socket"; path: string };
|
||||
|
||||
function parseListenString(listen: string): ListenTarget {
|
||||
// Unix socket: starts with / or ~ or contains .sock
|
||||
if (listen.startsWith("/") || listen.startsWith("~") || listen.includes(".sock")) {
|
||||
return { type: "socket", path: listen };
|
||||
}
|
||||
// Explicit unix:// prefix
|
||||
if (listen.startsWith("unix://")) {
|
||||
return { type: "socket", path: listen.slice(7) };
|
||||
}
|
||||
// TCP: host:port or just port
|
||||
if (listen.includes(":")) {
|
||||
const [host, portStr] = listen.split(":");
|
||||
const port = parseInt(portStr, 10);
|
||||
if (!Number.isFinite(port)) {
|
||||
throw new Error(`Invalid port in listen string: ${listen}`);
|
||||
}
|
||||
return { type: "tcp", host: host || "127.0.0.1", port };
|
||||
}
|
||||
// Just a port number
|
||||
const port = parseInt(listen, 10);
|
||||
if (Number.isFinite(port)) {
|
||||
return { type: "tcp", host: "127.0.0.1", port };
|
||||
}
|
||||
throw new Error(`Invalid listen string: ${listen}`);
|
||||
}
|
||||
|
||||
import { VoiceAssistantWebSocketServer } from "./websocket-server.js";
|
||||
import { DownloadTokenStore } from "./file-download/token-store.js";
|
||||
import { initializeSTT, type STTConfig } from "./agent/stt-openai.js";
|
||||
@@ -40,8 +70,9 @@ export type PaseoOpenAIConfig = {
|
||||
};
|
||||
|
||||
export type PaseoDaemonConfig = {
|
||||
port: number;
|
||||
listen: string;
|
||||
paseoHome: string;
|
||||
corsAllowedOrigins: string[];
|
||||
agentMcpRoute: string;
|
||||
agentMcpAllowedHosts: string[];
|
||||
auth: PaseoAuthConfig;
|
||||
@@ -54,18 +85,17 @@ export type PaseoDaemonConfig = {
|
||||
downloadTokenTtlMs?: number;
|
||||
};
|
||||
|
||||
export type PaseoDaemonHandles = {
|
||||
httpServer: HTTPServer;
|
||||
app: Express;
|
||||
wsServer: VoiceAssistantWebSocketServer;
|
||||
export interface PaseoDaemon {
|
||||
config: PaseoDaemonConfig;
|
||||
agentManager: AgentManager;
|
||||
agentRegistry: AgentRegistry;
|
||||
close: () => Promise<void>;
|
||||
};
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export async function createPaseoDaemon(
|
||||
config: PaseoDaemonConfig
|
||||
): Promise<PaseoDaemonHandles> {
|
||||
): Promise<PaseoDaemon> {
|
||||
const agentMcpRoute = config.agentMcpRoute;
|
||||
const basicAuthUsers = config.auth.basicUsers;
|
||||
const staticDir = config.staticDir;
|
||||
@@ -76,8 +106,38 @@ export async function createPaseoDaemon(
|
||||
|
||||
const downloadTokenStore = new DownloadTokenStore({ ttlMs: downloadTokenTtlMs });
|
||||
|
||||
const listenTarget = parseListenString(config.listen);
|
||||
|
||||
const app = express();
|
||||
|
||||
// CORS - allow same-origin + configured origins
|
||||
const allowedOrigins = new Set([
|
||||
...config.corsAllowedOrigins,
|
||||
// For TCP, add localhost variants
|
||||
...(listenTarget.type === "tcp"
|
||||
? [
|
||||
`http://${listenTarget.host}:${listenTarget.port}`,
|
||||
`http://localhost:${listenTarget.port}`,
|
||||
`http://127.0.0.1:${listenTarget.port}`,
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const origin = req.headers.origin;
|
||||
if (origin && allowedOrigins.has(origin)) {
|
||||
res.setHeader("Access-Control-Allow-Origin", origin);
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
res.setHeader("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
if (req.method === "OPTIONS") {
|
||||
res.status(204).end();
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Serve static files from public directory (no auth required for APK downloads)
|
||||
app.use("/public", express.static(staticDir));
|
||||
|
||||
@@ -314,7 +374,8 @@ export async function createPaseoDaemon(
|
||||
{
|
||||
agentMcpUrl: config.agentControlMcp.url,
|
||||
agentMcpHeaders: config.agentControlMcp.headers,
|
||||
}
|
||||
},
|
||||
{ allowedOrigins }
|
||||
);
|
||||
|
||||
const openaiApiKey = config.openai?.apiKey;
|
||||
@@ -349,21 +410,56 @@ export async function createPaseoDaemon(
|
||||
);
|
||||
}
|
||||
|
||||
const close = async () => {
|
||||
const start = async () => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onError = (err: Error) => {
|
||||
httpServer.off("listening", onListening);
|
||||
reject(err);
|
||||
};
|
||||
const onListening = () => {
|
||||
httpServer.off("error", onError);
|
||||
if (listenTarget.type === "tcp") {
|
||||
console.log(
|
||||
`✓ Server listening on http://${listenTarget.host}:${listenTarget.port}`
|
||||
);
|
||||
} else {
|
||||
console.log(`✓ Server listening on ${listenTarget.path}`);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
httpServer.once("error", onError);
|
||||
httpServer.once("listening", onListening);
|
||||
|
||||
if (listenTarget.type === "tcp") {
|
||||
httpServer.listen(listenTarget.port, listenTarget.host);
|
||||
} else {
|
||||
// Remove stale socket file if it exists
|
||||
if (existsSync(listenTarget.path)) {
|
||||
unlinkSync(listenTarget.path);
|
||||
}
|
||||
httpServer.listen(listenTarget.path);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const stop = async () => {
|
||||
await closeAllAgents(agentManager);
|
||||
await wsServer.close();
|
||||
await new Promise<void>((resolve) => {
|
||||
httpServer.close(() => resolve());
|
||||
});
|
||||
// Clean up socket file
|
||||
if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
|
||||
unlinkSync(listenTarget.path);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
httpServer,
|
||||
app,
|
||||
wsServer,
|
||||
config,
|
||||
agentManager,
|
||||
agentRegistry,
|
||||
close,
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +1,17 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdirSync } from "node:fs";
|
||||
|
||||
import type { PaseoDaemonConfig } from "./bootstrap.js";
|
||||
import type { STTConfig } from "./agent/stt-openai.js";
|
||||
import type { TTSConfig } from "./agent/tts-openai.js";
|
||||
import { loadPersistedConfig } from "./persisted-config.js";
|
||||
|
||||
function expandHomeDir(input: string): string {
|
||||
if (input.startsWith("~/")) {
|
||||
return path.join(os.homedir(), input.slice(2));
|
||||
}
|
||||
if (input === "~") {
|
||||
return os.homedir();
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
export function readPaseoHomeFromEnv(
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): string {
|
||||
const raw = env.PASEO_HOME ?? env.PASEO_HOME_DIR ?? "~/.paseo";
|
||||
const expanded = path.resolve(expandHomeDir(raw));
|
||||
mkdirSync(expanded, { recursive: true });
|
||||
return expanded;
|
||||
}
|
||||
|
||||
export function readPaseoPortFromEnv(
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): number {
|
||||
const raw = env.PASEO_PORT ?? env.PORT ?? "6767";
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
return Number.isFinite(parsed) ? parsed : 6767;
|
||||
}
|
||||
|
||||
const DEFAULT_LISTEN = "127.0.0.1:6767";
|
||||
const DEFAULT_BASIC_AUTH_USERS = { mo: "bo" } as const;
|
||||
const DEFAULT_AGENT_MCP_ROUTE = "/mcp/agents";
|
||||
|
||||
function readOpenAIConfigFromEnv(env: NodeJS.ProcessEnv = process.env) {
|
||||
function parseOpenAIConfig(env: NodeJS.ProcessEnv) {
|
||||
const apiKey = env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
return undefined;
|
||||
}
|
||||
if (!apiKey) return undefined;
|
||||
|
||||
const sttConfidenceThreshold = env.STT_CONFIDENCE_THRESHOLD
|
||||
? parseFloat(env.STT_CONFIDENCE_THRESHOLD)
|
||||
@@ -71,15 +42,34 @@ function readOpenAIConfigFromEnv(env: NodeJS.ProcessEnv = process.env) {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPaseoDaemonConfigFromEnv(
|
||||
function getListenForMcp(listen: string): string {
|
||||
// For Unix sockets, MCP still needs HTTP - use localhost
|
||||
if (listen.startsWith("/") || listen.startsWith("~") || listen.includes(".sock")) {
|
||||
return "127.0.0.1:6767";
|
||||
}
|
||||
if (listen.startsWith("unix://")) {
|
||||
return "127.0.0.1:6767";
|
||||
}
|
||||
// TCP: extract host:port
|
||||
if (listen.includes(":")) {
|
||||
const [host, port] = listen.split(":");
|
||||
return `${host || "127.0.0.1"}:${port}`;
|
||||
}
|
||||
// Just port
|
||||
return `127.0.0.1:${listen}`;
|
||||
}
|
||||
|
||||
export function loadConfig(
|
||||
paseoHome: string,
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): PaseoDaemonConfig {
|
||||
const paseoHome = readPaseoHomeFromEnv(env);
|
||||
const port = readPaseoPortFromEnv(env);
|
||||
const persisted = loadPersistedConfig(paseoHome);
|
||||
|
||||
const listen = env.PASEO_LISTEN ?? persisted.listen ?? DEFAULT_LISTEN;
|
||||
const mcpListen = getListenForMcp(listen);
|
||||
|
||||
const basicUsers = DEFAULT_BASIC_AUTH_USERS;
|
||||
const [agentMcpUser, agentMcpPassword] =
|
||||
Object.entries(basicUsers)[0] ?? [];
|
||||
const [agentMcpUser, agentMcpPassword] = Object.entries(basicUsers)[0] ?? [];
|
||||
const agentMcpAuthHeader =
|
||||
agentMcpUser && agentMcpPassword
|
||||
? `Basic ${Buffer.from(`${agentMcpUser}:${agentMcpPassword}`).toString("base64")}`
|
||||
@@ -89,13 +79,12 @@ export function buildPaseoDaemonConfigFromEnv(
|
||||
? Buffer.from(`${agentMcpUser}:${agentMcpPassword}`).toString("base64")
|
||||
: undefined;
|
||||
|
||||
const openai = readOpenAIConfigFromEnv(env);
|
||||
|
||||
return {
|
||||
port,
|
||||
listen,
|
||||
paseoHome,
|
||||
corsAllowedOrigins: persisted.cors.allowedOrigins,
|
||||
agentMcpRoute: DEFAULT_AGENT_MCP_ROUTE,
|
||||
agentMcpAllowedHosts: [`127.0.0.1:${port}`, `localhost:${port}`],
|
||||
agentMcpAllowedHosts: [mcpListen, `localhost:${mcpListen.split(":")[1]}`],
|
||||
auth: {
|
||||
basicUsers,
|
||||
agentMcpAuthHeader,
|
||||
@@ -104,7 +93,7 @@ export function buildPaseoDaemonConfigFromEnv(
|
||||
},
|
||||
mcpDebug: env.MCP_DEBUG === "1",
|
||||
agentControlMcp: {
|
||||
url: `http://127.0.0.1:${port}${DEFAULT_AGENT_MCP_ROUTE}`,
|
||||
url: `http://${mcpListen}${DEFAULT_AGENT_MCP_ROUTE}`,
|
||||
...(agentMcpAuthHeader
|
||||
? { headers: { Authorization: agentMcpAuthHeader } }
|
||||
: {}),
|
||||
@@ -112,6 +101,6 @@ export function buildPaseoDaemonConfigFromEnv(
|
||||
agentRegistryPath: path.join(paseoHome, "agents.json"),
|
||||
staticDir: "public",
|
||||
agentClients: {},
|
||||
openai,
|
||||
openai: parseOpenAIConfig(env),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import "dotenv/config";
|
||||
import { createPaseoDaemon } from "./bootstrap.js";
|
||||
import { buildPaseoDaemonConfigFromEnv } from "./config.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
import { resolvePaseoHome } from "./paseo-home.js";
|
||||
|
||||
async function main() {
|
||||
const daemonConfig = buildPaseoDaemonConfigFromEnv();
|
||||
const daemon = await createPaseoDaemon(daemonConfig);
|
||||
const paseoHome = resolvePaseoHome();
|
||||
const config = loadConfig(paseoHome);
|
||||
const daemon = await createPaseoDaemon(config);
|
||||
|
||||
await daemon.start();
|
||||
|
||||
daemon.httpServer.listen(daemonConfig.port, () => {
|
||||
console.log(
|
||||
`\n✓ Voice Assistant server running on http://localhost:${daemonConfig.port}`
|
||||
);
|
||||
});
|
||||
const handleShutdown = async (signal: string) => {
|
||||
console.log(`\n${signal} received, shutting down gracefully...`);
|
||||
|
||||
@@ -20,7 +19,7 @@ async function main() {
|
||||
}, 10000);
|
||||
|
||||
try {
|
||||
await daemon.close();
|
||||
await daemon.stop();
|
||||
clearTimeout(forceExit);
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdirSync } from "node:fs";
|
||||
|
||||
function expandHomeDir(input: string): string {
|
||||
if (input.startsWith("~/")) {
|
||||
return path.join(os.homedir(), input.slice(2));
|
||||
}
|
||||
if (input === "~") {
|
||||
return os.homedir();
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
export function resolvePaseoHome(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const raw = env.PASEO_HOME ?? env.PASEO_HOME_DIR ?? "~/.paseo";
|
||||
const resolved = path.resolve(expandHomeDir(raw));
|
||||
mkdirSync(resolved, { recursive: true });
|
||||
return resolved;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
|
||||
const PersistedConfigSchema = z.object({
|
||||
listen: z.string().optional(),
|
||||
cors: z
|
||||
.object({
|
||||
allowedOrigins: z.array(z.string()).default([]),
|
||||
})
|
||||
.default({}),
|
||||
});
|
||||
|
||||
export type PersistedConfig = z.infer<typeof PersistedConfigSchema>;
|
||||
|
||||
const CONFIG_FILENAME = "config.json";
|
||||
|
||||
function getConfigPath(paseoHome: string): string {
|
||||
return path.join(paseoHome, CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
export function loadPersistedConfig(paseoHome: string): PersistedConfig {
|
||||
const configPath = getConfigPath(paseoHome);
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
console.log(`[Config] No config file at ${configPath}, using defaults`);
|
||||
return PersistedConfigSchema.parse({});
|
||||
}
|
||||
|
||||
let raw: string;
|
||||
try {
|
||||
raw = readFileSync(configPath, "utf-8");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`[Config] Failed to read ${configPath}: ${message}`);
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`[Config] Invalid JSON in ${configPath}: ${message}`);
|
||||
}
|
||||
|
||||
const result = PersistedConfigSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues
|
||||
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
|
||||
.join("\n");
|
||||
throw new Error(`[Config] Invalid config in ${configPath}:\n${issues}`);
|
||||
}
|
||||
|
||||
console.log(`[Config] Loaded from ${configPath}`);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export function savePersistedConfig(
|
||||
paseoHome: string,
|
||||
config: PersistedConfig
|
||||
): void {
|
||||
const configPath = getConfigPath(paseoHome);
|
||||
|
||||
const result = PersistedConfigSchema.safeParse(config);
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues
|
||||
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
|
||||
.join("\n");
|
||||
throw new Error(`[Config] Invalid config to save:\n${issues}`);
|
||||
}
|
||||
|
||||
try {
|
||||
writeFileSync(configPath, JSON.stringify(result.data, null, 2) + "\n");
|
||||
console.log(`[Config] Saved to ${configPath}`);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`[Config] Failed to write ${configPath}: ${message}`);
|
||||
}
|
||||
}
|
||||
@@ -59,8 +59,9 @@ export async function createTestPaseoDaemon(
|
||||
const port = await getAvailablePort();
|
||||
|
||||
const config: PaseoDaemonConfig = {
|
||||
port,
|
||||
listen: `127.0.0.1:${port}`,
|
||||
paseoHome,
|
||||
corsAllowedOrigins: [],
|
||||
agentMcpRoute: "/mcp/agents",
|
||||
agentMcpAllowedHosts: [`127.0.0.1:${port}`, `localhost:${port}`],
|
||||
auth: {
|
||||
@@ -85,11 +86,10 @@ export async function createTestPaseoDaemon(
|
||||
|
||||
const daemon = await createPaseoDaemon(config);
|
||||
try {
|
||||
await listenOnPort(daemon.httpServer, port);
|
||||
await daemon.start();
|
||||
|
||||
const close = async (): Promise<void> => {
|
||||
await daemon.close().catch(() => undefined);
|
||||
// Wait a bit for file handles to release
|
||||
await daemon.stop().catch(() => undefined);
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
await rm(paseoHome, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
||||
await rm(staticDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
||||
@@ -107,7 +107,7 @@ export async function createTestPaseoDaemon(
|
||||
};
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await daemon.close().catch(() => undefined);
|
||||
await daemon.stop().catch(() => undefined);
|
||||
await rm(paseoHome, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
||||
await rm(staticDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
||||
|
||||
@@ -120,30 +120,6 @@ export async function createTestPaseoDaemon(
|
||||
throw lastError ?? new Error("Failed to start test daemon");
|
||||
}
|
||||
|
||||
async function listenOnPort(server: net.Server, port: number): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onError = (error: Error) => {
|
||||
server.off("listening", onListening);
|
||||
reject(error);
|
||||
};
|
||||
const onListening = () => {
|
||||
server.off("error", onError);
|
||||
resolve();
|
||||
};
|
||||
|
||||
server.once("error", onError);
|
||||
server.once("listening", onListening);
|
||||
|
||||
try {
|
||||
server.listen(port);
|
||||
} catch (error) {
|
||||
server.off("error", onError);
|
||||
server.off("listening", onListening);
|
||||
reject(error instanceof Error ? error : new Error("Failed to listen"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isAddressInUseError(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") {
|
||||
return false;
|
||||
|
||||
@@ -22,6 +22,10 @@ type AgentMcpClientConfig = {
|
||||
agentMcpHeaders?: Record<string, string>;
|
||||
};
|
||||
|
||||
type WebSocketServerConfig = {
|
||||
allowedOrigins: Set<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* WebSocket server that routes messages between clients and their sessions.
|
||||
* This is a thin transport layer with no business logic.
|
||||
@@ -43,7 +47,8 @@ export class VoiceAssistantWebSocketServer {
|
||||
agentManager: AgentManager,
|
||||
agentRegistry: AgentRegistry,
|
||||
downloadTokenStore: DownloadTokenStore,
|
||||
agentMcpConfig: AgentMcpClientConfig
|
||||
agentMcpConfig: AgentMcpClientConfig,
|
||||
wsConfig: WebSocketServerConfig
|
||||
) {
|
||||
this.agentManager = agentManager;
|
||||
this.agentRegistry = agentRegistry;
|
||||
@@ -51,7 +56,23 @@ export class VoiceAssistantWebSocketServer {
|
||||
this.pushTokenStore = new PushTokenStore();
|
||||
this.pushService = new PushService(this.pushTokenStore);
|
||||
this.agentMcpConfig = agentMcpConfig;
|
||||
this.wss = new WebSocketServer({ server, path: "/ws" });
|
||||
|
||||
const { allowedOrigins } = wsConfig;
|
||||
this.wss = new WebSocketServer({
|
||||
server,
|
||||
path: "/ws",
|
||||
verifyClient: ({ req }, callback) => {
|
||||
const origin = req.headers.origin;
|
||||
// Allow connections with no origin (native apps, curl, etc.)
|
||||
// or from allowed origins (browsers)
|
||||
if (!origin || allowedOrigins.has(origin)) {
|
||||
callback(true);
|
||||
} else {
|
||||
console.warn(`[WebSocket] Rejected connection from origin: ${origin}`);
|
||||
callback(false, 403, "Origin not allowed");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.wss.on("connection", (ws, request) => {
|
||||
this.handleConnection(ws, request);
|
||||
|
||||
Reference in New Issue
Block a user