pi-harness-runtime 1.1.376 → 1.1.378

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 (123) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/architecture-generator/package.json +1 -1
  5. package/packages/auth/package.json +1 -1
  6. package/packages/autonomous-refactor/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/cache-strategy/package.json +1 -1
  9. package/packages/capability-registry/package.json +1 -1
  10. package/packages/checkpoint/package.json +1 -1
  11. package/packages/cli-plugin-sdk/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/clipboard-plugin/package.json +1 -1
  14. package/packages/code-generation/package.json +1 -1
  15. package/packages/code-review/package.json +1 -1
  16. package/packages/codex-adapter/package.json +1 -1
  17. package/packages/codex-watcher/package.json +1 -1
  18. package/packages/config-capture/package.json +1 -1
  19. package/packages/connection-runtime/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/contracts/package.json +1 -1
  24. package/packages/cookie-sanitizer/package.json +1 -1
  25. package/packages/cost-optimizer/package.json +1 -1
  26. package/packages/dependency-analyzer/package.json +1 -1
  27. package/packages/django-plugin/package.json +1 -1
  28. package/packages/doc-generator/package.json +1 -1
  29. package/packages/durable-review/package.json +1 -1
  30. package/packages/e2e-tester/package.json +1 -1
  31. package/packages/evaluation-engine/package.json +1 -1
  32. package/packages/evaluation-runner/package.json +1 -1
  33. package/packages/event-bus/package.json +1 -1
  34. package/packages/event-sink/package.json +1 -1
  35. package/packages/event-store/package.json +1 -1
  36. package/packages/experience-replay/package.json +1 -1
  37. package/packages/feedback-collector/package.json +1 -1
  38. package/packages/file-copy-helper/package.json +1 -1
  39. package/packages/framework-detector/package.json +1 -1
  40. package/packages/framework-plugin-sdk/package.json +1 -1
  41. package/packages/frappe-plugin/package.json +1 -1
  42. package/packages/generic-web-plugin/package.json +1 -1
  43. package/packages/health-monitor/package.json +1 -1
  44. package/packages/intent-analyzer/package.json +1 -1
  45. package/packages/jev-judge/package.json +1 -1
  46. package/packages/knowledge-graph/package.json +1 -1
  47. package/packages/knowledge-retrieval/package.json +1 -1
  48. package/packages/laravel-plugin/package.json +1 -1
  49. package/packages/learning-engine/package.json +1 -1
  50. package/packages/mcp-adapter/package.json +1 -1
  51. package/packages/memory-engine/package.json +1 -1
  52. package/packages/milestone-manager/package.json +1 -1
  53. package/packages/model-registry/package.json +1 -1
  54. package/packages/nextjs-plugin/package.json +1 -1
  55. package/packages/notification/package.json +1 -1
  56. package/packages/observability/package.json +1 -1
  57. package/packages/okf-indexer/package.json +1 -1
  58. package/packages/paperclip-integration/package.json +1 -1
  59. package/packages/performance-optimizer/package.json +1 -1
  60. package/packages/permission-mode/package.json +1 -1
  61. package/packages/privilege-broker/package.json +1 -1
  62. package/packages/project-analyzer/package.json +1 -1
  63. package/packages/project-bootstrap/package.json +1 -1
  64. package/packages/projection-engine/package.json +1 -1
  65. package/packages/prompt-compiler/package.json +1 -1
  66. package/packages/prompt-versioning/package.json +1 -1
  67. package/packages/provider-adapter-sdk/package.json +1 -1
  68. package/packages/provider-router/package.json +1 -1
  69. package/packages/provider-selector/package.json +1 -1
  70. package/packages/providers/package.json +1 -1
  71. package/packages/qdrant-skills/package.json +1 -1
  72. package/packages/quota-manager/package.json +1 -1
  73. package/packages/rate-limiter/package.json +1 -1
  74. package/packages/react-vite-plugin/package.json +1 -1
  75. package/packages/relay/dist/client.d.ts +57 -35
  76. package/packages/relay/dist/client.js +284 -71
  77. package/packages/relay/dist/index.d.ts +12 -5
  78. package/packages/relay/dist/index.js +12 -4
  79. package/packages/relay/dist/server.d.ts +58 -20
  80. package/packages/relay/dist/server.js +344 -49
  81. package/packages/relay/dist/types.d.ts +137 -1
  82. package/packages/relay/dist/types.js +21 -2
  83. package/packages/relay/package.json +1 -1
  84. package/packages/relay/src/client.ts +380 -81
  85. package/packages/relay/src/index.ts +42 -5
  86. package/packages/relay/src/server.ts +452 -54
  87. package/packages/relay/src/types.ts +174 -1
  88. package/packages/release-manager/package.json +1 -1
  89. package/packages/requirement-compiler/package.json +1 -1
  90. package/packages/runtime/package.json +1 -1
  91. package/packages/scheduler/package.json +1 -1
  92. package/packages/scheduler-adapter/package.json +1 -1
  93. package/packages/session/package.json +1 -1
  94. package/packages/session-api/package.json +1 -1
  95. package/packages/session-export/package.json +1 -1
  96. package/packages/shared-context/package.json +1 -1
  97. package/packages/skill-mcp-client/package.json +1 -1
  98. package/packages/skill-registry/package.json +1 -1
  99. package/packages/skills/package.json +1 -1
  100. package/packages/sprint-planner/package.json +1 -1
  101. package/packages/subscription-engine/package.json +1 -1
  102. package/packages/task-compiler/package.json +1 -1
  103. package/packages/telegram/package.json +1 -1
  104. package/packages/tencentdb-memory/package.json +1 -1
  105. package/packages/tencentdb-sync/package.json +1 -1
  106. package/packages/test-data-generator/package.json +1 -1
  107. package/packages/test-generator/package.json +1 -1
  108. package/packages/todo-bd-sync/package.json +1 -1
  109. package/packages/token-estimation/package.json +1 -1
  110. package/packages/token-optimizer/package.json +1 -1
  111. package/packages/trajectory/package.json +1 -1
  112. package/packages/tui/package.json +1 -1
  113. package/packages/types/package.json +1 -1
  114. package/packages/voice-api/package.json +1 -1
  115. package/packages/webmcp-tester/package.json +1 -1
  116. package/packages/wiki/package.json +1 -1
  117. package/packages/workflow-events/package.json +1 -1
  118. package/packages/workspace-scanner/package.json +1 -1
  119. package/packages/worktree/package.json +1 -1
  120. package/packages/write-review/package.json +1 -1
  121. package/scripts/relay-client.ts +105 -96
  122. package/scripts/unified-webhook-server.ts +145 -143
  123. package/scripts/ws-server.ts +81 -0
@@ -1,58 +1,96 @@
1
1
  /**
2
- * Relay Server - Cloud relay server for tunnels
3
- */
4
- import type { RelayTunnel, RelayMessage } from './types.js';
5
- /**
6
- * Relay Server options
2
+ * Relay Server - Two-way streaming WebSocket relay
3
+ *
4
+ * Implements t3code-style streaming with:
5
+ * - Snapshot + Live event pattern
6
+ * - Typed message protocol
7
+ * - Connection state machine
8
+ * - Stream coalescing
7
9
  */
10
+ import { type RelayTunnel, type RelayMessage, type ConnectionState, type ConnectionStateChange, type CallbackPayload } from './types.js';
8
11
  export interface RelayServerOptions {
9
12
  /** Port to listen on */
10
13
  port?: number;
11
14
  /** Host to bind */
12
15
  host?: string;
16
+ /** Authentication token */
17
+ authToken?: string;
18
+ /** Server version string */
19
+ serverVersion?: string;
13
20
  /** Called when a tunnel connects */
14
21
  onTunnelConnect?: (tunnel: RelayTunnel) => void;
15
22
  /** Called when a tunnel disconnects */
16
23
  onTunnelDisconnect?: (tunnelId: string) => void;
17
24
  /** Called when a message is received */
18
25
  onMessage?: (tunnelId: string, message: RelayMessage) => void;
26
+ /** Called on state change */
27
+ onStateChange?: (change: ConnectionStateChange) => void;
19
28
  }
20
- /**
21
- * Relay Server
22
- *
23
- * Manages tunnels and relays messages between clients.
24
- */
25
29
  export declare class RelayServer {
30
+ private server;
31
+ private wss;
32
+ private clients;
26
33
  private tunnels;
27
34
  private options;
28
35
  private running;
36
+ private serverSessionId;
37
+ private eventBuffer;
38
+ private recentCallbacks;
29
39
  constructor(options?: RelayServerOptions);
30
40
  /**
31
- * Start the relay server
41
+ * Start the WebSocket server
32
42
  */
33
43
  start(): Promise<void>;
34
44
  /**
35
45
  * Stop the relay server
36
46
  */
37
47
  stop(): Promise<void>;
48
+ private updateState;
49
+ private handleMessage;
50
+ private handleAuth;
51
+ private handlePing;
52
+ private handleSubscribe;
53
+ /**
54
+ * Send initial snapshot followed by synchronized marker
55
+ * This is the t3code pattern: snapshot -> synchronized -> live events
56
+ */
57
+ private sendSnapshot;
58
+ /**
59
+ * Flush event buffer and send batched callbacks
60
+ */
61
+ private flushEventBuffer;
62
+ /**
63
+ * Broadcast callback to all connected clients
64
+ * Uses stream batching for high-frequency events
65
+ */
66
+ broadcastCallback(callback: CallbackPayload, batch?: boolean): void;
38
67
  /**
39
- * Get all active tunnels
68
+ * Send status to all clients
40
69
  */
41
- getTunnels(): RelayTunnel[];
70
+ broadcastStatus(kind: 'info' | 'warning' | 'error', code: string, message: string): void;
42
71
  /**
43
- * Get tunnel by ID
72
+ * Broadcast to all subscribed clients
44
73
  */
45
- getTunnel(id: string): RelayTunnel | undefined;
74
+ private broadcast;
46
75
  /**
47
- * Create a tunnel
76
+ * Check if client is connected and authenticated
48
77
  */
49
- createTunnel(localPort: number): RelayTunnel;
78
+ private isConnected;
50
79
  /**
51
- * Remove a tunnel
80
+ * Send a typed message to a client
52
81
  */
53
- removeTunnel(id: string): boolean;
82
+ private sendMessage;
54
83
  /**
55
- * Check if server is running
84
+ * Send status to specific client
56
85
  */
86
+ private sendStatus;
57
87
  isRunning(): boolean;
88
+ getClientCount(): number;
89
+ getClients(): Array<{
90
+ id: string;
91
+ state: ConnectionState;
92
+ connectedAt: string;
93
+ }>;
94
+ getRecentCallbacks(limit?: number): CallbackPayload[];
58
95
  }
96
+ export declare function createRelayServer(options?: RelayServerOptions): RelayServer;
@@ -1,93 +1,388 @@
1
1
  /**
2
- * Relay Server - Cloud relay server for tunnels
3
- */
4
- /**
5
- * Generate a UUID v4
6
- */
7
- function generateUUID() {
8
- // Use crypto.randomUUID if available
9
- const crypto = globalThis;
10
- if (crypto.crypto?.randomUUID) {
11
- return crypto.crypto.randomUUID();
12
- }
13
- // Fallback
14
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
15
- const r = (Math.random() * 16) | 0;
16
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
17
- return v.toString(16);
18
- });
19
- }
20
- /**
21
- * Relay Server
2
+ * Relay Server - Two-way streaming WebSocket relay
22
3
  *
23
- * Manages tunnels and relays messages between clients.
4
+ * Implements t3code-style streaming with:
5
+ * - Snapshot + Live event pattern
6
+ * - Typed message protocol
7
+ * - Connection state machine
8
+ * - Stream coalescing
24
9
  */
10
+ import { createServer } from "node:http";
11
+ import { WebSocketServer, WebSocket } from "ws";
12
+ import { randomUUID } from "node:crypto";
13
+ import { PROTOCOL_VERSION, generateMessageId, createTimestamp, } from './types.js';
14
+ // ============================================================================
15
+ // Constants
16
+ // ============================================================================
17
+ const DEFAULT_PORT = 8444;
18
+ const DEFAULT_HOST = "0.0.0.0";
19
+ const MAX_RECENT_CALLBACKS = 100;
20
+ const STREAM_BATCH_SIZE = 50; // Batch events for coalescing
21
+ const STREAM_BATCH_TIMEOUT_MS = 100; // Flush batch after this delay
22
+ function createEventBuffer() {
23
+ return {
24
+ callbacks: [],
25
+ timer: null,
26
+ };
27
+ }
28
+ // ============================================================================
29
+ // Relay Server - Two-way Streaming Implementation
30
+ // ============================================================================
25
31
  export class RelayServer {
32
+ server = null;
33
+ wss = null;
34
+ clients = new Map();
26
35
  tunnels = new Map();
27
36
  options;
28
37
  running = false;
38
+ serverSessionId;
39
+ // Event stream buffering for coalescing
40
+ eventBuffer = createEventBuffer();
41
+ // Recent callbacks for snapshot
42
+ recentCallbacks = [];
29
43
  constructor(options = {}) {
44
+ this.serverSessionId = randomUUID();
30
45
  this.options = {
31
- port: options.port ?? 3001,
32
- host: options.host ?? '0.0.0.0',
46
+ port: options.port ?? DEFAULT_PORT,
47
+ host: options.host ?? DEFAULT_HOST,
48
+ authToken: options.authToken ?? "change-me-in-production",
49
+ serverVersion: options.serverVersion ?? "1.0.0",
33
50
  onTunnelConnect: options.onTunnelConnect ?? (() => { }),
34
51
  onTunnelDisconnect: options.onTunnelDisconnect ?? (() => { }),
35
52
  onMessage: options.onMessage ?? (() => { }),
53
+ onStateChange: options.onStateChange ?? (() => { }),
36
54
  };
37
55
  }
56
+ // ========================================================================
57
+ // Lifecycle
58
+ // ========================================================================
38
59
  /**
39
- * Start the relay server
60
+ * Start the WebSocket server
40
61
  */
41
62
  async start() {
42
63
  if (this.running)
43
64
  return;
44
- this.running = true;
45
- // Note: Actual HTTP/WebSocket server would be implemented here
46
- // For now, this is a stub that can be expanded with actual server code
65
+ this.server = createServer();
66
+ this.wss = new WebSocketServer({ server: this.server });
67
+ this.wss.on("connection", (ws, req) => {
68
+ const clientId = randomUUID();
69
+ const clientIp = req.socket.remoteAddress;
70
+ console.log(`[RelayServer] Client connecting: ${clientId} from ${clientIp}`);
71
+ // Create client session
72
+ const session = {
73
+ id: clientId,
74
+ ws,
75
+ state: { state: 'connecting' },
76
+ subscriptions: new Set(['callback', 'status', 'sync']),
77
+ connectedAt: createTimestamp(),
78
+ lastPing: createTimestamp(),
79
+ pendingSnapshot: true,
80
+ };
81
+ this.clients.set(clientId, session);
82
+ this.updateState(session, { state: 'connecting' });
83
+ // Handle incoming messages
84
+ ws.on("message", (data) => {
85
+ this.handleMessage(clientId, data);
86
+ });
87
+ ws.on("close", () => {
88
+ this.clients.delete(clientId);
89
+ console.log(`[RelayServer] Client disconnected: ${clientId}`);
90
+ });
91
+ ws.on("error", (err) => {
92
+ console.error(`[RelayServer] Client error: ${clientId}`, err.message);
93
+ this.clients.delete(clientId);
94
+ });
95
+ // Send initial connected message
96
+ this.sendMessage(session, {
97
+ version: PROTOCOL_VERSION,
98
+ type: 'connected',
99
+ payload: {
100
+ clientId,
101
+ serverTime: createTimestamp(),
102
+ },
103
+ timestamp: createTimestamp(),
104
+ });
105
+ // Auto-auth for now (can be changed to require explicit auth)
106
+ this.updateState(session, { state: 'authenticating' });
107
+ this.handleAuth(clientId, this.options.authToken);
108
+ });
109
+ return new Promise((resolve, reject) => {
110
+ this.server.listen(this.options.port, this.options.host, () => {
111
+ this.running = true;
112
+ console.log(`[RelayServer] Server started on ${this.options.host}:${this.options.port}`);
113
+ resolve();
114
+ });
115
+ this.server.on("error", (err) => {
116
+ console.error("[RelayServer] Server error:", err);
117
+ reject(err);
118
+ });
119
+ });
47
120
  }
48
121
  /**
49
122
  * Stop the relay server
50
123
  */
51
124
  async stop() {
52
- this.running = false;
53
- this.tunnels.clear();
125
+ if (!this.running)
126
+ return;
127
+ // Close all client connections
128
+ for (const session of this.clients.values()) {
129
+ session.ws.close();
130
+ }
131
+ this.clients.clear();
132
+ // Close event buffer timer
133
+ if (this.eventBuffer.timer) {
134
+ clearTimeout(this.eventBuffer.timer);
135
+ }
136
+ return new Promise((resolve) => {
137
+ this.server.close(() => {
138
+ this.running = false;
139
+ console.log("[RelayServer] Server stopped");
140
+ resolve();
141
+ });
142
+ });
143
+ }
144
+ // ========================================================================
145
+ // State Management
146
+ // ========================================================================
147
+ updateState(session, newState) {
148
+ const previous = session.state;
149
+ session.state = newState;
150
+ this.options.onStateChange({
151
+ previous,
152
+ current: newState,
153
+ timestamp: createTimestamp(),
154
+ });
155
+ }
156
+ // ========================================================================
157
+ // Message Handling
158
+ // ========================================================================
159
+ handleMessage(clientId, data) {
160
+ const session = this.clients.get(clientId);
161
+ if (!session)
162
+ return;
163
+ try {
164
+ const msg = JSON.parse(data.toString());
165
+ switch (msg.type) {
166
+ case 'auth':
167
+ this.handleAuth(clientId, msg.payload.token);
168
+ break;
169
+ case 'ping':
170
+ this.handlePing(session);
171
+ break;
172
+ case 'subscribe':
173
+ this.handleSubscribe(session, msg);
174
+ break;
175
+ default:
176
+ const _msg = msg;
177
+ console.warn(`[RelayServer] Unknown message type: ${_msg?.type}`);
178
+ }
179
+ }
180
+ catch (err) {
181
+ console.error("[RelayServer] Invalid message:", err);
182
+ this.sendStatus(session, 'error', 'INVALID_MESSAGE', 'Failed to parse message');
183
+ }
184
+ }
185
+ handleAuth(clientId, token) {
186
+ const session = this.clients.get(clientId);
187
+ if (!session)
188
+ return;
189
+ const success = token === this.options.authToken;
190
+ const response = {
191
+ version: PROTOCOL_VERSION,
192
+ type: 'auth',
193
+ payload: {
194
+ success,
195
+ sessionId: success ? this.serverSessionId : undefined,
196
+ error: success ? undefined : 'Invalid token',
197
+ },
198
+ timestamp: createTimestamp(),
199
+ messageId: generateMessageId(),
200
+ };
201
+ this.sendMessage(session, response);
202
+ if (success) {
203
+ this.updateState(session, { state: 'connected', sessionId: this.serverSessionId });
204
+ console.log(`[RelayServer] Client authenticated: ${clientId}`);
205
+ // Send snapshot + synchronized (t3code pattern)
206
+ this.sendSnapshot(session);
207
+ }
208
+ else {
209
+ this.updateState(session, { state: 'error', error: 'Authentication failed' });
210
+ session.ws.close();
211
+ }
212
+ }
213
+ handlePing(session) {
214
+ session.lastPing = createTimestamp();
215
+ this.sendMessage(session, {
216
+ version: PROTOCOL_VERSION,
217
+ type: 'pong',
218
+ payload: {},
219
+ timestamp: createTimestamp(),
220
+ });
221
+ }
222
+ handleSubscribe(session, msg) {
223
+ session.subscriptions = new Set(msg.payload.events);
224
+ console.log(`[RelayServer] Client ${session.id} subscribed to: ${Array.from(session.subscriptions).join(', ')}`);
225
+ }
226
+ // ========================================================================
227
+ // Snapshot + Live Streaming (t3code pattern)
228
+ // ========================================================================
229
+ /**
230
+ * Send initial snapshot followed by synchronized marker
231
+ * This is the t3code pattern: snapshot -> synchronized -> live events
232
+ */
233
+ async sendSnapshot(session) {
234
+ // Build snapshot message
235
+ const snapshot = {
236
+ version: PROTOCOL_VERSION,
237
+ type: 'snapshot',
238
+ payload: {
239
+ sessionId: this.serverSessionId,
240
+ serverVersion: this.options.serverVersion,
241
+ connectedClients: this.clients.size,
242
+ recentCallbacks: this.recentCallbacks.slice(-20), // Last 20 callbacks
243
+ },
244
+ timestamp: createTimestamp(),
245
+ messageId: generateMessageId(),
246
+ };
247
+ // Send snapshot
248
+ this.sendMessage(session, snapshot);
249
+ // Send synchronized marker (t3code pattern: indicates snapshot complete)
250
+ const synchronized = {
251
+ version: PROTOCOL_VERSION,
252
+ type: 'synchronized',
253
+ payload: {},
254
+ timestamp: createTimestamp(),
255
+ messageId: generateMessageId(),
256
+ };
257
+ this.sendMessage(session, synchronized);
258
+ session.pendingSnapshot = false;
259
+ console.log(`[RelayServer] Snapshot sent to ${session.id}`);
54
260
  }
55
261
  /**
56
- * Get all active tunnels
262
+ * Flush event buffer and send batched callbacks
57
263
  */
58
- getTunnels() {
59
- return Array.from(this.tunnels.values());
264
+ flushEventBuffer() {
265
+ if (this.eventBuffer.callbacks.length === 0)
266
+ return;
267
+ const callbacks = this.eventBuffer.callbacks;
268
+ this.eventBuffer.callbacks = [];
269
+ this.eventBuffer.timer = null;
270
+ // Send each callback as a separate live event
271
+ for (const callback of callbacks) {
272
+ this.broadcastCallback(callback, false); // false = don't add to recent
273
+ }
60
274
  }
275
+ // ========================================================================
276
+ // Broadcasting
277
+ // ========================================================================
61
278
  /**
62
- * Get tunnel by ID
279
+ * Broadcast callback to all connected clients
280
+ * Uses stream batching for high-frequency events
63
281
  */
64
- getTunnel(id) {
65
- return this.tunnels.get(id);
282
+ broadcastCallback(callback, batch = true) {
283
+ // Add to recent callbacks (circular buffer)
284
+ this.recentCallbacks.push(callback);
285
+ if (this.recentCallbacks.length > MAX_RECENT_CALLBACKS) {
286
+ this.recentCallbacks.shift();
287
+ }
288
+ const message = {
289
+ version: PROTOCOL_VERSION,
290
+ type: 'callback',
291
+ payload: callback,
292
+ timestamp: createTimestamp(),
293
+ messageId: generateMessageId(),
294
+ };
295
+ if (batch) {
296
+ // Add to buffer for coalescing
297
+ this.eventBuffer.callbacks.push(callback);
298
+ if (!this.eventBuffer.timer) {
299
+ this.eventBuffer.timer = setTimeout(() => {
300
+ this.flushEventBuffer();
301
+ }, STREAM_BATCH_TIMEOUT_MS);
302
+ }
303
+ }
304
+ else {
305
+ // Send immediately
306
+ this.broadcast(message);
307
+ }
66
308
  }
67
309
  /**
68
- * Create a tunnel
310
+ * Send status to all clients
69
311
  */
70
- createTunnel(localPort) {
71
- const tunnel = {
72
- id: generateUUID(),
73
- url: `https://tunnel-${Date.now()}.relay.example.com`,
74
- localPort,
75
- status: 'connecting',
76
- createdAt: new Date().toISOString(),
312
+ broadcastStatus(kind, code, message) {
313
+ const msg = {
314
+ version: PROTOCOL_VERSION,
315
+ type: 'status',
316
+ payload: { kind, code, message },
317
+ timestamp: createTimestamp(),
318
+ messageId: generateMessageId(),
77
319
  };
78
- this.tunnels.set(tunnel.id, tunnel);
79
- return tunnel;
320
+ this.broadcast(msg);
80
321
  }
81
322
  /**
82
- * Remove a tunnel
323
+ * Broadcast to all subscribed clients
83
324
  */
84
- removeTunnel(id) {
85
- return this.tunnels.delete(id);
325
+ broadcast(message) {
326
+ let count = 0;
327
+ for (const session of this.clients.values()) {
328
+ if (this.isConnected(session)) {
329
+ this.sendMessage(session, message);
330
+ count++;
331
+ }
332
+ }
333
+ console.log(`[RelayServer] Broadcast ${message.type} to ${count} client(s)`);
86
334
  }
87
335
  /**
88
- * Check if server is running
336
+ * Check if client is connected and authenticated
89
337
  */
338
+ isConnected(session) {
339
+ return session.ws.readyState === WebSocket.OPEN &&
340
+ session.state.state === 'connected';
341
+ }
342
+ /**
343
+ * Send a typed message to a client
344
+ */
345
+ sendMessage(session, message) {
346
+ if (session.ws.readyState === WebSocket.OPEN) {
347
+ session.ws.send(JSON.stringify(message));
348
+ }
349
+ }
350
+ /**
351
+ * Send status to specific client
352
+ */
353
+ sendStatus(session, kind, code, message) {
354
+ const msg = {
355
+ version: PROTOCOL_VERSION,
356
+ type: 'status',
357
+ payload: { kind, code, message },
358
+ timestamp: createTimestamp(),
359
+ messageId: generateMessageId(),
360
+ };
361
+ this.sendMessage(session, msg);
362
+ }
363
+ // ========================================================================
364
+ // Public API
365
+ // ========================================================================
90
366
  isRunning() {
91
367
  return this.running;
92
368
  }
369
+ getClientCount() {
370
+ return this.clients.size;
371
+ }
372
+ getClients() {
373
+ return Array.from(this.clients.values()).map(s => ({
374
+ id: s.id,
375
+ state: s.state,
376
+ connectedAt: s.connectedAt,
377
+ }));
378
+ }
379
+ getRecentCallbacks(limit = 20) {
380
+ return this.recentCallbacks.slice(-limit);
381
+ }
382
+ }
383
+ // ============================================================================
384
+ // Export for use in webhook server
385
+ // ============================================================================
386
+ export function createRelayServer(options) {
387
+ return new RelayServer(options);
93
388
  }