@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.
Files changed (239) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +652 -0
  3. package/dist/api/logsRouter.d.ts +20 -0
  4. package/dist/api/logsRouter.d.ts.map +1 -0
  5. package/dist/api/logsRouter.js +515 -0
  6. package/dist/api/logsRouter.js.map +1 -0
  7. package/dist/cli/dev-server.d.ts +7 -0
  8. package/dist/cli/dev-server.d.ts.map +1 -0
  9. package/dist/cli/dev-server.js +640 -0
  10. package/dist/cli/dev-server.js.map +1 -0
  11. package/dist/cli/index.d.ts +7 -0
  12. package/dist/cli/index.d.ts.map +1 -0
  13. package/dist/cli/index.js +26 -0
  14. package/dist/cli/index.js.map +1 -0
  15. package/dist/core/StandardServer.d.ts +129 -0
  16. package/dist/core/StandardServer.d.ts.map +1 -0
  17. package/dist/core/StandardServer.js +453 -0
  18. package/dist/core/StandardServer.js.map +1 -0
  19. package/dist/core/apiResponse.d.ts +69 -0
  20. package/dist/core/apiResponse.d.ts.map +1 -0
  21. package/dist/core/apiResponse.js +127 -0
  22. package/dist/core/apiResponse.js.map +1 -0
  23. package/dist/core/healthCheck.d.ts +160 -0
  24. package/dist/core/healthCheck.d.ts.map +1 -0
  25. package/dist/core/healthCheck.js +398 -0
  26. package/dist/core/healthCheck.js.map +1 -0
  27. package/dist/core/index.d.ts +40 -0
  28. package/dist/core/index.d.ts.map +1 -0
  29. package/dist/core/index.js +40 -0
  30. package/dist/core/index.js.map +1 -0
  31. package/dist/core/logger.d.ts +117 -0
  32. package/dist/core/logger.d.ts.map +1 -0
  33. package/dist/core/logger.js +826 -0
  34. package/dist/core/logger.js.map +1 -0
  35. package/dist/core/portUtils.d.ts +71 -0
  36. package/dist/core/portUtils.d.ts.map +1 -0
  37. package/dist/core/portUtils.js +240 -0
  38. package/dist/core/portUtils.js.map +1 -0
  39. package/dist/core/storageService.d.ts +119 -0
  40. package/dist/core/storageService.d.ts.map +1 -0
  41. package/dist/core/storageService.js +405 -0
  42. package/dist/core/storageService.js.map +1 -0
  43. package/dist/desktop/bundler.d.ts +40 -0
  44. package/dist/desktop/bundler.d.ts.map +1 -0
  45. package/dist/desktop/bundler.js +176 -0
  46. package/dist/desktop/bundler.js.map +1 -0
  47. package/dist/desktop/index.d.ts +25 -0
  48. package/dist/desktop/index.d.ts.map +1 -0
  49. package/dist/desktop/index.js +15 -0
  50. package/dist/desktop/index.js.map +1 -0
  51. package/dist/desktop/native-modules.d.ts +66 -0
  52. package/dist/desktop/native-modules.d.ts.map +1 -0
  53. package/dist/desktop/native-modules.js +200 -0
  54. package/dist/desktop/native-modules.js.map +1 -0
  55. package/dist/index.d.ts +29 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +39 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/logging/LogCategories.d.ts +87 -0
  60. package/dist/logging/LogCategories.d.ts.map +1 -0
  61. package/dist/logging/LogCategories.js +205 -0
  62. package/dist/logging/LogCategories.js.map +1 -0
  63. package/dist/middleware/aiErrorHandler.d.ts +31 -0
  64. package/dist/middleware/aiErrorHandler.d.ts.map +1 -0
  65. package/dist/middleware/aiErrorHandler.js +181 -0
  66. package/dist/middleware/aiErrorHandler.js.map +1 -0
  67. package/dist/middleware/auth.d.ts +101 -0
  68. package/dist/middleware/auth.d.ts.map +1 -0
  69. package/dist/middleware/auth.js +230 -0
  70. package/dist/middleware/auth.js.map +1 -0
  71. package/dist/middleware/cors.d.ts +56 -0
  72. package/dist/middleware/cors.d.ts.map +1 -0
  73. package/dist/middleware/cors.js +123 -0
  74. package/dist/middleware/cors.js.map +1 -0
  75. package/dist/middleware/errorHandler.d.ts +13 -0
  76. package/dist/middleware/errorHandler.d.ts.map +1 -0
  77. package/dist/middleware/errorHandler.js +85 -0
  78. package/dist/middleware/errorHandler.js.map +1 -0
  79. package/dist/middleware/fileUpload.d.ts +62 -0
  80. package/dist/middleware/fileUpload.d.ts.map +1 -0
  81. package/dist/middleware/fileUpload.js +175 -0
  82. package/dist/middleware/fileUpload.js.map +1 -0
  83. package/dist/middleware/health.d.ts +48 -0
  84. package/dist/middleware/health.d.ts.map +1 -0
  85. package/dist/middleware/health.js +143 -0
  86. package/dist/middleware/health.js.map +1 -0
  87. package/dist/middleware/index.d.ts +20 -0
  88. package/dist/middleware/index.d.ts.map +1 -0
  89. package/dist/middleware/index.js +18 -0
  90. package/dist/middleware/index.js.map +1 -0
  91. package/dist/middleware/openapi.d.ts +64 -0
  92. package/dist/middleware/openapi.d.ts.map +1 -0
  93. package/dist/middleware/openapi.js +258 -0
  94. package/dist/middleware/openapi.js.map +1 -0
  95. package/dist/middleware/requestLogging.d.ts +22 -0
  96. package/dist/middleware/requestLogging.d.ts.map +1 -0
  97. package/dist/middleware/requestLogging.js +61 -0
  98. package/dist/middleware/requestLogging.js.map +1 -0
  99. package/dist/middleware/session.d.ts +84 -0
  100. package/dist/middleware/session.d.ts.map +1 -0
  101. package/dist/middleware/session.js +189 -0
  102. package/dist/middleware/session.js.map +1 -0
  103. package/dist/middleware/validation.d.ts +1337 -0
  104. package/dist/middleware/validation.d.ts.map +1 -0
  105. package/dist/middleware/validation.js +483 -0
  106. package/dist/middleware/validation.js.map +1 -0
  107. package/dist/services/aiService.d.ts +180 -0
  108. package/dist/services/aiService.d.ts.map +1 -0
  109. package/dist/services/aiService.js +547 -0
  110. package/dist/services/aiService.js.map +1 -0
  111. package/dist/services/conversationStorage.d.ts +38 -0
  112. package/dist/services/conversationStorage.d.ts.map +1 -0
  113. package/dist/services/conversationStorage.js +158 -0
  114. package/dist/services/conversationStorage.js.map +1 -0
  115. package/dist/services/crossPlatformBuffer.d.ts +84 -0
  116. package/dist/services/crossPlatformBuffer.d.ts.map +1 -0
  117. package/dist/services/crossPlatformBuffer.js +246 -0
  118. package/dist/services/crossPlatformBuffer.js.map +1 -0
  119. package/dist/services/index.d.ts +17 -0
  120. package/dist/services/index.d.ts.map +1 -0
  121. package/dist/services/index.js +18 -0
  122. package/dist/services/index.js.map +1 -0
  123. package/dist/services/networkService.d.ts +81 -0
  124. package/dist/services/networkService.d.ts.map +1 -0
  125. package/dist/services/networkService.js +268 -0
  126. package/dist/services/networkService.js.map +1 -0
  127. package/dist/services/queueService.d.ts +112 -0
  128. package/dist/services/queueService.d.ts.map +1 -0
  129. package/dist/services/queueService.js +338 -0
  130. package/dist/services/queueService.js.map +1 -0
  131. package/dist/services/settingsService.d.ts +135 -0
  132. package/dist/services/settingsService.d.ts.map +1 -0
  133. package/dist/services/settingsService.js +425 -0
  134. package/dist/services/settingsService.js.map +1 -0
  135. package/dist/services/systemMonitor.d.ts +208 -0
  136. package/dist/services/systemMonitor.d.ts.map +1 -0
  137. package/dist/services/systemMonitor.js +693 -0
  138. package/dist/services/systemMonitor.js.map +1 -0
  139. package/dist/services/updateService.d.ts +78 -0
  140. package/dist/services/updateService.d.ts.map +1 -0
  141. package/dist/services/updateService.js +252 -0
  142. package/dist/services/updateService.js.map +1 -0
  143. package/dist/services/websocketEvents.d.ts +372 -0
  144. package/dist/services/websocketEvents.d.ts.map +1 -0
  145. package/dist/services/websocketEvents.js +338 -0
  146. package/dist/services/websocketEvents.js.map +1 -0
  147. package/dist/services/websocketServer.d.ts +80 -0
  148. package/dist/services/websocketServer.d.ts.map +1 -0
  149. package/dist/services/websocketServer.js +299 -0
  150. package/dist/services/websocketServer.js.map +1 -0
  151. package/dist/settings/SettingsSchema.d.ts +151 -0
  152. package/dist/settings/SettingsSchema.d.ts.map +1 -0
  153. package/dist/settings/SettingsSchema.js +424 -0
  154. package/dist/settings/SettingsSchema.js.map +1 -0
  155. package/dist/testing/TestServer.d.ts +69 -0
  156. package/dist/testing/TestServer.d.ts.map +1 -0
  157. package/dist/testing/TestServer.js +250 -0
  158. package/dist/testing/TestServer.js.map +1 -0
  159. package/dist/types/index.d.ts +137 -0
  160. package/dist/types/index.d.ts.map +1 -0
  161. package/dist/types/index.js +5 -0
  162. package/dist/types/index.js.map +1 -0
  163. package/dist/utils/appPaths.d.ts +74 -0
  164. package/dist/utils/appPaths.d.ts.map +1 -0
  165. package/dist/utils/appPaths.js +162 -0
  166. package/dist/utils/appPaths.js.map +1 -0
  167. package/dist/utils/fs-utils.d.ts +50 -0
  168. package/dist/utils/fs-utils.d.ts.map +1 -0
  169. package/dist/utils/fs-utils.js +114 -0
  170. package/dist/utils/fs-utils.js.map +1 -0
  171. package/dist/utils/index.d.ts +12 -0
  172. package/dist/utils/index.d.ts.map +1 -0
  173. package/dist/utils/index.js +10 -0
  174. package/dist/utils/index.js.map +1 -0
  175. package/dist/utils/standardConfig.d.ts +61 -0
  176. package/dist/utils/standardConfig.d.ts.map +1 -0
  177. package/dist/utils/standardConfig.js +109 -0
  178. package/dist/utils/standardConfig.js.map +1 -0
  179. package/dist/utils/startupBanner.d.ts +34 -0
  180. package/dist/utils/startupBanner.d.ts.map +1 -0
  181. package/dist/utils/startupBanner.js +169 -0
  182. package/dist/utils/startupBanner.js.map +1 -0
  183. package/dist/utils/startupLogger.d.ts +45 -0
  184. package/dist/utils/startupLogger.d.ts.map +1 -0
  185. package/dist/utils/startupLogger.js +200 -0
  186. package/dist/utils/startupLogger.js.map +1 -0
  187. package/package.json +151 -0
  188. package/src/api/logsRouter.ts +600 -0
  189. package/src/cli/dev-server.ts +803 -0
  190. package/src/cli/index.ts +31 -0
  191. package/src/core/StandardServer.ts +587 -0
  192. package/src/core/apiResponse.ts +202 -0
  193. package/src/core/healthCheck.ts +565 -0
  194. package/src/core/index.ts +80 -0
  195. package/src/core/logger.ts +1092 -0
  196. package/src/core/portUtils.ts +319 -0
  197. package/src/core/storageService.ts +595 -0
  198. package/src/desktop/bundler.ts +271 -0
  199. package/src/desktop/index.ts +18 -0
  200. package/src/desktop/native-modules.ts +289 -0
  201. package/src/index.ts +142 -0
  202. package/src/logging/LogCategories.ts +302 -0
  203. package/src/middleware/aiErrorHandler.ts +278 -0
  204. package/src/middleware/auth.ts +329 -0
  205. package/src/middleware/cors.ts +187 -0
  206. package/src/middleware/errorHandler.ts +103 -0
  207. package/src/middleware/fileUpload.ts +252 -0
  208. package/src/middleware/health.ts +206 -0
  209. package/src/middleware/index.ts +71 -0
  210. package/src/middleware/openapi.ts +305 -0
  211. package/src/middleware/requestLogging.ts +92 -0
  212. package/src/middleware/session.ts +238 -0
  213. package/src/middleware/validation.ts +603 -0
  214. package/src/services/aiService.ts +789 -0
  215. package/src/services/conversationStorage.ts +232 -0
  216. package/src/services/crossPlatformBuffer.ts +341 -0
  217. package/src/services/index.ts +47 -0
  218. package/src/services/networkService.ts +351 -0
  219. package/src/services/queueService.ts +446 -0
  220. package/src/services/settingsService.ts +549 -0
  221. package/src/services/systemMonitor.ts +936 -0
  222. package/src/services/updateService.ts +334 -0
  223. package/src/services/websocketEvents.ts +409 -0
  224. package/src/services/websocketServer.ts +394 -0
  225. package/src/settings/SettingsSchema.ts +664 -0
  226. package/src/testing/TestServer.ts +312 -0
  227. package/src/types/index.ts +154 -0
  228. package/src/utils/appPaths.ts +196 -0
  229. package/src/utils/fs-utils.ts +130 -0
  230. package/src/utils/index.ts +15 -0
  231. package/src/utils/standardConfig.ts +178 -0
  232. package/src/utils/startupBanner.ts +287 -0
  233. package/src/utils/startupLogger.ts +268 -0
  234. package/ui/dist/index.d.mts +1221 -0
  235. package/ui/dist/index.d.ts +1221 -0
  236. package/ui/dist/index.js +73 -0
  237. package/ui/dist/index.js.map +1 -0
  238. package/ui/dist/index.mjs +73 -0
  239. package/ui/dist/index.mjs.map +1 -0
@@ -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
+ }