@scriptdb/server 1.1.0 → 1.1.1

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/dist/index.mjs ADDED
@@ -0,0 +1,3121 @@
1
+ // src/protocal.ts
2
+ import * as net from "net";
3
+ import * as tls from "tls";
4
+ import * as fs2 from "fs";
5
+ import * as jwt from "jsonwebtoken";
6
+ import * as crypto from "crypto";
7
+ import Bottleneck from "bottleneck";
8
+ import * as path from "path";
9
+ import * as os2 from "os";
10
+ import { Worker } from "worker_threads";
11
+ import { LRUCache } from "lru-cache";
12
+
13
+ // src/utils/basePath.ts
14
+ import * as fs from "fs";
15
+ function getBasePath() {
16
+ return fs.realpathSync(".");
17
+ }
18
+ var basePath_default = getBasePath;
19
+
20
+ // src/utils/homeDir.ts
21
+ import * as os from "os";
22
+ function getHomeDir() {
23
+ return os.homedir();
24
+ }
25
+
26
+ // src/protocal.ts
27
+ var fileConfig = {};
28
+ try {
29
+ const configPath = path.join(getHomeDir(), ".scriptdb", "config.json");
30
+ if (fs2.existsSync(configPath)) {
31
+ const configContent = fs2.readFileSync(configPath, "utf8");
32
+ fileConfig = JSON.parse(configContent);
33
+ } else {
34
+ console.log("No config file found at:", configPath, "- using defaults");
35
+ }
36
+ } catch (e) {
37
+ console.error("Failed to load config:", e);
38
+ fileConfig = {};
39
+ }
40
+ var Protocal = class {
41
+ // Helper to get the effective secret for JWT operations
42
+ getEffectiveSecret() {
43
+ return this.serverSecret || "default-dev-secret-change-in-production";
44
+ }
45
+ constructor(options = {}) {
46
+ this.host = options.host || fileConfig.host || "localhost";
47
+ this.port = Number.isFinite(Number(options.port)) ? Number(options.port) : fileConfig.port || 1234;
48
+ this.secure = options.secure !== void 0 ? options.secure : fileConfig.secure !== void 0 ? fileConfig.secure : true;
49
+ if (Array.isArray(options.users)) {
50
+ this.users = options.users;
51
+ console.log("Using users from options:", this.users.length, "users");
52
+ } else if (options.users && typeof options.users === "object") {
53
+ this.users = [options.users];
54
+ console.log("Using single user from options");
55
+ } else if (Array.isArray(fileConfig.users)) {
56
+ this.users = fileConfig.users;
57
+ console.log("Using users from fileConfig:", this.users.length, "users");
58
+ } else {
59
+ this.users = [];
60
+ console.log("No users configured!");
61
+ }
62
+ if (this.users.length > 0) {
63
+ console.log("First user config:", {
64
+ username: this.users[0].username,
65
+ hasPassword: !!this.users[0].password,
66
+ hash: this.users[0].hash,
67
+ algorithm: this.users[0].algorithm
68
+ });
69
+ }
70
+ if (options.vm) {
71
+ this.vm = options.vm;
72
+ } else {
73
+ try {
74
+ const basePath = basePath_default ? basePath_default() : process.cwd();
75
+ const folder = options.folder || fileConfig.folder || "databases";
76
+ } catch (err) {
77
+ console.error(
78
+ "Failed to create internal VM:",
79
+ err && err.message ? err.message : err
80
+ );
81
+ this.vm = null;
82
+ }
83
+ }
84
+ this.serverSecret = options.serverSecret || process.env.SCRIPTDB_JWT_SECRET || fileConfig.serverSecret || null;
85
+ if (!this.serverSecret) {
86
+ console.warn(
87
+ "Protocal: No serverSecret configured. In production a serverSecret is required."
88
+ );
89
+ }
90
+ const cpuCount = os2.cpus && os2.cpus().length ? os2.cpus().length : 1;
91
+ const defaultValidatorPool = Math.max(
92
+ 0,
93
+ Math.min(2, Math.floor(cpuCount / 4))
94
+ );
95
+ this.validatorPoolSize = typeof options.validatorPoolSize === "number" ? options.validatorPoolSize : defaultValidatorPool;
96
+ this.ipLimiterGroup = new Bottleneck.Group({
97
+ maxConcurrent: 1,
98
+ minTime: 50
99
+ });
100
+ this.loginAttemptCache = /* @__PURE__ */ new Map();
101
+ this.ipAttemptCache = /* @__PURE__ */ new Map();
102
+ this.IP_FAIL_WINDOW_MS = options.ipFailWindowMs || fileConfig.ipFailWindowMs || 15 * 60 * 1e3;
103
+ this.MAX_LOGIN_ATTEMPTS = options.maxLoginAttempts || fileConfig.maxLoginAttempts || 5;
104
+ this.LOCK_DURATION_MS = options.lockDurationMs || fileConfig.lockDurationMs || 15 * 60 * 1e3;
105
+ this.ENABLE_IP_LOCKOUT = options.enableIpLockout !== void 0 ? options.enableIpLockout : fileConfig.enableIpLockout !== void 0 ? fileConfig.enableIpLockout : true;
106
+ this.MAX_MESSAGE_BYTES = options.maxMessageBytes || 64 * 1024;
107
+ this.MAX_MESSAGES_PER_CONNECTION = options.maxMessagesPerConnection || 1e3;
108
+ this.CONNECTION_TIMEOUT_MS = typeof options.connectionTimeoutMs === "number" ? options.connectionTimeoutMs : typeof fileConfig.connectionTimeoutMs === "number" ? fileConfig.connectionTimeoutMs : 0;
109
+ this.tlsOptions = options.tlsOptions || fileConfig.tlsOptions || null;
110
+ this.requireTlsInProduction = options.requireTlsInProduction !== void 0 ? options.requireTlsInProduction : true;
111
+ this.payloadSchema = options.payloadSchema || fileConfig.payloadSchema || {
112
+ command: "string",
113
+ args: "object",
114
+ code: "string",
115
+ databaseName: "string"
116
+ };
117
+ this.allowPerUserSigning = options.allowPerUserSigning || false;
118
+ this._payloadMaxDepth = options.payloadMaxDepth || 4;
119
+ this._payloadMaxNodes = options.payloadMaxNodes || 1e3;
120
+ this._payloadMaxBytes = options.payloadMaxBytes || 16 * 1024;
121
+ this._sanitizeMaxDepth = options.sanitizeMaxDepth || 6;
122
+ this._sanitizeMaxNodes = options.sanitizeMaxNodes || 2e3;
123
+ this._sanitizeMaxString = options.sanitizeMaxString || 1e3;
124
+ this._attemptCacheTTL = options.attemptCacheTTL || 60 * 60 * 1e3;
125
+ this._attemptCacheCleanupInterval = setInterval(() => {
126
+ const now = Date.now();
127
+ this.loginAttemptCache.forEach((v, k) => {
128
+ if (v.expiresAt && v.expiresAt <= now) this.loginAttemptCache.delete(k);
129
+ });
130
+ this.ipAttemptCache.forEach((v, k) => {
131
+ if (v.expiresAt && v.expiresAt <= now) this.ipAttemptCache.delete(k);
132
+ });
133
+ }, Math.max(30 * 1e3, this._attemptCacheTTL / 4));
134
+ this.audit = function(event, meta) {
135
+ try {
136
+ const safeMeta = Object.assign({}, meta || {});
137
+ if (safeMeta.token) safeMeta.token = "[REDACTED]";
138
+ if (safeMeta.password) safeMeta.password = "[REDACTED]";
139
+ if (safeMeta.passwordHash) safeMeta.passwordHash = "[REDACTED]";
140
+ if (safeMeta.signingSecret) safeMeta.signingSecret = "[REDACTED]";
141
+ console.warn(
142
+ JSON.stringify({
143
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
144
+ event,
145
+ meta: safeMeta
146
+ })
147
+ );
148
+ } catch (e) {
149
+ }
150
+ };
151
+ this._bcryptWorkers = [];
152
+ this._bcryptNext = 0;
153
+ this._bcryptPending = /* @__PURE__ */ new Map();
154
+ const defaultBcrypt = Math.max(1, Math.min(4, Math.floor(cpuCount / 2)));
155
+ this.bcryptPoolSize = typeof options.bcryptPoolSize === "number" ? options.bcryptPoolSize : defaultBcrypt;
156
+ this.bcryptMaxPoolSize = options.bcryptMaxPoolSize || Math.max(4, this.bcryptPoolSize * 4);
157
+ for (let i = 0; i < this.bcryptPoolSize; i++) {
158
+ const w = new Worker(path.join(__dirname, "workers", "bcrypt-worker.js"));
159
+ w.on("message", (m) => {
160
+ const p = this._bcryptPending.get(m.id);
161
+ if (p) {
162
+ const start = p.start || 0;
163
+ const lat = start ? Date.now() - start : 0;
164
+ if (lat > 0)
165
+ this._recordLatency(
166
+ this._bcryptLatencies,
167
+ lat,
168
+ this._bcryptLatencyWindow
169
+ );
170
+ clearTimeout(p.timer);
171
+ this._bcryptPending.delete(m.id);
172
+ if (m.error) {
173
+ this._bcryptFailures.push(Date.now());
174
+ p.reject(new Error(m.error));
175
+ } else {
176
+ p.resolve(m.ok);
177
+ }
178
+ }
179
+ try {
180
+ this._autoscalePools();
181
+ } catch (e) {
182
+ }
183
+ });
184
+ w.on("error", (e) => {
185
+ console.error("bcrypt worker error", e && e.message);
186
+ this._bcryptFailures.push(Date.now());
187
+ try {
188
+ this._autoscalePools();
189
+ } catch (e2) {
190
+ }
191
+ });
192
+ this._bcryptWorkers.push(w);
193
+ }
194
+ this._bcryptLatencies = [];
195
+ this._bcryptLatencyWindow = options.bcryptLatencyWindow || 100;
196
+ this._bcryptFailures = [];
197
+ this._bcryptFailureWindowMs = options.bcryptFailureWindowMs || 60 * 1e3;
198
+ this._bcryptFailureThreshold = options.bcryptFailureThreshold || 5;
199
+ this._bcryptCircuitCooldownMs = options.bcryptCircuitCooldownMs || 2 * 60 * 1e3;
200
+ this._bcryptTrippedUntil = 0;
201
+ this._tokenVerifyCache = new LRUCache({ max: 1e3, ttl: 1e3 * 60 * 5 });
202
+ this.storage = options.storage || null;
203
+ this.alertPendingThreshold = typeof options.alertPendingThreshold === "number" ? options.alertPendingThreshold : 50;
204
+ this._vmWorkers = [];
205
+ this._vmNext = 0;
206
+ this._vmPending = /* @__PURE__ */ new Map();
207
+ this._vmLatencies = [];
208
+ this._vmLatencyWindow = options.vmLatencyWindow || 100;
209
+ this._vmFailures = [];
210
+ this._vmFailureWindowMs = options.vmFailureWindowMs || 60 * 1e3;
211
+ this._vmFailureThreshold = options.vmFailureThreshold || 3;
212
+ this._vmCircuitCooldownMs = options.vmCircuitCooldownMs || 2 * 60 * 1e3;
213
+ this._vmTrippedUntil = 0;
214
+ if (options.vmWorkerPoolSize && options.vmWorkerPoolSize > 0) {
215
+ const defaultVmPool = Math.max(
216
+ 0,
217
+ Math.min(2, Math.max(1, Math.floor(cpuCount / 2)) - 1)
218
+ );
219
+ const vmPool = typeof options.vmWorkerPoolSize === "number" ? options.vmWorkerPoolSize : defaultVmPool;
220
+ for (let i = 0; i < vmPool; i++) {
221
+ const w = new Worker(path.join(__dirname, "vm-worker.js"));
222
+ w.postMessage({
223
+ id: `init-${i}`,
224
+ action: "init",
225
+ options: {
226
+ baseDir: options.folder ? path.join(basePath_default ? basePath_default() : process.cwd(), options.folder) : void 0
227
+ }
228
+ });
229
+ w.on("message", (m) => {
230
+ const p = this._vmPending.get(m.id);
231
+ if (p) {
232
+ const start = p.start || 0;
233
+ const lat = start ? Date.now() - start : 0;
234
+ if (lat > 0)
235
+ this._recordLatency(
236
+ this._vmLatencies,
237
+ lat,
238
+ this._vmLatencyWindow
239
+ );
240
+ clearTimeout(p.timer);
241
+ this._vmPending.delete(m.id);
242
+ if (m.error) {
243
+ this._vmFailures.push(Date.now());
244
+ p.reject(new Error(m.error));
245
+ } else {
246
+ p.resolve(m.res);
247
+ }
248
+ }
249
+ try {
250
+ this._autoscalePools();
251
+ } catch (e) {
252
+ }
253
+ });
254
+ w.on("error", (e) => {
255
+ console.error("vm worker error", e && e.message);
256
+ this._vmFailures.push(Date.now());
257
+ try {
258
+ this._autoscalePools();
259
+ } catch (e2) {
260
+ }
261
+ });
262
+ this._vmWorkers.push(w);
263
+ }
264
+ }
265
+ this._validatorWorkers = [];
266
+ this._validatorNext = 0;
267
+ this._validatorPending = /* @__PURE__ */ new Map();
268
+ const validatorPoolSize = options.validatorPoolSize || 0;
269
+ this.validatorPoolSize = validatorPoolSize;
270
+ this._validatorLatencies = [];
271
+ this._validatorLatencyWindow = options.validatorLatencyWindow || 100;
272
+ this._validatorFailures = [];
273
+ this._validatorFailureWindowMs = options.validatorFailureWindowMs || 60 * 1e3;
274
+ this._validatorFailureThreshold = options.validatorFailureThreshold || 5;
275
+ this._validatorCircuitCooldownMs = options.validatorCircuitCooldownMs || 2 * 60 * 1e3;
276
+ this._validatorTrippedUntil = 0;
277
+ if (validatorPoolSize > 0) {
278
+ for (let i = 0; i < validatorPoolSize; i++) {
279
+ const vw = new Worker(path.join(__dirname, "validator-worker.js"));
280
+ vw.on("message", (m) => {
281
+ const p = this._validatorPending.get(m.id);
282
+ if (p) {
283
+ const start = p.start || 0;
284
+ const lat = start ? Date.now() - start : 0;
285
+ if (lat > 0)
286
+ this._recordLatency(
287
+ this._validatorLatencies,
288
+ lat,
289
+ this._validatorLatencyWindow
290
+ );
291
+ clearTimeout(p.timer);
292
+ this._validatorPending.delete(m.id);
293
+ if (m.error) {
294
+ this._validatorFailures.push(Date.now());
295
+ p.reject(new Error(m.error));
296
+ } else {
297
+ p.resolve(m.res);
298
+ }
299
+ }
300
+ });
301
+ vw.on("error", (e) => {
302
+ console.error("validator worker error", e && e.message);
303
+ this._validatorFailures.push(Date.now());
304
+ });
305
+ this._validatorWorkers.push(vw);
306
+ }
307
+ }
308
+ this.metrics = {
309
+ bcryptPending: () => this._bcryptPending.size,
310
+ vmPending: () => this._vmPending.size,
311
+ vmWorkers: () => this._vmWorkers.length,
312
+ activeConnections: 0,
313
+ validatorPending: () => this._validatorPending.size,
314
+ validatorWorkers: () => this._validatorWorkers.length,
315
+ bcryptAvgLatency: () => {
316
+ const a = this._bcryptLatencies;
317
+ return a.length ? a.reduce((s, v) => s + v, 0) / a.length : 0;
318
+ },
319
+ vmAvgLatency: () => {
320
+ const a = this._vmLatencies;
321
+ return a.length ? a.reduce((s, v) => s + v, 0) / a.length : 0;
322
+ },
323
+ validatorAvgLatency: () => {
324
+ const a = this._validatorLatencies;
325
+ return a.length ? a.reduce((s, v) => s + v, 0) / a.length : 0;
326
+ },
327
+ compactActive: () => this.storage && this.storage._compactActive ? this.storage._compactActive : 0,
328
+ compactTripped: () => this.storage && this.storage._compactTrippedUntil && Date.now() < this.storage._compactTrippedUntil ? true : false
329
+ };
330
+ this.maxConnections = options.maxConnections || 1e3;
331
+ this.backlog = options.backlog || 128;
332
+ this.bcryptWorkerTimeoutMs = options.bcryptWorkerTimeoutMs || 5e3;
333
+ this.vmWorkerTimeoutMs = options.vmWorkerTimeoutMs || (options.runTimeoutMs ? options.runTimeoutMs + 500 : 5e3);
334
+ this._autoscaleIntervalMs = options.autoscaleIntervalMs || 2e3;
335
+ this._autoscaleTimer = null;
336
+ }
337
+ // simple helper to record latency into a fixed window
338
+ _recordLatency(arr, val, cap) {
339
+ try {
340
+ arr.push(val);
341
+ if (arr.length > cap) arr.shift();
342
+ } catch (e) {
343
+ }
344
+ }
345
+ // check and perform simple autoscaling for bcrypt pool
346
+ _autoscalePools() {
347
+ try {
348
+ const pending = this._bcryptPending.size;
349
+ const workers = this._bcryptWorkers.length;
350
+ if (pending > workers * 2 && workers < this.bcryptMaxPoolSize) {
351
+ const w = new Worker(path.join(__dirname, "workers", "bcrypt-worker.js"));
352
+ w.on("message", (m) => {
353
+ const p = this._bcryptPending.get(m.id);
354
+ if (p) {
355
+ clearTimeout(p.timer);
356
+ this._bcryptPending.delete(m.id);
357
+ if (m.error) p.reject(new Error(m.error));
358
+ else p.resolve(m.ok);
359
+ }
360
+ });
361
+ w.on("error", (e) => {
362
+ console.error("bcrypt worker error (autoscale)", e && e.message);
363
+ });
364
+ this._bcryptWorkers.push(w);
365
+ }
366
+ if (pending > Math.max(this.alertPendingThreshold, workers * 5)) {
367
+ try {
368
+ this.audit("queue.high", { type: "bcrypt", pending, workers });
369
+ } catch (e) {
370
+ }
371
+ }
372
+ const vmPending = this._vmPending.size;
373
+ const vmWorkers = this._vmWorkers.length;
374
+ const vmMax = Math.max(
375
+ this._vmWorkers.length,
376
+ this.vmMaxPoolSize || this._vmWorkers.length + 4
377
+ );
378
+ if (vmPending > vmWorkers * 2 && vmWorkers < vmMax) {
379
+ const w = new Worker(path.join(__dirname, "vm-worker.js"));
380
+ w.postMessage({
381
+ id: `init-auto-${Date.now()}`,
382
+ action: "init",
383
+ options: { baseDir: void 0 }
384
+ });
385
+ w.on("message", (m) => {
386
+ const p = this._vmPending.get(m.id);
387
+ if (p) {
388
+ clearTimeout(p.timer);
389
+ this._vmPending.delete(m.id);
390
+ if (m.error) p.reject(new Error(m.error));
391
+ else p.resolve(m.res);
392
+ }
393
+ });
394
+ w.on("error", (e) => {
395
+ console.error("vm worker error (autoscale)", e && e.message);
396
+ });
397
+ this._vmWorkers.push(w);
398
+ }
399
+ if (vmPending > Math.max(this.alertPendingThreshold, vmWorkers * 5)) {
400
+ try {
401
+ this.audit("queue.high", {
402
+ type: "vm",
403
+ pending: vmPending,
404
+ workers: vmWorkers
405
+ });
406
+ } catch (e) {
407
+ }
408
+ }
409
+ } catch (e) {
410
+ }
411
+ }
412
+ start() {
413
+ const createListener = async (socket) => {
414
+ if (this.metrics.activeConnections >= this.maxConnections) {
415
+ try {
416
+ socket.end();
417
+ } catch (e) {
418
+ }
419
+ return;
420
+ }
421
+ this.metrics.activeConnections++;
422
+ console.log("Client connected");
423
+ socket.on("error", (err) => {
424
+ console.error("Socket error:", err && err.message ? err.message : err);
425
+ });
426
+ socket.on("close", (hadError) => {
427
+ console.log("Socket closed", hadError ? "(due to error)" : "");
428
+ this.metrics.activeConnections = Math.max(
429
+ 0,
430
+ this.metrics.activeConnections - 1
431
+ );
432
+ });
433
+ let recvBuf = "";
434
+ let messagesReceived = 0;
435
+ socket.setTimeout(this.CONNECTION_TIMEOUT_MS);
436
+ socket.on("timeout", () => {
437
+ try {
438
+ sendWithBackpressure({ command: "error", message: "TIMEOUT" });
439
+ } catch (e) {
440
+ }
441
+ socket.end();
442
+ });
443
+ const sendWithBackpressure = (obj) => {
444
+ try {
445
+ const s = JSON.stringify(obj) + "\n";
446
+ console.log("sendWithBackpressure: sending message to socket:", socket.remoteAddress, socket.remotePort, ":", s.trim());
447
+ const ok = socket.write(s);
448
+ console.log("sendWithBackpressure: write returned:", ok);
449
+ if (!ok) {
450
+ socket.pause();
451
+ socket.once("drain", () => socket.resume());
452
+ }
453
+ } catch (e) {
454
+ console.error("sendWithBackpressure error:", e);
455
+ }
456
+ };
457
+ const sendCritical = (obj) => {
458
+ return new Promise((resolve2, reject) => {
459
+ try {
460
+ const s = JSON.stringify(obj) + "\n";
461
+ console.log("sendCritical writing:", s.trim());
462
+ socket.write(s, (err) => {
463
+ if (err) {
464
+ console.error("sendCritical write error:", err);
465
+ return reject(err);
466
+ }
467
+ console.log("sendCritical write callback called, waiting 50ms...");
468
+ setTimeout(() => {
469
+ console.log("sendCritical resolved after 50ms");
470
+ resolve2();
471
+ }, 50);
472
+ });
473
+ } catch (e) {
474
+ console.error("sendCritical exception:", e);
475
+ reject(e);
476
+ }
477
+ });
478
+ };
479
+ socket.on("data", async (data) => {
480
+ if (data.length > this.MAX_MESSAGE_BYTES) {
481
+ try {
482
+ sendWithBackpressure({
483
+ command: "error",
484
+ message: "MSG_TOO_LARGE"
485
+ });
486
+ } catch (e) {
487
+ }
488
+ socket.end();
489
+ return;
490
+ }
491
+ recvBuf += data.toString();
492
+ let idx;
493
+ while ((idx = recvBuf.indexOf("\n")) !== -1) {
494
+ if (++messagesReceived > this.MAX_MESSAGES_PER_CONNECTION) {
495
+ try {
496
+ sendWithBackpressure({
497
+ command: "error",
498
+ message: "TOO_MANY_MESSAGES"
499
+ });
500
+ } catch (e) {
501
+ }
502
+ socket.end();
503
+ return;
504
+ }
505
+ const line = recvBuf.slice(0, idx).trim();
506
+ recvBuf = recvBuf.slice(idx + 1);
507
+ if (!line) continue;
508
+ let object;
509
+ try {
510
+ object = JSON.parse(line);
511
+ } catch (err) {
512
+ console.error(
513
+ "Invalid JSON from client:",
514
+ err && err.message ? err.message : err
515
+ );
516
+ try {
517
+ sendWithBackpressure({
518
+ command: "error",
519
+ message: "INVALID_JSON"
520
+ });
521
+ } catch (e) {
522
+ }
523
+ continue;
524
+ }
525
+ const commandOrAction = object.command || object.action;
526
+ switch (commandOrAction) {
527
+ case "login":
528
+ const username = object.data?.username || "";
529
+ const password = object.data?.password || "";
530
+ console.log("Login attempt:", { username, hasPassword: !!password, objectId: object.id, secure: this.secure });
531
+ if (!this.secure) {
532
+ console.log("Secure mode disabled - allowing anonymous access");
533
+ const secretToUse = this.getEffectiveSecret();
534
+ const token2 = jwt.sign(
535
+ { username: username || "", role: "anonymous" },
536
+ secretToUse,
537
+ { expiresIn: "24h" }
538
+ );
539
+ try {
540
+ sendWithBackpressure({
541
+ id: object.id,
542
+ action: "login",
543
+ command: "login",
544
+ message: "AUTH OK",
545
+ data: { token: token2 }
546
+ });
547
+ console.log("Anonymous AUTH OK sent (secure=false)");
548
+ } catch (e) {
549
+ console.error("Error sending AUTH OK:", e);
550
+ }
551
+ break;
552
+ }
553
+ if (!username || !password) {
554
+ console.log("Secure mode enabled - credentials required but not provided");
555
+ try {
556
+ await sendCritical({
557
+ id: object.id,
558
+ action: "login",
559
+ command: "login",
560
+ message: "AUTH FAIL",
561
+ data: "Username and password are required in secure mode"
562
+ });
563
+ console.log("AUTH FAIL message sent and flushed, now closing socket...");
564
+ socket.end();
565
+ console.log("Socket closed after AUTH FAIL");
566
+ } catch (e) {
567
+ console.error("Error sending AUTH FAIL:", e);
568
+ socket.end();
569
+ }
570
+ return;
571
+ }
572
+ const remoteIP = socket.remoteAddress || socket.remoteAddress || "unknown";
573
+ try {
574
+ const ipLimiter = this.ipLimiterGroup.key(remoteIP);
575
+ await ipLimiter.schedule(() => Promise.resolve());
576
+ } catch (e) {
577
+ this.audit("limiter.schedule.error", {
578
+ ip: remoteIP,
579
+ err: e && e.message || String(e)
580
+ });
581
+ }
582
+ if (this.ENABLE_IP_LOCKOUT) {
583
+ const nowTs = Date.now();
584
+ const ipRec = this.ipAttemptCache.get(remoteIP) || {
585
+ attempts: 0,
586
+ lockedUntil: 0,
587
+ expiresAt: nowTs + this._attemptCacheTTL
588
+ };
589
+ ipRec.attempts = (ipRec.attempts || 0) + 1;
590
+ ipRec.expiresAt = nowTs + this._attemptCacheTTL;
591
+ if (ipRec.attempts >= this.MAX_LOGIN_ATTEMPTS) {
592
+ ipRec.lockedUntil = nowTs + this.LOCK_DURATION_MS;
593
+ this.audit("ip.lockout", {
594
+ ip: remoteIP,
595
+ attempts: ipRec.attempts
596
+ });
597
+ }
598
+ this.ipAttemptCache.set(remoteIP, ipRec);
599
+ if (ipRec.lockedUntil && ipRec.lockedUntil > nowTs) {
600
+ this.audit("ip.locked", { ip: remoteIP });
601
+ try {
602
+ sendWithBackpressure({
603
+ command: "login",
604
+ message: "LOCKED_IP",
605
+ data: null
606
+ });
607
+ } catch (e) {
608
+ }
609
+ break;
610
+ }
611
+ }
612
+ const now = Date.now();
613
+ const record = this.loginAttemptCache.get(username) || {
614
+ attempts: 0,
615
+ lockedUntil: 0,
616
+ expiresAt: now + this._attemptCacheTTL
617
+ };
618
+ if (record.lockedUntil && record.lockedUntil > now) {
619
+ try {
620
+ sendWithBackpressure({
621
+ command: "login",
622
+ message: "LOCKED",
623
+ data: null
624
+ });
625
+ } catch (e) {
626
+ }
627
+ break;
628
+ }
629
+ const user = this.users.find((u) => u.username === username);
630
+ if (!user) {
631
+ record.attempts = (record.attempts || 0) + 1;
632
+ record.expiresAt = Date.now() + this._attemptCacheTTL;
633
+ if (record.attempts >= this.MAX_LOGIN_ATTEMPTS) {
634
+ record.lockedUntil = now + this.LOCK_DURATION_MS;
635
+ this.audit("login.lockout", { user: username, ip: remoteIP });
636
+ }
637
+ this.loginAttemptCache.set(username, record);
638
+ try {
639
+ sendWithBackpressure({
640
+ id: object.id,
641
+ action: "login",
642
+ command: "login",
643
+ message: "AUTH FAIL",
644
+ data: null
645
+ });
646
+ } catch (e) {
647
+ }
648
+ break;
649
+ }
650
+ console.log("User found:", {
651
+ username: user.username,
652
+ hasPassword: !!user.password,
653
+ passwordValue: user.password,
654
+ hash: user.hash,
655
+ algorithm: user.algorithm
656
+ });
657
+ console.log("Password received:", password);
658
+ let matches = false;
659
+ if (user.hash === false && user.password) {
660
+ console.log("Using plain password authentication (hash: false)");
661
+ matches = user.password === password;
662
+ } else if (user.hash === true && user.password) {
663
+ console.log("Using password hashing with algorithm:", user.algorithm || "bcrypt");
664
+ const algorithm = user.algorithm || "bcrypt";
665
+ if (algorithm === "bcrypt") {
666
+ const doBcryptCompare = (password2, hash) => new Promise((resolve2, reject) => {
667
+ if (!this._bcryptWorkers || this._bcryptWorkers.length === 0)
668
+ return resolve2(false);
669
+ const id = Math.random().toString(36).slice(2);
670
+ const wIndex = this._bcryptNext++ % this._bcryptWorkers.length;
671
+ const start = Date.now();
672
+ const timer = setTimeout(() => {
673
+ const p = this._bcryptPending.get(id);
674
+ if (p) {
675
+ this._bcryptPending.delete(id);
676
+ p.reject(new Error("bcrypt worker timeout"));
677
+ }
678
+ try {
679
+ this._bcryptWorkers[wIndex].terminate();
680
+ } catch (e) {
681
+ }
682
+ const nw = new Worker(
683
+ path.join(__dirname, "workers", "bcrypt-worker.js")
684
+ );
685
+ this._bcryptWorkers[wIndex] = nw;
686
+ }, this.bcryptWorkerTimeoutMs);
687
+ this._bcryptPending.set(id, {
688
+ resolve: resolve2,
689
+ reject,
690
+ timer,
691
+ start
692
+ });
693
+ try {
694
+ this._bcryptWorkers[wIndex].postMessage({
695
+ id,
696
+ password: password2,
697
+ hash
698
+ });
699
+ } catch (e) {
700
+ clearTimeout(timer);
701
+ this._bcryptPending.delete(id);
702
+ return resolve2(false);
703
+ }
704
+ });
705
+ try {
706
+ matches = user.password === password;
707
+ } catch (e) {
708
+ matches = false;
709
+ }
710
+ } else if (algorithm === "sha256" || algorithm === "sha512") {
711
+ const hashedPassword = crypto.createHash(algorithm).update(password).digest("hex");
712
+ matches = user.password === hashedPassword;
713
+ }
714
+ }
715
+ if (!matches) {
716
+ record.attempts = (record.attempts || 0) + 1;
717
+ record.expiresAt = Date.now() + this._attemptCacheTTL;
718
+ if (record.attempts >= this.MAX_LOGIN_ATTEMPTS) {
719
+ record.lockedUntil = now + this.LOCK_DURATION_MS;
720
+ this.audit("login.lockout", { user: username, ip: remoteIP });
721
+ }
722
+ this.loginAttemptCache.set(username, record);
723
+ try {
724
+ sendWithBackpressure({
725
+ id: object.id,
726
+ action: "login",
727
+ command: "login",
728
+ message: "AUTH FAIL",
729
+ data: null
730
+ });
731
+ } catch (e) {
732
+ }
733
+ break;
734
+ }
735
+ this.loginAttemptCache.delete(username);
736
+ let token;
737
+ try {
738
+ token = jwt.sign({ username: user.username }, this.getEffectiveSecret(), {
739
+ expiresIn: "1d"
740
+ });
741
+ } catch (e) {
742
+ try {
743
+ sendWithBackpressure({
744
+ id: object.id,
745
+ action: "login",
746
+ command: "login",
747
+ message: "ERROR",
748
+ data: { error: e && e.message || String(e) }
749
+ });
750
+ } catch (e2) {
751
+ }
752
+ break;
753
+ }
754
+ try {
755
+ sendWithBackpressure({
756
+ id: object.id,
757
+ action: "login",
758
+ command: "login",
759
+ message: "AUTH OK",
760
+ data: { token }
761
+ });
762
+ } catch (e) {
763
+ }
764
+ break;
765
+ case "script-code":
766
+ console.log("script-code command", object);
767
+ try {
768
+ const token2 = object.token || object.data && object.data.token;
769
+ if (!token2) throw new Error("Missing token");
770
+ if (typeof token2 === "string" && Buffer.byteLength(token2, "utf8") > 4096)
771
+ throw new Error("Token too large");
772
+ const decodedUnverified = jwt.decode(token2);
773
+ if (!decodedUnverified)
774
+ throw new Error("Invalid token payload");
775
+ const username2 = decodedUnverified.username || (decodedUnverified.role === "anonymous" ? "" : null);
776
+ if (username2 === null)
777
+ throw new Error("Invalid token payload");
778
+ const userRecord = this.users.find(
779
+ (u) => u.username === username2
780
+ );
781
+ if (!userRecord) {
782
+ if (username2 === "" && !this.secure) {
783
+ } else {
784
+ throw new Error("Unknown user");
785
+ }
786
+ }
787
+ let decoded;
788
+ const cacheKey = `${token2}:${this.serverSecret}`;
789
+ const cached = this._tokenVerifyCache.get(cacheKey);
790
+ if (cached) {
791
+ decoded = cached;
792
+ } else {
793
+ try {
794
+ decoded = jwt.verify(token2, this.getEffectiveSecret());
795
+ this._tokenVerifyCache.set(cacheKey, decoded);
796
+ } catch (eServer) {
797
+ this.audit("jwt.verify.failed", {
798
+ user: username2
799
+ });
800
+ throw new Error("Invalid token after verify");
801
+ }
802
+ }
803
+ if (!decoded)
804
+ throw new Error("Invalid token after verify");
805
+ console.log("script-code: token verified, checking VM...");
806
+ if (!this.vm || typeof this.vm.run !== "function") {
807
+ console.error("script-code: VM not available");
808
+ sendWithBackpressure({
809
+ command: "script-code",
810
+ message: "ERROR",
811
+ data: "VM_NOT_AVAILABLE"
812
+ });
813
+ break;
814
+ }
815
+ console.log("script-code: VM available, preparing data...");
816
+ const data2 = object.data && typeof object.data === "object" ? object.data : {};
817
+ const validateViaWorker = (payload) => new Promise((resolve2, reject) => {
818
+ try {
819
+ if (!this._validatorWorkers || this._validatorWorkers.length === 0)
820
+ return resolve2(null);
821
+ if (Date.now() < (this._validatorTrippedUntil || 0))
822
+ return resolve2(null);
823
+ const wIndex = this._validatorNext++ % this._validatorWorkers.length;
824
+ const w = this._validatorWorkers[wIndex];
825
+ const id = Math.random().toString(36).slice(2);
826
+ const timer = setTimeout(
827
+ () => {
828
+ const p = this._validatorPending.get(id);
829
+ if (p) {
830
+ this._validatorPending.delete(id);
831
+ p.reject(new Error("validator worker timeout"));
832
+ }
833
+ try {
834
+ w.terminate();
835
+ } catch (e) {
836
+ }
837
+ this._validatorFailures.push(Date.now());
838
+ const recent = this._validatorFailures.filter(
839
+ (t) => t > Date.now() - this._validatorFailureWindowMs
840
+ );
841
+ if (recent.length >= this._validatorFailureThreshold)
842
+ this._validatorTrippedUntil = Date.now() + this._validatorCircuitCooldownMs;
843
+ },
844
+ this._validatorTimeoutMs || 3e3
845
+ );
846
+ this._validatorPending.set(id, {
847
+ resolve: resolve2,
848
+ reject,
849
+ timer,
850
+ start: Date.now()
851
+ });
852
+ w.postMessage({
853
+ id,
854
+ action: "validate",
855
+ payload,
856
+ options: {
857
+ schema: this.payloadSchema,
858
+ maxDepth: this._payloadMaxDepth,
859
+ maxNodes: this._payloadMaxNodes
860
+ }
861
+ });
862
+ } catch (e) {
863
+ return resolve2(null);
864
+ }
865
+ });
866
+ const sanitizeViaWorker = (payload) => new Promise((resolve2, reject) => {
867
+ try {
868
+ if (!this._validatorWorkers || this._validatorWorkers.length === 0)
869
+ return resolve2(null);
870
+ if (Date.now() < (this._validatorTrippedUntil || 0))
871
+ return resolve2(null);
872
+ const wIndex = this._validatorNext++ % this._validatorWorkers.length;
873
+ const w = this._validatorWorkers[wIndex];
874
+ const id = Math.random().toString(36).slice(2);
875
+ const timer = setTimeout(
876
+ () => {
877
+ const p = this._validatorPending.get(id);
878
+ if (p) {
879
+ this._validatorPending.delete(id);
880
+ p.reject(new Error("validator worker timeout"));
881
+ }
882
+ try {
883
+ w.terminate();
884
+ } catch (e) {
885
+ }
886
+ this._validatorFailures.push(Date.now());
887
+ const recent = this._validatorFailures.filter(
888
+ (t) => t > Date.now() - this._validatorFailureWindowMs
889
+ );
890
+ if (recent.length >= this._validatorFailureThreshold)
891
+ this._validatorTrippedUntil = Date.now() + this._validatorCircuitCooldownMs;
892
+ },
893
+ this._validatorTimeoutMs || 3e3
894
+ );
895
+ this._validatorPending.set(id, {
896
+ resolve: resolve2,
897
+ reject,
898
+ timer,
899
+ start: Date.now()
900
+ });
901
+ w.postMessage({
902
+ id,
903
+ action: "sanitize",
904
+ payload,
905
+ options: {
906
+ allowedKeys: [
907
+ "result",
908
+ "value",
909
+ "items",
910
+ "length",
911
+ "status",
912
+ "message"
913
+ ],
914
+ maxNodes: this._sanitizeMaxNodes,
915
+ maxDepth: this._sanitizeMaxDepth,
916
+ maxString: this._sanitizeMaxString
917
+ }
918
+ });
919
+ } catch (e) {
920
+ return resolve2(null);
921
+ }
922
+ });
923
+ const localValidate = (payload) => {
924
+ const out = {};
925
+ let nodes = 0;
926
+ const checkDepth = (obj, depth) => {
927
+ if (depth > this._payloadMaxDepth) return false;
928
+ if (nodes++ > this._payloadMaxNodes) return false;
929
+ if (obj && typeof obj === "object") {
930
+ for (const k of Object.keys(obj)) {
931
+ const expected = (this.payloadSchema || {})[k];
932
+ if (!expected) continue;
933
+ const val = obj[k];
934
+ if (expected === "string" && typeof val === "string")
935
+ out[k] = val;
936
+ else if (expected === "number" && typeof val === "number")
937
+ out[k] = val;
938
+ else if (expected === "object" && val && typeof val === "object") {
939
+ if (depth + 1 <= this._payloadMaxDepth) {
940
+ out[k] = {};
941
+ for (const nk of Object.keys(val)) {
942
+ if ((this.payloadSchema || {})[nk])
943
+ out[k][nk] = val[nk];
944
+ }
945
+ }
946
+ }
947
+ }
948
+ return true;
949
+ }
950
+ return false;
951
+ };
952
+ if (!checkDepth(payload, 0)) return {};
953
+ return out;
954
+ };
955
+ let allowed = {};
956
+ try {
957
+ if (this._validatorWorkers && this._validatorWorkers.length > 0) {
958
+ const res = await Promise.race([
959
+ validateViaWorker(data2).catch(() => null),
960
+ new Promise((r) => setTimeout(() => r(null), 1500))
961
+ ]);
962
+ if (res && typeof res === "object") allowed = res;
963
+ else allowed = localValidate(data2);
964
+ } else {
965
+ allowed = localValidate(data2);
966
+ }
967
+ } catch (e) {
968
+ allowed = localValidate(data2);
969
+ }
970
+ console.log("script-code: validation complete, allowed keys:", Object.keys(allowed));
971
+ if (Object.keys(allowed).length === 0) {
972
+ console.error("script-code: empty payload after validation");
973
+ try {
974
+ sendWithBackpressure({
975
+ id: object.id,
976
+ command: "script-code",
977
+ message: "ERROR",
978
+ data: "INVALID_PAYLOAD"
979
+ });
980
+ } catch (e) {
981
+ }
982
+ break;
983
+ }
984
+ const runInVmWorker = (payload) => new Promise((resolve2, reject) => {
985
+ if (!this._vmWorkers || this._vmWorkers.length === 0)
986
+ return resolve2(null);
987
+ const wIndex = this._vmNext++ % this._vmWorkers.length;
988
+ const w = this._vmWorkers[wIndex];
989
+ const id = Math.random().toString(36).slice(2);
990
+ const timer = setTimeout(() => {
991
+ const p = this._vmPending.get(id);
992
+ if (p) {
993
+ this._vmPending.delete(id);
994
+ p.reject(new Error("vm worker timeout"));
995
+ }
996
+ try {
997
+ w.terminate();
998
+ } catch (e) {
999
+ }
1000
+ const nw = new Worker(
1001
+ path.join(__dirname, "vm-worker.js")
1002
+ );
1003
+ nw.on("message", (m) => {
1004
+ const p2 = this._vmPending.get(m.id);
1005
+ if (p2) {
1006
+ clearTimeout(p2.timer);
1007
+ this._vmPending.delete(m.id);
1008
+ if (m.error) p2.reject(new Error(m.error));
1009
+ else p2.resolve(m.res);
1010
+ }
1011
+ });
1012
+ this._vmWorkers[wIndex] = nw;
1013
+ }, this.vmWorkerTimeoutMs);
1014
+ this._vmPending.set(id, {
1015
+ resolve: resolve2,
1016
+ reject,
1017
+ timer,
1018
+ start: Date.now()
1019
+ });
1020
+ try {
1021
+ w.postMessage({ id, action: "run", payload });
1022
+ } catch (e) {
1023
+ clearTimeout(timer);
1024
+ this._vmPending.delete(id);
1025
+ return resolve2(null);
1026
+ }
1027
+ });
1028
+ const code = allowed.code;
1029
+ const databaseName = allowed.databaseName;
1030
+ if (!code || typeof code !== "string") {
1031
+ console.error("script-code: invalid code type");
1032
+ try {
1033
+ sendWithBackpressure({
1034
+ id: object.id,
1035
+ command: "script-code",
1036
+ message: "ERROR",
1037
+ data: "Missing or invalid code"
1038
+ });
1039
+ } catch (e) {
1040
+ }
1041
+ break;
1042
+ }
1043
+ console.log("script-code: running code for database:", databaseName);
1044
+ const runPromise = this._vmWorkers && this._vmWorkers.length > 0 ? runInVmWorker(code) : Promise.resolve(this.vm.run(code, databaseName));
1045
+ console.log("script-code: calling VM...");
1046
+ Promise.resolve(runPromise).then(async (res) => {
1047
+ console.log("script-code: VM returned result:", typeof res);
1048
+ try {
1049
+ let out;
1050
+ if (this._validatorWorkers && this._validatorWorkers.length > 0) {
1051
+ try {
1052
+ const wres = await Promise.race([
1053
+ sanitizeViaWorker(res).catch(() => null),
1054
+ new Promise((r) => setTimeout(() => r(null), 1500))
1055
+ ]);
1056
+ out = wres && typeof wres === "object" ? wres : null;
1057
+ } catch (e) {
1058
+ out = null;
1059
+ }
1060
+ }
1061
+ if (!out) {
1062
+ const allowedKeys = [
1063
+ "result",
1064
+ "value",
1065
+ "items",
1066
+ "length",
1067
+ "status",
1068
+ "message",
1069
+ "namespace",
1070
+ "logs",
1071
+ // Log object properties
1072
+ "type",
1073
+ "args",
1074
+ // Database properties
1075
+ "name",
1076
+ "id",
1077
+ "data",
1078
+ "properties",
1079
+ "fields",
1080
+ "records",
1081
+ "rows",
1082
+ "columns",
1083
+ // Allow database name from request
1084
+ databaseName
1085
+ ];
1086
+ let nodes = 0;
1087
+ const rec = (input, depth = 0, isTopLevel = false, path4 = []) => {
1088
+ if (nodes++ > this._sanitizeMaxNodes)
1089
+ return "[REDACTED_NODES]";
1090
+ if (depth > this._sanitizeMaxDepth)
1091
+ return "[REDACTED_DEPTH]";
1092
+ if (input === null || input === void 0)
1093
+ return input;
1094
+ if (typeof input === "string") {
1095
+ if (input.length > this._sanitizeMaxString)
1096
+ return input.slice(0, this._sanitizeMaxString) + "...[TRUNC]";
1097
+ return input;
1098
+ }
1099
+ if (typeof input === "number" || typeof input === "boolean")
1100
+ return input;
1101
+ if (typeof input === "function") {
1102
+ const isInsideNamespace = path4.includes("namespace");
1103
+ if (isInsideNamespace) {
1104
+ return `[Function: ${input.name || "anonymous"}]`;
1105
+ }
1106
+ return "[REDACTED]";
1107
+ }
1108
+ if (Array.isArray(input))
1109
+ return input.map((v) => rec(v, depth + 1, false, path4));
1110
+ if (typeof input === "object") {
1111
+ const out2 = {};
1112
+ const isInsideNamespace = path4.includes("namespace");
1113
+ const isInsideLogs = path4.includes("logs");
1114
+ for (const k of Object.keys(input)) {
1115
+ if (isTopLevel || allowedKeys.includes(k) || isInsideNamespace || isInsideLogs) {
1116
+ out2[k] = rec(input[k], depth + 1, false, [...path4, k]);
1117
+ } else {
1118
+ out2[k] = "[REDACTED]";
1119
+ }
1120
+ }
1121
+ return out2;
1122
+ }
1123
+ return "[REDACTED]";
1124
+ };
1125
+ out = rec(res, 0, true, []);
1126
+ }
1127
+ console.log("script-code: sanitized result:", typeof out, out);
1128
+ try {
1129
+ console.log("script-code: sending OK response with id:", object.id);
1130
+ sendWithBackpressure({
1131
+ id: object.id,
1132
+ command: "script-code",
1133
+ message: "OK",
1134
+ data: out
1135
+ });
1136
+ } catch (e) {
1137
+ console.error("script-code: failed to send OK response:", e);
1138
+ }
1139
+ } catch (e) {
1140
+ console.error("script-code: error during sanitize:", e);
1141
+ try {
1142
+ sendWithBackpressure({
1143
+ id: object.id,
1144
+ command: "script-code",
1145
+ message: "ERROR",
1146
+ data: e && e.message || String(e)
1147
+ });
1148
+ } catch (ee) {
1149
+ }
1150
+ }
1151
+ }).catch((err) => {
1152
+ console.error("script-code: VM error:", err);
1153
+ try {
1154
+ sendWithBackpressure({
1155
+ id: object.id,
1156
+ command: "script-code",
1157
+ message: "ERROR",
1158
+ data: err && err.message || String(err)
1159
+ });
1160
+ } catch (e) {
1161
+ console.error("script-code: failed to send ERROR response:", e);
1162
+ }
1163
+ });
1164
+ } catch (err) {
1165
+ try {
1166
+ await sendCritical({
1167
+ command: "script-code",
1168
+ message: "AUTH FAIL",
1169
+ data: err && err.message || null
1170
+ });
1171
+ console.log("Token verification AUTH FAIL sent and flushed - client will close");
1172
+ } catch (e) {
1173
+ console.error("Error sending token AUTH FAIL:", e);
1174
+ }
1175
+ return;
1176
+ }
1177
+ break;
1178
+ case "create-db":
1179
+ console.log("create-db command", object);
1180
+ try {
1181
+ const token2 = object.token || object.data && object.data.token;
1182
+ if (!token2) throw new Error("Missing token");
1183
+ const decoded = jwt.verify(token2, this.getEffectiveSecret());
1184
+ if (!decoded) throw new Error("Invalid token");
1185
+ const username2 = decoded.username || decoded.role || "anonymous";
1186
+ const databaseName = object.data?.databaseName;
1187
+ if (!databaseName || typeof databaseName !== "string") {
1188
+ throw new Error("Missing or invalid databaseName");
1189
+ }
1190
+ if (!/^[a-zA-Z0-9_-]+$/.test(databaseName)) {
1191
+ throw new Error("Invalid database name. Use only alphanumeric characters, hyphens, and underscores.");
1192
+ }
1193
+ if (!this.storage) {
1194
+ throw new Error("Storage not available");
1195
+ }
1196
+ console.log(`Creating database: ${databaseName} for user: ${username2}`);
1197
+ const initialContent = `// Database: ${databaseName}
1198
+ // Created: ${(/* @__PURE__ */ new Date()).toISOString()}
1199
+ // Author: ${username2}
1200
+
1201
+ export const ${databaseName} = {};
1202
+ `;
1203
+ const result = await this.storage.addFile(`${databaseName}.ts`, initialContent);
1204
+ console.log("Storage addFile result:", result);
1205
+ if (!result.success) {
1206
+ throw new Error(result.message);
1207
+ }
1208
+ sendWithBackpressure({
1209
+ id: object.id,
1210
+ action: commandOrAction,
1211
+ command: commandOrAction,
1212
+ message: "SUCCESS",
1213
+ data: {
1214
+ success: true,
1215
+ databaseName,
1216
+ path: result.path,
1217
+ message: `Database '${databaseName}' created successfully`
1218
+ }
1219
+ });
1220
+ } catch (err) {
1221
+ try {
1222
+ sendWithBackpressure({
1223
+ id: object.id,
1224
+ action: commandOrAction,
1225
+ command: commandOrAction,
1226
+ message: "ERROR",
1227
+ data: err && err.message || String(err)
1228
+ });
1229
+ } catch (e) {
1230
+ }
1231
+ }
1232
+ break;
1233
+ case "list-dbs":
1234
+ console.log("list-dbs command", object);
1235
+ try {
1236
+ const token2 = object.token || object.data && object.data.token;
1237
+ if (!token2) throw new Error("Missing token");
1238
+ console.log("list-dbs: verifying token...");
1239
+ const decoded = jwt.verify(token2, this.getEffectiveSecret());
1240
+ if (!decoded) throw new Error("Invalid token");
1241
+ console.log("list-dbs: token verified, listing files...");
1242
+ if (!this.storage) {
1243
+ throw new Error("Storage not available");
1244
+ }
1245
+ const files = await this.storage.listFiles();
1246
+ console.log("list-dbs: found files:", files);
1247
+ const databases = files.filter((file) => file.endsWith(".ts")).map((file) => {
1248
+ const name = file.replace(/\.ts$/, "");
1249
+ return { name, path: file };
1250
+ });
1251
+ console.log("list-dbs: sending response with", databases.length, "databases");
1252
+ sendWithBackpressure({
1253
+ id: object.id,
1254
+ action: commandOrAction,
1255
+ command: commandOrAction,
1256
+ message: "SUCCESS",
1257
+ data: {
1258
+ databases,
1259
+ count: databases.length
1260
+ }
1261
+ });
1262
+ } catch (err) {
1263
+ console.error("list-dbs ERROR:", err);
1264
+ try {
1265
+ sendWithBackpressure({
1266
+ id: object.id,
1267
+ action: commandOrAction,
1268
+ command: commandOrAction,
1269
+ message: "ERROR",
1270
+ data: err && err.message || String(err)
1271
+ });
1272
+ } catch (e) {
1273
+ console.error("Failed to send error response:", e);
1274
+ }
1275
+ }
1276
+ break;
1277
+ case "remove-db":
1278
+ console.log("remove-db command", object);
1279
+ try {
1280
+ const token2 = object.token || object.data && object.data.token;
1281
+ if (!token2) throw new Error("Missing token");
1282
+ const decoded = jwt.verify(token2, this.getEffectiveSecret());
1283
+ if (!decoded) throw new Error("Invalid token");
1284
+ const username2 = decoded.username || decoded.role || "anonymous";
1285
+ const databaseName = object.data?.databaseName;
1286
+ if (!databaseName || typeof databaseName !== "string") {
1287
+ throw new Error("Missing or invalid databaseName");
1288
+ }
1289
+ if (!this.storage) {
1290
+ throw new Error("Storage not available");
1291
+ }
1292
+ console.log(`Removing database: ${databaseName} by user: ${username2}`);
1293
+ const result = await this.storage.deleteFile(`${databaseName}.ts`);
1294
+ console.log("Storage deleteFile result:", result);
1295
+ if (!result.success) {
1296
+ throw new Error(result.message);
1297
+ }
1298
+ sendWithBackpressure({
1299
+ id: object.id,
1300
+ action: commandOrAction,
1301
+ command: commandOrAction,
1302
+ message: "SUCCESS",
1303
+ data: {
1304
+ success: true,
1305
+ databaseName,
1306
+ message: `Database '${databaseName}' removed successfully`
1307
+ }
1308
+ });
1309
+ } catch (err) {
1310
+ console.error("remove-db ERROR:", err);
1311
+ try {
1312
+ sendWithBackpressure({
1313
+ id: object.id,
1314
+ action: commandOrAction,
1315
+ command: commandOrAction,
1316
+ message: "ERROR",
1317
+ data: err && err.message || String(err)
1318
+ });
1319
+ } catch (e) {
1320
+ console.error("Failed to send error response:", e);
1321
+ }
1322
+ }
1323
+ break;
1324
+ case "rename-db":
1325
+ console.log("rename-db command", object);
1326
+ try {
1327
+ const token2 = object.token || object.data && object.data.token;
1328
+ if (!token2) throw new Error("Missing token");
1329
+ const decoded = jwt.verify(token2, this.getEffectiveSecret());
1330
+ if (!decoded) throw new Error("Invalid token");
1331
+ const username2 = decoded.username || decoded.role || "anonymous";
1332
+ const databaseName = object.data?.databaseName;
1333
+ const newName = object.data?.newName;
1334
+ if (!databaseName || typeof databaseName !== "string") {
1335
+ throw new Error("Missing or invalid databaseName");
1336
+ }
1337
+ if (!newName || typeof newName !== "string") {
1338
+ throw new Error("Missing or invalid newName");
1339
+ }
1340
+ if (!/^[a-zA-Z0-9_-]+$/.test(newName)) {
1341
+ throw new Error("Invalid new database name. Use only alphanumeric characters, hyphens, and underscores.");
1342
+ }
1343
+ if (!this.storage) {
1344
+ throw new Error("Storage not available");
1345
+ }
1346
+ console.log(`Renaming database: ${databaseName} to ${newName} by user: ${username2}`);
1347
+ const getResult = await this.storage.getFile(`${databaseName}.ts`);
1348
+ if (!getResult.success) {
1349
+ throw new Error(getResult.message || `Database '${databaseName}' not found`);
1350
+ }
1351
+ let content = getResult.content || "";
1352
+ content = content.replace(new RegExp(`// Database: ${databaseName}`, "g"), `// Database: ${newName}`).replace(new RegExp(`export const ${databaseName}`, "g"), `export const ${newName}`);
1353
+ const addResult = await this.storage.addFile(`${newName}.ts`, content);
1354
+ if (!addResult.success) {
1355
+ throw new Error(addResult.message);
1356
+ }
1357
+ const deleteResult = await this.storage.deleteFile(`${databaseName}.ts`);
1358
+ if (!deleteResult.success) {
1359
+ await this.storage.deleteFile(`${newName}.ts`);
1360
+ throw new Error(deleteResult.message);
1361
+ }
1362
+ console.log("Database renamed successfully");
1363
+ sendWithBackpressure({
1364
+ id: object.id,
1365
+ action: commandOrAction,
1366
+ command: commandOrAction,
1367
+ message: "SUCCESS",
1368
+ data: {
1369
+ success: true,
1370
+ oldName: databaseName,
1371
+ newName,
1372
+ message: `Database '${databaseName}' renamed to '${newName}' successfully`
1373
+ }
1374
+ });
1375
+ } catch (err) {
1376
+ console.error("rename-db ERROR:", err);
1377
+ try {
1378
+ sendWithBackpressure({
1379
+ id: object.id,
1380
+ action: commandOrAction,
1381
+ command: commandOrAction,
1382
+ message: "ERROR",
1383
+ data: err && err.message || String(err)
1384
+ });
1385
+ } catch (e) {
1386
+ console.error("Failed to send error response:", e);
1387
+ }
1388
+ }
1389
+ break;
1390
+ case "get-db":
1391
+ console.log("get-db command", object);
1392
+ try {
1393
+ const token2 = object.token || object.data && object.data.token;
1394
+ if (!token2) throw new Error("Missing token");
1395
+ const decoded = jwt.verify(token2, this.getEffectiveSecret());
1396
+ if (!decoded) throw new Error("Invalid token");
1397
+ const databaseName = object.data?.databaseName;
1398
+ if (!databaseName || typeof databaseName !== "string") {
1399
+ throw new Error("Missing or invalid databaseName");
1400
+ }
1401
+ if (!this.storage) {
1402
+ throw new Error("Storage not available");
1403
+ }
1404
+ console.log(`Getting database: ${databaseName}`);
1405
+ const result = await this.storage.getFile(`${databaseName}.ts`);
1406
+ console.log("Storage getFile result:", result);
1407
+ if (!result.success) {
1408
+ throw new Error(result.message);
1409
+ }
1410
+ sendWithBackpressure({
1411
+ id: object.id,
1412
+ action: commandOrAction,
1413
+ command: commandOrAction,
1414
+ message: "SUCCESS",
1415
+ data: {
1416
+ success: true,
1417
+ databaseName,
1418
+ content: result.content || "",
1419
+ path: result.path
1420
+ }
1421
+ });
1422
+ } catch (err) {
1423
+ console.error("get-db ERROR:", err);
1424
+ try {
1425
+ sendWithBackpressure({
1426
+ id: object.id,
1427
+ action: commandOrAction,
1428
+ command: commandOrAction,
1429
+ message: "ERROR",
1430
+ data: err && err.message || String(err)
1431
+ });
1432
+ } catch (e) {
1433
+ console.error("Failed to send error response:", e);
1434
+ }
1435
+ }
1436
+ break;
1437
+ case "save-db":
1438
+ case "update-db":
1439
+ case "get-info":
1440
+ console.log(`Command ${commandOrAction} not yet implemented`);
1441
+ try {
1442
+ sendWithBackpressure({
1443
+ id: object.id,
1444
+ action: commandOrAction,
1445
+ command: commandOrAction,
1446
+ message: "ERROR",
1447
+ data: `Command ${commandOrAction} not yet implemented in protocol server`
1448
+ });
1449
+ } catch (e) {
1450
+ }
1451
+ break;
1452
+ default:
1453
+ console.log("Unknown command:", commandOrAction, "object:", object);
1454
+ socket.write(
1455
+ JSON.stringify({
1456
+ command: "error",
1457
+ message: "UNKNOWN_COMMAND"
1458
+ }) + "\n"
1459
+ );
1460
+ socket.end();
1461
+ return;
1462
+ }
1463
+ }
1464
+ });
1465
+ socket.on("end", () => {
1466
+ console.log("Client disconnected");
1467
+ });
1468
+ };
1469
+ if (this.requireTlsInProduction && process.env.NODE_ENV === "production") {
1470
+ if (!(this.tlsOptions && (this.tlsOptions.key || this.tlsOptions.cert))) {
1471
+ throw new Error(
1472
+ "TLS is required in production but tlsOptions are not configured"
1473
+ );
1474
+ }
1475
+ }
1476
+ let serverImpl;
1477
+ if (this.tlsOptions && (this.tlsOptions.key || this.tlsOptions.cert)) {
1478
+ const tlsOpts = Object.assign({}, this.tlsOptions);
1479
+ try {
1480
+ if (typeof tlsOpts.key === "string" && fs2.existsSync(tlsOpts.key))
1481
+ tlsOpts.key = fs2.readFileSync(tlsOpts.key);
1482
+ if (typeof tlsOpts.cert === "string" && fs2.existsSync(tlsOpts.cert))
1483
+ tlsOpts.cert = fs2.readFileSync(tlsOpts.cert);
1484
+ } catch (e) {
1485
+ }
1486
+ serverImpl = tls.createServer(tlsOpts, createListener);
1487
+ } else {
1488
+ serverImpl = net.createServer(createListener);
1489
+ }
1490
+ this.server = serverImpl;
1491
+ this.server.listen(this.port, this.host, () => {
1492
+ let cred = "";
1493
+ try {
1494
+ if (this.users && Array.isArray(this.users) && this.users.length > 0) {
1495
+ const u = this.users[0] || { username: "" };
1496
+ const uname = u.username || null;
1497
+ if (uname && typeof u.password === "string" && u.password) {
1498
+ cred = encodeURIComponent(String(uname)) + ":*****@";
1499
+ } else if (uname) {
1500
+ cred = encodeURIComponent(String(uname)) + "@";
1501
+ }
1502
+ }
1503
+ } catch (e) {
1504
+ }
1505
+ console.log(
1506
+ `Server listening on scriptdb://${cred}${this.host}:${this.port}`
1507
+ );
1508
+ try {
1509
+ if (!this._autoscaleTimer && this._autoscaleIntervalMs > 0) {
1510
+ this._autoscaleTimer = setInterval(() => {
1511
+ try {
1512
+ this._autoscalePools();
1513
+ } catch (e) {
1514
+ }
1515
+ }, this._autoscaleIntervalMs);
1516
+ }
1517
+ } catch (e) {
1518
+ }
1519
+ });
1520
+ this.server.on("close", () => {
1521
+ try {
1522
+ if (this._autoscaleTimer) {
1523
+ clearInterval(this._autoscaleTimer);
1524
+ this._autoscaleTimer = null;
1525
+ }
1526
+ } catch (e) {
1527
+ }
1528
+ });
1529
+ }
1530
+ };
1531
+
1532
+ // src/index.ts
1533
+ import * as path3 from "path";
1534
+ import * as fs4 from "fs";
1535
+ import VM from "@scriptdb/vm";
1536
+ import { Storage as Storage2 } from "@scriptdb/storage";
1537
+
1538
+ // src/utils/setupDir.ts
1539
+ import path2 from "path";
1540
+ import fs3 from "fs";
1541
+ import { spawn } from "child_process";
1542
+ import Storage from "@scriptdb/storage";
1543
+ var pkgData = `{
1544
+ "name": "scriptdb-workspace",
1545
+ "version": "1.1.1",
1546
+ "description": "ScriptDB workspace for custom scripts, services, and databases",
1547
+ "private": true,
1548
+ "devDependencies": {
1549
+ "@types/bun": "^1.3.2",
1550
+ "@types/node": "^20.0.0",
1551
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
1552
+ "@typescript-eslint/parser": "^6.0.0",
1553
+ "bun-types": "latest",
1554
+ "eslint": "^8.0.0",
1555
+ "typescript": "^5.0.0"
1556
+ },
1557
+ "dependencies": {
1558
+ "lodash": "^4.17.21"
1559
+ }
1560
+ }
1561
+ `;
1562
+ var tsconfigData = `
1563
+ {
1564
+ "compilerOptions": {
1565
+ "target": "ES2020",
1566
+ "lib": ["ES2020", "DOM"],
1567
+ "module": "ESNext",
1568
+ "moduleResolution": "node",
1569
+ "allowSyntheticDefaultImports": true,
1570
+ "esModuleInterop": true,
1571
+ "allowJs": true,
1572
+ "strict": true,
1573
+ "noEmit": false,
1574
+ "declaration": true,
1575
+ "declarationMap": true,
1576
+ "sourceMap": true,
1577
+ "removeComments": false,
1578
+ "skipLibCheck": true,
1579
+ "forceConsistentCasingInFileNames": true,
1580
+ "resolveJsonModule": true,
1581
+ "isolatedModules": true,
1582
+ "incremental": true,
1583
+ "composite": true,
1584
+ "types": ["node"]
1585
+ }
1586
+ }
1587
+ `;
1588
+ var dotGitignoreData = `
1589
+ # Ignore all files in this directory
1590
+ node_modules/
1591
+ bin/
1592
+ `;
1593
+ var ecosystemData = `
1594
+ module.exports = {
1595
+ apps: [{
1596
+ name: 'scriptdb',
1597
+ script: process.platform === 'win32'
1598
+ ? require('path').join(require('os').homedir(), '.scriptdb', 'bin', 'scriptdb.exe')
1599
+ : 'scriptdb',
1600
+ args: 'start --force',
1601
+ cwd: require('path').join(require('os').homedir(), '.scriptdb'),
1602
+ instances: 1,
1603
+ autorestart: true,
1604
+ watch: false,
1605
+ max_memory_restart: '1G',
1606
+ env: {
1607
+ NODE_ENV: 'production'
1608
+ },
1609
+ error_file: require('path').join(require('os').homedir(), '.scriptdb', 'pm2-error.log'),
1610
+ out_file: require('path').join(require('os').homedir(), '.scriptdb', 'pm2-out.log'),
1611
+ log_file: require('path').join(require('os').homedir(), '.scriptdb', 'pm2-combined.log'),
1612
+ time: true,
1613
+ log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
1614
+ merge_logs: true
1615
+ }]
1616
+ };
1617
+ `;
1618
+ var configDefault = {
1619
+ PORT: 3800,
1620
+ HOST: "127.0.0.1",
1621
+ secure: false,
1622
+ jwtSecret: "scriptdb_secret_key",
1623
+ maxConnections: 100,
1624
+ logLevel: "info",
1625
+ DB_USERNAME: "admin",
1626
+ DB_PASSWORD: "password",
1627
+ GITHUB_USERNAME: "",
1628
+ GITHUB_EMAIL: "",
1629
+ GITHUB_URL: "",
1630
+ GITHUB_TOKEN: "",
1631
+ GITHUB_BRANCH: "main",
1632
+ enableIpLockout: true,
1633
+ ipFailWindowMs: 9e5,
1634
+ maxLoginAttempts: 5,
1635
+ lockDurationMs: 9e5,
1636
+ users: [
1637
+ {
1638
+ username: "admin",
1639
+ password: "password",
1640
+ hash: false,
1641
+ algorithm: "bcrypt"
1642
+ }
1643
+ ]
1644
+ };
1645
+ function isGitUrl(url) {
1646
+ const regex = /^(git@[\w.-]+:[\w./-]+\.git|https?:\/\/[\w.-]+\/[\w./-]+\.git)$/;
1647
+ return regex.test(url);
1648
+ }
1649
+ function injectGitToken(url, token) {
1650
+ const newUrl = url.replace(
1651
+ "https://",
1652
+ `https://${token}@`
1653
+ );
1654
+ return newUrl;
1655
+ }
1656
+ function installPackage(scriptdbDir) {
1657
+ return new Promise((resolve2, reject) => {
1658
+ console.log("Installing plugins in:", scriptdbDir);
1659
+ const proc = spawn("bun", ["i"], {
1660
+ cwd: scriptdbDir,
1661
+ stdio: "inherit",
1662
+ shell: true
1663
+ });
1664
+ proc.on("close", (code) => {
1665
+ if (code === 0) {
1666
+ console.log("bun i finished successfully");
1667
+ resolve2();
1668
+ } else {
1669
+ console.error("bun i failed with code:", code);
1670
+ reject(new Error(`bun install failed with code ${code}`));
1671
+ }
1672
+ });
1673
+ proc.on("error", (err) => {
1674
+ console.error("Failed to spawn bun install:", err);
1675
+ reject(err);
1676
+ });
1677
+ });
1678
+ }
1679
+ async function setupStorage(scriptdbDir) {
1680
+ try {
1681
+ console.log("Initializing storage...");
1682
+ const storage = new Storage(scriptdbDir);
1683
+ const configStr = fs3.readFileSync(path2.join(scriptdbDir, "config.json"), "utf8");
1684
+ const config = JSON.parse(configStr);
1685
+ await storage.initialize(scriptdbDir);
1686
+ console.log("Storage initialized");
1687
+ if (config.GITHUB_USERNAME && config.GITHUB_EMAIL) {
1688
+ const gitConfig = {
1689
+ userName: config.GITHUB_USERNAME,
1690
+ userEmail: config.GITHUB_EMAIL
1691
+ };
1692
+ await storage.setConfig(gitConfig);
1693
+ console.log("Git config set");
1694
+ } else {
1695
+ console.log("Git config not set (configure later with 'scriptdb config')");
1696
+ }
1697
+ if (config.GITHUB_URL && isGitUrl(config.GITHUB_URL)) {
1698
+ const remoteUrl = injectGitToken(config.GITHUB_URL, config.GITHUB_TOKEN);
1699
+ await storage.addRemote("origin", config.GITHUB_TOKEN ? remoteUrl : config.GITHUB_URL);
1700
+ await storage.pull("origin", config.GITHUB_BRANCH || "main");
1701
+ console.log("Remote repository configured");
1702
+ } else {
1703
+ console.log("Remote repository not configured (configure later with 'scriptdb config')");
1704
+ }
1705
+ console.log("Storage setup complete");
1706
+ } catch (err) {
1707
+ console.warn("Storage setup failed (non-critical):", err);
1708
+ }
1709
+ }
1710
+ async function setupDir() {
1711
+ const scriptdbDir = path2.join(getHomeDir(), ".scriptdb");
1712
+ const databasesDir = path2.join(scriptdbDir, "databases");
1713
+ const servicesDir = path2.join(scriptdbDir, "services");
1714
+ const clientsDir = path2.join(scriptdbDir, "clients");
1715
+ const binDir = path2.join(scriptdbDir, "bin");
1716
+ const configFile = path2.join(scriptdbDir, "config.json");
1717
+ const gitignoreFile = path2.join(scriptdbDir, ".gitignore");
1718
+ const packageJsonFile = path2.join(scriptdbDir, "package.json");
1719
+ const tsconfigFile = path2.join(scriptdbDir, "tsconfig.json");
1720
+ const ecosystemFile = path2.join(scriptdbDir, "ecosystem.config.js");
1721
+ const isFullySetup = fs3.existsSync(scriptdbDir) && fs3.existsSync(databasesDir) && fs3.existsSync(servicesDir) && fs3.existsSync(clientsDir) && fs3.existsSync(binDir) && fs3.existsSync(configFile) && fs3.existsSync(gitignoreFile) && fs3.existsSync(packageJsonFile) && fs3.existsSync(tsconfigFile) && fs3.existsSync(ecosystemFile);
1722
+ if (isFullySetup) {
1723
+ console.log("ScriptDB directory already fully configured:", scriptdbDir);
1724
+ return;
1725
+ }
1726
+ console.log("Setting up scriptdb directory...");
1727
+ if (!fs3.existsSync(scriptdbDir)) {
1728
+ fs3.mkdirSync(scriptdbDir, { recursive: true });
1729
+ fs3.mkdirSync(databasesDir, { recursive: true });
1730
+ fs3.mkdirSync(servicesDir, { recursive: true });
1731
+ fs3.mkdirSync(clientsDir, { recursive: true });
1732
+ fs3.mkdirSync(binDir, { recursive: true });
1733
+ fs3.writeFileSync(configFile, JSON.stringify(configDefault, null, 2), "utf8");
1734
+ fs3.writeFileSync(gitignoreFile, dotGitignoreData, "utf8");
1735
+ fs3.writeFileSync(packageJsonFile, pkgData, "utf8");
1736
+ fs3.writeFileSync(tsconfigFile, tsconfigData, "utf8");
1737
+ fs3.writeFileSync(ecosystemFile, ecosystemData, "utf8");
1738
+ console.log("Created:", scriptdbDir);
1739
+ await installPackage(scriptdbDir);
1740
+ await setupStorage(scriptdbDir);
1741
+ } else {
1742
+ if (!fs3.existsSync(databasesDir)) {
1743
+ fs3.mkdirSync(databasesDir, { recursive: true });
1744
+ }
1745
+ if (!fs3.existsSync(servicesDir)) {
1746
+ fs3.mkdirSync(servicesDir, { recursive: true });
1747
+ }
1748
+ if (!fs3.existsSync(clientsDir)) {
1749
+ fs3.mkdirSync(clientsDir, { recursive: true });
1750
+ }
1751
+ if (!fs3.existsSync(binDir)) {
1752
+ fs3.mkdirSync(binDir, { recursive: true });
1753
+ }
1754
+ if (!fs3.existsSync(configFile)) {
1755
+ fs3.writeFileSync(configFile, JSON.stringify(configDefault, null, 2), "utf8");
1756
+ }
1757
+ if (!fs3.existsSync(gitignoreFile)) {
1758
+ fs3.writeFileSync(gitignoreFile, dotGitignoreData, "utf8");
1759
+ }
1760
+ if (!fs3.existsSync(packageJsonFile)) {
1761
+ fs3.writeFileSync(packageJsonFile, pkgData, "utf8");
1762
+ }
1763
+ if (!fs3.existsSync(tsconfigFile)) {
1764
+ fs3.writeFileSync(tsconfigFile, tsconfigData, "utf8");
1765
+ }
1766
+ if (!fs3.existsSync(ecosystemFile)) {
1767
+ fs3.writeFileSync(ecosystemFile, ecosystemData, "utf8");
1768
+ }
1769
+ if (!fs3.existsSync(path2.join(scriptdbDir, "node_modules"))) {
1770
+ await installPackage(scriptdbDir);
1771
+ }
1772
+ console.log("Setup complete");
1773
+ await setupStorage(scriptdbDir);
1774
+ }
1775
+ }
1776
+
1777
+ // src/wsProxy.ts
1778
+ import { WebSocketServer, WebSocket } from "ws";
1779
+
1780
+ // ../client/src/index.ts
1781
+ import * as net2 from "net";
1782
+ import * as tls2 from "tls";
1783
+ import { URL } from "url";
1784
+ import * as crypto2 from "crypto";
1785
+ var noopLogger = {
1786
+ debug: () => {
1787
+ },
1788
+ info: () => {
1789
+ },
1790
+ warn: () => {
1791
+ },
1792
+ error: () => {
1793
+ }
1794
+ };
1795
+ var ScriptDBClient = class {
1796
+ /**
1797
+ * Create a new client. Do NOT auto-connect in constructor — call connect()
1798
+ * @param uri - Connection URI
1799
+ * @param options - Client options
1800
+ * @param options.secure - use TLS
1801
+ * @param options.logger - { debug, info, warn, error }
1802
+ * @param options.requestTimeout - Request timeout in ms
1803
+ * @param options.retries - Reconnection retries
1804
+ * @param options.retryDelay - Initial retry delay in ms
1805
+ * @param options.tlsOptions - Passed to tls.connect when secure
1806
+ */
1807
+ constructor(uri, options = {}) {
1808
+ this.socketTimeout = 0;
1809
+ this.maxMessageSize = 0;
1810
+ this._mask = () => {
1811
+ };
1812
+ this._maskArgs = () => [];
1813
+ this.logger = {};
1814
+ this.secure = true;
1815
+ this.requestTimeout = 0;
1816
+ this.retries = 0;
1817
+ this.retryDelay = 0;
1818
+ this.frame = "ndjson";
1819
+ this.uri = "";
1820
+ this.protocolName = "";
1821
+ this.username = null;
1822
+ this.password = null;
1823
+ this.host = "";
1824
+ this.port = 0;
1825
+ this.database = null;
1826
+ this.client = null;
1827
+ this.buffer = Buffer.alloc(0);
1828
+ this._nextId = 1;
1829
+ this._pending = /* @__PURE__ */ new Map();
1830
+ this._maxPending = 0;
1831
+ this._maxQueue = 0;
1832
+ this._pendingQueue = [];
1833
+ this._connected = false;
1834
+ this._authenticating = false;
1835
+ this.token = null;
1836
+ this._currentRetries = 0;
1837
+ this.tokenExpiry = null;
1838
+ this._destroyed = false;
1839
+ this._reconnectTimer = null;
1840
+ this.signing = null;
1841
+ this._stringify = JSON.stringify;
1842
+ this.ready = Promise.resolve();
1843
+ this._resolveReadyFn = null;
1844
+ this._rejectReadyFn = null;
1845
+ this._connecting = null;
1846
+ this._authPendingId = null;
1847
+ if (!uri || typeof uri !== "string") throw new Error("uri required");
1848
+ this.options = Object.assign({}, options);
1849
+ this.socketTimeout = Number.isFinite(this.options.socketTimeout) ? this.options.socketTimeout : 0;
1850
+ this.maxMessageSize = Number.isFinite(this.options.maxMessageSize) ? this.options.maxMessageSize : 5 * 1024 * 1024;
1851
+ this._mask = (obj) => {
1852
+ try {
1853
+ if (!obj || typeof obj !== "object") return obj;
1854
+ const copy = Array.isArray(obj) ? obj.slice() : Object.assign({}, obj);
1855
+ if (copy.token) copy.token = "****";
1856
+ if (copy.password) copy.password = "****";
1857
+ if (copy.data && copy.data.password) copy.data.password = "****";
1858
+ return copy;
1859
+ } catch (e) {
1860
+ return obj;
1861
+ }
1862
+ };
1863
+ const rawLogger = this.options.logger && typeof this.options.logger === "object" ? this.options.logger : noopLogger;
1864
+ this._maskArgs = (args) => {
1865
+ return args.map((a) => {
1866
+ if (!a) return a;
1867
+ if (typeof a === "string") return a;
1868
+ return this._mask(a);
1869
+ });
1870
+ };
1871
+ this.logger = {
1872
+ debug: (...args) => rawLogger.debug && rawLogger.debug(...this._maskArgs(args)),
1873
+ info: (...args) => rawLogger.info && rawLogger.info(...this._maskArgs(args)),
1874
+ warn: (...args) => rawLogger.warn && rawLogger.warn(...this._maskArgs(args)),
1875
+ error: (...args) => rawLogger.error && rawLogger.error(...this._maskArgs(args))
1876
+ };
1877
+ this.secure = typeof this.options.secure === "boolean" ? !!this.options.secure : true;
1878
+ if (!this.secure)
1879
+ this.logger.warn?.(
1880
+ "Warning: connecting in insecure mode (secure=false). This is not recommended."
1881
+ );
1882
+ this.requestTimeout = Number.isFinite(this.options.requestTimeout) ? this.options.requestTimeout : 0;
1883
+ this.retries = Number.isFinite(this.options.retries) ? this.options.retries : 3;
1884
+ this.retryDelay = Number.isFinite(this.options.retryDelay) ? this.options.retryDelay : 1e3;
1885
+ this.frame = this.options.frame === "length-prefix" || this.options.preferLengthPrefix ? "length-prefix" : "ndjson";
1886
+ let parsed;
1887
+ try {
1888
+ parsed = new URL(uri);
1889
+ } catch (e) {
1890
+ throw new Error("Invalid uri");
1891
+ }
1892
+ if (parsed.protocol !== "scriptdb:") {
1893
+ throw new Error("URI must use scriptdb:// protocol");
1894
+ }
1895
+ this.uri = uri;
1896
+ this.protocolName = parsed.protocol ? parsed.protocol.replace(":", "") : "scriptdb";
1897
+ this.username = (typeof this.options.username === "string" ? this.options.username : parsed.username) || null;
1898
+ this.password = (typeof this.options.password === "string" ? this.options.password : parsed.password) || null;
1899
+ if (parsed.username && !(typeof this.options.username === "string")) {
1900
+ this.logger.warn?.(
1901
+ "Credentials found in URI \u2014 consider passing credentials via options instead of embedding in URI"
1902
+ );
1903
+ }
1904
+ try {
1905
+ parsed.username = "";
1906
+ parsed.password = "";
1907
+ this.uri = parsed.toString();
1908
+ } catch (e) {
1909
+ this.uri = uri;
1910
+ }
1911
+ this.host = parsed.hostname || "localhost";
1912
+ this.port = parsed.port ? parseInt(parsed.port, 10) : 1234;
1913
+ this.database = parsed.pathname && parsed.pathname.length > 1 ? parsed.pathname.slice(1) : null;
1914
+ this.client = null;
1915
+ this.buffer = Buffer.alloc(0);
1916
+ this._nextId = 1;
1917
+ this._pending = /* @__PURE__ */ new Map();
1918
+ this._maxPending = Number.isFinite(this.options.maxPending) ? this.options.maxPending : 100;
1919
+ this._maxQueue = Number.isFinite(this.options.maxQueue) ? this.options.maxQueue : 1e3;
1920
+ this._pendingQueue = [];
1921
+ this._connected = false;
1922
+ this._authenticating = false;
1923
+ this.token = null;
1924
+ this._currentRetries = 0;
1925
+ this.tokenExpiry = null;
1926
+ this._destroyed = false;
1927
+ this._reconnectTimer = null;
1928
+ this.signing = this.options.signing && this.options.signing.secret ? this.options.signing : null;
1929
+ this._stringify = typeof this.options.stringify === "function" ? this.options.stringify : JSON.stringify;
1930
+ this._createReady();
1931
+ }
1932
+ _createReady() {
1933
+ this.ready = new Promise((resolve2, reject) => {
1934
+ this._resolveReadyFn = resolve2;
1935
+ this._rejectReadyFn = reject;
1936
+ });
1937
+ }
1938
+ _resolveReady(value) {
1939
+ if (this._resolveReadyFn) {
1940
+ try {
1941
+ this._resolveReadyFn(value);
1942
+ } catch (e) {
1943
+ }
1944
+ this._resolveReadyFn = null;
1945
+ this._rejectReadyFn = null;
1946
+ }
1947
+ }
1948
+ _rejectReady(err) {
1949
+ if (this._rejectReadyFn) {
1950
+ const rejectFn = this._rejectReadyFn;
1951
+ this._resolveReadyFn = null;
1952
+ this._rejectReadyFn = null;
1953
+ process.nextTick(() => {
1954
+ try {
1955
+ rejectFn(err);
1956
+ } catch (e) {
1957
+ }
1958
+ });
1959
+ }
1960
+ }
1961
+ /**
1962
+ * Check if connected
1963
+ */
1964
+ get connected() {
1965
+ return this._connected;
1966
+ }
1967
+ /**
1968
+ * Connect to server and authenticate. Returns a Promise that resolves when authenticated.
1969
+ */
1970
+ connect() {
1971
+ if (this._connecting) return this._connecting;
1972
+ this._connecting = new Promise((resolve2, reject) => {
1973
+ const opts = { host: this.host, port: this.port };
1974
+ const onConnect = () => {
1975
+ console.log("ScriptDBClient: Connected to server at", opts.host, opts.port);
1976
+ this.logger?.info?.("Connected to server");
1977
+ this._connected = true;
1978
+ this._currentRetries = 0;
1979
+ console.log("ScriptDBClient: Setting up listeners...");
1980
+ this._setupListeners();
1981
+ console.log("ScriptDBClient: Authenticating...");
1982
+ this.authenticate().then(() => {
1983
+ this._processQueue();
1984
+ resolve2(this);
1985
+ }).catch((err) => {
1986
+ reject(err);
1987
+ });
1988
+ };
1989
+ try {
1990
+ if (this.secure) {
1991
+ const connectionOpts = { host: opts.host, port: opts.port };
1992
+ const tlsOptions = Object.assign(
1993
+ {},
1994
+ this.options.tlsOptions || {},
1995
+ connectionOpts
1996
+ );
1997
+ if (typeof tlsOptions.rejectUnauthorized === "undefined")
1998
+ tlsOptions.rejectUnauthorized = true;
1999
+ this.client = tls2.connect(tlsOptions, onConnect);
2000
+ } else {
2001
+ this.client = net2.createConnection(opts, onConnect);
2002
+ }
2003
+ } catch (e) {
2004
+ const error = e;
2005
+ this.logger?.error?.("Connection failed", error.message);
2006
+ this._rejectReady(error);
2007
+ this._createReady();
2008
+ this._connecting = null;
2009
+ return reject(error);
2010
+ }
2011
+ if (!this.client) {
2012
+ const error = new Error("Failed to create client socket");
2013
+ this.logger?.error?.("Connection failed", error.message);
2014
+ this._connecting = null;
2015
+ return reject(error);
2016
+ }
2017
+ const onError = (err) => {
2018
+ this.logger?.error?.(
2019
+ "Client socket error:",
2020
+ err && err.message ? err.message : err
2021
+ );
2022
+ this._handleDisconnect(err);
2023
+ };
2024
+ const onClose = (hadError) => {
2025
+ this.logger?.info?.("Server closed connection");
2026
+ this._handleDisconnect(null);
2027
+ };
2028
+ this.client.on("error", onError);
2029
+ this.client.on("close", onClose);
2030
+ if (this.socketTimeout > 0 && this.client) {
2031
+ this.client.setTimeout(this.socketTimeout);
2032
+ this.client.on("timeout", () => {
2033
+ this.logger?.warn?.("Socket timeout, destroying connection");
2034
+ try {
2035
+ this.client?.destroy();
2036
+ } catch (e) {
2037
+ }
2038
+ });
2039
+ }
2040
+ }).catch((err) => {
2041
+ this._connecting = null;
2042
+ throw err;
2043
+ });
2044
+ return this._connecting;
2045
+ }
2046
+ _setupListeners() {
2047
+ if (!this.client) return;
2048
+ console.log("ScriptDBClient _setupListeners: called, client exists:", !!this.client);
2049
+ this.client.removeAllListeners("data");
2050
+ this.buffer = Buffer.alloc(0);
2051
+ console.log("ScriptDBClient _setupListeners: frame mode:", this.frame);
2052
+ if (this.frame === "length-prefix") {
2053
+ this.client.on("data", (chunk) => {
2054
+ if (!Buffer.isBuffer(chunk)) {
2055
+ try {
2056
+ chunk = Buffer.from(chunk);
2057
+ } catch (e) {
2058
+ return;
2059
+ }
2060
+ }
2061
+ this.buffer = Buffer.concat([this.buffer, chunk]);
2062
+ while (this.buffer.length >= 4) {
2063
+ const len = this.buffer.readUInt32BE(0);
2064
+ if (len > this.maxMessageSize) {
2065
+ this.logger?.error?.(
2066
+ "Incoming length-prefixed frame exceeds maxMessageSize \u2014 closing connection"
2067
+ );
2068
+ try {
2069
+ this.client?.destroy();
2070
+ } catch (e) {
2071
+ }
2072
+ return;
2073
+ }
2074
+ if (this.buffer.length < 4 + len) break;
2075
+ const payload = this.buffer.slice(4, 4 + len);
2076
+ this.buffer = this.buffer.slice(4 + len);
2077
+ let msg;
2078
+ try {
2079
+ msg = JSON.parse(payload.toString("utf8"));
2080
+ } catch (e) {
2081
+ this.logger?.error?.(
2082
+ "Invalid JSON frame",
2083
+ e && e.message ? e.message : e
2084
+ );
2085
+ continue;
2086
+ }
2087
+ if (!msg || typeof msg !== "object") continue;
2088
+ const validSchema = (typeof msg.id === "undefined" || typeof msg.id === "number") && (typeof msg.action === "string" || typeof msg.action === "undefined") && (typeof msg.command === "string" || typeof msg.command === "undefined") && (typeof msg.message === "string" || typeof msg.message === "undefined") && (typeof msg.data === "object" || typeof msg.data === "undefined" || msg.data === null);
2089
+ if (!validSchema) {
2090
+ this.logger?.warn?.("Message failed schema validation \u2014 ignoring");
2091
+ continue;
2092
+ }
2093
+ this._handleMessage(msg);
2094
+ }
2095
+ });
2096
+ } else {
2097
+ console.log("ScriptDBClient _setupListeners: Setting up NDJSON data listener");
2098
+ this.client.on("data", (chunk) => {
2099
+ console.log("ScriptDBClient: Received data chunk, length:", chunk.length);
2100
+ if (!Buffer.isBuffer(chunk)) {
2101
+ try {
2102
+ chunk = Buffer.from(chunk);
2103
+ } catch (e) {
2104
+ return;
2105
+ }
2106
+ }
2107
+ const idxLastNewline = chunk.indexOf(10);
2108
+ if (this.buffer.length === 0 && idxLastNewline === chunk.length - 1) {
2109
+ let start = 0;
2110
+ let idx2;
2111
+ while ((idx2 = chunk.indexOf(10, start)) !== -1) {
2112
+ const lineBuf = chunk.slice(start, idx2);
2113
+ start = idx2 + 1;
2114
+ if (lineBuf.length === 0) continue;
2115
+ let msg;
2116
+ try {
2117
+ msg = JSON.parse(lineBuf.toString("utf8"));
2118
+ } catch (e) {
2119
+ this.logger?.error?.(
2120
+ "Invalid JSON from server",
2121
+ e && e.message ? e.message : e
2122
+ );
2123
+ continue;
2124
+ }
2125
+ if (!msg || typeof msg !== "object") continue;
2126
+ const validSchema = (typeof msg.id === "undefined" || typeof msg.id === "number") && (typeof msg.action === "string" || typeof msg.action === "undefined") && (typeof msg.command === "string" || typeof msg.command === "undefined") && (typeof msg.message === "string" || typeof msg.message === "undefined");
2127
+ if (!validSchema) continue;
2128
+ this._handleMessage(msg);
2129
+ }
2130
+ return;
2131
+ }
2132
+ this.buffer = Buffer.concat([this.buffer, chunk]);
2133
+ if (this.buffer.length > this.maxMessageSize) {
2134
+ this.logger?.error?.(
2135
+ "Incoming message exceeds maxMessageSize \u2014 closing connection"
2136
+ );
2137
+ try {
2138
+ this.client?.destroy();
2139
+ } catch (e) {
2140
+ }
2141
+ return;
2142
+ }
2143
+ let idx;
2144
+ while ((idx = this.buffer.indexOf(10)) !== -1) {
2145
+ const lineBuf = this.buffer.slice(0, idx);
2146
+ this.buffer = this.buffer.slice(idx + 1);
2147
+ if (lineBuf.length === 0) continue;
2148
+ let msg;
2149
+ try {
2150
+ msg = JSON.parse(lineBuf.toString("utf8"));
2151
+ } catch (e) {
2152
+ this.logger?.error?.(
2153
+ "Invalid JSON from server",
2154
+ e && e.message ? e.message : e
2155
+ );
2156
+ continue;
2157
+ }
2158
+ const validSchema = (typeof msg.id === "undefined" || typeof msg.id === "number") && (typeof msg.action === "string" || typeof msg.action === "undefined") && (typeof msg.command === "string" || typeof msg.command === "undefined") && (typeof msg.message === "string" || typeof msg.message === "undefined");
2159
+ if (!validSchema) {
2160
+ this.logger?.warn?.("Message failed schema validation \u2014 ignoring");
2161
+ continue;
2162
+ }
2163
+ this._handleMessage(msg);
2164
+ }
2165
+ });
2166
+ }
2167
+ }
2168
+ // build the final buffer for sending using current token and signing settings
2169
+ _buildFinalBuffer(payloadBase, id) {
2170
+ const payloadObj = Object.assign(
2171
+ { id, action: payloadBase.action },
2172
+ payloadBase.data !== void 0 ? { data: payloadBase.data } : {}
2173
+ );
2174
+ if (this.token) payloadObj.token = this.token;
2175
+ const payloadStr = this._stringify(payloadObj);
2176
+ if (this.signing && this.signing.secret) {
2177
+ const hmac = crypto2.createHmac(
2178
+ this.signing.algorithm || "sha256",
2179
+ this.signing.secret
2180
+ );
2181
+ hmac.update(payloadStr);
2182
+ const sig = hmac.digest("hex");
2183
+ const envelope = { id, signature: sig, payload: payloadObj };
2184
+ const envelopeStr = this._stringify(envelope);
2185
+ if (this.frame === "length-prefix") {
2186
+ const body = Buffer.from(envelopeStr, "utf8");
2187
+ const buf = Buffer.allocUnsafe(4 + body.length);
2188
+ buf.writeUInt32BE(body.length, 0);
2189
+ body.copy(buf, 4);
2190
+ return buf;
2191
+ }
2192
+ return Buffer.from(envelopeStr + "\n", "utf8");
2193
+ }
2194
+ if (this.frame === "length-prefix") {
2195
+ const body = Buffer.from(payloadStr, "utf8");
2196
+ const buf = Buffer.allocUnsafe(4 + body.length);
2197
+ buf.writeUInt32BE(body.length, 0);
2198
+ body.copy(buf, 4);
2199
+ return buf;
2200
+ }
2201
+ return Buffer.from(payloadStr + "\n", "utf8");
2202
+ }
2203
+ _handleMessage(msg) {
2204
+ console.log("ScriptDBClient _handleMessage:", JSON.stringify(msg));
2205
+ console.log("ScriptDBClient _handleMessage msg.id:", msg.id, "msg.action:", msg.action, "msg.command:", msg.command, "msg.message:", msg.message);
2206
+ if (msg && typeof msg.id !== "undefined") {
2207
+ console.log("Handling message with id:", msg.id, "action:", msg.action, "message:", msg.message);
2208
+ const pending = this._pending.get(msg.id);
2209
+ if (!pending) {
2210
+ console.log("No pending request for id", msg.id, "pending map size:", this._pending.size);
2211
+ this.logger?.debug?.("No pending request for id", msg.id);
2212
+ return;
2213
+ }
2214
+ const { resolve: resolve2, reject, timer } = pending;
2215
+ if (timer) clearTimeout(timer);
2216
+ this._pending.delete(msg.id);
2217
+ this._processQueue();
2218
+ if (msg.action === "login" || msg.command === "login") {
2219
+ console.log("Processing login response with id");
2220
+ if (msg.message === "AUTH OK") {
2221
+ console.log("AUTH OK - setting token and resolving");
2222
+ this.token = msg.data && msg.data.token ? msg.data.token : null;
2223
+ this._resolveReady(null);
2224
+ return resolve2(msg.data);
2225
+ } else {
2226
+ console.log("AUTH FAILED:", msg.data);
2227
+ this._rejectReady(new Error("Authentication failed"));
2228
+ const errorMsg = msg.data || "Authentication failed";
2229
+ try {
2230
+ this.client?.end();
2231
+ } catch (e) {
2232
+ }
2233
+ return reject(new Error(typeof errorMsg === "string" ? errorMsg : "Authentication failed"));
2234
+ }
2235
+ }
2236
+ if ((msg.action === "script-code" || msg.command === "script-code") && msg.message === "OK")
2237
+ return resolve2(msg.data);
2238
+ if ((msg.action === "script-code" || msg.command === "script-code") && msg.message === "ERROR")
2239
+ return reject(new Error(typeof msg.data === "string" ? msg.data : "Server returned ERROR"));
2240
+ if (msg.action === "create-db" && msg.message === "SUCCESS")
2241
+ return resolve2(msg.data);
2242
+ if (msg.action === "create-db" && msg.message === "ERROR")
2243
+ return reject(new Error(typeof msg.data === "string" ? msg.data : "Failed to create database"));
2244
+ if (msg.message === "OK" || msg.message === "SUCCESS") {
2245
+ return resolve2(msg.data);
2246
+ } else if (msg.message === "ERROR") {
2247
+ return reject(new Error(typeof msg.data === "string" ? msg.data : "Request failed"));
2248
+ }
2249
+ return reject(new Error("Invalid response from server"));
2250
+ }
2251
+ console.log("Unhandled message:", msg);
2252
+ this.logger?.debug?.("Unhandled message from server", this._mask(msg));
2253
+ }
2254
+ authenticate() {
2255
+ if (this._authenticating)
2256
+ return Promise.reject(new Error("Already authenticating"));
2257
+ this._authenticating = true;
2258
+ return new Promise((resolve2, reject) => {
2259
+ const id = this._nextId++;
2260
+ const payload = {
2261
+ id,
2262
+ action: "login",
2263
+ data: { username: this.username, password: this.password }
2264
+ };
2265
+ let timer = null;
2266
+ if (this.requestTimeout > 0) {
2267
+ timer = setTimeout(() => {
2268
+ this._pending.delete(id);
2269
+ this._authenticating = false;
2270
+ reject(new Error("Auth timeout"));
2271
+ this.close();
2272
+ }, this.requestTimeout);
2273
+ }
2274
+ this._pending.set(id, {
2275
+ resolve: (data) => {
2276
+ if (timer) clearTimeout(timer);
2277
+ this._authenticating = false;
2278
+ if (data && data.token) this.token = data.token;
2279
+ resolve2(data);
2280
+ },
2281
+ reject: (err) => {
2282
+ if (timer) clearTimeout(timer);
2283
+ this._authenticating = false;
2284
+ reject(err);
2285
+ },
2286
+ timer
2287
+ });
2288
+ try {
2289
+ const buf = Buffer.from(JSON.stringify(payload) + "\n", "utf8");
2290
+ this._write(buf).catch((err) => {
2291
+ if (timer) clearTimeout(timer);
2292
+ this._pending.delete(id);
2293
+ this._authenticating = false;
2294
+ reject(err);
2295
+ });
2296
+ } catch (e) {
2297
+ clearTimeout(timer);
2298
+ this._pending.delete(id);
2299
+ this._authenticating = false;
2300
+ reject(e);
2301
+ }
2302
+ }).then((data) => {
2303
+ if (data && data.token) {
2304
+ this.token = data.token;
2305
+ this._resolveReady(null);
2306
+ }
2307
+ return data;
2308
+ }).catch((err) => {
2309
+ this._rejectReady(err);
2310
+ throw err;
2311
+ });
2312
+ }
2313
+ // internal write helper that respects backpressure
2314
+ _write(buf) {
2315
+ return new Promise((resolve2, reject) => {
2316
+ if (!this.client || !this._connected)
2317
+ return reject(new Error("Not connected"));
2318
+ try {
2319
+ const ok = this.client.write(buf, (err) => {
2320
+ if (err) return reject(err);
2321
+ resolve2(void 0);
2322
+ });
2323
+ if (!ok) {
2324
+ this.client.once("drain", () => resolve2(void 0));
2325
+ }
2326
+ } catch (e) {
2327
+ reject(e);
2328
+ }
2329
+ });
2330
+ }
2331
+ async _processQueue() {
2332
+ while (this._pending.size < this._maxPending && this._pendingQueue.length > 0) {
2333
+ const item = this._pendingQueue.shift();
2334
+ const { payloadBase, id, resolve: resolve2, reject, timer } = item;
2335
+ if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
2336
+ const refreshed = await this._maybeRefreshToken();
2337
+ if (!refreshed) {
2338
+ clearTimeout(timer);
2339
+ try {
2340
+ reject(new Error("Token expired and refresh failed"));
2341
+ } catch (e) {
2342
+ }
2343
+ continue;
2344
+ }
2345
+ }
2346
+ let buf;
2347
+ try {
2348
+ buf = this._buildFinalBuffer(payloadBase, id);
2349
+ } catch (e) {
2350
+ clearTimeout(timer);
2351
+ try {
2352
+ reject(e);
2353
+ } catch (er) {
2354
+ }
2355
+ continue;
2356
+ }
2357
+ this._pending.set(id, { resolve: resolve2, reject, timer });
2358
+ try {
2359
+ await this._write(buf);
2360
+ } catch (e) {
2361
+ clearTimeout(timer);
2362
+ this._pending.delete(id);
2363
+ try {
2364
+ reject(e);
2365
+ } catch (er) {
2366
+ }
2367
+ }
2368
+ }
2369
+ }
2370
+ /**
2371
+ * Execute a command. Supports concurrent requests via request id mapping.
2372
+ * Returns a Promise resolved with response.data or rejected on ERROR/timeout.
2373
+ */
2374
+ async execute(payload) {
2375
+ try {
2376
+ await this.ready;
2377
+ } catch (err) {
2378
+ throw new Error(
2379
+ "Not authenticated: " + (err && err.message ? err.message : err)
2380
+ );
2381
+ }
2382
+ if (!this.token) throw new Error("Not authenticated");
2383
+ const id = this._nextId++;
2384
+ const payloadBase = { action: payload.action, data: payload.data };
2385
+ if (this.tokenExpiry && Date.now() >= this.tokenExpiry) {
2386
+ const refreshed = await this._maybeRefreshToken();
2387
+ if (!refreshed) throw new Error("Token expired");
2388
+ }
2389
+ return new Promise((resolve2, reject) => {
2390
+ let timer = null;
2391
+ if (this.requestTimeout > 0) {
2392
+ timer = setTimeout(() => {
2393
+ if (this._pending.has(id)) {
2394
+ this._pending.delete(id);
2395
+ reject(new Error("Request timeout"));
2396
+ }
2397
+ }, this.requestTimeout);
2398
+ }
2399
+ if (this._pending.size >= this._maxPending) {
2400
+ if (this._pendingQueue.length >= this._maxQueue) {
2401
+ if (timer) clearTimeout(timer);
2402
+ return reject(new Error("Pending queue full"));
2403
+ }
2404
+ this._pendingQueue.push({ payloadBase, id, resolve: resolve2, reject, timer });
2405
+ this._processQueue().catch(() => {
2406
+ });
2407
+ return;
2408
+ }
2409
+ let finalBuf;
2410
+ try {
2411
+ finalBuf = this._buildFinalBuffer(payloadBase, id);
2412
+ } catch (e) {
2413
+ clearTimeout(timer);
2414
+ return reject(e);
2415
+ }
2416
+ this._pending.set(id, { resolve: resolve2, reject, timer });
2417
+ this._write(finalBuf).catch((e) => {
2418
+ if (timer) clearTimeout(timer);
2419
+ this._pending.delete(id);
2420
+ reject(e);
2421
+ });
2422
+ });
2423
+ }
2424
+ /**
2425
+ * Attempt to refresh token using provided tokenRefresh option if token expired.
2426
+ * options.tokenRefresh should be async function that returns { token, expiresAt }
2427
+ */
2428
+ async _maybeRefreshToken() {
2429
+ if (!this.options.tokenRefresh || typeof this.options.tokenRefresh !== "function")
2430
+ return false;
2431
+ try {
2432
+ const res = await this.options.tokenRefresh();
2433
+ if (res && res.token) {
2434
+ this.token = res.token;
2435
+ if (res.expiresAt) this.tokenExpiry = res.expiresAt;
2436
+ return true;
2437
+ }
2438
+ } catch (e) {
2439
+ this.logger?.error?.(
2440
+ "Token refresh failed",
2441
+ e && e.message ? e.message : String(e)
2442
+ );
2443
+ }
2444
+ return false;
2445
+ }
2446
+ destroy() {
2447
+ this._destroyed = true;
2448
+ if (this._reconnectTimer) {
2449
+ clearTimeout(this._reconnectTimer);
2450
+ this._reconnectTimer = null;
2451
+ }
2452
+ this._rejectReady(new Error("Client destroyed"));
2453
+ this._pending.forEach((pending, id) => {
2454
+ if (pending.timer) clearTimeout(pending.timer);
2455
+ try {
2456
+ pending.reject(new Error("Client destroyed"));
2457
+ } catch (e) {
2458
+ }
2459
+ this._pending.delete(id);
2460
+ });
2461
+ this._pendingQueue = [];
2462
+ try {
2463
+ if (this.client) this.client.destroy();
2464
+ } catch (e) {
2465
+ }
2466
+ this.client = null;
2467
+ }
2468
+ _handleDisconnect(err) {
2469
+ if (!this._connected && !this._authenticating) {
2470
+ return;
2471
+ }
2472
+ const wasAuthenticating = this._authenticating;
2473
+ this._pending.forEach((pending, id) => {
2474
+ if (pending.timer) clearTimeout(pending.timer);
2475
+ const errorMsg = wasAuthenticating ? "Authentication failed - credentials may be required" : err && err.message ? err.message : "Disconnected";
2476
+ process.nextTick(() => {
2477
+ try {
2478
+ pending.reject(new Error(errorMsg));
2479
+ } catch (e) {
2480
+ }
2481
+ });
2482
+ this._pending.delete(id);
2483
+ });
2484
+ this._connected = false;
2485
+ this._connecting = null;
2486
+ this._authenticating = false;
2487
+ if (wasAuthenticating && this.retries === 0) {
2488
+ const rejectErr = err || new Error("Authentication failed and no retries configured");
2489
+ try {
2490
+ this._rejectReady(rejectErr);
2491
+ } catch (e) {
2492
+ }
2493
+ return;
2494
+ }
2495
+ if (this._currentRetries < this.retries) {
2496
+ this._createReady();
2497
+ const base = Math.min(
2498
+ this.retryDelay * Math.pow(2, this._currentRetries),
2499
+ 3e4
2500
+ );
2501
+ const jitter = Math.floor(Math.random() * Math.min(1e3, base));
2502
+ const delay = base + jitter;
2503
+ this._currentRetries += 1;
2504
+ this.logger?.info?.(
2505
+ "Attempting reconnect in " + delay + "ms (attempt " + this._currentRetries + ")"
2506
+ );
2507
+ this.token = null;
2508
+ setTimeout(() => {
2509
+ this.connect().catch((e) => {
2510
+ this.logger?.error?.("Reconnect failed", e && e.message ? e.message : e);
2511
+ });
2512
+ }, delay);
2513
+ } else {
2514
+ try {
2515
+ this._rejectReady(err || new Error("Disconnected and no retries left"));
2516
+ } catch (e) {
2517
+ }
2518
+ }
2519
+ }
2520
+ close() {
2521
+ if (!this.client) return;
2522
+ try {
2523
+ this.client.removeAllListeners("data");
2524
+ this.client.removeAllListeners("error");
2525
+ this.client.removeAllListeners("close");
2526
+ this.client.end();
2527
+ } catch (e) {
2528
+ }
2529
+ this.client = null;
2530
+ }
2531
+ /**
2532
+ * Disconnect (alias for close)
2533
+ */
2534
+ disconnect() {
2535
+ this.close();
2536
+ }
2537
+ /**
2538
+ * Send request helper (for public API methods)
2539
+ */
2540
+ async sendRequest(action, data = {}) {
2541
+ return this.execute({ action, data });
2542
+ }
2543
+ // ========== Public API Methods ==========
2544
+ /**
2545
+ * Login with username and password
2546
+ */
2547
+ async login(username, password) {
2548
+ return this.sendRequest("login", { username, password });
2549
+ }
2550
+ /**
2551
+ * Logout from server
2552
+ */
2553
+ async logout() {
2554
+ return this.sendRequest("logout", {});
2555
+ }
2556
+ /**
2557
+ * List all databases
2558
+ */
2559
+ async listDatabases() {
2560
+ return this.sendRequest("list-dbs", {});
2561
+ }
2562
+ /**
2563
+ * Create a new database
2564
+ */
2565
+ async createDatabase(name) {
2566
+ return this.sendRequest("create-db", { databaseName: name });
2567
+ }
2568
+ /**
2569
+ * Remove a database
2570
+ */
2571
+ async removeDatabase(name) {
2572
+ return this.sendRequest("remove-db", { databaseName: name });
2573
+ }
2574
+ /**
2575
+ * Rename a database
2576
+ */
2577
+ async renameDatabase(oldName, newName) {
2578
+ return this.sendRequest("rename-db", { databaseName: oldName, newName });
2579
+ }
2580
+ /**
2581
+ * Execute code in a database
2582
+ */
2583
+ async run(code, databaseName) {
2584
+ return this.sendRequest("script-code", { code, databaseName });
2585
+ }
2586
+ /**
2587
+ * Save a database to disk
2588
+ */
2589
+ async saveDatabase(databaseName) {
2590
+ return this.sendRequest("save-db", { databaseName });
2591
+ }
2592
+ /**
2593
+ * Update database metadata
2594
+ */
2595
+ async updateDatabase(databaseName, data) {
2596
+ return this.sendRequest("update-db", { databaseName, ...data });
2597
+ }
2598
+ /**
2599
+ * Get server information
2600
+ */
2601
+ async getInfo() {
2602
+ return this.sendRequest("get-info", {});
2603
+ }
2604
+ /**
2605
+ * Execute shell command on server
2606
+ */
2607
+ async executeShell(command) {
2608
+ return this.sendRequest("shell-command", { command });
2609
+ }
2610
+ };
2611
+ var src_default = ScriptDBClient;
2612
+
2613
+ // src/wsProxy.ts
2614
+ import { exec } from "child_process";
2615
+ import { promisify } from "util";
2616
+ var execPromise = promisify(exec);
2617
+ var noopLogger2 = {
2618
+ debug: () => {
2619
+ },
2620
+ info: () => {
2621
+ },
2622
+ warn: () => {
2623
+ },
2624
+ error: () => {
2625
+ }
2626
+ };
2627
+ var WebSocketProxy = class {
2628
+ constructor(options) {
2629
+ this.wss = null;
2630
+ this.sessions = /* @__PURE__ */ new Map();
2631
+ this.options = options;
2632
+ this.logger = options.logger || noopLogger2;
2633
+ }
2634
+ /**
2635
+ * Start the WebSocket proxy server
2636
+ */
2637
+ start() {
2638
+ return new Promise((resolve2, reject) => {
2639
+ try {
2640
+ this.wss = new WebSocketServer({ port: this.options.wsPort });
2641
+ this.wss.on("listening", () => {
2642
+ this.logger.info?.(`WebSocket proxy listening on port ${this.options.wsPort}`);
2643
+ resolve2();
2644
+ });
2645
+ this.wss.on("error", (error) => {
2646
+ this.logger.error?.("WebSocket server error:", error);
2647
+ reject(error);
2648
+ });
2649
+ this.wss.on("connection", (ws) => {
2650
+ this.handleConnection(ws);
2651
+ });
2652
+ } catch (error) {
2653
+ reject(error);
2654
+ }
2655
+ });
2656
+ }
2657
+ /**
2658
+ * Handle new WebSocket connection
2659
+ */
2660
+ handleConnection(ws) {
2661
+ this.logger.info?.("New WebSocket connection");
2662
+ const session = {
2663
+ ws,
2664
+ client: null,
2665
+ authenticated: false
2666
+ };
2667
+ this.sessions.set(ws, session);
2668
+ ws.on("message", async (data) => {
2669
+ await this.handleMessage(session, data);
2670
+ });
2671
+ ws.on("close", () => {
2672
+ this.handleClose(session);
2673
+ });
2674
+ ws.on("error", (error) => {
2675
+ this.logger.error?.("WebSocket error:", error);
2676
+ this.handleClose(session);
2677
+ });
2678
+ }
2679
+ /**
2680
+ * Handle incoming message from browser
2681
+ */
2682
+ async handleMessage(session, data) {
2683
+ try {
2684
+ const message = JSON.parse(data.toString());
2685
+ const { id, action, data: payload } = message;
2686
+ if (action === "login") {
2687
+ await this.handleLogin(session, id, payload);
2688
+ return;
2689
+ }
2690
+ if (!session.authenticated || !session.client) {
2691
+ this.sendError(session.ws, id, "Not authenticated");
2692
+ return;
2693
+ }
2694
+ await this.forwardAction(session, id, action, payload);
2695
+ } catch (error) {
2696
+ this.logger.error?.("Error handling message:", error);
2697
+ this.sendError(session.ws, void 0, error.message);
2698
+ }
2699
+ }
2700
+ /**
2701
+ * Handle login action
2702
+ */
2703
+ async handleLogin(session, id, payload) {
2704
+ try {
2705
+ const { username, password, secure: clientSecure } = payload;
2706
+ const serverSecure = this.options.secure !== void 0 ? this.options.secure : false;
2707
+ console.log("Login validation:", { clientSecure, serverSecure, username: username || "(empty)" });
2708
+ if (clientSecure !== void 0 && clientSecure !== serverSecure) {
2709
+ const errorMsg = serverSecure ? 'Server is in secure mode (secure: true). Please uncheck "Anonymous Connection" and provide credentials.' : 'Server is in non-secure mode (secure: false). Please check "Anonymous Connection".';
2710
+ this.logger.warn?.("Secure mode mismatch:", { clientSecure, serverSecure });
2711
+ throw new Error(errorMsg);
2712
+ }
2713
+ let uri;
2714
+ if (username && username !== "") {
2715
+ uri = `scriptdb://${username}:${password || ""}@${this.options.scriptdbHost}:${this.options.scriptdbPort}`;
2716
+ } else {
2717
+ uri = `scriptdb://${this.options.scriptdbHost}:${this.options.scriptdbPort}`;
2718
+ }
2719
+ this.logger.info?.("Creating ScriptDB client with URI:", uri.replace(/:[^:@]+@/, ":****@"));
2720
+ const client = new src_default(uri, {
2721
+ secure: false,
2722
+ // Always use plain TCP (secure option controls authentication, not transport)
2723
+ logger: this.logger,
2724
+ requestTimeout: 0,
2725
+ // Disable timeout - let browser handle it
2726
+ retries: 0
2727
+ // No retries in proxy
2728
+ });
2729
+ console.log("Connecting to ScriptDB server...");
2730
+ this.logger.info?.("Connecting to ScriptDB server...");
2731
+ const connectStartTime = Date.now();
2732
+ await client.connect().catch((err) => {
2733
+ throw err;
2734
+ });
2735
+ const connectDuration = Date.now() - connectStartTime;
2736
+ console.log(`ScriptDB client connected and authenticated in ${connectDuration}ms`);
2737
+ this.logger.info?.("ScriptDB client connected and authenticated");
2738
+ session.client = client;
2739
+ session.authenticated = true;
2740
+ console.log("Sending AUTH OK to browser with id:", id);
2741
+ this.logger.info?.("Sending AUTH OK to browser");
2742
+ this.sendResponse(session.ws, id, "login", "AUTH OK", { token: "proxy-session" });
2743
+ console.log("AUTH OK sent to browser");
2744
+ } catch (error) {
2745
+ this.logger.error?.("Login failed:", error);
2746
+ this.sendResponse(session.ws, id, "login", "AUTH FAILED", error.message);
2747
+ if (session.client) {
2748
+ try {
2749
+ session.client.destroy();
2750
+ } catch (e) {
2751
+ }
2752
+ session.client = null;
2753
+ }
2754
+ this.sessions.delete(session.ws);
2755
+ session.ws.close();
2756
+ }
2757
+ }
2758
+ /**
2759
+ * Forward action to ScriptDB client
2760
+ */
2761
+ async forwardAction(session, id, action, payload) {
2762
+ try {
2763
+ console.log("Forwarding action:", action, "payload:", payload);
2764
+ const client = session.client;
2765
+ let result;
2766
+ switch (action) {
2767
+ case "logout":
2768
+ console.log("Executing logout");
2769
+ result = await client.execute({ action: "logout" });
2770
+ session.authenticated = false;
2771
+ session.client = null;
2772
+ break;
2773
+ case "list-dbs":
2774
+ console.log("Executing list-dbs");
2775
+ result = await client.execute({ action: "list-dbs" });
2776
+ console.log("list-dbs result:", result);
2777
+ break;
2778
+ case "create-db":
2779
+ result = await client.execute({
2780
+ action: "create-db",
2781
+ data: { databaseName: payload.databaseName }
2782
+ });
2783
+ break;
2784
+ case "remove-db":
2785
+ result = await client.execute({
2786
+ action: "remove-db",
2787
+ data: { databaseName: payload.databaseName }
2788
+ });
2789
+ break;
2790
+ case "rename-db":
2791
+ result = await client.execute({
2792
+ action: "rename-db",
2793
+ data: {
2794
+ databaseName: payload.databaseName,
2795
+ newName: payload.newName
2796
+ }
2797
+ });
2798
+ break;
2799
+ case "script-code":
2800
+ console.log("wsProxy: executing script-code");
2801
+ result = await client.execute({
2802
+ action: "script-code",
2803
+ data: {
2804
+ code: payload.code,
2805
+ databaseName: payload.databaseName
2806
+ }
2807
+ });
2808
+ console.log("wsProxy: script-code result:", result);
2809
+ break;
2810
+ case "save-db":
2811
+ result = await client.execute({
2812
+ action: "save-db",
2813
+ data: { databaseName: payload.databaseName }
2814
+ });
2815
+ break;
2816
+ case "update-db":
2817
+ result = await client.execute({
2818
+ action: "update-db",
2819
+ data: payload
2820
+ });
2821
+ break;
2822
+ case "get-info":
2823
+ result = await client.execute({ action: "get-info" });
2824
+ break;
2825
+ case "get-db":
2826
+ result = await client.execute({
2827
+ action: "get-db",
2828
+ data: { databaseName: payload.databaseName }
2829
+ });
2830
+ break;
2831
+ case "shell-command":
2832
+ result = await this.executeShellCommand(payload.command);
2833
+ break;
2834
+ default:
2835
+ throw new Error(`Unknown action: ${action}`);
2836
+ }
2837
+ this.sendResponse(session.ws, id, action, "OK", result);
2838
+ } catch (error) {
2839
+ this.logger.error?.(`Error executing ${action}:`, error);
2840
+ this.sendResponse(session.ws, id, action, "ERROR", error.message);
2841
+ }
2842
+ }
2843
+ /**
2844
+ * Send response to browser
2845
+ */
2846
+ sendResponse(ws, id, action, message, data) {
2847
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
2848
+ console.error("Cannot send response - WebSocket not ready:", {
2849
+ exists: !!ws,
2850
+ readyState: ws?.readyState,
2851
+ expectedState: WebSocket.OPEN
2852
+ });
2853
+ this.logger.error?.("Cannot send response: WebSocket not ready");
2854
+ return;
2855
+ }
2856
+ const response = {
2857
+ id,
2858
+ action,
2859
+ message,
2860
+ data
2861
+ };
2862
+ console.log("Sending response to browser:", response);
2863
+ this.logger.info?.("Sending response to browser:", { id, action, message });
2864
+ try {
2865
+ ws.send(JSON.stringify(response));
2866
+ console.log("Response sent successfully via ws.send()");
2867
+ this.logger.info?.("Response sent successfully");
2868
+ } catch (error) {
2869
+ console.error("Failed to send response:", error);
2870
+ this.logger.error?.("Failed to send response:", error);
2871
+ }
2872
+ }
2873
+ /**
2874
+ * Send error to browser
2875
+ */
2876
+ sendError(ws, id, error) {
2877
+ this.sendResponse(ws, id, "error", "ERROR", error);
2878
+ }
2879
+ /**
2880
+ * Execute shell command
2881
+ */
2882
+ async executeShellCommand(command) {
2883
+ try {
2884
+ this.logger.info?.(`Executing shell command: ${command}`);
2885
+ if (command.length > 1e3) {
2886
+ throw new Error("Command too long");
2887
+ }
2888
+ const { stdout, stderr } = await execPromise(command, {
2889
+ timeout: 3e4,
2890
+ // 30 second timeout
2891
+ maxBuffer: 1024 * 1024,
2892
+ // 1MB buffer
2893
+ cwd: process.cwd()
2894
+ });
2895
+ return {
2896
+ stdout: stdout || "",
2897
+ stderr: stderr || ""
2898
+ };
2899
+ } catch (error) {
2900
+ this.logger.error?.("Shell command failed:", error);
2901
+ return {
2902
+ stdout: "",
2903
+ stderr: error.message || String(error)
2904
+ };
2905
+ }
2906
+ }
2907
+ /**
2908
+ * Handle connection close
2909
+ */
2910
+ handleClose(session) {
2911
+ this.logger.info?.("WebSocket connection closed");
2912
+ if (session.client) {
2913
+ try {
2914
+ session.client.destroy();
2915
+ } catch (error) {
2916
+ this.logger.error?.("Error destroying client:", error);
2917
+ }
2918
+ session.client = null;
2919
+ }
2920
+ this.sessions.delete(session.ws);
2921
+ }
2922
+ /**
2923
+ * Stop the WebSocket proxy server
2924
+ */
2925
+ async stop() {
2926
+ return new Promise((resolve2) => {
2927
+ this.sessions.forEach((session) => {
2928
+ if (session.client) {
2929
+ try {
2930
+ session.client.destroy();
2931
+ } catch (error) {
2932
+ this.logger.error?.("Error destroying client:", error);
2933
+ }
2934
+ }
2935
+ session.ws.close();
2936
+ });
2937
+ this.sessions.clear();
2938
+ if (this.wss) {
2939
+ this.wss.close(() => {
2940
+ this.logger.info?.("WebSocket proxy stopped");
2941
+ resolve2();
2942
+ });
2943
+ } else {
2944
+ resolve2();
2945
+ }
2946
+ });
2947
+ }
2948
+ };
2949
+ async function createWebSocketProxy(options) {
2950
+ const proxy = new WebSocketProxy(options);
2951
+ await proxy.start();
2952
+ return proxy;
2953
+ }
2954
+
2955
+ // src/index.ts
2956
+ import SystemModuleResolver from "@scriptdb/system-modules";
2957
+ function exitWithError(message, err) {
2958
+ console.error(message);
2959
+ if (err) console.error(err);
2960
+ process.exit(1);
2961
+ }
2962
+ async function server() {
2963
+ let basePath = path3.join(getHomeDir(), ".scriptdb");
2964
+ try {
2965
+ console.log("Setting up scriptdb directory...");
2966
+ await setupDir();
2967
+ console.log("Setup complete");
2968
+ } catch (err) {
2969
+ exitWithError("Failed to setup scriptdb directory", err);
2970
+ }
2971
+ console.log("Loading configuration...");
2972
+ let config = {
2973
+ host: "localhost",
2974
+ port: 1234,
2975
+ users: [
2976
+ {
2977
+ username: "",
2978
+ password: void 0
2979
+ }
2980
+ ],
2981
+ folder: "databases",
2982
+ secure: false
2983
+ };
2984
+ const configPath = path3.join(basePath, "config.json");
2985
+ if (fs4.existsSync(configPath)) {
2986
+ try {
2987
+ const fileContent = fs4.readFileSync(configPath, "utf8");
2988
+ const parsed = JSON.parse(fileContent);
2989
+ if (parsed && typeof parsed === "object") {
2990
+ const parsedConfig = parsed;
2991
+ const allowed = {};
2992
+ if (typeof parsedConfig.host === "string") allowed.host = parsedConfig.host;
2993
+ if (parsedConfig.port !== void 0) allowed.port = parsedConfig.port;
2994
+ if (parsedConfig.users !== void 0) {
2995
+ if (Array.isArray(parsedConfig.users)) {
2996
+ if (parsedConfig.users.length === 0) {
2997
+ allowed.users = [];
2998
+ } else if (typeof parsedConfig.users[0] === "string") {
2999
+ allowed.users = parsedConfig.users.map((username) => ({
3000
+ username,
3001
+ password: "",
3002
+ hash: false
3003
+ }));
3004
+ } else {
3005
+ allowed.users = parsedConfig.users;
3006
+ }
3007
+ } else {
3008
+ allowed.users = [parsedConfig.users];
3009
+ }
3010
+ }
3011
+ if (typeof parsedConfig.folder === "string") allowed.folder = parsedConfig.folder;
3012
+ if (typeof parsedConfig.secure === "boolean") allowed.secure = parsedConfig.secure;
3013
+ config = { ...config, ...allowed };
3014
+ } else {
3015
+ console.warn("config.json parsed to a non-object, ignoring");
3016
+ }
3017
+ } catch (err) {
3018
+ console.warn("Failed to read/parse config.json, using defaults:", err);
3019
+ }
3020
+ }
3021
+ if (typeof config.port === "string") {
3022
+ const p = parseInt(config.port, 10);
3023
+ if (!Number.isNaN(p)) config.port = p;
3024
+ }
3025
+ if (!Number.isFinite(config.port)) config.port = 1234;
3026
+ if (config.port < 1 || config.port > 65535) {
3027
+ console.warn("Configured port out of range, falling back to 1234");
3028
+ config.port = 1234;
3029
+ }
3030
+ if (!Array.isArray(config.users)) {
3031
+ if (config.users && typeof config.users === "object") config.users = [config.users];
3032
+ else config.users = [{ username: "", password: void 0 }];
3033
+ }
3034
+ config.users = config.users.map((u) => {
3035
+ if (!u) return { username: "", password: void 0 };
3036
+ if (typeof u === "string") return { username: u, password: void 0 };
3037
+ if (typeof u === "object") {
3038
+ return {
3039
+ username: typeof u.username === "string" ? u.username : "",
3040
+ password: typeof u.password === "string" ? u.password : void 0,
3041
+ hash: typeof u.hash === "boolean" ? u.hash : void 0,
3042
+ algorithm: u.algorithm === "bcrypt" || u.algorithm === "sha256" || u.algorithm === "sha512" ? u.algorithm : void 0
3043
+ };
3044
+ }
3045
+ return { username: "", password: void 0 };
3046
+ });
3047
+ const baseDir = path3.resolve(basePath, String(config.folder || "databases"));
3048
+ const pluginDir = path3.resolve(basePath, "plugins");
3049
+ try {
3050
+ if (!fs4.existsSync(baseDir)) {
3051
+ fs4.mkdirSync(baseDir, { recursive: true });
3052
+ console.log(`Created folder for databases at ${baseDir}`);
3053
+ } else if (!fs4.statSync(baseDir).isDirectory()) {
3054
+ exitWithError(`Configured folder exists but is not a directory: ${baseDir}`);
3055
+ }
3056
+ } catch (err) {
3057
+ exitWithError("Failed to ensure database folder exists", err);
3058
+ }
3059
+ let vm;
3060
+ try {
3061
+ vm = new VM({ language: "ts", registerModules: {} });
3062
+ console.log("Loading databases and plugins into VM...");
3063
+ const systemModules = await SystemModuleResolver();
3064
+ vm.register(systemModules);
3065
+ console.log("VM initialized with databases and plugins");
3066
+ } catch (err) {
3067
+ exitWithError("Failed to initialize VM", err);
3068
+ }
3069
+ let storage = null;
3070
+ try {
3071
+ storage = new Storage2(baseDir, basePath);
3072
+ await storage.initialize();
3073
+ console.log(`Storage initialized at ${baseDir} (git at ${basePath})`);
3074
+ } catch (err) {
3075
+ exitWithError("Failed to initialize Storage", err);
3076
+ }
3077
+ if (!storage) {
3078
+ exitWithError("Storage was not initialized");
3079
+ }
3080
+ let scriptDBServer;
3081
+ try {
3082
+ scriptDBServer = new Protocal({
3083
+ host: config.host,
3084
+ port: config.port,
3085
+ users: config.users,
3086
+ vm,
3087
+ storage
3088
+ });
3089
+ if (typeof scriptDBServer.start !== "function") {
3090
+ exitWithError("Protocal.start is not a function");
3091
+ }
3092
+ scriptDBServer.start();
3093
+ console.log(`ScriptDB server started on ${config.host}:${config.port}`);
3094
+ } catch (err) {
3095
+ exitWithError("Failed to start Protocal", err);
3096
+ }
3097
+ try {
3098
+ const wsPort = config.port + 1;
3099
+ await createWebSocketProxy({
3100
+ wsPort,
3101
+ scriptdbHost: config.host,
3102
+ scriptdbPort: config.port,
3103
+ secure: config.secure !== void 0 ? config.secure : false,
3104
+ logger: {
3105
+ debug: console.log,
3106
+ info: console.log,
3107
+ warn: console.warn,
3108
+ error: console.error
3109
+ }
3110
+ });
3111
+ console.log(`WebSocket proxy started on port ${wsPort}`);
3112
+ } catch (err) {
3113
+ console.warn("Failed to start WebSocket proxy (browser support disabled)", err);
3114
+ }
3115
+ }
3116
+ var index_default = server;
3117
+ export {
3118
+ index_default as default,
3119
+ server
3120
+ };
3121
+ //# sourceMappingURL=index.mjs.map