bs9 1.0.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.
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { join, basename, resolve, dirname } from "node:path";
5
+ import { execSync } from "node:child_process";
6
+ import { randomUUID } from "node:crypto";
7
+ import { writeFileSync, mkdirSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+
10
+ interface StartOptions {
11
+ name?: string;
12
+ port?: string;
13
+ env?: string[];
14
+ otel?: boolean;
15
+ prometheus?: boolean;
16
+ build?: boolean;
17
+ }
18
+
19
+ export async function startCommand(file: string, options: StartOptions): Promise<void> {
20
+ const fullPath = resolve(file);
21
+ if (!existsSync(fullPath)) {
22
+ console.error(`❌ File not found: ${fullPath}`);
23
+ process.exit(1);
24
+ }
25
+
26
+ const serviceName = options.name || basename(fullPath, fullPath.endsWith('.ts') ? '.ts' : '.js').replace(/[^a-zA-Z0-9-_]/g, "_");
27
+ const port = options.port || "3000";
28
+
29
+ // Port warning for privileged ports
30
+ const portNum = Number(port);
31
+ if (portNum < 1024) {
32
+ console.warn(`⚠️ Port ${port} is privileged (< 1024).`);
33
+ console.warn(" Options:");
34
+ console.warn(" - Use port >= 1024 (recommended)");
35
+ console.warn(" - Run with sudo (not recommended for user services)");
36
+ console.warn(" - Use port forwarding: `sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000`");
37
+ }
38
+
39
+ // Handle TypeScript files and build option
40
+ let execPath = fullPath;
41
+ let isBuilt = false;
42
+
43
+ if (fullPath.endsWith('.ts')) {
44
+ if (options.build) {
45
+ // AOT: Build TypeScript to single executable
46
+ console.log("🔨 Building TypeScript for production...");
47
+ const buildDir = join(dirname(fullPath), ".bs9-build");
48
+ mkdirSync(buildDir, { recursive: true });
49
+
50
+ const outputFile = join(buildDir, `${serviceName}.js`);
51
+ try {
52
+ execSync(`bun build ${fullPath} --outdir ${buildDir} --target bun --minify --splitting`, { stdio: "inherit" });
53
+ execPath = outputFile;
54
+ isBuilt = true;
55
+ console.log(`✅ Built to: ${execPath}`);
56
+ } catch (err) {
57
+ console.error(`❌ Build failed: ${err}`);
58
+ process.exit(1);
59
+ }
60
+ } else {
61
+ // JIT: Run TypeScript directly (default)
62
+ console.log("⚡ Running TypeScript in JIT mode");
63
+ }
64
+ }
65
+
66
+ // Phase 2: Pre-start Security Audit
67
+ const auditResult = await securityAudit(execPath);
68
+ if (auditResult.critical.length > 0) {
69
+ console.error("🚨 Critical security issues found:");
70
+ auditResult.critical.forEach(issue => console.error(` - ${issue}`));
71
+ console.error("\nRefusing to start. Fix issues or use --force to override.");
72
+ process.exit(1);
73
+ }
74
+
75
+ if (auditResult.warning.length > 0) {
76
+ console.warn("⚠️ Security warnings:");
77
+ auditResult.warning.forEach(issue => console.warn(` - ${issue}`));
78
+ }
79
+
80
+ // Phase 1: Generate hardened systemd unit
81
+ const unitContent = generateSystemdUnit({
82
+ serviceName,
83
+ fullPath: execPath,
84
+ port,
85
+ env: options.env || [],
86
+ otel: options.otel ?? true,
87
+ prometheus: options.prometheus ?? true,
88
+ });
89
+
90
+ const unitPath = join(homedir(), ".config/systemd/user", `${serviceName}.service`);
91
+
92
+ // Create user systemd directory if it doesn't exist
93
+ const userSystemdDir = join(homedir(), ".config/systemd/user");
94
+ if (!existsSync(userSystemdDir)) {
95
+ mkdirSync(userSystemdDir, { recursive: true });
96
+ console.log(`📁 Created user systemd directory: ${userSystemdDir}`);
97
+ }
98
+
99
+ try {
100
+ writeFileSync(unitPath, unitContent, { encoding: "utf-8" });
101
+ console.log(`✅ Systemd user unit written to: ${unitPath}`);
102
+ } catch (err) {
103
+ console.error(`❌ Failed to write systemd user unit: ${err}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ // Reload and start using user systemd
108
+ try {
109
+ execSync("systemctl --user daemon-reload", { stdio: "inherit" });
110
+ execSync(`systemctl --user enable ${serviceName}`, { stdio: "inherit" });
111
+ execSync(`systemctl --user start ${serviceName}`, { stdio: "inherit" });
112
+ console.log(`🚀 Service '${serviceName}' started successfully`);
113
+ console.log(` Health: http://localhost:${port}/healthz`);
114
+ console.log(` Metrics: http://localhost:${port}/metrics`);
115
+ } catch (err) {
116
+ console.error(`❌ Failed to start user service: ${err}`);
117
+ process.exit(1);
118
+ }
119
+ }
120
+
121
+ interface SecurityAuditResult {
122
+ critical: string[];
123
+ warning: string[];
124
+ }
125
+
126
+ async function securityAudit(filePath: string): Promise<SecurityAuditResult> {
127
+ const result: SecurityAuditResult = { critical: [], warning: [] };
128
+ const content = readFileSync(filePath, "utf-8");
129
+ const stat = statSync(filePath);
130
+
131
+ // Check file permissions
132
+ if (stat.mode & 0o002) {
133
+ result.critical.push("File is world-writable");
134
+ }
135
+
136
+ // Check for dangerous patterns
137
+ const dangerousPatterns = [
138
+ { pattern: /eval\s*\(/, msg: "Use of eval() detected" },
139
+ { pattern: /Function\s*\(/, msg: "Dynamic function construction detected" },
140
+ { pattern: /child_process\.exec\s*\(/, msg: "Unsafe child_process.exec() detected" },
141
+ { pattern: /require\s*\(\s*["']fs["']\s*\)/, msg: "Direct fs module usage (potential file system access)" },
142
+ { pattern: /process\.env\.\w+\s*\+\s*["']/, msg: "Potential command injection via env concatenation" },
143
+ ];
144
+
145
+ for (const { pattern, msg } of dangerousPatterns) {
146
+ if (pattern.test(content)) {
147
+ result.critical.push(msg);
148
+ }
149
+ }
150
+
151
+ // Check for network access patterns
152
+ if (content.includes("fetch(") || content.includes("http.request")) {
153
+ result.warning.push("Network access detected - ensure outbound rules are in place");
154
+ }
155
+
156
+ // Check for file system writes
157
+ if (content.includes("writeFileSync") || content.includes("createWriteStream")) {
158
+ result.warning.push("File system write access detected - ensure proper sandboxing");
159
+ }
160
+
161
+ return result;
162
+ }
163
+
164
+ interface SystemdUnitOptions {
165
+ serviceName: string;
166
+ fullPath: string;
167
+ port: string;
168
+ env: string[];
169
+ otel: boolean;
170
+ prometheus: boolean;
171
+ }
172
+
173
+ function generateSystemdUnit(opts: SystemdUnitOptions): string {
174
+ const envVars = [
175
+ `PORT=${opts.port}`,
176
+ `NODE_ENV=production`,
177
+ `SERVICE_NAME=${opts.serviceName}`,
178
+ ...opts.env,
179
+ ];
180
+
181
+ if (opts.otel) {
182
+ envVars.push("OTEL_SERVICE_NAME=" + opts.serviceName);
183
+ envVars.push("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces");
184
+ }
185
+
186
+ const envSection = envVars.map(e => `Environment=${e}`).join("\n");
187
+ const workingDir = dirname(opts.fullPath);
188
+
189
+ const bunPath = execSync("which bun", { encoding: "utf-8" }).trim();
190
+ return `[Unit]
191
+ Description=BS9 Service: ${opts.serviceName}
192
+ After=network.target
193
+
194
+ [Service]
195
+ Type=simple
196
+ Restart=on-failure
197
+ RestartSec=2s
198
+ TimeoutStartSec=30s
199
+ TimeoutStopSec=30s
200
+ WorkingDirectory=${workingDir}
201
+ ExecStart=${bunPath} run ${opts.fullPath}
202
+ ${envSection}
203
+
204
+ [Install]
205
+ WantedBy=default.target
206
+ `;
207
+ }
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { readFileSync } from "node:fs";
5
+
6
+ interface StatusOptions {
7
+ watch?: boolean;
8
+ }
9
+
10
+ interface ServiceStatus {
11
+ name: string;
12
+ loaded: string;
13
+ active: string;
14
+ sub: string;
15
+ description: string;
16
+ cpu?: string;
17
+ memory?: string;
18
+ uptime?: string;
19
+ tasks?: string;
20
+ }
21
+
22
+ export async function statusCommand(options: StatusOptions): Promise<void> {
23
+ const doStatus = () => {
24
+ try {
25
+ // Get all bsn-managed user services
26
+ const listOutput = execSync("systemctl --user list-units --type=service --no-pager --no-legend", { encoding: "utf-8" });
27
+ const lines = listOutput.split("\n").filter(line => line.includes(".service"));
28
+
29
+ // Debug: show all lines
30
+ // console.error("All service lines:", lines);
31
+
32
+ const services: ServiceStatus[] = [];
33
+
34
+ for (const line of lines) {
35
+ // Skip empty lines
36
+ if (!line.trim()) continue;
37
+
38
+ // Try to match the line format - handle both ● and regular spaces
39
+ const match = line.match(/^(?:\s*([●\s○]))?\s*([^\s]+)\.service\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+(.+)$/);
40
+ if (!match) {
41
+ continue;
42
+ }
43
+
44
+ const [, statusIndicator, name, loaded, active, sub, description] = match;
45
+
46
+ // Only include services that look like BSN-managed (check for "Bun Service:" pattern)
47
+ if (!description.includes("Bun Service:") && !description.includes("BS9 Service:")) {
48
+ continue;
49
+ }
50
+
51
+ const status: ServiceStatus = {
52
+ name,
53
+ loaded,
54
+ active,
55
+ sub,
56
+ description,
57
+ };
58
+
59
+ // Get additional metrics
60
+ try {
61
+ const showOutput = execSync(`systemctl --user show ${name} -p CPUUsageNSec MemoryCurrent ActiveEnterTimestamp TasksCurrent`, { encoding: "utf-8" });
62
+ const cpuMatch = showOutput.match(/CPUUsageNSec=(\d+)/);
63
+ const memMatch = showOutput.match(/MemoryCurrent=(\d+)/);
64
+ const timeMatch = showOutput.match(/ActiveEnterTimestamp=(.+)/);
65
+ const tasksMatch = showOutput.match(/TasksCurrent=(\d+)/);
66
+
67
+ if (cpuMatch) status.cpu = formatCPU(Number(cpuMatch[1]));
68
+ if (memMatch) status.memory = formatMemory(Number(memMatch[1]));
69
+ if (timeMatch) status.uptime = formatUptime(timeMatch[1]);
70
+ if (tasksMatch) status.tasks = tasksMatch[1];
71
+ } catch {
72
+ // Ignore metrics errors
73
+ }
74
+
75
+ services.push(status);
76
+ }
77
+
78
+ // Display table
79
+ console.clear();
80
+ console.log("🔍 BSN Service Status\n");
81
+
82
+ if (services.length === 0) {
83
+ console.log("No BSN-managed services running.");
84
+ return;
85
+ }
86
+
87
+ // Header
88
+ console.log(`${"SERVICE".padEnd(20)} ${"STATE".padEnd(12)} ${"CPU".padEnd(8)} ${"MEMORY".padEnd(10)} ${"UPTIME".padEnd(12)} ${"TASKS".padEnd(6)} DESCRIPTION`);
89
+ console.log("-".repeat(90));
90
+
91
+ for (const svc of services) {
92
+ const state = `${svc.active}/${svc.sub}`;
93
+ console.log(
94
+ `${svc.name.padEnd(20)} ${state.padEnd(12)} ${(svc.cpu || "-").padEnd(8)} ${(svc.memory || "-").padEnd(10)} ${(svc.uptime || "-").padEnd(12)} ${(svc.tasks || "-").padEnd(6)} ${svc.description}`
95
+ );
96
+ }
97
+
98
+ console.log("\n📊 SRE Metrics Summary:");
99
+ const totalServices = services.length;
100
+ const runningServices = services.filter(s => s.active === "active").length;
101
+ const totalMemory = services.reduce((sum, s) => sum + (s.memory ? parseMemory(s.memory) : 0), 0);
102
+
103
+ console.log(` Services: ${runningServices}/${totalServices} running`);
104
+ console.log(` Memory: ${formatMemory(totalMemory)}`);
105
+ console.log(` Last updated: ${new Date().toISOString()}`);
106
+
107
+ } catch (err) {
108
+ console.error(`❌ Failed to get status: ${err}`);
109
+ process.exit(1);
110
+ }
111
+ };
112
+
113
+ if (options.watch) {
114
+ console.log("🔄 Watch mode (Ctrl+C to exit)\n");
115
+ const interval = setInterval(doStatus, 2000);
116
+ process.on("SIGINT", () => {
117
+ clearInterval(interval);
118
+ console.log("\n👋 Exiting watch mode");
119
+ process.exit(0);
120
+ });
121
+ doStatus();
122
+ } else {
123
+ doStatus();
124
+ }
125
+ }
126
+
127
+ function formatCPU(nsec: number): string {
128
+ const ms = nsec / 1_000_000;
129
+ return `${ms.toFixed(1)}ms`;
130
+ }
131
+
132
+ function formatMemory(bytes: number): string {
133
+ const units = ["B", "KB", "MB", "GB"];
134
+ let size = bytes;
135
+ let unit = 0;
136
+ while (size >= 1024 && unit < units.length - 1) {
137
+ size /= 1024;
138
+ unit++;
139
+ }
140
+ return `${size.toFixed(1)}${units[unit]}`;
141
+ }
142
+
143
+ function parseMemory(str: string): number {
144
+ const match = str.match(/^([\d.]+)(B|KB|MB|GB)$/);
145
+ if (!match) return 0;
146
+ const [, value, unit] = match;
147
+ const mult = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 }[unit] || 1;
148
+ return Number(value) * mult;
149
+ }
150
+
151
+ function formatUptime(timestamp: string): string {
152
+ try {
153
+ const date = new Date(timestamp);
154
+ const now = new Date();
155
+ const diff = now.getTime() - date.getTime();
156
+ const hours = Math.floor(diff / (1000 * 60 * 60));
157
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
158
+ return `${hours}h ${minutes}m`;
159
+ } catch {
160
+ return "-";
161
+ }
162
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execSync } from "node:child_process";
4
+
5
+ export async function stopCommand(name: string): Promise<void> {
6
+ try {
7
+ execSync(`systemctl --user stop ${name}`, { stdio: "inherit" });
8
+ console.log(`🛑 User service '${name}' stopped`);
9
+ } catch (err) {
10
+ console.error(`❌ Failed to stop user service '${name}': ${err}`);
11
+ process.exit(1);
12
+ }
13
+ }
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { spawn } from "node:child_process";
5
+
6
+ interface WebOptions {
7
+ port?: string;
8
+ detach?: boolean;
9
+ }
10
+
11
+ export async function webCommand(options: WebOptions): Promise<void> {
12
+ const port = options.port || "8080";
13
+ const dashboardPath = `${import.meta.dir}/../web/dashboard.ts`;
14
+
15
+ console.log(`🌐 Starting BS9 Web Dashboard on port ${port}`);
16
+
17
+ if (options.detach) {
18
+ // Run in background
19
+ const child = spawn("bun", ["run", dashboardPath], {
20
+ detached: true,
21
+ stdio: 'ignore',
22
+ env: {
23
+ ...process.env,
24
+ WEB_DASHBOARD_PORT: port,
25
+ },
26
+ });
27
+
28
+ child.unref();
29
+
30
+ console.log(`✅ Web dashboard started in background`);
31
+ console.log(` URL: http://localhost:${port}`);
32
+ console.log(` Process ID: ${child.pid}`);
33
+ console.log(` Stop with: kill ${child.pid}`);
34
+ } else {
35
+ // Run in foreground
36
+ console.log(` URL: http://localhost:${port}`);
37
+ console.log(` Press Ctrl+C to stop`);
38
+ console.log('');
39
+
40
+ try {
41
+ execSync(`WEB_DASHBOARD_PORT=${port} bun run ${dashboardPath}`, {
42
+ stdio: "inherit"
43
+ });
44
+ } catch (error) {
45
+ console.error(`❌ Failed to start web dashboard: ${error}`);
46
+ process.exit(1);
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,44 @@
1
+ # BS9 Docker Image
2
+ FROM oven/bun:1.3.6-alpine
3
+
4
+ # Install system dependencies
5
+ RUN apk add --no-cache \
6
+ systemd \
7
+ curl \
8
+ bash \
9
+ && rm -rf /var/cache/apk/*
10
+
11
+ # Create app directory
12
+ WORKDIR /app
13
+
14
+ # Copy package files
15
+ COPY package.json bun.lockb ./
16
+
17
+ # Install dependencies
18
+ RUN bun install --frozen-lockfile --production
19
+
20
+ # Copy BS9 source code
21
+ COPY bin/ ./bin/
22
+ COPY src/ ./src/
23
+
24
+ # Create necessary directories
25
+ RUN mkdir -p /app/.config/bs9 /app/.config/systemd/user
26
+
27
+ # Make BS9 executable
28
+ RUN chmod +x /app/bin/bs9
29
+
30
+ # Add BS9 to PATH
31
+ ENV PATH="/app/bin:${PATH}"
32
+
33
+ # Expose web dashboard port
34
+ EXPOSE 8080
35
+
36
+ # Health check
37
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
38
+ CMD curl -f http://localhost:8080/ || exit 1
39
+
40
+ # Set up systemd for user mode
41
+ RUN echo "user" > /etc/hostname
42
+
43
+ # Default command
44
+ CMD ["/bin/bash"]
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFileSync, readFileSync, existsSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+
6
+ export function injectOpenTelemetry(entryFile: string, serviceName: string): void {
7
+ const wrapper = `
8
+ // BSN OpenTelemetry Auto-injection
9
+ import { NodeSDK } from "@opentelemetry/sdk-node";
10
+ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
11
+ import { Resource } from "@opentelemetry/resources";
12
+ import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
13
+
14
+ const sdk = new NodeSDK({
15
+ resource: new Resource({
16
+ [SemanticResourceAttributes.SERVICE_NAME]: "${serviceName}",
17
+ [SemanticResourceAttributes.SERVICE_VERSION]: "1.0.0",
18
+ }),
19
+ instrumentations: [getNodeAutoInstrumentations()],
20
+ });
21
+
22
+ sdk.start();
23
+
24
+ // Original user code below
25
+ `;
26
+
27
+ const originalContent = readFileSync(entryFile, "utf-8");
28
+ const newContent = wrapper + originalContent;
29
+
30
+ writeFileSync(entryFile, newContent, { encoding: "utf-8" });
31
+ }
32
+
33
+ export function injectPrometheus(entryFile: string, port: string): void {
34
+ const prometheusInject = `
35
+ // BSN Prometheus Auto-injection
36
+ import promClient from "prom-client";
37
+
38
+ const register = new promClient.Registry();
39
+ promClient.collectDefaultMetrics({ register });
40
+
41
+ const httpRequestDuration = new promClient.Histogram({
42
+ name: "http_request_duration_seconds",
43
+ help: "Duration of HTTP requests in seconds",
44
+ labelNames: ["method", "route", "status_code"],
45
+ registers: [register],
46
+ });
47
+
48
+ const httpRequestTotal = new promClient.Counter({
49
+ name: "http_requests_total",
50
+ help: "Total number of HTTP requests",
51
+ labelNames: ["method", "route", "status_code"],
52
+ registers: [register],
53
+ });
54
+
55
+ // Export for use in user code
56
+ globalThis.prometheus = { register, httpRequestDuration, httpRequestTotal };
57
+ `;
58
+
59
+ // This would need to be injected at the right place in the user's code
60
+ // For now, we'll write it to a temporary file that the user can import
61
+ const metricsFile = join(dirname(entryFile), "bsn-metrics.js");
62
+ writeFileSync(metricsFile, prometheusInject, { encoding: "utf-8" });
63
+
64
+ console.log(`📊 Prometheus metrics written to: ${metricsFile}`);
65
+ console.log(` Add to your app: import "./bsn-metrics.js"`);
66
+ }