bs9 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -9
- package/bin/bs9 +58 -7
- package/dist/bs9-064xs9r9. +148 -0
- package/dist/bs9-0gqcrp5t. +144 -0
- package/dist/bs9-nv7nseny. +197 -0
- package/dist/bs9-r6b9zpw0. +156 -0
- package/dist/bs9.js +2 -0
- package/package.json +3 -4
- package/src/commands/deps.ts +295 -0
- package/src/commands/profile.ts +338 -0
- package/src/commands/restart.ts +28 -3
- package/src/commands/start.ts +201 -21
- package/src/commands/status.ts +154 -109
- package/src/commands/stop.ts +31 -3
- package/src/commands/web.ts +29 -3
- package/src/database/pool.ts +335 -0
- package/src/loadbalancer/manager.ts +481 -0
- package/src/macos/launchd.ts +402 -0
- package/src/platform/detect.ts +137 -0
- package/src/windows/service.ts +391 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// Security: Input validation functions
|
|
4
|
+
function isValidHost(host: string): boolean {
|
|
5
|
+
const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
6
|
+
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
7
|
+
const localhostRegex = /^(localhost|127\.0\.0\.1)$/;
|
|
8
|
+
|
|
9
|
+
return hostnameRegex.test(host) && host.length <= 253 ||
|
|
10
|
+
ipv4Regex.test(host) ||
|
|
11
|
+
localhostRegex.test(host);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isValidDatabaseName(name: string): boolean {
|
|
15
|
+
// Allow alphanumeric, underscores, and hyphens only
|
|
16
|
+
return /^[a-zA-Z0-9_-]+$/.test(name) && name.length <= 64;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isValidUsername(username: string): boolean {
|
|
20
|
+
// Allow alphanumeric, underscores, and hyphens only
|
|
21
|
+
return /^[a-zA-Z0-9_-]+$/.test(username) && username.length <= 32;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sanitizeSQL(sql: string): string {
|
|
25
|
+
// Basic SQL injection prevention
|
|
26
|
+
const dangerousPatterns = [
|
|
27
|
+
/drop\s+table/i,
|
|
28
|
+
/delete\s+from/i,
|
|
29
|
+
/truncate\s+table/i,
|
|
30
|
+
/exec\s*\(/i,
|
|
31
|
+
/xp_cmdshell/i,
|
|
32
|
+
/sp_executesql/i,
|
|
33
|
+
/union\s+select/i,
|
|
34
|
+
/insert\s+into/i,
|
|
35
|
+
/update\s+set/i
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const pattern of dangerousPatterns) {
|
|
39
|
+
if (pattern.test(sql)) {
|
|
40
|
+
throw new Error(`❌ Security: Dangerous SQL pattern detected`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return sql.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface DatabaseConnection {
|
|
48
|
+
id: string;
|
|
49
|
+
created: number;
|
|
50
|
+
lastUsed: number;
|
|
51
|
+
inUse: boolean;
|
|
52
|
+
host: string;
|
|
53
|
+
port: number;
|
|
54
|
+
database: string;
|
|
55
|
+
username: string;
|
|
56
|
+
checkedOut: boolean;
|
|
57
|
+
query: (sql: string, params?: any[]) => Promise<any[]>;
|
|
58
|
+
close: () => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface DatabaseConfig {
|
|
62
|
+
host: string;
|
|
63
|
+
port: number;
|
|
64
|
+
database: string;
|
|
65
|
+
username: string;
|
|
66
|
+
password: string;
|
|
67
|
+
ssl?: boolean;
|
|
68
|
+
maxConnections?: number;
|
|
69
|
+
minConnections?: number;
|
|
70
|
+
acquireTimeoutMillis?: number;
|
|
71
|
+
idleTimeoutMillis?: number;
|
|
72
|
+
reapIntervalMillis?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface PoolStats {
|
|
76
|
+
totalConnections: number;
|
|
77
|
+
activeConnections: number;
|
|
78
|
+
idleConnections: number;
|
|
79
|
+
waitingClients: number;
|
|
80
|
+
maxConnections: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class DatabasePool {
|
|
84
|
+
private config: DatabaseConfig;
|
|
85
|
+
private connections: DatabaseConnection[] = [];
|
|
86
|
+
private waitingQueue: Array<{
|
|
87
|
+
resolve: (connection: DatabaseConnection) => void;
|
|
88
|
+
reject: (error: Error) => void;
|
|
89
|
+
timestamp: number;
|
|
90
|
+
}> = [];
|
|
91
|
+
private reapingInterval?: NodeJS.Timeout;
|
|
92
|
+
|
|
93
|
+
constructor(config: DatabaseConfig) {
|
|
94
|
+
// Security: Validate configuration
|
|
95
|
+
if (!isValidHost(config.host)) {
|
|
96
|
+
throw new Error(`❌ Security: Invalid database host: ${config.host}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isValidDatabaseName(config.database)) {
|
|
100
|
+
throw new Error(`❌ Security: Invalid database name: ${config.database}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!isValidUsername(config.username)) {
|
|
104
|
+
throw new Error(`❌ Security: Invalid database username: ${config.username}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const portNum = Number(config.port);
|
|
108
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
109
|
+
throw new Error(`❌ Security: Invalid port number: ${config.port}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.config = {
|
|
113
|
+
maxConnections: 10,
|
|
114
|
+
minConnections: 2,
|
|
115
|
+
acquireTimeoutMillis: 30000,
|
|
116
|
+
idleTimeoutMillis: 30000,
|
|
117
|
+
reapIntervalMillis: 1000,
|
|
118
|
+
...config,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.startReaper();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async createConnection(): Promise<DatabaseConnection> {
|
|
125
|
+
const id = `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
|
|
128
|
+
// Simulate database connection
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
id,
|
|
133
|
+
created: now,
|
|
134
|
+
lastUsed: now,
|
|
135
|
+
inUse: false,
|
|
136
|
+
host: this.config.host,
|
|
137
|
+
port: this.config.port,
|
|
138
|
+
database: this.config.database,
|
|
139
|
+
username: this.config.username,
|
|
140
|
+
checkedOut: false,
|
|
141
|
+
query: async (sql: string, params?: any[]) => {
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, Math.random() * 50 + 10));
|
|
143
|
+
return [{ id: 1, data: 'mock_result' }];
|
|
144
|
+
},
|
|
145
|
+
close: async () => {
|
|
146
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async acquire(): Promise<DatabaseConnection> {
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
|
|
154
|
+
const availableConnection = this.connections.find(conn => !conn.inUse);
|
|
155
|
+
if (availableConnection) {
|
|
156
|
+
availableConnection.inUse = true;
|
|
157
|
+
availableConnection.lastUsed = now;
|
|
158
|
+
return availableConnection;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (this.connections.length < this.config.maxConnections!) {
|
|
162
|
+
const newConnection = await this.createConnection();
|
|
163
|
+
newConnection.inUse = true;
|
|
164
|
+
this.connections.push(newConnection);
|
|
165
|
+
return newConnection;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const timeoutId = setTimeout(() => {
|
|
170
|
+
const index = this.waitingQueue.findIndex(item => item.resolve === resolve);
|
|
171
|
+
if (index !== -1) {
|
|
172
|
+
this.waitingQueue.splice(index, 1);
|
|
173
|
+
}
|
|
174
|
+
reject(new Error('Connection acquire timeout'));
|
|
175
|
+
}, this.config.acquireTimeoutMillis);
|
|
176
|
+
|
|
177
|
+
this.waitingQueue.push({
|
|
178
|
+
resolve: (connection) => {
|
|
179
|
+
clearTimeout(timeoutId);
|
|
180
|
+
resolve(connection);
|
|
181
|
+
},
|
|
182
|
+
reject: (error) => {
|
|
183
|
+
clearTimeout(timeoutId);
|
|
184
|
+
reject(error);
|
|
185
|
+
},
|
|
186
|
+
timestamp: now,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async release(connection: DatabaseConnection): Promise<void> {
|
|
192
|
+
connection.inUse = false;
|
|
193
|
+
connection.lastUsed = Date.now();
|
|
194
|
+
|
|
195
|
+
const waiting = this.waitingQueue.shift();
|
|
196
|
+
if (waiting) {
|
|
197
|
+
connection.inUse = true;
|
|
198
|
+
waiting.resolve(connection);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async startReaper(): Promise<void> {
|
|
203
|
+
this.reapingInterval = setInterval(async () => {
|
|
204
|
+
await this.reapIdleConnections();
|
|
205
|
+
}, this.config.reapIntervalMillis);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async reapIdleConnections(): Promise<void> {
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const minConnections = this.config.minConnections!;
|
|
211
|
+
|
|
212
|
+
const idleConnections = this.connections.filter(conn =>
|
|
213
|
+
!conn.inUse &&
|
|
214
|
+
now - conn.lastUsed > this.config.idleTimeoutMillis! &&
|
|
215
|
+
this.connections.length > minConnections
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
for (const conn of idleConnections) {
|
|
219
|
+
const index = this.connections.indexOf(conn);
|
|
220
|
+
if (index !== -1) {
|
|
221
|
+
await conn.close();
|
|
222
|
+
this.connections.splice(index, 1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getStats(): PoolStats {
|
|
228
|
+
const activeConnections = this.connections.filter(conn => conn.inUse).length;
|
|
229
|
+
const idleConnections = this.connections.filter(conn => !conn.inUse).length;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
totalConnections: this.connections.length,
|
|
233
|
+
activeConnections,
|
|
234
|
+
idleConnections,
|
|
235
|
+
waitingClients: this.waitingQueue.length,
|
|
236
|
+
maxConnections: this.config.maxConnections!,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async close(): Promise<void> {
|
|
241
|
+
if (this.reapingInterval) {
|
|
242
|
+
clearInterval(this.reapingInterval);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const closePromises = this.connections.map(conn => conn.close());
|
|
246
|
+
await Promise.all(closePromises);
|
|
247
|
+
this.connections = [];
|
|
248
|
+
|
|
249
|
+
for (const waiting of this.waitingQueue) {
|
|
250
|
+
waiting.reject(new Error('Pool is closing'));
|
|
251
|
+
}
|
|
252
|
+
this.waitingQueue = [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async testConnection(): Promise<boolean> {
|
|
256
|
+
try {
|
|
257
|
+
const conn = await this.acquire();
|
|
258
|
+
await conn.query('SELECT 1');
|
|
259
|
+
await this.release(conn);
|
|
260
|
+
return true;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function dbpoolCommand(action: string, options: any): Promise<void> {
|
|
268
|
+
console.log('🗄️ BS9 Database Connection Pool Management');
|
|
269
|
+
console.log('='.repeat(80));
|
|
270
|
+
|
|
271
|
+
const config: DatabaseConfig = {
|
|
272
|
+
host: options.host || 'localhost',
|
|
273
|
+
port: parseInt(options.port) || 5432,
|
|
274
|
+
database: options.database || 'testdb',
|
|
275
|
+
username: options.username || 'user',
|
|
276
|
+
password: options.password || 'password',
|
|
277
|
+
maxConnections: parseInt(options.maxConnections) || 10,
|
|
278
|
+
minConnections: parseInt(options.minConnections) || 2,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
switch (action) {
|
|
283
|
+
case 'start':
|
|
284
|
+
console.log(`🚀 Starting database pool...`);
|
|
285
|
+
console.log(` Host: ${config.host}:${config.port}`);
|
|
286
|
+
console.log(` Database: ${config.database}`);
|
|
287
|
+
console.log(` Max Connections: ${config.maxConnections}`);
|
|
288
|
+
console.log(` Min Connections: ${config.minConnections}`);
|
|
289
|
+
|
|
290
|
+
const pool = new DatabasePool(config);
|
|
291
|
+
const isConnected = await pool.testConnection();
|
|
292
|
+
|
|
293
|
+
if (isConnected) {
|
|
294
|
+
console.log('✅ Database pool started successfully');
|
|
295
|
+
const stats = pool.getStats();
|
|
296
|
+
console.log(`📊 Initial Stats: ${stats.totalConnections} connections`);
|
|
297
|
+
console.log('✅ Pool management commands ready');
|
|
298
|
+
} else {
|
|
299
|
+
console.error('❌ Failed to connect to database');
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
|
|
304
|
+
case 'test':
|
|
305
|
+
console.log('🧪 Testing database pool...');
|
|
306
|
+
const testPool = new DatabasePool(config);
|
|
307
|
+
const connected = await testPool.testConnection();
|
|
308
|
+
console.log(connected ? '✅ Connection test passed' : '❌ Connection test failed');
|
|
309
|
+
await testPool.close();
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case 'stats':
|
|
313
|
+
console.log('📊 Database Pool Statistics');
|
|
314
|
+
console.log('-'.repeat(40));
|
|
315
|
+
const statsPool = new DatabasePool(config);
|
|
316
|
+
const stats = statsPool.getStats();
|
|
317
|
+
console.log(`Total Connections: ${stats.totalConnections}`);
|
|
318
|
+
console.log(`Active Connections: ${stats.activeConnections}`);
|
|
319
|
+
console.log(`Idle Connections: ${stats.idleConnections}`);
|
|
320
|
+
console.log(`Waiting Clients: ${stats.waitingClients}`);
|
|
321
|
+
console.log(`Max Connections: ${stats.maxConnections}`);
|
|
322
|
+
console.log(`Pool Utilization: ${((stats.activeConnections / stats.maxConnections) * 100).toFixed(1)}%`);
|
|
323
|
+
await statsPool.close();
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
default:
|
|
327
|
+
console.error(`❌ Unknown action: ${action}`);
|
|
328
|
+
console.log('Available actions: start, test, stats');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error(`❌ Failed to ${action} pool: ${error}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
}
|