@graphty/remote-logger 0.0.1 → 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 +636 -28
- package/bin/remote-log-server.js +3 -0
- package/dist/client/RemoteLogClient.d.ts +114 -0
- package/dist/client/RemoteLogClient.d.ts.map +1 -0
- package/dist/client/RemoteLogClient.js +238 -0
- package/dist/client/RemoteLogClient.js.map +1 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +6 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +47 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +6 -0
- package/dist/client/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/log-server.d.ts +75 -0
- package/dist/server/log-server.d.ts.map +1 -0
- package/dist/server/log-server.js +453 -0
- package/dist/server/log-server.js.map +1 -0
- package/dist/server/self-signed-cert.d.ts +30 -0
- package/dist/server/self-signed-cert.d.ts.map +1 -0
- package/dist/server/self-signed-cert.js +83 -0
- package/dist/server/self-signed-cert.js.map +1 -0
- package/dist/ui/ConsoleCaptureUI.d.ts +118 -0
- package/dist/ui/ConsoleCaptureUI.d.ts.map +1 -0
- package/dist/ui/ConsoleCaptureUI.js +571 -0
- package/dist/ui/ConsoleCaptureUI.js.map +1 -0
- package/dist/ui/index.d.ts +15 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +80 -7
- package/src/client/RemoteLogClient.ts +280 -0
- package/src/client/index.ts +7 -0
- package/src/client/types.ts +49 -0
- package/src/index.ts +28 -0
- package/src/server/index.ts +17 -0
- package/src/server/log-server.ts +571 -0
- package/src/server/self-signed-cert.ts +93 -0
- package/src/ui/ConsoleCaptureUI.ts +649 -0
- package/src/ui/index.ts +15 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Log Server - A standalone HTTP/HTTPS log server for remote debugging.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - HTTPS with auto-generated self-signed certs or custom certs
|
|
6
|
+
* - Receives logs from browser via POST /log
|
|
7
|
+
* - Pretty terminal output with colors
|
|
8
|
+
* - REST API for querying logs
|
|
9
|
+
* - Optional file logging for Claude Code to read
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* npx remote-log-server --port 9080
|
|
13
|
+
* npx remote-log-server --cert /path/to/cert.crt --key /path/to/key.key
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from "fs";
|
|
17
|
+
import * as http from "http";
|
|
18
|
+
import * as https from "https";
|
|
19
|
+
import { URL } from "url";
|
|
20
|
+
|
|
21
|
+
import { certFilesExist, generateSelfSignedCert, readCertFiles } from "./self-signed-cert.js";
|
|
22
|
+
|
|
23
|
+
// ANSI color codes for terminal output
|
|
24
|
+
const colors = {
|
|
25
|
+
reset: "\x1b[0m",
|
|
26
|
+
bright: "\x1b[1m",
|
|
27
|
+
dim: "\x1b[2m",
|
|
28
|
+
red: "\x1b[31m",
|
|
29
|
+
green: "\x1b[32m",
|
|
30
|
+
yellow: "\x1b[33m",
|
|
31
|
+
blue: "\x1b[34m",
|
|
32
|
+
magenta: "\x1b[35m",
|
|
33
|
+
cyan: "\x1b[36m",
|
|
34
|
+
white: "\x1b[37m",
|
|
35
|
+
bgRed: "\x1b[41m",
|
|
36
|
+
bgYellow: "\x1b[43m",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface LogServerOptions {
|
|
40
|
+
/** Port to listen on (default: 9080) */
|
|
41
|
+
port?: number;
|
|
42
|
+
/** Hostname to bind to (default: localhost) */
|
|
43
|
+
host?: string;
|
|
44
|
+
/** Path to SSL certificate file */
|
|
45
|
+
certPath?: string;
|
|
46
|
+
/** Path to SSL private key file */
|
|
47
|
+
keyPath?: string;
|
|
48
|
+
/** Path to file for writing logs (optional) */
|
|
49
|
+
logFile?: string;
|
|
50
|
+
/** Use HTTP instead of HTTPS (default: false) */
|
|
51
|
+
useHttp?: boolean;
|
|
52
|
+
/** Suppress startup banner (default: false) */
|
|
53
|
+
quiet?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface LogEntry {
|
|
57
|
+
time: string;
|
|
58
|
+
level: string;
|
|
59
|
+
message: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface LogBatch {
|
|
63
|
+
sessionId: string;
|
|
64
|
+
logs: LogEntry[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Store for remote logs by session
|
|
68
|
+
const remoteLogs = new Map<string, LogEntry[]>();
|
|
69
|
+
|
|
70
|
+
// File stream for log file
|
|
71
|
+
let logFileStream: fs.WriteStream | null = null;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Clear all stored logs.
|
|
75
|
+
* Useful for testing.
|
|
76
|
+
*/
|
|
77
|
+
export function clearLogs(): void {
|
|
78
|
+
remoteLogs.clear();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Format log level for terminal output with colors.
|
|
83
|
+
* @param level - The log level string
|
|
84
|
+
* @returns Colored and formatted log level string
|
|
85
|
+
*/
|
|
86
|
+
function formatLogLevel(level: string): string {
|
|
87
|
+
switch (level.toUpperCase()) {
|
|
88
|
+
case "ERROR":
|
|
89
|
+
return `${colors.bgRed}${colors.white} ERROR ${colors.reset}`;
|
|
90
|
+
case "WARN":
|
|
91
|
+
case "WARNING":
|
|
92
|
+
return `${colors.bgYellow}${colors.bright} WARN ${colors.reset}`;
|
|
93
|
+
case "INFO":
|
|
94
|
+
return `${colors.blue} INFO ${colors.reset}`;
|
|
95
|
+
case "DEBUG":
|
|
96
|
+
return `${colors.cyan} DEBUG ${colors.reset}`;
|
|
97
|
+
case "TRACE":
|
|
98
|
+
return `${colors.dim} TRACE ${colors.reset}`;
|
|
99
|
+
case "LOG":
|
|
100
|
+
default:
|
|
101
|
+
return `${colors.green} LOG ${colors.reset}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Display a log entry in the terminal.
|
|
107
|
+
* @param sessionId - The session ID for this log entry
|
|
108
|
+
* @param log - The log entry to display
|
|
109
|
+
* @param quiet - If true, suppress terminal output
|
|
110
|
+
*/
|
|
111
|
+
function displayLog(sessionId: string, log: LogEntry, quiet: boolean): void {
|
|
112
|
+
if (!quiet) {
|
|
113
|
+
const time = new Date(log.time).toLocaleTimeString();
|
|
114
|
+
const level = formatLogLevel(log.level);
|
|
115
|
+
const session = `${colors.cyan}[${sessionId.substring(0, 12)}]${colors.reset}`;
|
|
116
|
+
|
|
117
|
+
// Truncate very long messages for display
|
|
118
|
+
let { message } = log;
|
|
119
|
+
if (message.length > 1000) {
|
|
120
|
+
message = `${message.substring(0, 1000)}... [truncated]`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// eslint-disable-next-line no-console
|
|
124
|
+
console.log(`${time} ${session} ${level} ${message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Write to log file if configured
|
|
128
|
+
if (logFileStream) {
|
|
129
|
+
const logLine = JSON.stringify({
|
|
130
|
+
time: log.time,
|
|
131
|
+
sessionId,
|
|
132
|
+
level: log.level,
|
|
133
|
+
message: log.message,
|
|
134
|
+
});
|
|
135
|
+
logFileStream.write(`${logLine}\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Handle incoming HTTP request.
|
|
141
|
+
* @param req - The incoming HTTP request
|
|
142
|
+
* @param res - The HTTP response object
|
|
143
|
+
* @param host - The server hostname
|
|
144
|
+
* @param port - The server port number
|
|
145
|
+
* @param useHttps - Whether HTTPS is being used
|
|
146
|
+
* @param quiet - If true, suppress terminal output
|
|
147
|
+
*/
|
|
148
|
+
function handleRequest(
|
|
149
|
+
req: http.IncomingMessage,
|
|
150
|
+
res: http.ServerResponse,
|
|
151
|
+
host: string,
|
|
152
|
+
port: number,
|
|
153
|
+
useHttps: boolean,
|
|
154
|
+
quiet: boolean,
|
|
155
|
+
): void {
|
|
156
|
+
// CORS headers
|
|
157
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
158
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
159
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
160
|
+
|
|
161
|
+
if (req.method === "OPTIONS") {
|
|
162
|
+
res.writeHead(204);
|
|
163
|
+
res.end();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const url = req.url ?? "/";
|
|
168
|
+
const protocol = useHttps ? "https" : "http";
|
|
169
|
+
|
|
170
|
+
// Handle log endpoint - receive logs from browser
|
|
171
|
+
if (url === "/log" && req.method === "POST") {
|
|
172
|
+
let body = "";
|
|
173
|
+
req.on("data", (chunk: Buffer) => {
|
|
174
|
+
body += chunk.toString();
|
|
175
|
+
});
|
|
176
|
+
req.on("end", () => {
|
|
177
|
+
try {
|
|
178
|
+
const data = JSON.parse(body) as LogBatch;
|
|
179
|
+
const { sessionId, logs } = data;
|
|
180
|
+
|
|
181
|
+
// Initialize session if new
|
|
182
|
+
if (!remoteLogs.has(sessionId)) {
|
|
183
|
+
remoteLogs.set(sessionId, []);
|
|
184
|
+
if (!quiet) {
|
|
185
|
+
// eslint-disable-next-line no-console
|
|
186
|
+
console.log(
|
|
187
|
+
`\n${colors.bright}${colors.magenta}═══════════════════════════════════════════════════════════${colors.reset}`,
|
|
188
|
+
);
|
|
189
|
+
// eslint-disable-next-line no-console
|
|
190
|
+
console.log(`${colors.bright}${colors.magenta} NEW SESSION: ${sessionId}${colors.reset}`);
|
|
191
|
+
// eslint-disable-next-line no-console
|
|
192
|
+
console.log(
|
|
193
|
+
`${colors.bright}${colors.magenta}═══════════════════════════════════════════════════════════${colors.reset}\n`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const sessionLogs = remoteLogs.get(sessionId);
|
|
199
|
+
if (!sessionLogs) {
|
|
200
|
+
// Should not happen since we just set it above, but satisfy TypeScript
|
|
201
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
202
|
+
res.end(JSON.stringify({ error: "Internal error" }));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Display and store each log
|
|
207
|
+
for (const log of logs) {
|
|
208
|
+
sessionLogs.push(log);
|
|
209
|
+
displayLog(sessionId, log, quiet);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
213
|
+
res.end(JSON.stringify({ success: true }));
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (!quiet) {
|
|
216
|
+
console.error("Error parsing log data:", error);
|
|
217
|
+
}
|
|
218
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
219
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Handle logs viewer endpoint - GET all logs
|
|
226
|
+
if (url === "/logs" && req.method === "GET") {
|
|
227
|
+
const allLogs: Record<string, LogEntry[]> = {};
|
|
228
|
+
for (const [sessionId, logs] of remoteLogs) {
|
|
229
|
+
allLogs[sessionId] = logs;
|
|
230
|
+
}
|
|
231
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
232
|
+
res.end(JSON.stringify(allLogs, null, 2));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Handle recent logs endpoint - GET last N logs across all sessions
|
|
237
|
+
if (url.startsWith("/logs/recent") && req.method === "GET") {
|
|
238
|
+
const urlObj = new URL(url, `${protocol}://${host}:${port}`);
|
|
239
|
+
const count = parseInt(urlObj.searchParams.get("n") ?? "50", 10);
|
|
240
|
+
const errorsOnly = urlObj.searchParams.get("errors") === "true";
|
|
241
|
+
|
|
242
|
+
// Collect all logs with session info
|
|
243
|
+
const allLogs: (LogEntry & { sessionId: string })[] = [];
|
|
244
|
+
for (const [sessionId, logs] of remoteLogs) {
|
|
245
|
+
for (const log of logs) {
|
|
246
|
+
if (!errorsOnly || log.level.toUpperCase() === "ERROR") {
|
|
247
|
+
allLogs.push({ sessionId, ...log });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Sort by time descending and take last N
|
|
253
|
+
allLogs.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime());
|
|
254
|
+
const recentLogs = allLogs.slice(0, count).reverse(); // Reverse to show oldest first
|
|
255
|
+
|
|
256
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
257
|
+
res.end(
|
|
258
|
+
JSON.stringify(
|
|
259
|
+
{
|
|
260
|
+
total: allLogs.length,
|
|
261
|
+
showing: recentLogs.length,
|
|
262
|
+
logs: recentLogs,
|
|
263
|
+
},
|
|
264
|
+
null,
|
|
265
|
+
2,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Handle errors-only endpoint
|
|
272
|
+
if (url === "/logs/errors" && req.method === "GET") {
|
|
273
|
+
const errorLogs: (LogEntry & { sessionId: string })[] = [];
|
|
274
|
+
for (const [sessionId, logs] of remoteLogs) {
|
|
275
|
+
for (const log of logs) {
|
|
276
|
+
if (log.level.toUpperCase() === "ERROR") {
|
|
277
|
+
errorLogs.push({ sessionId, ...log });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
errorLogs.sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime());
|
|
282
|
+
|
|
283
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
284
|
+
res.end(
|
|
285
|
+
JSON.stringify(
|
|
286
|
+
{
|
|
287
|
+
total: errorLogs.length,
|
|
288
|
+
logs: errorLogs,
|
|
289
|
+
},
|
|
290
|
+
null,
|
|
291
|
+
2,
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Handle clear logs endpoint
|
|
298
|
+
if (url === "/logs/clear" && req.method === "POST") {
|
|
299
|
+
remoteLogs.clear();
|
|
300
|
+
if (!quiet) {
|
|
301
|
+
// eslint-disable-next-line no-console
|
|
302
|
+
console.log(`\n${colors.yellow}Logs cleared${colors.reset}\n`);
|
|
303
|
+
}
|
|
304
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
305
|
+
res.end(JSON.stringify({ success: true }));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Health check endpoint
|
|
310
|
+
if (url === "/health" && req.method === "GET") {
|
|
311
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
312
|
+
res.end(JSON.stringify({ status: "ok", sessions: remoteLogs.size }));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Default: 404
|
|
317
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
318
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Print startup banner.
|
|
323
|
+
* @param host - The server hostname
|
|
324
|
+
* @param port - The server port number
|
|
325
|
+
* @param useHttps - Whether HTTPS is being used
|
|
326
|
+
*/
|
|
327
|
+
function printBanner(host: string, port: number, useHttps: boolean): void {
|
|
328
|
+
const protocol = useHttps ? "https" : "http";
|
|
329
|
+
|
|
330
|
+
// eslint-disable-next-line no-console
|
|
331
|
+
console.log("");
|
|
332
|
+
// eslint-disable-next-line no-console
|
|
333
|
+
console.log(
|
|
334
|
+
`${colors.bright}${colors.cyan}════════════════════════════════════════════════════════════${colors.reset}`,
|
|
335
|
+
);
|
|
336
|
+
// eslint-disable-next-line no-console
|
|
337
|
+
console.log(`${colors.bright}${colors.cyan} Remote Log Server${colors.reset}`);
|
|
338
|
+
// eslint-disable-next-line no-console
|
|
339
|
+
console.log(
|
|
340
|
+
`${colors.bright}${colors.cyan}════════════════════════════════════════════════════════════${colors.reset}`,
|
|
341
|
+
);
|
|
342
|
+
// eslint-disable-next-line no-console
|
|
343
|
+
console.log("");
|
|
344
|
+
// eslint-disable-next-line no-console
|
|
345
|
+
console.log(
|
|
346
|
+
`${colors.green}Server running at:${colors.reset} ${colors.bright}${protocol}://${host}:${port}/${colors.reset}`,
|
|
347
|
+
);
|
|
348
|
+
// eslint-disable-next-line no-console
|
|
349
|
+
console.log("");
|
|
350
|
+
// eslint-disable-next-line no-console
|
|
351
|
+
console.log(`${colors.yellow}API Endpoints:${colors.reset}`);
|
|
352
|
+
// eslint-disable-next-line no-console
|
|
353
|
+
console.log(` ${colors.cyan}POST /log ${colors.reset} - Receive logs from browser`);
|
|
354
|
+
// eslint-disable-next-line no-console
|
|
355
|
+
console.log(` ${colors.cyan}GET /logs ${colors.reset} - Get all logs as JSON`);
|
|
356
|
+
// eslint-disable-next-line no-console
|
|
357
|
+
console.log(` ${colors.cyan}GET /logs/recent ${colors.reset} - Get last 50 logs (?n=100 for more)`);
|
|
358
|
+
// eslint-disable-next-line no-console
|
|
359
|
+
console.log(` ${colors.cyan}GET /logs/errors ${colors.reset} - Get only error logs`);
|
|
360
|
+
// eslint-disable-next-line no-console
|
|
361
|
+
console.log(` ${colors.cyan}POST /logs/clear ${colors.reset} - Clear all logs`);
|
|
362
|
+
// eslint-disable-next-line no-console
|
|
363
|
+
console.log(` ${colors.cyan}GET /health ${colors.reset} - Health check`);
|
|
364
|
+
// eslint-disable-next-line no-console
|
|
365
|
+
console.log("");
|
|
366
|
+
// eslint-disable-next-line no-console
|
|
367
|
+
console.log(`${colors.dim}Remote logs will appear below:${colors.reset}`);
|
|
368
|
+
// eslint-disable-next-line no-console
|
|
369
|
+
console.log(`${colors.cyan}────────────────────────────────────────────────────────────${colors.reset}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Start the log server.
|
|
374
|
+
* @param options - Server configuration options
|
|
375
|
+
* @returns The HTTP or HTTPS server instance
|
|
376
|
+
*/
|
|
377
|
+
export function startLogServer(options: LogServerOptions = {}): http.Server | https.Server {
|
|
378
|
+
const port = options.port ?? 9080;
|
|
379
|
+
const host = options.host ?? "localhost";
|
|
380
|
+
const useHttp = options.useHttp ?? false;
|
|
381
|
+
const quiet = options.quiet ?? false;
|
|
382
|
+
|
|
383
|
+
// Set up log file if specified
|
|
384
|
+
if (options.logFile) {
|
|
385
|
+
logFileStream = fs.createWriteStream(options.logFile, { flags: "a" });
|
|
386
|
+
if (!quiet) {
|
|
387
|
+
// eslint-disable-next-line no-console
|
|
388
|
+
console.log(`${colors.green}Writing logs to: ${options.logFile}${colors.reset}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Determine SSL configuration
|
|
393
|
+
let server: https.Server | http.Server;
|
|
394
|
+
|
|
395
|
+
if (useHttp) {
|
|
396
|
+
// Plain HTTP server
|
|
397
|
+
server = http.createServer((req, res) => {
|
|
398
|
+
handleRequest(req, res, host, port, false, quiet);
|
|
399
|
+
});
|
|
400
|
+
} else {
|
|
401
|
+
// HTTPS server
|
|
402
|
+
let cert: string;
|
|
403
|
+
let key: string;
|
|
404
|
+
|
|
405
|
+
if (options.certPath && options.keyPath && certFilesExist(options.certPath, options.keyPath)) {
|
|
406
|
+
// Use provided certificates
|
|
407
|
+
({ cert, key } = readCertFiles(options.certPath, options.keyPath));
|
|
408
|
+
if (!quiet) {
|
|
409
|
+
// eslint-disable-next-line no-console
|
|
410
|
+
console.log(`${colors.green}Using SSL certificates from: ${options.certPath}${colors.reset}`);
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
// Generate self-signed certificate
|
|
414
|
+
if (!quiet) {
|
|
415
|
+
// eslint-disable-next-line no-console
|
|
416
|
+
console.log(`${colors.yellow}Generating self-signed certificate for ${host}...${colors.reset}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
({ cert, key } = generateSelfSignedCert(host));
|
|
420
|
+
if (!quiet) {
|
|
421
|
+
// eslint-disable-next-line no-console
|
|
422
|
+
console.log(
|
|
423
|
+
`${colors.yellow}Note: Browser will show certificate warning - this is expected for self-signed certs${colors.reset}`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
server = https.createServer({ cert, key }, (req, res) => {
|
|
429
|
+
handleRequest(req, res, host, port, true, quiet);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Start listening
|
|
434
|
+
server.listen(port, host, () => {
|
|
435
|
+
if (!quiet) {
|
|
436
|
+
printBanner(host, port, !useHttp);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Handle graceful shutdown
|
|
441
|
+
process.on("SIGINT", () => {
|
|
442
|
+
// eslint-disable-next-line no-console
|
|
443
|
+
console.log(`\n${colors.yellow}Shutting down...${colors.reset}`);
|
|
444
|
+
if (logFileStream) {
|
|
445
|
+
logFileStream.end();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
server.close(() => {
|
|
449
|
+
process.exit(0);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return server;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Help text displayed when --help is passed.
|
|
458
|
+
*/
|
|
459
|
+
export const HELP_TEXT = `
|
|
460
|
+
Remote Log Server - Remote logging for browser debugging
|
|
461
|
+
|
|
462
|
+
Usage:
|
|
463
|
+
npx remote-log-server [options]
|
|
464
|
+
npx @graphty/remote-logger [options]
|
|
465
|
+
|
|
466
|
+
Options:
|
|
467
|
+
--port, -p <port> Port to listen on (default: 9080)
|
|
468
|
+
--host, -h <host> Hostname to bind to (default: localhost)
|
|
469
|
+
--cert, -c <path> Path to SSL certificate file
|
|
470
|
+
--key, -k <path> Path to SSL private key file
|
|
471
|
+
--log-file, -l <path> Write logs to file
|
|
472
|
+
--http Use HTTP instead of HTTPS
|
|
473
|
+
--quiet, -q Suppress startup banner
|
|
474
|
+
--help Show this help message
|
|
475
|
+
|
|
476
|
+
Examples:
|
|
477
|
+
npx remote-log-server # Start with defaults (port 9080, self-signed cert)
|
|
478
|
+
npx remote-log-server --port 9085 # Custom port
|
|
479
|
+
npx remote-log-server --http # Use HTTP instead of HTTPS
|
|
480
|
+
npx remote-log-server --cert cert.crt --key key.key # Custom SSL certs
|
|
481
|
+
npx remote-log-server --log-file ./tmp/logs.jsonl # Also write to file
|
|
482
|
+
`;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Result of parsing command line arguments.
|
|
486
|
+
*/
|
|
487
|
+
export interface ParseArgsResult {
|
|
488
|
+
/** Parsed options for the log server */
|
|
489
|
+
options: LogServerOptions;
|
|
490
|
+
/** Whether --help was requested */
|
|
491
|
+
showHelp: boolean;
|
|
492
|
+
/** Error message if parsing failed */
|
|
493
|
+
error?: string;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Parse command line arguments into LogServerOptions.
|
|
498
|
+
* This is separated from main() to enable testing.
|
|
499
|
+
* @param args - Array of command line arguments (excluding node and script name)
|
|
500
|
+
* @returns ParseArgsResult with options, help flag, or error
|
|
501
|
+
*/
|
|
502
|
+
export function parseArgs(args: string[]): ParseArgsResult {
|
|
503
|
+
const options: LogServerOptions = {};
|
|
504
|
+
|
|
505
|
+
for (let i = 0; i < args.length; i++) {
|
|
506
|
+
const arg = args[i];
|
|
507
|
+
const nextArg = args[i + 1];
|
|
508
|
+
|
|
509
|
+
switch (arg) {
|
|
510
|
+
case "--port":
|
|
511
|
+
case "-p":
|
|
512
|
+
options.port = parseInt(nextArg, 10);
|
|
513
|
+
i++;
|
|
514
|
+
break;
|
|
515
|
+
case "--host":
|
|
516
|
+
case "-h":
|
|
517
|
+
options.host = nextArg;
|
|
518
|
+
i++;
|
|
519
|
+
break;
|
|
520
|
+
case "--cert":
|
|
521
|
+
case "-c":
|
|
522
|
+
options.certPath = nextArg;
|
|
523
|
+
i++;
|
|
524
|
+
break;
|
|
525
|
+
case "--key":
|
|
526
|
+
case "-k":
|
|
527
|
+
options.keyPath = nextArg;
|
|
528
|
+
i++;
|
|
529
|
+
break;
|
|
530
|
+
case "--log-file":
|
|
531
|
+
case "-l":
|
|
532
|
+
options.logFile = nextArg;
|
|
533
|
+
i++;
|
|
534
|
+
break;
|
|
535
|
+
case "--http":
|
|
536
|
+
options.useHttp = true;
|
|
537
|
+
break;
|
|
538
|
+
case "--quiet":
|
|
539
|
+
case "-q":
|
|
540
|
+
options.quiet = true;
|
|
541
|
+
break;
|
|
542
|
+
case "--help":
|
|
543
|
+
return { options, showHelp: true };
|
|
544
|
+
default:
|
|
545
|
+
return { options, showHelp: false, error: `Unknown option: ${arg}` };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return { options, showHelp: false };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Parse command line arguments and start the server.
|
|
554
|
+
*/
|
|
555
|
+
export function main(): void {
|
|
556
|
+
const args = process.argv.slice(2);
|
|
557
|
+
const result = parseArgs(args);
|
|
558
|
+
|
|
559
|
+
if (result.showHelp) {
|
|
560
|
+
// eslint-disable-next-line no-console
|
|
561
|
+
console.log(HELP_TEXT);
|
|
562
|
+
process.exit(0);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (result.error) {
|
|
566
|
+
console.error(result.error);
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
startLogServer(result.options);
|
|
571
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-signed certificate generation for the log server.
|
|
3
|
+
* Uses the 'selfsigned' npm package to generate proper X.509 certificates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import selfsigned from "selfsigned";
|
|
8
|
+
|
|
9
|
+
export interface GeneratedCert {
|
|
10
|
+
cert: string;
|
|
11
|
+
key: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a self-signed certificate for HTTPS.
|
|
16
|
+
* The certificate is valid for localhost and common local development hostnames.
|
|
17
|
+
* @param hostname - Optional hostname to include in the certificate (default: localhost)
|
|
18
|
+
* @returns Object containing PEM-encoded certificate and private key
|
|
19
|
+
*/
|
|
20
|
+
export function generateSelfSignedCert(hostname = "localhost"): GeneratedCert {
|
|
21
|
+
const attrs = [
|
|
22
|
+
{ name: "commonName", value: hostname },
|
|
23
|
+
{ name: "organizationName", value: "Remote Log Server" },
|
|
24
|
+
{ name: "countryName", value: "US" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const options = {
|
|
28
|
+
keySize: 2048,
|
|
29
|
+
days: 365,
|
|
30
|
+
algorithm: "sha256" as const,
|
|
31
|
+
extensions: [
|
|
32
|
+
{
|
|
33
|
+
name: "basicConstraints",
|
|
34
|
+
cA: false,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "keyUsage",
|
|
38
|
+
keyCertSign: false,
|
|
39
|
+
digitalSignature: true,
|
|
40
|
+
keyEncipherment: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "extKeyUsage",
|
|
44
|
+
serverAuth: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "subjectAltName",
|
|
48
|
+
altNames: [
|
|
49
|
+
{ type: 2, value: hostname }, // DNS name
|
|
50
|
+
{ type: 2, value: "localhost" },
|
|
51
|
+
{ type: 7, ip: "127.0.0.1" }, // IP address
|
|
52
|
+
{ type: 7, ip: "::1" }, // IPv6 localhost
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const pems = selfsigned.generate(attrs, options);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
cert: pems.cert,
|
|
62
|
+
key: pems.private,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if certificate files exist and are readable.
|
|
68
|
+
* @param certPath - Path to the certificate file
|
|
69
|
+
* @param keyPath - Path to the private key file
|
|
70
|
+
* @returns true if both files exist and are readable
|
|
71
|
+
*/
|
|
72
|
+
export function certFilesExist(certPath: string, keyPath: string): boolean {
|
|
73
|
+
try {
|
|
74
|
+
fs.accessSync(certPath, fs.constants.R_OK);
|
|
75
|
+
fs.accessSync(keyPath, fs.constants.R_OK);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read certificate and key from files.
|
|
84
|
+
* @param certPath - Path to the certificate file
|
|
85
|
+
* @param keyPath - Path to the private key file
|
|
86
|
+
* @returns Object containing PEM-encoded certificate and private key
|
|
87
|
+
*/
|
|
88
|
+
export function readCertFiles(certPath: string, keyPath: string): GeneratedCert {
|
|
89
|
+
return {
|
|
90
|
+
cert: fs.readFileSync(certPath, "utf-8"),
|
|
91
|
+
key: fs.readFileSync(keyPath, "utf-8"),
|
|
92
|
+
};
|
|
93
|
+
}
|