@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/README.md +1 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +813 -3469
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3121 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +6 -9
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
|