@graphty/remote-logger 1.2.3 → 1.3.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.
Files changed (37) hide show
  1. package/README.md +52 -1
  2. package/dist/bundle/browser-entry.d.ts +34 -0
  3. package/dist/bundle/browser-entry.d.ts.map +1 -0
  4. package/dist/bundle/browser-entry.js +129 -0
  5. package/dist/bundle/browser-entry.js.map +1 -0
  6. package/dist/mcp/mcp-server.d.ts +1 -1
  7. package/dist/mcp/mcp-server.d.ts.map +1 -1
  8. package/dist/mcp/mcp-server.js +48 -3
  9. package/dist/mcp/mcp-server.js.map +1 -1
  10. package/dist/mcp/tools/logs-receive.d.ts +8 -8
  11. package/dist/mcp/tools/logs-status.d.ts.map +1 -1
  12. package/dist/mcp/tools/logs-status.js +4 -1
  13. package/dist/mcp/tools/logs-status.js.map +1 -1
  14. package/dist/remote-logger.browser.js +166 -0
  15. package/dist/remote-logger.browser.js.map +1 -0
  16. package/dist/server/dual-server.d.ts.map +1 -1
  17. package/dist/server/dual-server.js +16 -4
  18. package/dist/server/dual-server.js.map +1 -1
  19. package/dist/server/log-server.d.ts +10 -0
  20. package/dist/server/log-server.d.ts.map +1 -1
  21. package/dist/server/log-server.js +77 -8
  22. package/dist/server/log-server.js.map +1 -1
  23. package/dist/server/log-storage.d.ts +4 -0
  24. package/dist/server/log-storage.d.ts.map +1 -1
  25. package/dist/server/log-storage.js.map +1 -1
  26. package/dist/server/proxy.d.ts +39 -0
  27. package/dist/server/proxy.d.ts.map +1 -0
  28. package/dist/server/proxy.js +290 -0
  29. package/dist/server/proxy.js.map +1 -0
  30. package/package.json +6 -2
  31. package/src/bundle/browser-entry.ts +157 -0
  32. package/src/mcp/mcp-server.ts +48 -3
  33. package/src/mcp/tools/logs-status.ts +4 -1
  34. package/src/server/dual-server.ts +19 -4
  35. package/src/server/log-server.ts +91 -8
  36. package/src/server/log-storage.ts +4 -0
  37. package/src/server/proxy.ts +357 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Browser bundle entry point for the remote logger.
3
+ *
4
+ * When loaded as a script tag, this auto-initializes a RemoteLogClient
5
+ * that intercepts console methods and forwards them to the server.
6
+ *
7
+ * Server URL detection priority:
8
+ * 1. window.__REMOTE_LOG_SERVER_URL__ (injected by server when serving this file)
9
+ * 2. document.currentScript.src (parse origin from script tag URL)
10
+ * 3. window.location.origin (fallback)
11
+ *
12
+ * Exposes window.__remoteLogger__ for manual control.
13
+ * @module bundle/browser-entry
14
+ */
15
+
16
+ import { RemoteLogClient } from "../client/RemoteLogClient.js";
17
+ import { ConsoleCaptureUI } from "../ui/ConsoleCaptureUI.js";
18
+
19
+ declare global {
20
+ interface Window {
21
+ __REMOTE_LOG_SERVER_URL__?: string;
22
+ __remoteLogger__?: RemoteLoggerGlobal;
23
+ }
24
+ }
25
+
26
+ type ConsoleMethod = "log" | "warn" | "error" | "info" | "debug";
27
+
28
+ const CONSOLE_LEVEL_MAP: Record<ConsoleMethod, string> = {
29
+ log: "LOG",
30
+ warn: "WARN",
31
+ error: "ERROR",
32
+ info: "INFO",
33
+ debug: "DEBUG",
34
+ };
35
+
36
+ const INTERCEPTED_METHODS: ConsoleMethod[] = ["log", "warn", "error", "info", "debug"];
37
+
38
+ export interface RemoteLoggerGlobal {
39
+ client: RemoteLogClient;
40
+ ui?: ConsoleCaptureUI;
41
+ destroy: () => void;
42
+ }
43
+
44
+ function detectServerUrl(): string | undefined {
45
+ if (typeof window !== "undefined" && window.__REMOTE_LOG_SERVER_URL__) {
46
+ return window.__REMOTE_LOG_SERVER_URL__;
47
+ }
48
+
49
+ if (typeof document !== "undefined" && document.currentScript) {
50
+ try {
51
+ const scriptUrl = new URL((document.currentScript as HTMLScriptElement).src);
52
+ return scriptUrl.origin;
53
+ } catch {
54
+ // Invalid URL, fall through
55
+ }
56
+ }
57
+
58
+ if (typeof window !== "undefined" && window.location) {
59
+ return window.location.origin;
60
+ }
61
+
62
+ return undefined;
63
+ }
64
+
65
+ function shouldShowUI(): boolean {
66
+ if (typeof document === "undefined" || !document.currentScript) {
67
+ return false;
68
+ }
69
+
70
+ try {
71
+ const {src} = (document.currentScript as HTMLScriptElement);
72
+ if (!src) {return false;}
73
+ const params = new URL(src).searchParams;
74
+ return params.get("ui") === "true";
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ function formatArg(arg: unknown): string {
81
+ if (typeof arg === "string") {return arg;}
82
+ if (arg instanceof Error) {return `${arg.name}: ${arg.message}`;}
83
+ try {
84
+ return JSON.stringify(arg);
85
+ } catch {
86
+ return String(arg);
87
+ }
88
+ }
89
+
90
+ function formatArgs(args: unknown[]): string {
91
+ return args.map(formatArg).join(" ");
92
+ }
93
+
94
+ /**
95
+ * Initialize the remote logger, intercepting console methods.
96
+ * @returns The remote logger global object, or undefined if no server URL detected
97
+ */
98
+ export function initRemoteLogger(): RemoteLoggerGlobal | undefined {
99
+ const serverUrl = detectServerUrl();
100
+ if (!serverUrl) {
101
+ console.warn("[RemoteLogger] Could not detect server URL. Set window.__REMOTE_LOG_SERVER_URL__ before loading.");
102
+ return undefined;
103
+ }
104
+
105
+ const client = new RemoteLogClient({
106
+ serverUrl,
107
+ sessionPrefix: "remote",
108
+ batchIntervalMs: 500,
109
+ });
110
+
111
+ const originalMethods: Record<ConsoleMethod, typeof console.log> = {} as Record<ConsoleMethod, typeof console.log>;
112
+
113
+ for (const method of INTERCEPTED_METHODS) {
114
+ // eslint-disable-next-line no-console
115
+ originalMethods[method] = console[method];
116
+
117
+ // eslint-disable-next-line no-console
118
+ console[method] = (...args: unknown[]) => {
119
+ originalMethods[method].apply(console, args);
120
+ const message = formatArgs(args);
121
+ client.log(CONSOLE_LEVEL_MAP[method], message);
122
+ };
123
+ }
124
+
125
+ let ui: ConsoleCaptureUI | undefined;
126
+ if (shouldShowUI()) {
127
+ ui = new ConsoleCaptureUI();
128
+ }
129
+
130
+ const destroy = (): void => {
131
+ for (const method of INTERCEPTED_METHODS) {
132
+ // eslint-disable-next-line no-console
133
+ console[method] = originalMethods[method];
134
+ }
135
+ void client.close();
136
+ if (ui) {
137
+ ui.destroy();
138
+ }
139
+ if (typeof window !== "undefined") {
140
+ delete window.__remoteLogger__;
141
+ }
142
+ };
143
+
144
+ const global: RemoteLoggerGlobal = { client, ui, destroy };
145
+
146
+ if (typeof window !== "undefined") {
147
+ window.__remoteLogger__ = global;
148
+ }
149
+
150
+ return global;
151
+ }
152
+
153
+ // Auto-initialize on load
154
+ initRemoteLogger();
155
+
156
+ // Re-export for manual use via the IIFE global
157
+ export { ConsoleCaptureUI,RemoteLogClient };
@@ -108,7 +108,29 @@ logger.interceptConsole();
108
108
  logger.log("INFO", "Hello from browser!");
109
109
  \`\`\`
110
110
 
111
- ### Option 2: Raw fetch() calls
111
+ ### Option 2: Script tag (zero install)
112
+
113
+ The server serves a browser-ready script that auto-configures itself.
114
+ Get the script URL from logs_status - look for server.scriptUrl in the response.
115
+
116
+ Add to HTML:
117
+ \`\`\`html
118
+ <script src="http://localhost:9080/remote-logger.js"></script>
119
+ \`\`\`
120
+
121
+ Or paste in browser console:
122
+ \`\`\`javascript
123
+ var s=document.createElement('script');s.src='http://localhost:9080/remote-logger.js';document.head.appendChild(s);
124
+ \`\`\`
125
+
126
+ The script automatically:
127
+ - Intercepts all console.log/warn/error/info/debug calls
128
+ - Sends them to the server it was loaded from (zero config)
129
+ - Exposes window.__remoteLogger__ for manual control (e.g., window.__remoteLogger__.destroy())
130
+
131
+ Add ?ui=true to the script URL to show a floating console capture widget.
132
+
133
+ ### Option 3: Raw fetch() calls
112
134
 
113
135
  \`\`\`typescript
114
136
  fetch("http://localhost:9080/log", {
@@ -123,6 +145,28 @@ fetch("http://localhost:9080/log", {
123
145
  });
124
146
  \`\`\`
125
147
 
148
+ ## Debugging Third-Party Websites
149
+
150
+ The server includes a reverse proxy that injects the remote logger into any website.
151
+ Get the proxy base URL from logs_status - look for server.proxyBaseUrl.
152
+
153
+ To debug a third-party site, prepend the proxy base URL to the target URL:
154
+ {proxyBaseUrl}https://example.com/page
155
+
156
+ Example: http://192.168.1.x:9080/proxy/https://example.com
157
+
158
+ The proxy automatically:
159
+ - Injects the remote-logger script into HTML responses
160
+ - Strips Content-Security-Policy headers that would block the script
161
+ - Forwards cookies, auth headers, and other request data
162
+ - Passes non-HTML resources (CSS, JS, images) through unmodified
163
+
164
+ Limitations:
165
+ - Resources using absolute paths (e.g., /fonts/..., /media/...) bypass the base tag and return 404
166
+ - JavaScript fetch() calls using absolute paths may bypass the proxy
167
+ - OAuth redirect flows that check the origin domain will not work
168
+ - WebSocket connections are not proxied (future enhancement)
169
+
126
170
  ## Querying Logs (MCP Tools)
127
171
 
128
172
  Once browser logs are flowing to the server:
@@ -138,8 +182,9 @@ Once browser logs are flowing to the server:
138
182
 
139
183
  ## Typical Debugging Workflow
140
184
 
141
- 1. Call logs_status to verify server is running and get the endpoint URL
142
- 2. Ensure the browser app is configured to send logs to that endpoint
185
+ 1. Call logs_status to verify server is running and get the endpoint URL, script URL, and proxy URL
186
+ 2. Add logging to the browser app using one of the options above (script tag is simplest)
187
+ - For third-party sites, use the proxy: navigate to {proxyBaseUrl}https://target-site.com
143
188
  3. Trigger the action in the browser you want to debug
144
189
  4. Call logs_get_recent to see what happened
145
190
  5. Use logs_search if looking for specific errors or messages`;
@@ -61,9 +61,12 @@ export const logsStatusTool = {
61
61
  description:
62
62
  "Get the status of the remote log server. " +
63
63
  "Returns health metrics (uptime, session count, log count, memory usage), " +
64
- "HTTP endpoint configuration (port, host, URL for browser clients), " +
64
+ "HTTP endpoint configuration (port, host, URL for browser clients, script URL for zero-config injection, " +
65
+ "proxy base URL for debugging third-party sites), " +
65
66
  "and retention settings (how long logs are kept before automatic cleanup). " +
66
67
  "Use this to verify the server is running, find the endpoint URL for configuring browser clients, " +
68
+ "get the script URL (server.scriptUrl) for injecting via a script tag, " +
69
+ "get the proxy URL (server.proxyBaseUrl) for debugging third-party sites, " +
67
70
  "or check server configuration.",
68
71
  inputSchema: logsStatusInputSchema,
69
72
  };
@@ -17,8 +17,9 @@ import * as path from "path";
17
17
 
18
18
  import { createMcpServer } from "../mcp/mcp-server.js";
19
19
  import { JsonlWriter } from "./jsonl-writer.js";
20
- import { createLogServer } from "./log-server.js";
20
+ import { createLogServer, setProxy } from "./log-server.js";
21
21
  import { LogStorage, type ServerMode } from "./log-storage.js";
22
+ import { createProxy, type ProxyInstance } from "./proxy.js";
22
23
  import { certFilesExist } from "./self-signed-cert.js";
23
24
 
24
25
  // ANSI color codes for terminal output
@@ -291,6 +292,7 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
291
292
  let httpServer: http.Server | https.Server | undefined;
292
293
  let mcpServer: McpServer | undefined;
293
294
  let actualHttpPort: number | undefined;
295
+ let proxyInstance: ProxyInstance | undefined;
294
296
 
295
297
  // Track active connections for graceful shutdown
296
298
  const activeConnections = new Set<net.Socket>();
@@ -332,9 +334,8 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
332
334
  });
333
335
  });
334
336
 
335
- // Set short keep-alive timeout for tests (connections close faster)
336
- httpServer.keepAliveTimeout = 1000;
337
- httpServer.headersTimeout = 2000;
337
+ httpServer.keepAliveTimeout = 30000;
338
+ httpServer.headersTimeout = 35000;
338
339
 
339
340
  // Set server config in storage so MCP tools can report it
340
341
  // Determine protocol based on whether valid cert files were provided
@@ -366,13 +367,22 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
366
367
  }
367
368
  }
368
369
  }
370
+ const scriptUrl = `${protocol}://${endpointHost}:${actualHttpPort}/remote-logger.js`;
371
+ const proxyBaseUrl = `/proxy/`;
372
+
369
373
  storage.setServerConfig({
370
374
  httpPort: actualHttpPort,
371
375
  httpHost,
372
376
  protocol,
373
377
  httpEndpoint: `${protocol}://${endpointHost}:${actualHttpPort}/log`,
378
+ scriptUrl,
379
+ proxyBaseUrl: `${protocol}://${endpointHost}:${actualHttpPort}${proxyBaseUrl}`,
374
380
  mode,
375
381
  });
382
+
383
+ // Create and register the proxy instance
384
+ proxyInstance = createProxy(proxyBaseUrl, quiet);
385
+ setProxy(proxyInstance);
376
386
  }
377
387
 
378
388
  // Create and connect MCP server if enabled
@@ -400,6 +410,11 @@ export async function createDualServer(options: DualServerOptions = {}): Promise
400
410
  });
401
411
  }
402
412
 
413
+ // Close proxy
414
+ if (proxyInstance) {
415
+ proxyInstance.close();
416
+ }
417
+
403
418
  // Close JSONL writer only if we created it internally
404
419
  if (ownsJsonlWriter) {
405
420
  await jsonlWriter.close();
@@ -18,10 +18,11 @@ import * as http from "http";
18
18
  import * as https from "https";
19
19
  import * as os from "os";
20
20
  import * as path from "path";
21
- import { URL } from "url";
21
+ import { fileURLToPath,URL } from "url";
22
22
 
23
23
  import { JsonlWriter } from "./jsonl-writer.js";
24
24
  import { type LogEntry, LogStorage } from "./log-storage.js";
25
+ import type { ProxyInstance } from "./proxy.js";
25
26
  import { certFilesExist, readCertFiles } from "./self-signed-cert.js";
26
27
 
27
28
  // Shared log storage instance
@@ -62,6 +63,55 @@ export function setLogStorage(storage: LogStorage): void {
62
63
  sharedStorage = storage;
63
64
  }
64
65
 
66
+ // Shared proxy instance
67
+ let sharedProxy: ProxyInstance | null = null;
68
+
69
+ /**
70
+ * Set the shared proxy instance.
71
+ * @param proxy - The proxy instance to use
72
+ */
73
+ export function setProxy(proxy: ProxyInstance): void {
74
+ sharedProxy = proxy;
75
+ }
76
+
77
+
78
+ // Browser bundle cache (loaded on first request)
79
+ let browserBundleCache: string | null | undefined;
80
+
81
+ function loadBrowserBundle(): string | null {
82
+ if (browserBundleCache !== undefined) {
83
+ return browserBundleCache;
84
+ }
85
+
86
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
87
+
88
+ // Try multiple locations: dist/ (when running from built output)
89
+ // and the package root dist/ (when running from source during tests)
90
+ const candidates = [
91
+ path.resolve(thisDir, "..", "remote-logger.browser.js"),
92
+ path.resolve(thisDir, "..", "..", "dist", "remote-logger.browser.js"),
93
+ ];
94
+
95
+ for (const candidate of candidates) {
96
+ try {
97
+ browserBundleCache = fs.readFileSync(candidate, "utf-8");
98
+ return browserBundleCache;
99
+ } catch {
100
+ // Try next candidate
101
+ }
102
+ }
103
+
104
+ browserBundleCache = null;
105
+ return browserBundleCache;
106
+ }
107
+
108
+ /**
109
+ * Reset the browser bundle cache. Used for testing.
110
+ */
111
+ export function resetBrowserBundleCache(): void {
112
+ browserBundleCache = undefined;
113
+ }
114
+
65
115
  // ANSI color codes for terminal output
66
116
  const colors = {
67
117
  reset: "\x1b[0m",
@@ -269,7 +319,36 @@ function handleRequest(
269
319
  return;
270
320
  }
271
321
 
272
- // In logReceiveOnly mode, only /log and /health are available
322
+ // Serve browser-ready script bundle (available in all modes including logReceiveOnly)
323
+ if ((url === "/remote-logger.js" || url.startsWith("/remote-logger.js?")) && req.method === "GET") {
324
+ const bundle = loadBrowserBundle();
325
+ if (!bundle) {
326
+ res.writeHead(404, { "Content-Type": "application/json" });
327
+ res.end(JSON.stringify({ error: "Browser bundle not found. Run 'npm run build' first." }));
328
+ return;
329
+ }
330
+
331
+ const hostHeader = req.headers.host ?? `${host}:${port}`;
332
+ const serverUrl = `${protocol}://${hostHeader}`;
333
+ const configPrefix = `window.__REMOTE_LOG_SERVER_URL__="${serverUrl}";\n`;
334
+
335
+ res.writeHead(200, {
336
+ "Content-Type": "application/javascript",
337
+ "Cache-Control": "no-cache",
338
+ });
339
+ res.end(configPrefix + bundle);
340
+ return;
341
+ }
342
+
343
+ // Reverse proxy with auto-injection (available in all modes including logReceiveOnly)
344
+ if (url.startsWith("/proxy/") && sharedProxy) {
345
+ const targetUrl = decodeURIComponent(url.substring("/proxy/".length));
346
+ const hostHeader = req.headers.host ?? `${host}:${port}`;
347
+ sharedProxy.handleRequest(req, res, targetUrl, protocol, hostHeader);
348
+ return;
349
+ }
350
+
351
+ // In logReceiveOnly mode, only /log, /health, /remote-logger.js, and /proxy/ are available
273
352
  if (logReceiveOnly) {
274
353
  res.writeHead(404, { "Content-Type": "application/json" });
275
354
  res.end(JSON.stringify({ error: "Not found (log receive only mode)" }));
@@ -377,17 +456,21 @@ function printBanner(host: string, port: number, useHttps: boolean): void {
377
456
  // eslint-disable-next-line no-console
378
457
  console.log(`${colors.yellow}API Endpoints:${colors.reset}`);
379
458
  // eslint-disable-next-line no-console
380
- console.log(` ${colors.cyan}POST /log ${colors.reset} - Receive logs from browser`);
459
+ console.log(` ${colors.cyan}POST /log ${colors.reset} - Receive logs from browser`);
460
+ // eslint-disable-next-line no-console
461
+ console.log(` ${colors.cyan}GET /remote-logger.js ${colors.reset} - Browser-ready auto-config script`);
462
+ // eslint-disable-next-line no-console
463
+ console.log(` ${colors.cyan}* /proxy/<url> ${colors.reset} - Reverse proxy with script injection`);
381
464
  // eslint-disable-next-line no-console
382
- console.log(` ${colors.cyan}GET /logs ${colors.reset} - Get all logs as JSON`);
465
+ console.log(` ${colors.cyan}GET /logs ${colors.reset} - Get all logs as JSON`);
383
466
  // eslint-disable-next-line no-console
384
- console.log(` ${colors.cyan}GET /logs/recent ${colors.reset} - Get last 50 logs (?n=100 for more)`);
467
+ console.log(` ${colors.cyan}GET /logs/recent ${colors.reset} - Get last 50 logs (?n=100 for more)`);
385
468
  // eslint-disable-next-line no-console
386
- console.log(` ${colors.cyan}GET /logs/errors ${colors.reset} - Get only error logs`);
469
+ console.log(` ${colors.cyan}GET /logs/errors ${colors.reset} - Get only error logs`);
387
470
  // eslint-disable-next-line no-console
388
- console.log(` ${colors.cyan}POST /logs/clear ${colors.reset} - Clear all logs`);
471
+ console.log(` ${colors.cyan}POST /logs/clear ${colors.reset} - Clear all logs`);
389
472
  // eslint-disable-next-line no-console
390
- console.log(` ${colors.cyan}GET /health ${colors.reset} - Health check`);
473
+ console.log(` ${colors.cyan}GET /health ${colors.reset} - Health check`);
391
474
  // eslint-disable-next-line no-console
392
475
  console.log("");
393
476
  // eslint-disable-next-line no-console
@@ -141,6 +141,10 @@ export interface ServerConfig {
141
141
  protocol: "http" | "https";
142
142
  /** Full URL for browser clients to send logs to */
143
143
  httpEndpoint: string;
144
+ /** URL to the browser-ready auto-config script */
145
+ scriptUrl?: string;
146
+ /** Base URL for the reverse proxy (e.g., "http://host:port/proxy/") */
147
+ proxyBaseUrl?: string;
144
148
  /** Server mode (mcp-only, http-only, or dual) */
145
149
  mode: ServerMode;
146
150
  }