@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.
Files changed (48) hide show
  1. package/README.md +636 -28
  2. package/bin/remote-log-server.js +3 -0
  3. package/dist/client/RemoteLogClient.d.ts +114 -0
  4. package/dist/client/RemoteLogClient.d.ts.map +1 -0
  5. package/dist/client/RemoteLogClient.js +238 -0
  6. package/dist/client/RemoteLogClient.js.map +1 -0
  7. package/dist/client/index.d.ts +7 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +6 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/types.d.ts +47 -0
  12. package/dist/client/types.d.ts.map +1 -0
  13. package/dist/client/types.js +6 -0
  14. package/dist/client/types.js.map +1 -0
  15. package/dist/index.d.ts +22 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +23 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/server/index.d.ts +8 -0
  20. package/dist/server/index.d.ts.map +1 -0
  21. package/dist/server/index.js +8 -0
  22. package/dist/server/index.js.map +1 -0
  23. package/dist/server/log-server.d.ts +75 -0
  24. package/dist/server/log-server.d.ts.map +1 -0
  25. package/dist/server/log-server.js +453 -0
  26. package/dist/server/log-server.js.map +1 -0
  27. package/dist/server/self-signed-cert.d.ts +30 -0
  28. package/dist/server/self-signed-cert.d.ts.map +1 -0
  29. package/dist/server/self-signed-cert.js +83 -0
  30. package/dist/server/self-signed-cert.js.map +1 -0
  31. package/dist/ui/ConsoleCaptureUI.d.ts +118 -0
  32. package/dist/ui/ConsoleCaptureUI.d.ts.map +1 -0
  33. package/dist/ui/ConsoleCaptureUI.js +571 -0
  34. package/dist/ui/ConsoleCaptureUI.js.map +1 -0
  35. package/dist/ui/index.d.ts +15 -0
  36. package/dist/ui/index.d.ts.map +1 -0
  37. package/dist/ui/index.js +15 -0
  38. package/dist/ui/index.js.map +1 -0
  39. package/package.json +80 -7
  40. package/src/client/RemoteLogClient.ts +280 -0
  41. package/src/client/index.ts +7 -0
  42. package/src/client/types.ts +49 -0
  43. package/src/index.ts +28 -0
  44. package/src/server/index.ts +17 -0
  45. package/src/server/log-server.ts +571 -0
  46. package/src/server/self-signed-cert.ts +93 -0
  47. package/src/ui/ConsoleCaptureUI.ts +649 -0
  48. 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
+ }