wslog-proxy 0.0.0-stage → 0.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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -2
  3. package/dist/cli/args.d.ts +14 -0
  4. package/dist/cli/args.js +98 -0
  5. package/dist/cli/args.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +78 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/index.d.ts +12 -0
  10. package/dist/index.js +10 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/server/app.d.ts +19 -0
  13. package/dist/server/app.js +96 -0
  14. package/dist/server/app.js.map +1 -0
  15. package/dist/server/config.d.ts +51 -0
  16. package/dist/server/config.js +62 -0
  17. package/dist/server/config.js.map +1 -0
  18. package/dist/server/connections.d.ts +21 -0
  19. package/dist/server/connections.js +81 -0
  20. package/dist/server/connections.js.map +1 -0
  21. package/dist/server/context.d.ts +37 -0
  22. package/dist/server/context.js +64 -0
  23. package/dist/server/context.js.map +1 -0
  24. package/dist/server/demo/bots.d.ts +4 -0
  25. package/dist/server/demo/bots.js +90 -0
  26. package/dist/server/demo/bots.js.map +1 -0
  27. package/dist/server/demo/chat-server.d.ts +9 -0
  28. package/dist/server/demo/chat-server.js +90 -0
  29. package/dist/server/demo/chat-server.js.map +1 -0
  30. package/dist/server/demo/controller.d.ts +21 -0
  31. package/dist/server/demo/controller.js +37 -0
  32. package/dist/server/demo/controller.js.map +1 -0
  33. package/dist/server/events.d.ts +19 -0
  34. package/dist/server/events.js +19 -0
  35. package/dist/server/events.js.map +1 -0
  36. package/dist/server/http/api.d.ts +6 -0
  37. package/dist/server/http/api.js +89 -0
  38. package/dist/server/http/api.js.map +1 -0
  39. package/dist/server/http/auth.d.ts +26 -0
  40. package/dist/server/http/auth.js +78 -0
  41. package/dist/server/http/auth.js.map +1 -0
  42. package/dist/server/http/live.d.ts +26 -0
  43. package/dist/server/http/live.js +117 -0
  44. package/dist/server/http/live.js.map +1 -0
  45. package/dist/server/http/router.d.ts +31 -0
  46. package/dist/server/http/router.js +89 -0
  47. package/dist/server/http/router.js.map +1 -0
  48. package/dist/server/http/server.d.ts +17 -0
  49. package/dist/server/http/server.js +82 -0
  50. package/dist/server/http/server.js.map +1 -0
  51. package/dist/server/http/static.d.ts +3 -0
  52. package/dist/server/http/static.js +56 -0
  53. package/dist/server/http/static.js.map +1 -0
  54. package/dist/server/http/upgrade.d.ts +3 -0
  55. package/dist/server/http/upgrade.js +13 -0
  56. package/dist/server/http/upgrade.js.map +1 -0
  57. package/dist/server/masking.d.ts +11 -0
  58. package/dist/server/masking.js +104 -0
  59. package/dist/server/masking.js.map +1 -0
  60. package/dist/server/paths.d.ts +12 -0
  61. package/dist/server/paths.js +30 -0
  62. package/dist/server/paths.js.map +1 -0
  63. package/dist/server/plugins.d.ts +20 -0
  64. package/dist/server/plugins.js +4 -0
  65. package/dist/server/plugins.js.map +1 -0
  66. package/dist/server/proxy/headers.d.ts +4 -0
  67. package/dist/server/proxy/headers.js +39 -0
  68. package/dist/server/proxy/headers.js.map +1 -0
  69. package/dist/server/proxy/proxy.d.ts +41 -0
  70. package/dist/server/proxy/proxy.js +188 -0
  71. package/dist/server/proxy/proxy.js.map +1 -0
  72. package/dist/server/recorder.d.ts +34 -0
  73. package/dist/server/recorder.js +65 -0
  74. package/dist/server/recorder.js.map +1 -0
  75. package/dist/server/storage/index.d.ts +14 -0
  76. package/dist/server/storage/index.js +40 -0
  77. package/dist/server/storage/index.js.map +1 -0
  78. package/dist/server/storage/jsonl.d.ts +30 -0
  79. package/dist/server/storage/jsonl.js +107 -0
  80. package/dist/server/storage/jsonl.js.map +1 -0
  81. package/dist/server/storage/memory.d.ts +24 -0
  82. package/dist/server/storage/memory.js +74 -0
  83. package/dist/server/storage/memory.js.map +1 -0
  84. package/dist/server/storage/sqlite.d.ts +23 -0
  85. package/dist/server/storage/sqlite.js +125 -0
  86. package/dist/server/storage/sqlite.js.map +1 -0
  87. package/dist/server/storage/types.d.ts +30 -0
  88. package/dist/server/storage/types.js +2 -0
  89. package/dist/server/storage/types.js.map +1 -0
  90. package/dist/server/targets.d.ts +41 -0
  91. package/dist/server/targets.js +156 -0
  92. package/dist/server/targets.js.map +1 -0
  93. package/dist/server/util/logger.d.ts +16 -0
  94. package/dist/server/util/logger.js +19 -0
  95. package/dist/server/util/logger.js.map +1 -0
  96. package/dist/shared/filter.d.ts +14 -0
  97. package/dist/shared/filter.js +41 -0
  98. package/dist/shared/filter.js.map +1 -0
  99. package/dist/shared/index.d.ts +2 -0
  100. package/dist/shared/index.js +3 -0
  101. package/dist/shared/index.js.map +1 -0
  102. package/dist/shared/protocol.d.ts +124 -0
  103. package/dist/shared/protocol.js +9 -0
  104. package/dist/shared/protocol.js.map +1 -0
  105. package/dist/web/assets/index-C3OwozRd.css +1 -0
  106. package/dist/web/assets/index-D2oUxHhH.js +11 -0
  107. package/dist/web/favicon.svg +1 -0
  108. package/dist/web/index.html +15 -0
  109. package/package.json +84 -4
@@ -0,0 +1,21 @@
1
+ import type { ConnectionInfo, Direction } from '../shared/protocol.js';
2
+ import type { EventBus } from './events.js';
3
+ export type NewConnection = Omit<ConnectionInfo, 'id' | 'status' | 'openedAt' | 'counts'>;
4
+ /** Tracks proxied client connections and their per-second message rates. */
5
+ export declare class ConnectionRegistry {
6
+ private readonly bus;
7
+ private readonly connections;
8
+ private ticks;
9
+ constructor(bus: EventBus);
10
+ create(init: NewConnection): ConnectionInfo;
11
+ update(id: string, patch: Partial<ConnectionInfo>): void;
12
+ /** Count a message; connection updates are pushed by the rate tick, not per message. */
13
+ countMessage(id: string, direction: Direction, bytes: number): void;
14
+ /** Returns and resets message counts since the last call. */
15
+ drainRates(): Record<string, number>;
16
+ get(id: string): ConnectionInfo | undefined;
17
+ list(): ConnectionInfo[];
18
+ /** "Clear" means a fresh start: forget finished connections and zero the counters of open ones. */
19
+ reset(): void;
20
+ private pruneClosed;
21
+ }
@@ -0,0 +1,81 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ /** Keeps the last N closed connections so the sidebar does not grow forever. */
3
+ const MAX_CLOSED = 200;
4
+ /** Tracks proxied client connections and their per-second message rates. */
5
+ export class ConnectionRegistry {
6
+ bus;
7
+ connections = new Map();
8
+ ticks = new Map();
9
+ constructor(bus) {
10
+ this.bus = bus;
11
+ }
12
+ create(init) {
13
+ const connection = {
14
+ ...init,
15
+ id: randomBytes(4).toString('hex'),
16
+ status: 'connecting',
17
+ openedAt: Date.now(),
18
+ counts: { in: 0, out: 0, bytesIn: 0, bytesOut: 0 },
19
+ };
20
+ this.connections.set(connection.id, connection);
21
+ this.bus.emit('connection', connection);
22
+ return connection;
23
+ }
24
+ update(id, patch) {
25
+ const connection = this.connections.get(id);
26
+ if (!connection)
27
+ return;
28
+ Object.assign(connection, patch);
29
+ if (patch.status === 'closed' || patch.status === 'error') {
30
+ connection.closedAt ??= Date.now();
31
+ this.pruneClosed();
32
+ }
33
+ this.bus.emit('connection', connection);
34
+ }
35
+ /** Count a message; connection updates are pushed by the rate tick, not per message. */
36
+ countMessage(id, direction, bytes) {
37
+ const connection = this.connections.get(id);
38
+ if (!connection)
39
+ return;
40
+ connection.counts[direction]++;
41
+ if (direction === 'in')
42
+ connection.counts.bytesIn += bytes;
43
+ else
44
+ connection.counts.bytesOut += bytes;
45
+ this.ticks.set(id, (this.ticks.get(id) ?? 0) + 1);
46
+ }
47
+ /** Returns and resets message counts since the last call. */
48
+ drainRates() {
49
+ const rates = {};
50
+ for (const connection of this.connections.values()) {
51
+ if (connection.status === 'open')
52
+ rates[connection.id] = 0;
53
+ }
54
+ for (const [id, n] of this.ticks)
55
+ rates[id] = n;
56
+ this.ticks = new Map();
57
+ return rates;
58
+ }
59
+ get(id) {
60
+ return this.connections.get(id);
61
+ }
62
+ list() {
63
+ return [...this.connections.values()];
64
+ }
65
+ /** "Clear" means a fresh start: forget finished connections and zero the counters of open ones. */
66
+ reset() {
67
+ for (const c of this.list()) {
68
+ if (c.status === 'closed' || c.status === 'error')
69
+ this.connections.delete(c.id);
70
+ else
71
+ c.counts = { in: 0, out: 0, bytesIn: 0, bytesOut: 0 };
72
+ }
73
+ this.ticks = new Map();
74
+ }
75
+ pruneClosed() {
76
+ const closed = this.list().filter((c) => c.status === 'closed' || c.status === 'error');
77
+ for (const c of closed.slice(0, Math.max(0, closed.length - MAX_CLOSED)))
78
+ this.connections.delete(c.id);
79
+ }
80
+ }
81
+ //# sourceMappingURL=connections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connections.js","sourceRoot":"","sources":["../../src/server/connections.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI1C,gFAAgF;AAChF,MAAM,UAAU,GAAG,GAAG,CAAC;AAIvB,4EAA4E;AAC5E,MAAM,OAAO,kBAAkB;IAIA;IAHZ,WAAW,GAAG,IAAI,GAAG,EAA0B,CAAC;IACzD,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE1C,YAA6B,GAAa;QAAb,QAAG,GAAH,GAAG,CAAU;IAAG,CAAC;IAE9C,MAAM,CAAC,IAAmB;QACxB,MAAM,UAAU,GAAmB;YACjC,GAAG,IAAI;YACP,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;YAClC,MAAM,EAAE,YAAY;YACpB,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE;YACpB,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;SACnD,CAAC;QACF,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;QAChD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QACxC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,MAAM,CAAC,EAAU,EAAE,KAA8B;QAC/C,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5C,IAAI,CAAC,UAAU;YAAE,OAAO;QACxB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACjC,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;YAC1D,UAAU,CAAC,QAAQ,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC;YACnC,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;IAC1C,CAAC;IAED,wFAAwF;IACxF,YAAY,CAAC,EAAU,EAAE,SAAoB,EAAE,KAAa;QAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5C,IAAI,CAAC,UAAU;YAAE,OAAO;QACxB,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/B,IAAI,SAAS,KAAK,IAAI;YAAE,UAAU,CAAC,MAAM,CAAC,OAAO,IAAI,KAAK,CAAC;;YACtD,UAAU,CAAC,MAAM,CAAC,QAAQ,IAAI,KAAK,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,6DAA6D;IAC7D,UAAU;QACR,MAAM,KAAK,GAA2B,EAAE,CAAC;QACzC,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC;YACnD,IAAI,UAAU,CAAC,MAAM,KAAK,MAAM;gBAAE,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QAC7D,CAAC;QACD,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK;YAAE,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC;IACxC,CAAC;IAED,mGAAmG;IACnG,KAAK;QACH,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5B,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO;gBAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;;gBAC5E,CAAC,CAAC,MAAM,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,EAAE,CAAC;IACzB,CAAC;IAEO,WAAW;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC;QACxF,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC;YAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC1G,CAAC;CACF"}
@@ -0,0 +1,37 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import type { AppInfo, Role, TargetInfo } from '../shared/protocol.js';
3
+ import { type WslogConfig } from './config.js';
4
+ import type { ConnectionRegistry } from './connections.js';
5
+ import type { DemoController } from './demo/controller.js';
6
+ import type { EventBus } from './events.js';
7
+ import type { Router } from './http/router.js';
8
+ import type { Masker } from './masking.js';
9
+ import type { Recorder } from './recorder.js';
10
+ import type { MessageStore } from './storage/types.js';
11
+ import type { TargetRegistry } from './targets.js';
12
+ import type { Logger } from './util/logger.js';
13
+ /** Everything a route, plugin, or subsystem may need. Built once in `app.ts`. */
14
+ export interface WslogContext {
15
+ readonly config: WslogConfig;
16
+ readonly version: string;
17
+ readonly logger: Logger;
18
+ readonly bus: EventBus;
19
+ readonly store: MessageStore;
20
+ readonly targets: TargetRegistry;
21
+ readonly connections: ConnectionRegistry;
22
+ readonly recorder: Recorder;
23
+ readonly masker: Masker;
24
+ readonly router: Router;
25
+ readonly demo: DemoController;
26
+ /** Port actually listened on (differs from config when config.port is 0). */
27
+ port(): number;
28
+ }
29
+ export declare function lanAddress(): string | undefined;
30
+ /** The dashboard address wslog prints and hands out. `publicUrl` wins (e.g. Docker, where wslog cannot see the host's port). */
31
+ export declare function dashboardUrl(ctx: WslogContext): string;
32
+ export declare function shareUrl(ctx: WslogContext): string;
33
+ /** A target as dashboards, share links, and exports see it: secrets in its URL masked. Proxying uses the real URL. */
34
+ export declare function publicTarget<T extends TargetInfo>(ctx: WslogContext, target: T): T;
35
+ export declare function publicTargets(ctx: WslogContext, targets?: TargetInfo[]): TargetInfo[];
36
+ export declare function baseInfo(ctx: WslogContext, req?: IncomingMessage): Omit<AppInfo, 'role'>;
37
+ export declare function appInfo(ctx: WslogContext, role: Role, req?: IncomingMessage): AppInfo;
@@ -0,0 +1,64 @@
1
+ import { networkInterfaces } from 'node:os';
2
+ import { isLoopbackHost, isWildcardHost } from './config.js';
3
+ /** Hostname other machines/apps should use to reach this wslog, when no request tells us better. */
4
+ function publicHostname(config) {
5
+ if (isWildcardHost(config.host))
6
+ return lanAddress() || 'localhost';
7
+ return config.host.includes(':') ? `[${config.host}]` : config.host;
8
+ }
9
+ export function lanAddress() {
10
+ for (const addresses of Object.values(networkInterfaces())) {
11
+ for (const a of addresses ?? []) {
12
+ if (a.family === 'IPv4' && !a.internal)
13
+ return a.address;
14
+ }
15
+ }
16
+ return undefined;
17
+ }
18
+ /** The dashboard address wslog prints and hands out. `publicUrl` wins (e.g. Docker, where wslog cannot see the host's port). */
19
+ export function dashboardUrl(ctx) {
20
+ if (ctx.config.publicUrl)
21
+ return ctx.config.publicUrl;
22
+ const hostname = publicHostname(ctx.config);
23
+ const display = isLoopbackHost(hostname) ? 'localhost' : hostname;
24
+ return `http://${display}:${ctx.port()}`;
25
+ }
26
+ export function shareUrl(ctx) {
27
+ return `${dashboardUrl(ctx)}/?token=${encodeURIComponent(ctx.config.shareToken)}`;
28
+ }
29
+ /**
30
+ * Base for the proxy URLs a dashboard shows. When wslog listens on every
31
+ * interface, the address the viewer's browser used is the one their apps can
32
+ * use too, including a Docker port mapping (`-p 8080:4000`).
33
+ */
34
+ function proxyBaseUrl(ctx, req) {
35
+ if (isWildcardHost(ctx.config.host) && req?.headers.host)
36
+ return `ws://${req.headers.host}`;
37
+ if (ctx.config.publicUrl) {
38
+ const url = new URL(ctx.config.publicUrl);
39
+ return `${url.protocol === 'https:' ? 'wss' : 'ws'}://${url.host}`;
40
+ }
41
+ return `ws://${publicHostname(ctx.config)}:${ctx.port()}`;
42
+ }
43
+ /** A target as dashboards, share links, and exports see it: secrets in its URL masked. Proxying uses the real URL. */
44
+ export function publicTarget(ctx, target) {
45
+ return { ...target, url: ctx.masker.url(target.url) };
46
+ }
47
+ export function publicTargets(ctx, targets = ctx.targets.list()) {
48
+ return targets.map((t) => publicTarget(ctx, t));
49
+ }
50
+ export function baseInfo(ctx, req) {
51
+ return {
52
+ name: 'wslog',
53
+ version: ctx.version,
54
+ proxyBaseUrl: proxyBaseUrl(ctx, req),
55
+ maskFields: [...ctx.masker.fields],
56
+ maxMessages: ctx.config.maxMessages,
57
+ storage: ctx.store.kind,
58
+ demoRunning: ctx.demo.running,
59
+ };
60
+ }
61
+ export function appInfo(ctx, role, req) {
62
+ return { ...baseInfo(ctx, req), role };
63
+ }
64
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/server/context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export interface Bots {
2
+ stop(): void;
3
+ }
4
+ export declare function startBots(proxyUrl: string): Bots;
@@ -0,0 +1,90 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import WebSocket from 'ws';
3
+ /** Simulated chat users that talk through the wslog proxy in demo mode. */
4
+ const LINES = [
5
+ 'anyone seen the deploy logs?',
6
+ 'pushed a fix for the reconnect bug 🎉',
7
+ 'lunch?',
8
+ 'the websocket keeps dropping after 60s, probably the load balancer idle timeout',
9
+ 'LGTM, merging',
10
+ 'can someone review #42',
11
+ 'coffee ☕',
12
+ 'rate limit is 100 msg/s per connection',
13
+ 'I think the heartbeat interval should be 25s',
14
+ 'ship it 🚀',
15
+ ];
16
+ const BOTS = ['ada', 'linus', 'grace'];
17
+ const pick = (items) => items[Math.floor(Math.random() * items.length)];
18
+ const between = (min, max) => min + Math.random() * (max - min);
19
+ export function startBots(proxyUrl) {
20
+ const timers = new Set();
21
+ const sockets = new Set();
22
+ let stopped = false;
23
+ const later = (ms, fn) => {
24
+ const t = setTimeout(() => {
25
+ timers.delete(t);
26
+ if (!stopped)
27
+ fn();
28
+ }, ms);
29
+ timers.add(t);
30
+ };
31
+ function connect(user, leavesAfterMs) {
32
+ const ws = new WebSocket(`${proxyUrl}?room=general&client=${user}`);
33
+ sockets.add(ws);
34
+ const send = (payload) => ws.readyState === WebSocket.OPEN && ws.send(JSON.stringify(payload));
35
+ const talk = () => {
36
+ if (ws.readyState !== WebSocket.OPEN)
37
+ return;
38
+ const roll = Math.random();
39
+ if (roll < 0.6) {
40
+ send({ type: 'typing' });
41
+ later(between(400, 1200), () => send({ type: 'message', text: pick(LINES) }));
42
+ }
43
+ else if (roll < 0.75) {
44
+ send({
45
+ type: 'message',
46
+ text: 'here are the numbers',
47
+ attachments: [
48
+ { kind: 'table', rows: [{ region: 'eu-west', p50: 41, p99: 180 }, { region: 'us-east', p50: 37, p99: 152 }] },
49
+ { kind: 'link', url: 'https://example.com/dashboards/latency', preview: { title: 'Latency', width: 1200, height: 630 } },
50
+ ],
51
+ });
52
+ }
53
+ else if (roll < 0.85) {
54
+ ws.send(randomBytes(Math.floor(between(32, 256))), { binary: true });
55
+ }
56
+ else if (roll < 0.95) {
57
+ send({ type: 'ping', t: Date.now() });
58
+ }
59
+ else {
60
+ send({ type: 'auth.refresh', password: 'hunter2' });
61
+ }
62
+ later(between(1500, 5000), talk);
63
+ };
64
+ ws.on('open', () => {
65
+ send({ type: 'join', user, token: `tok_${randomBytes(10).toString('hex')}` });
66
+ later(between(500, 2000), talk);
67
+ if (leavesAfterMs)
68
+ later(leavesAfterMs, () => ws.close(1000, 'bye 👋'));
69
+ });
70
+ ws.on('close', () => {
71
+ sockets.delete(ws);
72
+ if (!stopped && leavesAfterMs)
73
+ later(3000, () => connect(user, between(30_000, 60_000)));
74
+ });
75
+ ws.on('error', () => {
76
+ // The proxy or chat server went away; the close handler decides what to do.
77
+ });
78
+ }
79
+ BOTS.forEach((user, i) => later(i * 700, () => connect(user, i === 2 ? between(30_000, 45_000) : undefined)));
80
+ return {
81
+ stop() {
82
+ stopped = true;
83
+ for (const t of timers)
84
+ clearTimeout(t);
85
+ for (const ws of sockets)
86
+ ws.close(1001, 'demo stopped');
87
+ },
88
+ };
89
+ }
90
+ //# sourceMappingURL=bots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bots.js","sourceRoot":"","sources":["../../../src/server/demo/bots.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A tiny fake chat server used by demo mode. It speaks a JSON protocol
3
+ * with a few message shapes so the dashboard has something realistic to show.
4
+ */
5
+ export interface ChatServer {
6
+ url: string;
7
+ close(): Promise<void>;
8
+ }
9
+ export declare function startChatServer(): Promise<ChatServer>;
@@ -0,0 +1,90 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import WebSocket, { WebSocketServer } from 'ws';
3
+ export async function startChatServer() {
4
+ const wss = new WebSocketServer({ host: '127.0.0.1', port: 0, path: '/chat' });
5
+ await new Promise((resolve) => wss.once('listening', resolve));
6
+ const { port } = wss.address();
7
+ const members = new Set();
8
+ let nextMessageId = 1;
9
+ const startedAt = Date.now();
10
+ const broadcast = (payload) => {
11
+ const data = JSON.stringify(payload);
12
+ for (const m of members)
13
+ if (m.ws.readyState === WebSocket.OPEN)
14
+ m.ws.send(data);
15
+ };
16
+ const online = () => [...members].map((m) => m.user);
17
+ wss.on('connection', (ws, req) => {
18
+ const room = new URL(req.url ?? '/', 'http://x').searchParams.get('room') ?? 'general';
19
+ let member;
20
+ ws.on('message', (data, isBinary) => {
21
+ if (isBinary) {
22
+ ws.send(JSON.stringify({ type: 'ack', kind: 'binary', bytes: data.length }));
23
+ return;
24
+ }
25
+ let msg;
26
+ try {
27
+ msg = JSON.parse(String(data));
28
+ }
29
+ catch {
30
+ ws.send(JSON.stringify({ type: 'error', code: 'bad_json', message: 'Expected JSON' }));
31
+ return;
32
+ }
33
+ switch (msg.type) {
34
+ case 'join': {
35
+ member = { user: String(msg.user ?? 'anon'), ws };
36
+ members.add(member);
37
+ ws.send(JSON.stringify({
38
+ type: 'welcome',
39
+ room,
40
+ session: { id: randomBytes(6).toString('hex'), token: `sess_${randomBytes(12).toString('hex')}`, expiresIn: 3600 },
41
+ online: online(),
42
+ }));
43
+ broadcast({ type: 'presence', event: 'joined', user: member.user, online: online() });
44
+ break;
45
+ }
46
+ case 'typing':
47
+ broadcast({ type: 'typing', user: member?.user });
48
+ break;
49
+ case 'message':
50
+ broadcast({
51
+ type: 'message',
52
+ id: nextMessageId++,
53
+ room,
54
+ user: member?.user,
55
+ text: msg.text,
56
+ attachments: msg.attachments ?? [],
57
+ ts: new Date().toISOString(),
58
+ });
59
+ break;
60
+ case 'auth.refresh':
61
+ ws.send(JSON.stringify({ type: 'auth.ok', token: `sess_${randomBytes(12).toString('hex')}` }));
62
+ break;
63
+ case 'ping':
64
+ ws.send(JSON.stringify({ type: 'pong', t: msg.t }));
65
+ break;
66
+ default:
67
+ ws.send(JSON.stringify({ type: 'error', code: 'unknown_type', message: `Unknown type ${String(msg.type)}` }));
68
+ }
69
+ });
70
+ ws.on('close', () => {
71
+ if (!member)
72
+ return;
73
+ members.delete(member);
74
+ broadcast({ type: 'presence', event: 'left', user: member.user, online: online() });
75
+ });
76
+ });
77
+ const stats = setInterval(() => {
78
+ broadcast({ type: 'stats', online: members.size, messages: nextMessageId - 1, uptimeSec: Math.round((Date.now() - startedAt) / 1000) });
79
+ }, 10_000);
80
+ return {
81
+ url: `ws://127.0.0.1:${port}/chat`,
82
+ close: () => new Promise((resolve) => {
83
+ clearInterval(stats);
84
+ for (const client of wss.clients)
85
+ client.terminate();
86
+ wss.close(() => resolve());
87
+ }),
88
+ };
89
+ }
90
+ //# sourceMappingURL=chat-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-server.js","sourceRoot":"","sources":["../../../src/server/demo/chat-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { TargetInfo } from '../../shared/protocol.js';
2
+ import type { TargetRegistry } from '../targets.js';
3
+ import type { Logger } from '../util/logger.js';
4
+ export interface DemoDeps {
5
+ targets: TargetRegistry;
6
+ logger: Logger;
7
+ /** Base URL the bots use to reach the proxy, e.g. `ws://127.0.0.1:4000`. */
8
+ localProxyBase(): string;
9
+ }
10
+ /** Starts the fake chat server plus bots that talk to it through the proxy. */
11
+ export declare class DemoController {
12
+ private readonly deps;
13
+ private server?;
14
+ private bots?;
15
+ private target?;
16
+ private starting?;
17
+ constructor(deps: DemoDeps);
18
+ get running(): boolean;
19
+ start(): Promise<TargetInfo>;
20
+ stop(): Promise<void>;
21
+ }
@@ -0,0 +1,37 @@
1
+ import { startBots } from './bots.js';
2
+ import { startChatServer } from './chat-server.js';
3
+ /** Starts the fake chat server plus bots that talk to it through the proxy. */
4
+ export class DemoController {
5
+ deps;
6
+ server;
7
+ bots;
8
+ target;
9
+ starting;
10
+ constructor(deps) {
11
+ this.deps = deps;
12
+ }
13
+ get running() {
14
+ return Boolean(this.target);
15
+ }
16
+ start() {
17
+ if (this.target)
18
+ return Promise.resolve(this.target);
19
+ this.starting ??= (async () => {
20
+ this.server = await startChatServer();
21
+ const target = this.deps.targets.add(this.server.url, { demo: true });
22
+ this.bots = startBots(`${this.deps.localProxyBase()}${target.proxyPath}`);
23
+ this.target = target;
24
+ this.deps.logger.debug(`demo chat server at ${this.server.url}`);
25
+ return target;
26
+ })();
27
+ return this.starting;
28
+ }
29
+ async stop() {
30
+ this.bots?.stop();
31
+ await this.server?.close();
32
+ if (this.target)
33
+ this.deps.targets.remove(this.target.id);
34
+ this.server = this.bots = this.target = this.starting = undefined;
35
+ }
36
+ }
37
+ //# sourceMappingURL=controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.js","sourceRoot":"","sources":["../../../src/server/demo/controller.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAa,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,eAAe,EAAmB,MAAM,kBAAkB,CAAC;AASpE,+EAA+E;AAC/E,MAAM,OAAO,cAAc;IAMI;IALrB,MAAM,CAAc;IACpB,IAAI,CAAQ;IACZ,MAAM,CAAc;IACpB,QAAQ,CAAuB;IAEvC,YAA6B,IAAc;QAAd,SAAI,GAAJ,IAAI,CAAU;IAAG,CAAC;IAE/C,IAAI,OAAO;QACT,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACrD,IAAI,CAAC,QAAQ,KAAK,CAAC,KAAK,IAAI,EAAE;YAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,eAAe,EAAE,CAAC;YACtC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACtE,IAAI,CAAC,IAAI,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;YAC1E,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;YACjE,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,EAAE,CAAC;QACL,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;QAClB,MAAM,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;QAC3B,IAAI,IAAI,CAAC,MAAM;YAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;IACpE,CAAC;CACF"}
@@ -0,0 +1,19 @@
1
+ import type { ConnectionInfo, MessageRecord, TargetInfo } from '../shared/protocol.js';
2
+ /** Everything that happens inside wslog. Plugins and the live hub subscribe here. */
3
+ export interface WslogEvents {
4
+ message: [MessageRecord];
5
+ connection: [ConnectionInfo];
6
+ targets: [TargetInfo[]];
7
+ cleared: [];
8
+ info: [];
9
+ }
10
+ type Listener<K extends keyof WslogEvents> = (...args: WslogEvents[K]) => void;
11
+ /** Thin typed wrapper over EventEmitter. */
12
+ export declare class EventBus {
13
+ private readonly emitter;
14
+ constructor();
15
+ on<K extends keyof WslogEvents>(event: K, listener: Listener<K>): () => void;
16
+ off<K extends keyof WslogEvents>(event: K, listener: Listener<K>): void;
17
+ emit<K extends keyof WslogEvents>(event: K, ...args: WslogEvents[K]): void;
18
+ }
19
+ export {};
@@ -0,0 +1,19 @@
1
+ import { EventEmitter } from 'node:events';
2
+ /** Thin typed wrapper over EventEmitter. */
3
+ export class EventBus {
4
+ emitter = new EventEmitter();
5
+ constructor() {
6
+ this.emitter.setMaxListeners(100);
7
+ }
8
+ on(event, listener) {
9
+ this.emitter.on(event, listener);
10
+ return () => this.off(event, listener);
11
+ }
12
+ off(event, listener) {
13
+ this.emitter.off(event, listener);
14
+ }
15
+ emit(event, ...args) {
16
+ this.emitter.emit(event, ...args);
17
+ }
18
+ }
19
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/server/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAc3C,4CAA4C;AAC5C,MAAM,OAAO,QAAQ;IACF,OAAO,GAAG,IAAI,YAAY,EAAE,CAAC;IAE9C;QACE,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IACpC,CAAC;IAED,EAAE,CAA8B,KAAQ,EAAE,QAAqB;QAC7D,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,QAAwC,CAAC,CAAC;QACjE,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,GAAG,CAA8B,KAAQ,EAAE,QAAqB;QAC9D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAwC,CAAC,CAAC;IACpE,CAAC;IAED,IAAI,CAA8B,KAAQ,EAAE,GAAG,IAAoB;QACjE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import { type MessageRecord } from '../../shared/protocol.js';
2
+ import { type WslogContext } from '../context.js';
3
+ import type { Router } from './router.js';
4
+ /** Shorten payloads for list/live transport. Full records come from `GET /api/messages/:id`. */
5
+ export declare function toPartial(record: MessageRecord): MessageRecord;
6
+ export declare function registerApiRoutes(router: Router, ctx: WslogContext): void;
@@ -0,0 +1,89 @@
1
+ import { filterFromSearchParams } from '../../shared/filter.js';
2
+ import { LIVE_PAYLOAD_LIMIT } from '../../shared/protocol.js';
3
+ import { appInfo, publicTarget, publicTargets, shareUrl } from '../context.js';
4
+ import { isWildcardHost } from '../config.js';
5
+ import { probeTarget, TargetError } from '../targets.js';
6
+ import { HttpError } from './auth.js';
7
+ const DEFAULT_PAGE = 500;
8
+ const MAX_PAGE = 5000;
9
+ /** Shorten payloads for list/live transport. Full records come from `GET /api/messages/:id`. */
10
+ export function toPartial(record) {
11
+ if (record.payload.length <= LIVE_PAYLOAD_LIMIT)
12
+ return record;
13
+ return { ...record, payload: record.payload.slice(0, LIVE_PAYLOAD_LIMIT), partial: true };
14
+ }
15
+ export function registerApiRoutes(router, ctx) {
16
+ const { store, targets, connections, bus } = ctx;
17
+ router.get('/api/health', () => ({ ok: true }));
18
+ router.get('/api/info', ({ role, req }) => appInfo(ctx, role, req));
19
+ router.get('/api/targets', () => publicTargets(ctx));
20
+ router.post('/api/targets', async ({ body }) => {
21
+ const { url } = await body();
22
+ if (typeof url !== 'string')
23
+ throw new HttpError(400, 'Body must be {"url": "ws://..."}');
24
+ let target;
25
+ try {
26
+ target = targets.add(url);
27
+ }
28
+ catch (err) {
29
+ if (err instanceof TargetError)
30
+ throw new HttpError(400, err.message);
31
+ throw err;
32
+ }
33
+ // Say so now, not when the app first fails to connect.
34
+ const unreachable = await probeTarget(target.url);
35
+ return publicTarget(ctx, unreachable ? { ...target, unreachable } : target);
36
+ }, { role: 'admin' });
37
+ router.delete('/api/targets/:id', ({ params }) => {
38
+ if (!targets.remove(params.id))
39
+ throw new HttpError(404, 'Unknown target');
40
+ }, { role: 'admin' });
41
+ router.get('/api/connections', () => connections.list());
42
+ router.get('/api/messages', ({ url }) => {
43
+ const limit = clamp(Number(url.searchParams.get('limit') ?? DEFAULT_PAGE), 1, MAX_PAGE);
44
+ const before = url.searchParams.get('beforeId');
45
+ return store
46
+ .query({ ...filterFromSearchParams(url.searchParams), limit, beforeId: before ? Number(before) : undefined })
47
+ .map(toPartial);
48
+ });
49
+ router.get('/api/messages/:id', ({ params }) => {
50
+ const record = store.get(Number(params.id));
51
+ if (!record)
52
+ throw new HttpError(404, 'Message not found (it may have been dropped by the storage limit)');
53
+ return record;
54
+ });
55
+ router.delete('/api/messages', () => {
56
+ store.clear();
57
+ connections.reset();
58
+ bus.emit('cleared');
59
+ }, { role: 'admin' });
60
+ router.get('/api/export', ({ url, res }) => {
61
+ const filter = filterFromSearchParams(url.searchParams);
62
+ const body = JSON.stringify({
63
+ exportedAt: new Date().toISOString(),
64
+ wslog: ctx.version,
65
+ filter,
66
+ targets: publicTargets(ctx),
67
+ connections: connections.list(),
68
+ messages: store.query(filter),
69
+ }, null, 2);
70
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
71
+ res.writeHead(200, {
72
+ 'Content-Type': 'application/json; charset=utf-8',
73
+ 'Content-Disposition': `attachment; filename="wslog-${stamp}.json"`,
74
+ 'Content-Length': Buffer.byteLength(body),
75
+ 'Cache-Control': 'no-store',
76
+ });
77
+ res.end(body);
78
+ });
79
+ router.get('/api/share', () => ({ url: shareUrl(ctx), networkReachable: isWildcardHost(ctx.config.host) }), { role: 'admin' });
80
+ router.post('/api/demo', async () => {
81
+ const target = await ctx.demo.start();
82
+ bus.emit('info');
83
+ return publicTarget(ctx, target);
84
+ }, { role: 'admin' });
85
+ }
86
+ function clamp(n, min, max) {
87
+ return Number.isFinite(n) ? Math.min(max, Math.max(min, Math.floor(n))) : min;
88
+ }
89
+ //# sourceMappingURL=api.js.map