@superdangerous/app-framework 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +652 -0
- package/dist/api/logsRouter.d.ts +20 -0
- package/dist/api/logsRouter.d.ts.map +1 -0
- package/dist/api/logsRouter.js +515 -0
- package/dist/api/logsRouter.js.map +1 -0
- package/dist/cli/dev-server.d.ts +7 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +640 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +26 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/StandardServer.d.ts +129 -0
- package/dist/core/StandardServer.d.ts.map +1 -0
- package/dist/core/StandardServer.js +453 -0
- package/dist/core/StandardServer.js.map +1 -0
- package/dist/core/apiResponse.d.ts +69 -0
- package/dist/core/apiResponse.d.ts.map +1 -0
- package/dist/core/apiResponse.js +127 -0
- package/dist/core/apiResponse.js.map +1 -0
- package/dist/core/healthCheck.d.ts +160 -0
- package/dist/core/healthCheck.d.ts.map +1 -0
- package/dist/core/healthCheck.js +398 -0
- package/dist/core/healthCheck.js.map +1 -0
- package/dist/core/index.d.ts +40 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +40 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +117 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +826 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/portUtils.d.ts +71 -0
- package/dist/core/portUtils.d.ts.map +1 -0
- package/dist/core/portUtils.js +240 -0
- package/dist/core/portUtils.js.map +1 -0
- package/dist/core/storageService.d.ts +119 -0
- package/dist/core/storageService.d.ts.map +1 -0
- package/dist/core/storageService.js +405 -0
- package/dist/core/storageService.js.map +1 -0
- package/dist/desktop/bundler.d.ts +40 -0
- package/dist/desktop/bundler.d.ts.map +1 -0
- package/dist/desktop/bundler.js +176 -0
- package/dist/desktop/bundler.js.map +1 -0
- package/dist/desktop/index.d.ts +25 -0
- package/dist/desktop/index.d.ts.map +1 -0
- package/dist/desktop/index.js +15 -0
- package/dist/desktop/index.js.map +1 -0
- package/dist/desktop/native-modules.d.ts +66 -0
- package/dist/desktop/native-modules.d.ts.map +1 -0
- package/dist/desktop/native-modules.js +200 -0
- package/dist/desktop/native-modules.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/LogCategories.d.ts +87 -0
- package/dist/logging/LogCategories.d.ts.map +1 -0
- package/dist/logging/LogCategories.js +205 -0
- package/dist/logging/LogCategories.js.map +1 -0
- package/dist/middleware/aiErrorHandler.d.ts +31 -0
- package/dist/middleware/aiErrorHandler.d.ts.map +1 -0
- package/dist/middleware/aiErrorHandler.js +181 -0
- package/dist/middleware/aiErrorHandler.js.map +1 -0
- package/dist/middleware/auth.d.ts +101 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +230 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +56 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +123 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +13 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/errorHandler.js +85 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/fileUpload.d.ts +62 -0
- package/dist/middleware/fileUpload.d.ts.map +1 -0
- package/dist/middleware/fileUpload.js +175 -0
- package/dist/middleware/fileUpload.js.map +1 -0
- package/dist/middleware/health.d.ts +48 -0
- package/dist/middleware/health.d.ts.map +1 -0
- package/dist/middleware/health.js +143 -0
- package/dist/middleware/health.js.map +1 -0
- package/dist/middleware/index.d.ts +20 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +18 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/openapi.d.ts +64 -0
- package/dist/middleware/openapi.d.ts.map +1 -0
- package/dist/middleware/openapi.js +258 -0
- package/dist/middleware/openapi.js.map +1 -0
- package/dist/middleware/requestLogging.d.ts +22 -0
- package/dist/middleware/requestLogging.d.ts.map +1 -0
- package/dist/middleware/requestLogging.js +61 -0
- package/dist/middleware/requestLogging.js.map +1 -0
- package/dist/middleware/session.d.ts +84 -0
- package/dist/middleware/session.d.ts.map +1 -0
- package/dist/middleware/session.js +189 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/middleware/validation.d.ts +1337 -0
- package/dist/middleware/validation.d.ts.map +1 -0
- package/dist/middleware/validation.js +483 -0
- package/dist/middleware/validation.js.map +1 -0
- package/dist/services/aiService.d.ts +180 -0
- package/dist/services/aiService.d.ts.map +1 -0
- package/dist/services/aiService.js +547 -0
- package/dist/services/aiService.js.map +1 -0
- package/dist/services/conversationStorage.d.ts +38 -0
- package/dist/services/conversationStorage.d.ts.map +1 -0
- package/dist/services/conversationStorage.js +158 -0
- package/dist/services/conversationStorage.js.map +1 -0
- package/dist/services/crossPlatformBuffer.d.ts +84 -0
- package/dist/services/crossPlatformBuffer.d.ts.map +1 -0
- package/dist/services/crossPlatformBuffer.js +246 -0
- package/dist/services/crossPlatformBuffer.js.map +1 -0
- package/dist/services/index.d.ts +17 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +18 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/networkService.d.ts +81 -0
- package/dist/services/networkService.d.ts.map +1 -0
- package/dist/services/networkService.js +268 -0
- package/dist/services/networkService.js.map +1 -0
- package/dist/services/queueService.d.ts +112 -0
- package/dist/services/queueService.d.ts.map +1 -0
- package/dist/services/queueService.js +338 -0
- package/dist/services/queueService.js.map +1 -0
- package/dist/services/settingsService.d.ts +135 -0
- package/dist/services/settingsService.d.ts.map +1 -0
- package/dist/services/settingsService.js +425 -0
- package/dist/services/settingsService.js.map +1 -0
- package/dist/services/systemMonitor.d.ts +208 -0
- package/dist/services/systemMonitor.d.ts.map +1 -0
- package/dist/services/systemMonitor.js +693 -0
- package/dist/services/systemMonitor.js.map +1 -0
- package/dist/services/updateService.d.ts +78 -0
- package/dist/services/updateService.d.ts.map +1 -0
- package/dist/services/updateService.js +252 -0
- package/dist/services/updateService.js.map +1 -0
- package/dist/services/websocketEvents.d.ts +372 -0
- package/dist/services/websocketEvents.d.ts.map +1 -0
- package/dist/services/websocketEvents.js +338 -0
- package/dist/services/websocketEvents.js.map +1 -0
- package/dist/services/websocketServer.d.ts +80 -0
- package/dist/services/websocketServer.d.ts.map +1 -0
- package/dist/services/websocketServer.js +299 -0
- package/dist/services/websocketServer.js.map +1 -0
- package/dist/settings/SettingsSchema.d.ts +151 -0
- package/dist/settings/SettingsSchema.d.ts.map +1 -0
- package/dist/settings/SettingsSchema.js +424 -0
- package/dist/settings/SettingsSchema.js.map +1 -0
- package/dist/testing/TestServer.d.ts +69 -0
- package/dist/testing/TestServer.d.ts.map +1 -0
- package/dist/testing/TestServer.js +250 -0
- package/dist/testing/TestServer.js.map +1 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/appPaths.d.ts +74 -0
- package/dist/utils/appPaths.d.ts.map +1 -0
- package/dist/utils/appPaths.js +162 -0
- package/dist/utils/appPaths.js.map +1 -0
- package/dist/utils/fs-utils.d.ts +50 -0
- package/dist/utils/fs-utils.d.ts.map +1 -0
- package/dist/utils/fs-utils.js +114 -0
- package/dist/utils/fs-utils.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/standardConfig.d.ts +61 -0
- package/dist/utils/standardConfig.d.ts.map +1 -0
- package/dist/utils/standardConfig.js +109 -0
- package/dist/utils/standardConfig.js.map +1 -0
- package/dist/utils/startupBanner.d.ts +34 -0
- package/dist/utils/startupBanner.d.ts.map +1 -0
- package/dist/utils/startupBanner.js +169 -0
- package/dist/utils/startupBanner.js.map +1 -0
- package/dist/utils/startupLogger.d.ts +45 -0
- package/dist/utils/startupLogger.d.ts.map +1 -0
- package/dist/utils/startupLogger.js +200 -0
- package/dist/utils/startupLogger.js.map +1 -0
- package/package.json +151 -0
- package/src/api/logsRouter.ts +600 -0
- package/src/cli/dev-server.ts +803 -0
- package/src/cli/index.ts +31 -0
- package/src/core/StandardServer.ts +587 -0
- package/src/core/apiResponse.ts +202 -0
- package/src/core/healthCheck.ts +565 -0
- package/src/core/index.ts +80 -0
- package/src/core/logger.ts +1092 -0
- package/src/core/portUtils.ts +319 -0
- package/src/core/storageService.ts +595 -0
- package/src/desktop/bundler.ts +271 -0
- package/src/desktop/index.ts +18 -0
- package/src/desktop/native-modules.ts +289 -0
- package/src/index.ts +142 -0
- package/src/logging/LogCategories.ts +302 -0
- package/src/middleware/aiErrorHandler.ts +278 -0
- package/src/middleware/auth.ts +329 -0
- package/src/middleware/cors.ts +187 -0
- package/src/middleware/errorHandler.ts +103 -0
- package/src/middleware/fileUpload.ts +252 -0
- package/src/middleware/health.ts +206 -0
- package/src/middleware/index.ts +71 -0
- package/src/middleware/openapi.ts +305 -0
- package/src/middleware/requestLogging.ts +92 -0
- package/src/middleware/session.ts +238 -0
- package/src/middleware/validation.ts +603 -0
- package/src/services/aiService.ts +789 -0
- package/src/services/conversationStorage.ts +232 -0
- package/src/services/crossPlatformBuffer.ts +341 -0
- package/src/services/index.ts +47 -0
- package/src/services/networkService.ts +351 -0
- package/src/services/queueService.ts +446 -0
- package/src/services/settingsService.ts +549 -0
- package/src/services/systemMonitor.ts +936 -0
- package/src/services/updateService.ts +334 -0
- package/src/services/websocketEvents.ts +409 -0
- package/src/services/websocketServer.ts +394 -0
- package/src/settings/SettingsSchema.ts +664 -0
- package/src/testing/TestServer.ts +312 -0
- package/src/types/index.ts +154 -0
- package/src/utils/appPaths.ts +196 -0
- package/src/utils/fs-utils.ts +130 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/standardConfig.ts +178 -0
- package/src/utils/startupBanner.ts +287 -0
- package/src/utils/startupLogger.ts +268 -0
- package/ui/dist/index.d.mts +1221 -0
- package/ui/dist/index.d.ts +1221 -0
- package/ui/dist/index.js +73 -0
- package/ui/dist/index.js.map +1 -0
- package/ui/dist/index.mjs +73 -0
- package/ui/dist/index.mjs.map +1 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SuperDangerous App Framework CLI
|
|
5
|
+
* Provides utilities for building and managing SuperDangerous applications
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
// Get version from package.json
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
const packageJsonPath = join(__dirname, "../../package.json");
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
17
|
+
|
|
18
|
+
const program = new Command();
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.name("app-framework")
|
|
22
|
+
.description("Node.js Application Framework CLI")
|
|
23
|
+
.version(packageJson.version);
|
|
24
|
+
|
|
25
|
+
// Parse arguments
|
|
26
|
+
program.parse(process.argv);
|
|
27
|
+
|
|
28
|
+
// If no command provided, show help
|
|
29
|
+
if (!process.argv.slice(2).length) {
|
|
30
|
+
program.outputHelp();
|
|
31
|
+
}
|
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard Server Implementation
|
|
3
|
+
* A simplified, consistent server pattern for all SuperDangerous applications
|
|
4
|
+
* Combines the best of StartupOrchestrator with the simplicity apps need
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import express, { Express } from "express";
|
|
8
|
+
import { createServer, Server as HttpServer } from "http";
|
|
9
|
+
import { Server as HttpsServer } from "https";
|
|
10
|
+
import type { Socket as NetSocket } from "net";
|
|
11
|
+
import cors from "cors";
|
|
12
|
+
import { createWebSocketServer } from "../services/websocketServer.js";
|
|
13
|
+
import { displayStartupBanner } from "../utils/startupBanner.js";
|
|
14
|
+
import { getProcessOnPort } from "./portUtils.js";
|
|
15
|
+
import { createLogger, getLogger } from "./index.js";
|
|
16
|
+
import { aiErrorHandler } from "../middleware/aiErrorHandler.js";
|
|
17
|
+
import { apiErrorHandler } from "./apiResponse.js";
|
|
18
|
+
import { createRequestLoggingMiddleware } from "../middleware/requestLogging.js";
|
|
19
|
+
import {
|
|
20
|
+
getAppDataPath,
|
|
21
|
+
getLogsPath,
|
|
22
|
+
isDesktopApp,
|
|
23
|
+
} from "../utils/appPaths.js";
|
|
24
|
+
|
|
25
|
+
let logger: any; // Will be initialized when needed
|
|
26
|
+
|
|
27
|
+
function ensureLogger() {
|
|
28
|
+
if (!logger) {
|
|
29
|
+
logger = createLogger("Server");
|
|
30
|
+
}
|
|
31
|
+
return logger;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StandardServerConfig {
|
|
35
|
+
appName: string;
|
|
36
|
+
appVersion: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
port?: number;
|
|
39
|
+
webPort?: number; // Optional separate web UI port
|
|
40
|
+
host?: string;
|
|
41
|
+
environment?: string;
|
|
42
|
+
enableWebSocket?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* When true (default), startup failures will terminate the Node process.
|
|
45
|
+
* Disable in test environments to surface errors as rejections instead.
|
|
46
|
+
*/
|
|
47
|
+
exitOnStartupError?: boolean;
|
|
48
|
+
// Optional logging configuration (not all fields are used yet)
|
|
49
|
+
logging?: {
|
|
50
|
+
level?: string;
|
|
51
|
+
maxSize?: string;
|
|
52
|
+
maxFiles?: string | number;
|
|
53
|
+
datePattern?: string;
|
|
54
|
+
zippedArchive?: boolean;
|
|
55
|
+
};
|
|
56
|
+
// Desktop app specific
|
|
57
|
+
appId?: string; // App identifier for desktop (e.g. 'com.superdangerous.appname')
|
|
58
|
+
enableDesktopIntegration?: boolean; // Auto-configure for desktop apps
|
|
59
|
+
desktopDataPath?: string; // Override desktop data path
|
|
60
|
+
corsOrigins?: string[]; // Additional CORS origins for desktop apps
|
|
61
|
+
onInitialize?: (app: Express, io?: any) => Promise<void>;
|
|
62
|
+
onStart?: () => Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Request payload size limit passed to express.json/urlencoded
|
|
65
|
+
*/
|
|
66
|
+
bodyLimit?: string | number;
|
|
67
|
+
/**
|
|
68
|
+
* Whether to log requests using the framework logger (defaults to true)
|
|
69
|
+
*/
|
|
70
|
+
enableRequestLogging?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Options forwarded to the request logging middleware
|
|
73
|
+
*/
|
|
74
|
+
requestLogging?: Parameters<typeof createRequestLoggingMiddleware>[0];
|
|
75
|
+
/**
|
|
76
|
+
* Configure trust proxy setting for Express (defaults to true for prod)
|
|
77
|
+
*/
|
|
78
|
+
trustProxy?: boolean | string;
|
|
79
|
+
/**
|
|
80
|
+
* Custom timeouts for the underlying HTTP server
|
|
81
|
+
*/
|
|
82
|
+
requestTimeoutMs?: number;
|
|
83
|
+
headersTimeoutMs?: number;
|
|
84
|
+
keepAliveTimeoutMs?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Signals that should trigger a graceful shutdown. Set to [] to disable.
|
|
87
|
+
*/
|
|
88
|
+
gracefulShutdownSignals?: NodeJS.Signals[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Standard server implementation that all apps should use
|
|
93
|
+
* Handles startup, error handling, and banner display consistently
|
|
94
|
+
*/
|
|
95
|
+
export class StandardServer {
|
|
96
|
+
private app: Express;
|
|
97
|
+
private httpServer: HttpServer | HttpsServer;
|
|
98
|
+
private config: StandardServerConfig;
|
|
99
|
+
private wsServer: any;
|
|
100
|
+
private startTime: number;
|
|
101
|
+
private isInitialized: boolean = false;
|
|
102
|
+
private shuttingDown = false;
|
|
103
|
+
private signalsBound = false;
|
|
104
|
+
private connections: Set<NetSocket> = new Set();
|
|
105
|
+
|
|
106
|
+
constructor(config: StandardServerConfig) {
|
|
107
|
+
const environment = process.env.NODE_ENV || "development";
|
|
108
|
+
// Default to localhost for development/test, 0.0.0.0 for production/containerized environments
|
|
109
|
+
const defaultHost =
|
|
110
|
+
environment === "development" || environment === "test"
|
|
111
|
+
? "localhost"
|
|
112
|
+
: "0.0.0.0";
|
|
113
|
+
|
|
114
|
+
// Auto-enable desktop integration if running in Electron or explicitly enabled
|
|
115
|
+
const enableDesktopIntegration =
|
|
116
|
+
config.enableDesktopIntegration ?? isDesktopApp();
|
|
117
|
+
|
|
118
|
+
this.config = {
|
|
119
|
+
port: 8080,
|
|
120
|
+
host: process.env.HOST || config.host || defaultHost,
|
|
121
|
+
environment,
|
|
122
|
+
enableWebSocket: true,
|
|
123
|
+
bodyLimit: "10mb",
|
|
124
|
+
requestTimeoutMs: 120_000,
|
|
125
|
+
headersTimeoutMs: 65_000,
|
|
126
|
+
keepAliveTimeoutMs: 60_000,
|
|
127
|
+
gracefulShutdownSignals: ["SIGTERM", "SIGINT"],
|
|
128
|
+
enableRequestLogging: true,
|
|
129
|
+
enableDesktopIntegration,
|
|
130
|
+
appId:
|
|
131
|
+
config.appId ||
|
|
132
|
+
`com.company.${config.appName.toLowerCase().replace(/[^a-z0-9]/g, "-")}`,
|
|
133
|
+
exitOnStartupError:
|
|
134
|
+
config.exitOnStartupError ?? process.env.NODE_ENV !== "test",
|
|
135
|
+
...config,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
this.app = express();
|
|
139
|
+
this.httpServer = createServer(this.app);
|
|
140
|
+
this.httpServer.on("connection", (socket: NetSocket) => {
|
|
141
|
+
this.connections.add(socket);
|
|
142
|
+
socket.on("close", () => this.connections.delete(socket));
|
|
143
|
+
});
|
|
144
|
+
this.startTime = Date.now();
|
|
145
|
+
this.bindShutdownSignals();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the Express app instance for middleware setup
|
|
150
|
+
*/
|
|
151
|
+
public getApp(): Express {
|
|
152
|
+
return this.app;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the HTTP server instance
|
|
157
|
+
*/
|
|
158
|
+
public getServer(): HttpServer | HttpsServer {
|
|
159
|
+
return this.httpServer;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Initialize the server (setup middleware, routes, etc.)
|
|
164
|
+
*/
|
|
165
|
+
public async initialize(): Promise<void> {
|
|
166
|
+
if (this.isInitialized) return;
|
|
167
|
+
this.isInitialized = true;
|
|
168
|
+
try {
|
|
169
|
+
// Setup desktop-specific paths, CORS, and logging if running as desktop app
|
|
170
|
+
if (this.config.enableDesktopIntegration) {
|
|
171
|
+
await this.setupDesktopIntegration();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Initialize the logger first to ensure file output works
|
|
175
|
+
const logger = getLogger;
|
|
176
|
+
if (
|
|
177
|
+
logger &&
|
|
178
|
+
typeof logger.isInitialized === "function" &&
|
|
179
|
+
!logger.isInitialized()
|
|
180
|
+
) {
|
|
181
|
+
// Use proper logs directory for desktop apps
|
|
182
|
+
const logsDir = this.config.enableDesktopIntegration
|
|
183
|
+
? getLogsPath(this.config.appId!, this.config.appName)
|
|
184
|
+
: "./data/logs";
|
|
185
|
+
|
|
186
|
+
await logger.initialize({
|
|
187
|
+
appName: this.config.appName,
|
|
188
|
+
logLevel:
|
|
189
|
+
this.config.logging?.level || process.env.LOG_LEVEL || "info",
|
|
190
|
+
consoleOutput: true,
|
|
191
|
+
fileOutput: true,
|
|
192
|
+
logsDir,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Setup default middleware
|
|
197
|
+
this.setupDefaultMiddleware();
|
|
198
|
+
|
|
199
|
+
// Initialize WebSocket if enabled (before onInitialize so it can be passed)
|
|
200
|
+
if (this.config.enableWebSocket) {
|
|
201
|
+
this.wsServer = await Promise.resolve(
|
|
202
|
+
createWebSocketServer(this.httpServer),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Call custom initialization if provided, passing the WebSocket server
|
|
207
|
+
if (this.config.onInitialize) {
|
|
208
|
+
const wantsWebSocket = this.config.onInitialize.length >= 2;
|
|
209
|
+
if (wantsWebSocket) {
|
|
210
|
+
await this.config.onInitialize(this.app, this.wsServer);
|
|
211
|
+
} else {
|
|
212
|
+
await this.config.onInitialize(this.app);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Setup error handlers (should be last)
|
|
217
|
+
this.setupErrorHandlers();
|
|
218
|
+
} catch (_error: any) {
|
|
219
|
+
ensureLogger().error("Server initialization failed:", _error);
|
|
220
|
+
this.isInitialized = false;
|
|
221
|
+
throw _error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Setup desktop app integration (CORS, data paths, logging, etc.)
|
|
227
|
+
*/
|
|
228
|
+
private async setupDesktopIntegration(): Promise<void> {
|
|
229
|
+
ensureLogger().info("Setting up desktop app integration", {
|
|
230
|
+
appId: this.config.appId,
|
|
231
|
+
isDesktopApp: isDesktopApp(),
|
|
232
|
+
dataPath:
|
|
233
|
+
this.config.desktopDataPath ||
|
|
234
|
+
getAppDataPath(this.config.appId!, this.config.appName),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Initialize logging for desktop apps
|
|
238
|
+
const logger = getLogger;
|
|
239
|
+
if (!logger.isInitialized()) {
|
|
240
|
+
const logsDir = getLogsPath(this.config.appId!, this.config.appName);
|
|
241
|
+
await logger.initialize({
|
|
242
|
+
appName: this.config.appName,
|
|
243
|
+
logLevel: process.env.LOG_LEVEL || "info",
|
|
244
|
+
consoleOutput: true,
|
|
245
|
+
fileOutput: true,
|
|
246
|
+
logsDir,
|
|
247
|
+
});
|
|
248
|
+
ensureLogger().info("Logging initialized for desktop app", { logsDir });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Setup CORS for desktop apps
|
|
252
|
+
const corsOrigins: string[] = [...(this.config.corsOrigins || [])];
|
|
253
|
+
|
|
254
|
+
// Add localhost origins based on webPort if specified
|
|
255
|
+
if (this.config.webPort) {
|
|
256
|
+
corsOrigins.push(
|
|
257
|
+
`http://localhost:${this.config.webPort}`,
|
|
258
|
+
`http://localhost:${this.config.webPort + 1}`, // Common development pattern
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Add Electron origins when running in desktop mode
|
|
263
|
+
if (isDesktopApp()) {
|
|
264
|
+
// Electron file:// protocol sends null or file:// origin
|
|
265
|
+
corsOrigins.push("file://", "null");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.app.use(
|
|
269
|
+
cors({
|
|
270
|
+
origin: corsOrigins,
|
|
271
|
+
credentials: true,
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Setup default middleware for all applications
|
|
278
|
+
*/
|
|
279
|
+
private setupDefaultMiddleware(): void {
|
|
280
|
+
// Basic middleware that should be present in all apps
|
|
281
|
+
this.app.use(express.json({ limit: this.config.bodyLimit }));
|
|
282
|
+
this.app.use(
|
|
283
|
+
express.urlencoded({ extended: true, limit: this.config.bodyLimit }),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// CORS setup for non-desktop apps (desktop setup happens in setupDesktopIntegration)
|
|
287
|
+
if (!this.config.enableDesktopIntegration) {
|
|
288
|
+
const corsOrigins = [...(this.config.corsOrigins || [])];
|
|
289
|
+
|
|
290
|
+
// Add localhost origins based on webPort if specified
|
|
291
|
+
if (this.config.webPort) {
|
|
292
|
+
corsOrigins.push(
|
|
293
|
+
`http://localhost:${this.config.webPort}`,
|
|
294
|
+
`http://localhost:${this.config.webPort + 1}`, // Common development pattern
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (corsOrigins.length > 0) {
|
|
299
|
+
this.app.use(
|
|
300
|
+
cors({
|
|
301
|
+
origin: corsOrigins,
|
|
302
|
+
credentials: true,
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
} else {
|
|
306
|
+
this.app.use(cors());
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Request logging (can be disabled)
|
|
311
|
+
if (this.config.enableRequestLogging) {
|
|
312
|
+
this.app.use(
|
|
313
|
+
createRequestLoggingMiddleware({
|
|
314
|
+
logger: ensureLogger(),
|
|
315
|
+
...this.config.requestLogging,
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Trust proxy to respect X-Forwarded-* headers in containerized/proxy envs
|
|
321
|
+
const trustProxy =
|
|
322
|
+
this.config.trustProxy ??
|
|
323
|
+
(this.config.environment !== "development" ? true : false);
|
|
324
|
+
if (typeof this.app.set === "function") {
|
|
325
|
+
this.app.set("trust proxy", trustProxy);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Setup error handlers (should be last in middleware chain)
|
|
331
|
+
*/
|
|
332
|
+
private setupErrorHandlers(): void {
|
|
333
|
+
// AI error handler for AI service errors
|
|
334
|
+
this.app.use(aiErrorHandler);
|
|
335
|
+
|
|
336
|
+
// Standardized API error handler
|
|
337
|
+
this.app.use(apiErrorHandler);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Start the server and listen on the configured port
|
|
342
|
+
*/
|
|
343
|
+
public async start(): Promise<void> {
|
|
344
|
+
if (!this.isInitialized) {
|
|
345
|
+
throw new Error("Server not initialized. Call initialize() first.");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const port = this.config.port!;
|
|
349
|
+
const host = this.config.host!;
|
|
350
|
+
const exitOnError = this.config.exitOnStartupError !== false;
|
|
351
|
+
|
|
352
|
+
// Check if API port is available (skip for port 0 which means "pick any available")
|
|
353
|
+
if (port !== 0) {
|
|
354
|
+
const processOnPort = await getProcessOnPort(port);
|
|
355
|
+
if (processOnPort) {
|
|
356
|
+
const message = `Port ${port} is already in use`;
|
|
357
|
+
if (exitOnError) {
|
|
358
|
+
ensureLogger().error(message);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
} else {
|
|
361
|
+
ensureLogger().warn(
|
|
362
|
+
`${message} (PID ${processOnPort.pid} - ${processOnPort.command})`,
|
|
363
|
+
);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return new Promise((resolve, reject) => {
|
|
370
|
+
const fail = (error: Error) => {
|
|
371
|
+
ensureLogger().error("Server error:", error);
|
|
372
|
+
if (exitOnError) {
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
reject(error);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Handle server errors
|
|
379
|
+
this.httpServer.on("error", (error: any) => {
|
|
380
|
+
if (error.code === "EADDRINUSE") {
|
|
381
|
+
const message = `Port ${port} is already in use`;
|
|
382
|
+
ensureLogger().error(message);
|
|
383
|
+
if (exitOnError) {
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
reject(new Error(message));
|
|
387
|
+
return;
|
|
388
|
+
} else if (error.code === "EACCES") {
|
|
389
|
+
const message = `Port ${port} requires elevated privileges`;
|
|
390
|
+
ensureLogger().error(message);
|
|
391
|
+
if (exitOnError) {
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
reject(new Error(message));
|
|
395
|
+
return;
|
|
396
|
+
} else {
|
|
397
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Start listening
|
|
403
|
+
// Apply safer timeouts for production workloads
|
|
404
|
+
this.httpServer.requestTimeout = this.config.requestTimeoutMs!;
|
|
405
|
+
this.httpServer.headersTimeout = this.config.headersTimeoutMs!;
|
|
406
|
+
this.httpServer.keepAliveTimeout = this.config.keepAliveTimeoutMs!;
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
this.httpServer.listen(port, host, async () => {
|
|
410
|
+
// Display banner only after successful binding
|
|
411
|
+
// Note: In production, webPort is typically not used because the API server
|
|
412
|
+
// serves the UI assets directly from the main port. However, if webPort
|
|
413
|
+
// is explicitly configured, respect it.
|
|
414
|
+
|
|
415
|
+
// Only display banner if not suppressed
|
|
416
|
+
if (process.env.SUPPRESS_STARTUP_BANNER !== "true") {
|
|
417
|
+
displayStartupBanner({
|
|
418
|
+
appName: this.config.appName,
|
|
419
|
+
appVersion: this.config.appVersion,
|
|
420
|
+
description: this.config.description,
|
|
421
|
+
port: this.config.port!,
|
|
422
|
+
webPort: this.config.webPort, // Pass the actual configured webPort
|
|
423
|
+
environment: this.config.environment,
|
|
424
|
+
startTime: this.startTime,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Call custom start handler if provided
|
|
429
|
+
if (this.config.onStart) {
|
|
430
|
+
try {
|
|
431
|
+
await this.config.onStart();
|
|
432
|
+
} catch (_error) {
|
|
433
|
+
const error =
|
|
434
|
+
_error instanceof Error ? _error : new Error(String(_error));
|
|
435
|
+
ensureLogger().error("Custom start handler failed:", error);
|
|
436
|
+
if (exitOnError) {
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
reject(error);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (this.config.webPort) {
|
|
445
|
+
ensureLogger().info(`Web UI Port: ${this.config.webPort}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Bind graceful shutdown signals once per instance
|
|
449
|
+
this.bindShutdownSignals();
|
|
450
|
+
|
|
451
|
+
resolve();
|
|
452
|
+
});
|
|
453
|
+
} catch (err: any) {
|
|
454
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
455
|
+
ensureLogger().error("Failed to start server:", error);
|
|
456
|
+
if (exitOnError) {
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
reject(error);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get the data path for desktop apps (platform-specific)
|
|
466
|
+
*/
|
|
467
|
+
public getDataPath(): string {
|
|
468
|
+
if (this.config.enableDesktopIntegration) {
|
|
469
|
+
return (
|
|
470
|
+
this.config.desktopDataPath ||
|
|
471
|
+
getAppDataPath(this.config.appId!, this.config.appName)
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return "./data";
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Check if running as desktop app
|
|
479
|
+
*/
|
|
480
|
+
public isDesktopApp(): boolean {
|
|
481
|
+
return this.config.enableDesktopIntegration || false;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Attach signal listeners for graceful shutdown
|
|
486
|
+
*/
|
|
487
|
+
private bindShutdownSignals(): void {
|
|
488
|
+
if (this.signalsBound) return;
|
|
489
|
+
if (!this.config.gracefulShutdownSignals?.length) return;
|
|
490
|
+
|
|
491
|
+
const signals = this.config.gracefulShutdownSignals;
|
|
492
|
+
if (process.setMaxListeners) {
|
|
493
|
+
const current = process.getMaxListeners ? process.getMaxListeners() : 10;
|
|
494
|
+
process.setMaxListeners(Math.max(current, signals.length + 20));
|
|
495
|
+
}
|
|
496
|
+
signals.forEach((signal) => {
|
|
497
|
+
process.on(signal, async () => {
|
|
498
|
+
ensureLogger().info(`Received ${signal}, shutting down gracefully...`);
|
|
499
|
+
try {
|
|
500
|
+
await this.stop();
|
|
501
|
+
process.exit(0);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
ensureLogger().error("Graceful shutdown failed", error);
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
this.signalsBound = true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Stop the server gracefully
|
|
514
|
+
*/
|
|
515
|
+
public async stop(): Promise<void> {
|
|
516
|
+
if (this.shuttingDown) return Promise.resolve();
|
|
517
|
+
this.shuttingDown = true;
|
|
518
|
+
|
|
519
|
+
return new Promise((resolve, reject) => {
|
|
520
|
+
const timeout = setTimeout(() => {
|
|
521
|
+
ensureLogger().warn("Force closing server after timeout");
|
|
522
|
+
this.forceCloseConnections();
|
|
523
|
+
resolve();
|
|
524
|
+
}, 10_000);
|
|
525
|
+
|
|
526
|
+
const close = () => {
|
|
527
|
+
clearTimeout(timeout);
|
|
528
|
+
ensureLogger().info("Server stopped gracefully");
|
|
529
|
+
resolve();
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
if (this.wsServer?.shutdown) {
|
|
534
|
+
this.wsServer.shutdown();
|
|
535
|
+
}
|
|
536
|
+
if (this.wsServer?.close) {
|
|
537
|
+
this.wsServer.close();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
this.httpServer.close((err?: Error) => {
|
|
541
|
+
if (err) {
|
|
542
|
+
ensureLogger().error("Error while stopping server", err);
|
|
543
|
+
reject(err);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
this.forceCloseConnections();
|
|
547
|
+
close();
|
|
548
|
+
});
|
|
549
|
+
} catch (error) {
|
|
550
|
+
clearTimeout(timeout);
|
|
551
|
+
reject(error);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private forceCloseConnections(): void {
|
|
557
|
+
// Prefer native closeAllConnections if available
|
|
558
|
+
if (typeof (this.httpServer as any).closeAllConnections === "function") {
|
|
559
|
+
try {
|
|
560
|
+
(this.httpServer as any).closeAllConnections();
|
|
561
|
+
} catch (err) {
|
|
562
|
+
ensureLogger().warn("closeAllConnections failed", err);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
this.connections.forEach((socket) => {
|
|
567
|
+
try {
|
|
568
|
+
socket.destroy();
|
|
569
|
+
} catch {
|
|
570
|
+
// ignore
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
this.connections.clear();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Convenience function to create and start a standard server
|
|
579
|
+
*/
|
|
580
|
+
export async function createStandardServer(
|
|
581
|
+
config: StandardServerConfig,
|
|
582
|
+
): Promise<StandardServer> {
|
|
583
|
+
const server = new StandardServer(config);
|
|
584
|
+
await server.initialize();
|
|
585
|
+
await server.start();
|
|
586
|
+
return server;
|
|
587
|
+
}
|