cynx-lib 1.3.93 → 1.3.94

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cynx-lib",
3
- "version": "1.3.93",
3
+ "version": "1.3.94",
4
4
  "type": "module",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -41,7 +41,11 @@
41
41
  "import": "./utils/utils/session.js",
42
42
  "require": "./utils/utils/session.cjs"
43
43
  },
44
- "./gen": "./gen.js"
44
+ "./gen": "./gen.js",
45
+ "./server": {
46
+ "import": "./utils/utils/server.js",
47
+ "require": "./utils/utils/server.cjs"
48
+ }
45
49
  },
46
50
  "peerDependencies": {
47
51
  "lucide-react": ">=0.300",
package/utils/index.cjs CHANGED
@@ -32,6 +32,7 @@ __export(index_exports, {
32
32
  durationUtil: () => import_duration.default,
33
33
  oidcUtil: () => import_oidc.default,
34
34
  realtimeUtil: () => import_realtime.default,
35
+ serverUtil: () => import_server.default,
35
36
  sessionManagerUtil: () => import_session_manager.default,
36
37
  sessionUtil: () => import_session.default,
37
38
  storageUtil: () => import_storage.default,
@@ -60,12 +61,15 @@ var import_session = __toESM(require("./utils/session.cjs"), 1);
60
61
  var import_session_manager = __toESM(require("./utils/session_manager.cjs"), 1);
61
62
  var import_ui = __toESM(require("./utils/ui.cjs"), 1);
62
63
  var import_db = __toESM(require("./utils/db.cjs"), 1);
64
+ __reExport(index_exports, require("./utils/server.cjs"), module.exports);
65
+ var import_server = __toESM(require("./utils/server.cjs"), 1);
63
66
  // Annotate the CommonJS export names for ESM import in node:
64
67
  0 && (module.exports = {
65
68
  dbUtil,
66
69
  durationUtil,
67
70
  oidcUtil,
68
71
  realtimeUtil,
72
+ serverUtil,
69
73
  sessionManagerUtil,
70
74
  sessionUtil,
71
75
  storageUtil,
@@ -81,5 +85,6 @@ var import_db = __toESM(require("./utils/db.cjs"), 1);
81
85
  ...require("./utils/session.cjs"),
82
86
  ...require("./utils/session_manager.cjs"),
83
87
  ...require("./utils/ui.cjs"),
84
- ...require("./utils/db.cjs")
88
+ ...require("./utils/db.cjs"),
89
+ ...require("./utils/server.cjs")
85
90
  });
package/utils/index.js CHANGED
@@ -19,4 +19,6 @@ export { default as durationUtil } from './utils/duration.js';
19
19
  export { default as sessionUtil } from './utils/session.js';
20
20
  export { default as sessionManagerUtil } from './utils/session_manager.js';
21
21
  export { default as uiUtil } from './utils/ui.js';
22
- export { default as dbUtil } from './utils/db.js';
22
+ export { default as dbUtil } from './utils/db.js';
23
+ export * from './utils/server.js';
24
+ export { default as serverUtil } from './utils/server.js';
@@ -0,0 +1,309 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var server_exports = {};
29
+ __export(server_exports, {
30
+ app_build_info: () => app_build_info,
31
+ app_commit: () => app_commit,
32
+ createRouter: () => createRouter,
33
+ createServer: () => createServer,
34
+ default: () => server_default,
35
+ execGit: () => execGit,
36
+ getNetworkIp: () => getNetworkIp,
37
+ loadRoutes: () => loadRoutes,
38
+ printServerBanner: () => printServerBanner
39
+ });
40
+ module.exports = __toCommonJS(server_exports);
41
+ var import_fs = __toESM(require("fs"), 1);
42
+ var import_path = __toESM(require("path"), 1);
43
+ var import_os = __toESM(require("os"), 1);
44
+ var import_http = __toESM(require("http"), 1);
45
+ var import_child_process = require("child_process");
46
+ let cachedCommit = null;
47
+ let cachedBuildInfo = null;
48
+ const bootTime = (/* @__PURE__ */ new Date()).toISOString();
49
+ function execGit(cmd, cwd) {
50
+ try {
51
+ const stdout = (0, import_child_process.execSync)(cmd, {
52
+ cwd: cwd || process.cwd(),
53
+ stdio: ["ignore", "pipe", "ignore"],
54
+ timeout: 2e3,
55
+ encoding: "utf8"
56
+ });
57
+ return stdout ? stdout.trim() : "";
58
+ } catch {
59
+ return "";
60
+ }
61
+ }
62
+ function app_commit(cwd) {
63
+ if (cachedCommit !== null) {
64
+ return cachedCommit;
65
+ }
66
+ if (process.env.APP_COMMIT && process.env.APP_COMMIT.trim()) {
67
+ cachedCommit = process.env.APP_COMMIT.trim();
68
+ return cachedCommit;
69
+ }
70
+ if (process.env.GIT_COMMIT && process.env.GIT_COMMIT.trim()) {
71
+ cachedCommit = process.env.GIT_COMMIT.trim().slice(0, 7);
72
+ return cachedCommit;
73
+ }
74
+ cachedCommit = execGit("git rev-parse --short HEAD", cwd);
75
+ return cachedCommit;
76
+ }
77
+ function app_build_info(cwd) {
78
+ if (cachedBuildInfo !== null) {
79
+ return cachedBuildInfo;
80
+ }
81
+ const commit = app_commit(cwd);
82
+ const branch = process.env.APP_BRANCH && process.env.APP_BRANCH.trim() || process.env.GIT_BRANCH && process.env.GIT_BRANCH.trim() || execGit("git rev-parse --abbrev-ref HEAD", cwd) || "main";
83
+ const rawDate = process.env.APP_COMMIT_DATE && process.env.APP_COMMIT_DATE.trim() || execGit("git log -1 --format=%ci", cwd);
84
+ const commitDate = rawDate ? rawDate.slice(0, 16) : "";
85
+ const buildTime = process.env.BUILD_TIME && process.env.BUILD_TIME.trim() || bootTime;
86
+ cachedBuildInfo = {
87
+ commit,
88
+ branch,
89
+ commitDate,
90
+ buildTime
91
+ };
92
+ return cachedBuildInfo;
93
+ }
94
+ function getNetworkIp() {
95
+ try {
96
+ const interfaces = import_os.default.networkInterfaces();
97
+ for (const name of Object.keys(interfaces)) {
98
+ for (const iface of interfaces[name]) {
99
+ if (iface.family === "IPv4" && !iface.internal) {
100
+ return iface.address;
101
+ }
102
+ }
103
+ }
104
+ } catch {
105
+ }
106
+ return null;
107
+ }
108
+ function printServerBanner({
109
+ port,
110
+ elapsed,
111
+ commit,
112
+ shouldSyncDb = false,
113
+ isSynced = false,
114
+ networkIp = null
115
+ }) {
116
+ const commitTag = commit ? " \x1B[32m" + commit + "\x1B[0m" : "";
117
+ const resolvedIp = networkIp !== void 0 ? networkIp : getNetworkIp();
118
+ console.log("\n \x1B[32m\x1B[1mSERVER\x1B[0m" + commitTag + " \x1B[90mready in " + elapsed + " ms\x1B[0m\n");
119
+ console.log(" \x1B[32m\u279C\x1B[0m \x1B[1mLocal:\x1B[0m \x1B[36mhttp://localhost:" + port + "/\x1B[0m");
120
+ if (resolvedIp) {
121
+ console.log(" \x1B[32m\u279C\x1B[0m \x1B[1mNetwork:\x1B[0m \x1B[36mhttp://" + resolvedIp + ":" + port + "/\x1B[0m");
122
+ }
123
+ const dbStatus = isSynced ? "\x1B[32mSynced\x1B[0m" : "\x1B[90mSkipped (pass -s to sync)\x1B[0m";
124
+ console.log(" \x1B[32m\u279C\x1B[0m \x1B[1mDatabase:\x1B[0m " + dbStatus + "\n");
125
+ }
126
+ function createRouter(options = {}) {
127
+ const expressModule = options.express || (typeof require !== "undefined" ? require("express") : null);
128
+ if (!expressModule) {
129
+ throw new Error("[cynx/server] express is required to use createRouter");
130
+ }
131
+ const router = expressModule.Router(options.routerOptions || {});
132
+ if (options.prefix) {
133
+ router._cynxPrefix = options.prefix;
134
+ }
135
+ return router;
136
+ }
137
+ function loadRoutes(app, routesDir, options = {}) {
138
+ if (!routesDir || !import_fs.default.existsSync(routesDir)) {
139
+ return [];
140
+ }
141
+ const customPrefixes = options.routePrefixes || {};
142
+ const mounted = [];
143
+ const files = import_fs.default.readdirSync(routesDir);
144
+ for (const file of files) {
145
+ const ext = import_path.default.extname(file);
146
+ if (![".js", ".cjs", ".mjs"].includes(ext)) continue;
147
+ if (file.startsWith(".") || file.startsWith("_")) continue;
148
+ const baseName = import_path.default.basename(file, ext);
149
+ const fullPath = import_path.default.resolve(routesDir, file);
150
+ let routeModule;
151
+ try {
152
+ routeModule = typeof require !== "undefined" ? require(fullPath) : null;
153
+ } catch (err) {
154
+ console.error(`[cynx/server] Error loading route file ${file}:`, err);
155
+ continue;
156
+ }
157
+ if (!routeModule) continue;
158
+ const router = routeModule.default || routeModule.router || routeModule;
159
+ if (typeof router !== "function" && (!router || typeof router.use !== "function")) {
160
+ continue;
161
+ }
162
+ let prefixes = router._cynxPrefix || router.prefix || customPrefixes[baseName];
163
+ if (!prefixes) {
164
+ prefixes = `/api/${baseName}`;
165
+ }
166
+ const prefixList = Array.isArray(prefixes) ? prefixes : [prefixes];
167
+ for (const prefix of prefixList) {
168
+ app.use(prefix, router);
169
+ mounted.push({ file, prefix });
170
+ }
171
+ }
172
+ return mounted;
173
+ }
174
+ function createServer(options = {}) {
175
+ const expressModule = options.express || (typeof require !== "undefined" ? require("express") : null);
176
+ if (!expressModule) {
177
+ throw new Error("[cynx/server] express is required to create a server");
178
+ }
179
+ const app = options.app || expressModule();
180
+ const startTime = Date.now();
181
+ if (options.cors !== false) {
182
+ const corsModule = options.corsMiddleware || (typeof require !== "undefined" ? require("cors") : null);
183
+ if (corsModule) {
184
+ app.use(typeof options.cors === "object" ? corsModule(options.cors) : corsModule());
185
+ }
186
+ }
187
+ if (options.compression) {
188
+ if (typeof options.compression === "function") {
189
+ app.use(options.compression);
190
+ } else {
191
+ const compModule = typeof require !== "undefined" ? require("compression") : null;
192
+ if (compModule) app.use(compModule());
193
+ }
194
+ }
195
+ const limit = options.bodyLimit || "5mb";
196
+ app.use(expressModule.json({ limit }));
197
+ app.use(expressModule.urlencoded({ extended: true, limit }));
198
+ if (typeof options.beforeRoutes === "function") {
199
+ options.beforeRoutes(app);
200
+ }
201
+ if (options.routesDir) {
202
+ loadRoutes(app, options.routesDir, options);
203
+ }
204
+ if (Array.isArray(options.routes)) {
205
+ for (const route of options.routes) {
206
+ if (route.prefix && route.router) {
207
+ const list = Array.isArray(route.prefix) ? route.prefix : [route.prefix];
208
+ for (const p of list) {
209
+ app.use(p, route.router);
210
+ }
211
+ }
212
+ }
213
+ }
214
+ if (options.healthcheck !== false) {
215
+ app.get("/api/health", (_req, res) => res.json({ status: "ok" }));
216
+ }
217
+ if (typeof options.beforeErrorHandler === "function") {
218
+ options.beforeErrorHandler(app);
219
+ }
220
+ if (options.errorHandler !== false) {
221
+ app.use((err, _req, res, _next) => {
222
+ console.error("[HTTP]", err);
223
+ res.status(err.status || 500).json({ error: err.message || "Internal server error" });
224
+ });
225
+ }
226
+ if (options.processHandlers !== false) {
227
+ process.on("unhandledRejection", (reason) => {
228
+ console.error("[System]", reason);
229
+ });
230
+ process.on("uncaughtException", (err) => {
231
+ console.error("[System]", err);
232
+ });
233
+ }
234
+ const port = options.port || process.env.BACKEND_PORT || process.env.PORT || 3e3;
235
+ const host = options.host || "0.0.0.0";
236
+ let httpServer = null;
237
+ return {
238
+ app,
239
+ get httpServer() {
240
+ return httpServer;
241
+ },
242
+ port,
243
+ host,
244
+ start: function(onReady) {
245
+ return new Promise((resolve, reject) => {
246
+ try {
247
+ httpServer = app.listen(port, host, async () => {
248
+ const elapsed = Date.now() - startTime;
249
+ const build = app_build_info();
250
+ if (options.banner !== false) {
251
+ printServerBanner({
252
+ port,
253
+ elapsed,
254
+ commit: build.commit,
255
+ shouldSyncDb: options.shouldSyncDb,
256
+ isSynced: options.isSynced
257
+ });
258
+ }
259
+ if (typeof options.onStart === "function") {
260
+ try {
261
+ await options.onStart({ app, httpServer, port, host, elapsed, build });
262
+ } catch (onStartErr) {
263
+ console.error("[cynx/server] onStart error:", onStartErr);
264
+ }
265
+ }
266
+ if (typeof onReady === "function") {
267
+ onReady({ app, httpServer, port, host, elapsed, build });
268
+ }
269
+ resolve({ app, httpServer, port, host, elapsed, build });
270
+ });
271
+ httpServer.keepAliveTimeout = options.keepAliveTimeout || 65e3;
272
+ httpServer.headersTimeout = options.headersTimeout || 66e3;
273
+ } catch (listenErr) {
274
+ reject(listenErr);
275
+ }
276
+ });
277
+ },
278
+ stop: function() {
279
+ return new Promise((resolve) => {
280
+ if (httpServer) {
281
+ httpServer.close(() => resolve());
282
+ } else {
283
+ resolve();
284
+ }
285
+ });
286
+ }
287
+ };
288
+ }
289
+ var server_default = {
290
+ createServer,
291
+ createRouter,
292
+ loadRoutes,
293
+ app_commit,
294
+ app_build_info,
295
+ printServerBanner,
296
+ getNetworkIp,
297
+ execGit
298
+ };
299
+ // Annotate the CommonJS export names for ESM import in node:
300
+ 0 && (module.exports = {
301
+ app_build_info,
302
+ app_commit,
303
+ createRouter,
304
+ createServer,
305
+ execGit,
306
+ getNetworkIp,
307
+ loadRoutes,
308
+ printServerBanner
309
+ });
@@ -0,0 +1,369 @@
1
+ /**
2
+ * cynx/server — Universal Microservice & HTTP Server Engine (server.js)
3
+ *
4
+ * Provides a standardized, zero-boilerplate Node.js / Express server runner:
5
+ * - Automatic route discovery and declarative mounting from routes directory
6
+ * - Standardized start-up banner with commit info, timing, and network IP
7
+ * - Git commit & build info extraction (app_commit, app_build_info)
8
+ * - Preconfigured middleware: CORS, Compression, Body parsers with limits
9
+ * - Lifecycle hooks (beforeRoutes, beforeErrorHandler, onStart)
10
+ * - Resilience settings (keepAliveTimeout, headersTimeout, process error catchers)
11
+ * - Built-in /api/health endpoint
12
+ *
13
+ * STRICT NO EMOJIS!
14
+ */
15
+
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+ import os from 'os';
19
+ import http from 'http';
20
+ import { execSync } from 'child_process';
21
+
22
+ let cachedCommit = null;
23
+ let cachedBuildInfo = null;
24
+ const bootTime = new Date().toISOString();
25
+
26
+ /**
27
+ * Execute git command safely with timeout.
28
+ */
29
+ export function execGit(cmd, cwd) {
30
+ try {
31
+ const stdout = execSync(cmd, {
32
+ cwd: cwd || process.cwd(),
33
+ stdio: ['ignore', 'pipe', 'ignore'],
34
+ timeout: 2000,
35
+ encoding: 'utf8',
36
+ });
37
+ return stdout ? stdout.trim() : '';
38
+ } catch {
39
+ return '';
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Get current git commit hash (short 7-char).
45
+ */
46
+ export function app_commit(cwd) {
47
+ if (cachedCommit !== null) {
48
+ return cachedCommit;
49
+ }
50
+
51
+ if (process.env.APP_COMMIT && process.env.APP_COMMIT.trim()) {
52
+ cachedCommit = process.env.APP_COMMIT.trim();
53
+ return cachedCommit;
54
+ }
55
+
56
+ if (process.env.GIT_COMMIT && process.env.GIT_COMMIT.trim()) {
57
+ cachedCommit = process.env.GIT_COMMIT.trim().slice(0, 7);
58
+ return cachedCommit;
59
+ }
60
+
61
+ cachedCommit = execGit('git rev-parse --short HEAD', cwd);
62
+ return cachedCommit;
63
+ }
64
+
65
+ /**
66
+ * Get application build and git metadata.
67
+ */
68
+ export function app_build_info(cwd) {
69
+ if (cachedBuildInfo !== null) {
70
+ return cachedBuildInfo;
71
+ }
72
+
73
+ const commit = app_commit(cwd);
74
+
75
+ const branch =
76
+ (process.env.APP_BRANCH && process.env.APP_BRANCH.trim()) ||
77
+ (process.env.GIT_BRANCH && process.env.GIT_BRANCH.trim()) ||
78
+ execGit('git rev-parse --abbrev-ref HEAD', cwd) ||
79
+ 'main';
80
+
81
+ const rawDate =
82
+ (process.env.APP_COMMIT_DATE && process.env.APP_COMMIT_DATE.trim()) ||
83
+ execGit('git log -1 --format=%ci', cwd);
84
+ const commitDate = rawDate ? rawDate.slice(0, 16) : '';
85
+
86
+ const buildTime =
87
+ (process.env.BUILD_TIME && process.env.BUILD_TIME.trim()) ||
88
+ bootTime;
89
+
90
+ cachedBuildInfo = {
91
+ commit,
92
+ branch,
93
+ commitDate,
94
+ buildTime,
95
+ };
96
+
97
+ return cachedBuildInfo;
98
+ }
99
+
100
+ /**
101
+ * Resolve local network IPv4 address.
102
+ */
103
+ export function getNetworkIp() {
104
+ try {
105
+ const interfaces = os.networkInterfaces();
106
+ for (const name of Object.keys(interfaces)) {
107
+ for (const iface of interfaces[name]) {
108
+ if (iface.family === 'IPv4' && !iface.internal) {
109
+ return iface.address;
110
+ }
111
+ }
112
+ }
113
+ } catch {
114
+ // fallback
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Print standardized start-up banner.
121
+ */
122
+ export function printServerBanner({
123
+ port,
124
+ elapsed,
125
+ commit,
126
+ shouldSyncDb = false,
127
+ isSynced = false,
128
+ networkIp = null,
129
+ }) {
130
+ const commitTag = commit ? ' \x1b[32m' + commit + '\x1b[0m' : '';
131
+ const resolvedIp = networkIp !== undefined ? networkIp : getNetworkIp();
132
+
133
+ console.log('\n \x1b[32m\x1b[1mSERVER\x1b[0m' + commitTag + ' \x1b[90mready in ' + elapsed + ' ms\x1b[0m\n');
134
+ console.log(' \x1b[32m➜\x1b[0m \x1b[1mLocal:\x1b[0m \x1b[36mhttp://localhost:' + port + '/\x1b[0m');
135
+ if (resolvedIp) {
136
+ console.log(' \x1b[32m➜\x1b[0m \x1b[1mNetwork:\x1b[0m \x1b[36mhttp://' + resolvedIp + ':' + port + '/\x1b[0m');
137
+ }
138
+ const dbStatus = isSynced
139
+ ? '\x1b[32mSynced\x1b[0m'
140
+ : '\x1b[90mSkipped (pass -s to sync)\x1b[0m';
141
+ console.log(' \x1b[32m➜\x1b[0m \x1b[1mDatabase:\x1b[0m ' + dbStatus + '\n');
142
+ }
143
+
144
+ /**
145
+ * Helper to create a router with route metadata.
146
+ */
147
+ export function createRouter(options = {}) {
148
+ const expressModule = options.express || (typeof require !== 'undefined' ? require('express') : null);
149
+ if (!expressModule) {
150
+ throw new Error('[cynx/server] express is required to use createRouter');
151
+ }
152
+
153
+ const router = expressModule.Router(options.routerOptions || {});
154
+ if (options.prefix) {
155
+ router._cynxPrefix = options.prefix;
156
+ }
157
+ return router;
158
+ }
159
+
160
+ /**
161
+ * Auto-discover and mount routes from a directory.
162
+ */
163
+ export function loadRoutes(app, routesDir, options = {}) {
164
+ if (!routesDir || !fs.existsSync(routesDir)) {
165
+ return [];
166
+ }
167
+
168
+ const customPrefixes = options.routePrefixes || {};
169
+ const mounted = [];
170
+ const files = fs.readdirSync(routesDir);
171
+
172
+ for (const file of files) {
173
+ const ext = path.extname(file);
174
+ if (!['.js', '.cjs', '.mjs'].includes(ext)) continue;
175
+ if (file.startsWith('.') || file.startsWith('_')) continue;
176
+
177
+ const baseName = path.basename(file, ext);
178
+ const fullPath = path.resolve(routesDir, file);
179
+
180
+ let routeModule;
181
+ try {
182
+ routeModule = typeof require !== 'undefined' ? require(fullPath) : null;
183
+ } catch (err) {
184
+ console.error(`[cynx/server] Error loading route file ${file}:`, err);
185
+ continue;
186
+ }
187
+
188
+ if (!routeModule) continue;
189
+
190
+ const router = routeModule.default || routeModule.router || routeModule;
191
+ if (typeof router !== 'function' && (!router || typeof router.use !== 'function')) {
192
+ continue;
193
+ }
194
+
195
+ // Determine mount prefix(es)
196
+ let prefixes = router._cynxPrefix || router.prefix || customPrefixes[baseName];
197
+ if (!prefixes) {
198
+ prefixes = `/api/${baseName}`;
199
+ }
200
+
201
+ const prefixList = Array.isArray(prefixes) ? prefixes : [prefixes];
202
+ for (const prefix of prefixList) {
203
+ app.use(prefix, router);
204
+ mounted.push({ file, prefix });
205
+ }
206
+ }
207
+
208
+ return mounted;
209
+ }
210
+
211
+ /**
212
+ * Create and configure standardized Express application and HTTP server.
213
+ */
214
+ export function createServer(options = {}) {
215
+ const expressModule = options.express || (typeof require !== 'undefined' ? require('express') : null);
216
+ if (!expressModule) {
217
+ throw new Error('[cynx/server] express is required to create a server');
218
+ }
219
+
220
+ const app = options.app || expressModule();
221
+ const startTime = Date.now();
222
+
223
+ // 1. CORS
224
+ if (options.cors !== false) {
225
+ const corsModule = options.corsMiddleware || (typeof require !== 'undefined' ? require('cors') : null);
226
+ if (corsModule) {
227
+ app.use(typeof options.cors === 'object' ? corsModule(options.cors) : corsModule());
228
+ }
229
+ }
230
+
231
+ // 2. Compression
232
+ if (options.compression) {
233
+ if (typeof options.compression === 'function') {
234
+ app.use(options.compression);
235
+ } else {
236
+ const compModule = typeof require !== 'undefined' ? require('compression') : null;
237
+ if (compModule) app.use(compModule());
238
+ }
239
+ }
240
+
241
+ // 3. Body Parsers
242
+ const limit = options.bodyLimit || '5mb';
243
+ app.use(expressModule.json({ limit }));
244
+ app.use(expressModule.urlencoded({ extended: true, limit }));
245
+
246
+ // 4. Hook before routes (custom loggers, metrics, etc.)
247
+ if (typeof options.beforeRoutes === 'function') {
248
+ options.beforeRoutes(app);
249
+ }
250
+
251
+ // 5. Auto-mount routes
252
+ if (options.routesDir) {
253
+ loadRoutes(app, options.routesDir, options);
254
+ }
255
+
256
+ // 6. Explicit routes list
257
+ if (Array.isArray(options.routes)) {
258
+ for (const route of options.routes) {
259
+ if (route.prefix && route.router) {
260
+ const list = Array.isArray(route.prefix) ? route.prefix : [route.prefix];
261
+ for (const p of list) {
262
+ app.use(p, route.router);
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ // 7. Built-in healthcheck
269
+ if (options.healthcheck !== false) {
270
+ app.get('/api/health', (_req, res) => res.json({ status: 'ok' }));
271
+ }
272
+
273
+ // 8. Hook before error handler (e.g. Sentry.setupExpressErrorHandler)
274
+ if (typeof options.beforeErrorHandler === 'function') {
275
+ options.beforeErrorHandler(app);
276
+ }
277
+
278
+ // 9. Standardized JSON Error Handler
279
+ if (options.errorHandler !== false) {
280
+ app.use((err, _req, res, _next) => {
281
+ console.error('[HTTP]', err);
282
+ res.status(err.status || 500).json({ error: err.message || 'Internal server error' });
283
+ });
284
+ }
285
+
286
+ // 10. Process level unhandled error safety
287
+ if (options.processHandlers !== false) {
288
+ process.on('unhandledRejection', (reason) => {
289
+ console.error('[System]', reason);
290
+ });
291
+
292
+ process.on('uncaughtException', (err) => {
293
+ console.error('[System]', err);
294
+ });
295
+ }
296
+
297
+ const port = options.port || process.env.BACKEND_PORT || process.env.PORT || 3000;
298
+ const host = options.host || '0.0.0.0';
299
+ let httpServer = null;
300
+
301
+ return {
302
+ app,
303
+ get httpServer() {
304
+ return httpServer;
305
+ },
306
+ port,
307
+ host,
308
+ start: function (onReady) {
309
+ return new Promise((resolve, reject) => {
310
+ try {
311
+ httpServer = app.listen(port, host, async () => {
312
+ const elapsed = Date.now() - startTime;
313
+ const build = app_build_info();
314
+
315
+ // Print banner if not disabled
316
+ if (options.banner !== false) {
317
+ printServerBanner({
318
+ port,
319
+ elapsed,
320
+ commit: build.commit,
321
+ shouldSyncDb: options.shouldSyncDb,
322
+ isSynced: options.isSynced,
323
+ });
324
+ }
325
+
326
+ if (typeof options.onStart === 'function') {
327
+ try {
328
+ await options.onStart({ app, httpServer, port, host, elapsed, build });
329
+ } catch (onStartErr) {
330
+ console.error('[cynx/server] onStart error:', onStartErr);
331
+ }
332
+ }
333
+
334
+ if (typeof onReady === 'function') {
335
+ onReady({ app, httpServer, port, host, elapsed, build });
336
+ }
337
+
338
+ resolve({ app, httpServer, port, host, elapsed, build });
339
+ });
340
+
341
+ httpServer.keepAliveTimeout = options.keepAliveTimeout || 65000;
342
+ httpServer.headersTimeout = options.headersTimeout || 66000;
343
+ } catch (listenErr) {
344
+ reject(listenErr);
345
+ }
346
+ });
347
+ },
348
+ stop: function () {
349
+ return new Promise((resolve) => {
350
+ if (httpServer) {
351
+ httpServer.close(() => resolve());
352
+ } else {
353
+ resolve();
354
+ }
355
+ });
356
+ },
357
+ };
358
+ }
359
+
360
+ export default {
361
+ createServer,
362
+ createRouter,
363
+ loadRoutes,
364
+ app_commit,
365
+ app_build_info,
366
+ printServerBanner,
367
+ getNetworkIp,
368
+ execGit,
369
+ };