pi-harness-runtime 1.1.375 → 1.1.377

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 (129) hide show
  1. package/README.md +68 -39
  2. package/package.json +2 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/package.json +1 -1
  5. package/packages/architecture-generator/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-refactor/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/cache-strategy/package.json +1 -1
  10. package/packages/capability-registry/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/cli-plugin-sdk/package.json +1 -1
  13. package/packages/clipboard/package.json +1 -1
  14. package/packages/clipboard-plugin/package.json +1 -1
  15. package/packages/code-generation/package.json +1 -1
  16. package/packages/code-review/package.json +1 -1
  17. package/packages/codex-adapter/package.json +1 -1
  18. package/packages/codex-watcher/package.json +1 -1
  19. package/packages/config-capture/package.json +1 -1
  20. package/packages/connection-runtime/package.json +1 -1
  21. package/packages/context-compiler/package.json +1 -1
  22. package/packages/context-discovery/package.json +1 -1
  23. package/packages/context-manager/package.json +1 -1
  24. package/packages/contracts/package.json +1 -1
  25. package/packages/cookie-sanitizer/package.json +1 -1
  26. package/packages/cost-optimizer/package.json +1 -1
  27. package/packages/dependency-analyzer/package.json +1 -1
  28. package/packages/django-plugin/package.json +1 -1
  29. package/packages/doc-generator/package.json +1 -1
  30. package/packages/durable-review/package.json +1 -1
  31. package/packages/e2e-tester/package.json +1 -1
  32. package/packages/evaluation-engine/package.json +1 -1
  33. package/packages/evaluation-runner/package.json +1 -1
  34. package/packages/event-bus/package.json +1 -1
  35. package/packages/event-sink/package.json +1 -1
  36. package/packages/event-store/package.json +1 -1
  37. package/packages/experience-replay/package.json +1 -1
  38. package/packages/feedback-collector/package.json +1 -1
  39. package/packages/file-copy-helper/package.json +1 -1
  40. package/packages/framework-detector/package.json +1 -1
  41. package/packages/framework-plugin-sdk/package.json +1 -1
  42. package/packages/frappe-plugin/package.json +1 -1
  43. package/packages/generic-web-plugin/package.json +1 -1
  44. package/packages/health-monitor/package.json +1 -1
  45. package/packages/intent-analyzer/package.json +1 -1
  46. package/packages/jev-judge/package.json +1 -1
  47. package/packages/knowledge-graph/package.json +1 -1
  48. package/packages/knowledge-retrieval/package.json +1 -1
  49. package/packages/laravel-plugin/package.json +1 -1
  50. package/packages/learning-engine/package.json +1 -1
  51. package/packages/mcp-adapter/package.json +1 -1
  52. package/packages/memory-engine/package.json +1 -1
  53. package/packages/milestone-manager/package.json +1 -1
  54. package/packages/model-registry/package.json +1 -1
  55. package/packages/nextjs-plugin/package.json +1 -1
  56. package/packages/notification/package.json +1 -1
  57. package/packages/observability/package.json +1 -1
  58. package/packages/okf-indexer/package.json +1 -1
  59. package/packages/paperclip-integration/package.json +1 -1
  60. package/packages/performance-optimizer/package.json +1 -1
  61. package/packages/permission-mode/package.json +1 -1
  62. package/packages/privilege-broker/package.json +1 -1
  63. package/packages/project-analyzer/package.json +1 -1
  64. package/packages/project-bootstrap/package.json +1 -1
  65. package/packages/projection-engine/package.json +1 -1
  66. package/packages/prompt-compiler/package.json +1 -1
  67. package/packages/prompt-versioning/package.json +1 -1
  68. package/packages/provider-adapter-sdk/package.json +1 -1
  69. package/packages/provider-router/package.json +1 -1
  70. package/packages/provider-selector/package.json +1 -1
  71. package/packages/providers/package.json +1 -1
  72. package/packages/qdrant-skills/package.json +1 -1
  73. package/packages/quota-manager/package.json +1 -1
  74. package/packages/rate-limiter/package.json +1 -1
  75. package/packages/react-vite-plugin/package.json +1 -1
  76. package/packages/relay/dist/client.d.ts +56 -35
  77. package/packages/relay/dist/client.js +278 -71
  78. package/packages/relay/dist/index.d.ts +12 -5
  79. package/packages/relay/dist/index.js +12 -4
  80. package/packages/relay/dist/server.d.ts +58 -20
  81. package/packages/relay/dist/server.js +344 -49
  82. package/packages/relay/dist/types.d.ts +137 -1
  83. package/packages/relay/dist/types.js +21 -2
  84. package/packages/relay/dist/ws-server.d.ts +48 -0
  85. package/packages/relay/dist/ws-server.js +130 -0
  86. package/packages/relay/package.json +1 -1
  87. package/packages/relay/src/client.ts +375 -81
  88. package/packages/relay/src/index.ts +42 -5
  89. package/packages/relay/src/server.ts +452 -54
  90. package/packages/relay/src/types.ts +174 -1
  91. package/packages/relay/src/ws-server.ts +165 -0
  92. package/packages/release-manager/package.json +1 -1
  93. package/packages/requirement-compiler/package.json +1 -1
  94. package/packages/runtime/package.json +1 -1
  95. package/packages/scheduler/package.json +1 -1
  96. package/packages/scheduler-adapter/package.json +1 -1
  97. package/packages/session/package.json +1 -1
  98. package/packages/session-api/package.json +1 -1
  99. package/packages/session-export/package.json +1 -1
  100. package/packages/shared-context/package.json +1 -1
  101. package/packages/skill-mcp-client/package.json +1 -1
  102. package/packages/skill-registry/package.json +1 -1
  103. package/packages/skills/package.json +1 -1
  104. package/packages/sprint-planner/package.json +1 -1
  105. package/packages/subscription-engine/package.json +1 -1
  106. package/packages/task-compiler/package.json +1 -1
  107. package/packages/telegram/package.json +1 -1
  108. package/packages/tencentdb-memory/package.json +1 -1
  109. package/packages/tencentdb-sync/package.json +1 -1
  110. package/packages/test-data-generator/package.json +1 -1
  111. package/packages/test-generator/package.json +1 -1
  112. package/packages/todo-bd-sync/package.json +1 -1
  113. package/packages/token-estimation/package.json +1 -1
  114. package/packages/token-optimizer/package.json +1 -1
  115. package/packages/trajectory/package.json +1 -1
  116. package/packages/tui/package.json +1 -1
  117. package/packages/types/package.json +1 -1
  118. package/packages/voice-api/package.json +1 -1
  119. package/packages/webmcp-tester/package.json +1 -1
  120. package/packages/wiki/package.json +1 -1
  121. package/packages/workflow-events/package.json +1 -1
  122. package/packages/workspace-scanner/package.json +1 -1
  123. package/packages/worktree/package.json +1 -1
  124. package/packages/write-review/package.json +1 -1
  125. package/scripts/relay-client.ts +184 -0
  126. package/scripts/sync-callback-queue.ts +57 -0
  127. package/scripts/telegram-callback-processor.ts +16 -0
  128. package/scripts/unified-webhook-server.ts +160 -145
  129. package/scripts/ws-server.ts +81 -0
@@ -1,26 +1,30 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Unified Webhook Server — Handles both Telegram and LINE on single domain
3
+ * Unified Webhook Server — Handles Telegram, LINE, and WebSocket relay
4
+ *
5
+ * Two-way streaming implementation using relay package:
6
+ * - Snapshot + Live event pattern (t3code style)
7
+ * - Typed message protocol
8
+ * - Connection state machine
9
+ * - Stream coalescing for high-frequency events
4
10
  *
5
11
  * Routes:
6
12
  * /telegram → Telegram webhook handler
7
13
  * /line → LINE webhook handler
14
+ * /ws → WebSocket relay (port 8444)
8
15
  *
9
16
  * Usage:
10
17
  * bun run scripts/unified-webhook-server.ts
11
- *
12
- * Environment variables (or keys/*.txt files):
13
- * TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET, TELEGRAM_ALLOWED_USERS
14
- * LINE_CHANNEL_SECRET, LINE_ACCESS_TOKEN
15
- *
16
- * Webhook URLs to configure:
17
- * Telegram: https://webhook.bunchee.online/telegram
18
- * LINE: https://webhook.bunchee.online/line
19
18
  */
20
19
 
21
- import { existsSync, readFileSync, appendFileSync } from "node:fs";
20
+ import { existsSync, readFileSync } from "node:fs";
22
21
  import { join } from "node:path";
22
+ import { homedir } from "os";
23
23
  import { createServer, IncomingMessage, ServerResponse } from "node:http";
24
+ import { createRelayServer } from "../packages/relay/src/server";
25
+ import { generateMessageId } from "../packages/relay/src/types";
26
+ import type { CallbackPayload } from "../packages/relay/src/types";
27
+ import type { RelayServerOptions } from "../packages/relay/src/server";
24
28
 
25
29
  // --- Configuration ---
26
30
  interface Config {
@@ -30,77 +34,58 @@ interface Config {
30
34
  allowedUsers: Set<number>;
31
35
  };
32
36
  line?: {
33
- channelSecret: string;
34
37
  accessToken: string;
38
+ channelSecret: string;
35
39
  };
36
40
  }
37
41
 
38
- function getConfig(): Config {
39
- const homedir = process.env.HOME || "/home/frappe";
40
- const keysDir = join(homedir, ".pi-harness-runtime", "keys");
42
+ const PORT = parseInt(process.env.PORT || "8443", 10);
43
+ const WS_PORT = parseInt(process.env.WS_PORT || "8444", 10);
44
+ const AUTH_TOKEN = process.env.RELAY_AUTH_TOKEN || "change-me-in-production";
45
+ const SERVER_VERSION = process.env.SERVER_VERSION || "1.0.0";
46
+
47
+ // --- Keys Loader ---
48
+ function loadConfig(): Config {
49
+ const keysDir = join(homedir(), ".pi-harness-runtime", "keys");
41
50
  const config: Config = {};
42
51
 
43
- // Telegram config
52
+ // Telegram
44
53
  const telegramTokenPath = join(keysDir, "telegram-bot-token.txt");
45
54
  const telegramSecretPath = join(keysDir, "telegram-webhook-secret.txt");
46
55
  const telegramUsersPath = join(keysDir, "telegram-allowed-users.txt");
47
56
 
48
57
  if (existsSync(telegramTokenPath)) {
49
- const token = process.env.TELEGRAM_BOT_TOKEN || readFileSync(telegramTokenPath, "utf8").trim();
50
- const secret = process.env.TELEGRAM_WEBHOOK_SECRET || (existsSync(telegramSecretPath) ? readFileSync(telegramSecretPath, "utf8").trim() : "");
58
+ const token = readFileSync(telegramTokenPath, "utf8").trim();
59
+ const secret = existsSync(telegramSecretPath)
60
+ ? readFileSync(telegramSecretPath, "utf8").trim()
61
+ : process.env.TELEGRAM_WEBHOOK_SECRET || "";
51
62
 
52
63
  const allowedUsers = new Set<number>();
53
- const envUsers = process.env.TELEGRAM_ALLOWED_USERS;
54
- if (envUsers) {
55
- for (const part of envUsers.split(",")) {
56
- const num = parseInt(part.trim(), 10);
57
- if (!isNaN(num) && num > 0) allowedUsers.add(num);
58
- }
59
- }
60
64
  if (existsSync(telegramUsersPath)) {
61
- const content = readFileSync(telegramUsersPath, "utf8").trim();
62
- for (const part of content.split(",")) {
63
- const num = parseInt(part.trim(), 10);
64
- if (!isNaN(num) && num > 0) allowedUsers.add(num);
65
+ const users = readFileSync(telegramUsersPath, "utf8").trim();
66
+ for (const id of users.split(",").map((s) => parseInt(s.trim(), 10))) {
67
+ if (!isNaN(id) && id > 0) allowedUsers.add(id);
65
68
  }
66
69
  }
67
70
 
68
71
  config.telegram = { botToken: token, secret, allowedUsers };
69
72
  }
70
73
 
71
- // LINE config
72
- const lineSecretPath = join(keysDir, "line-channel-secret.txt");
74
+ // LINE
73
75
  const lineTokenPath = join(keysDir, "line-access-token.txt");
76
+ const lineSecretPath = join(keysDir, "line-channel-secret.txt");
74
77
 
75
- if (existsSync(lineSecretPath) && existsSync(lineTokenPath)) {
76
- const channelSecret = process.env.LINE_CHANNEL_SECRET || readFileSync(lineSecretPath, "utf8").trim();
77
- const accessToken = process.env.LINE_ACCESS_TOKEN || readFileSync(lineTokenPath, "utf8").trim();
78
- config.line = { channelSecret, accessToken };
78
+ if (existsSync(lineTokenPath) && existsSync(lineSecretPath)) {
79
+ config.line = {
80
+ accessToken: readFileSync(lineTokenPath, "utf8").trim(),
81
+ channelSecret: readFileSync(lineSecretPath, "utf8").trim(),
82
+ };
79
83
  }
80
84
 
81
85
  return config;
82
86
  }
83
87
 
84
- // --- Logging ---
85
- const LOG_FILE = "/tmp/unified-webhook.log";
86
- function log(prefix: string, ...args: unknown[]) {
87
- const msg = args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ");
88
- const line = `[${new Date().toISOString()}] [${prefix}] ${msg}\n`;
89
- appendFileSync(LOG_FILE, line);
90
- console.log(line.trim());
91
- }
92
-
93
- // --- Crypto utilities ---
94
- async function verifyTelegram(data: string, secret: string, hash: string): Promise<boolean> {
95
- const encoder = new TextEncoder();
96
- const key = await crypto.subtle.importKey(
97
- "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
98
- );
99
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
100
- const expected = Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, "0")).join("");
101
- return expected === hash;
102
- }
103
-
88
+ // --- LINE Verification ---
104
89
  async function verifyLINE(body: string, channelSecret: string, xLineSignature: string): Promise<boolean> {
105
90
  const encoder = new TextEncoder();
106
91
  const key = await crypto.subtle.importKey(
@@ -111,14 +96,49 @@ async function verifyLINE(body: string, channelSecret: string, xLineSignature: s
111
96
  return expected === xLineSignature;
112
97
  }
113
98
 
114
- // --- Callback queue ---
115
- const QUEUE_FILE = join(process.env.HOME || "/tmp", ".pi-harness-runtime", "callback-queue.jsonl");
116
- function queueCallback(callback: Record<string, unknown>) {
117
- appendFileSync(QUEUE_FILE, JSON.stringify({ ...callback, timestamp: new Date().toISOString() }) + "\n");
99
+ // --- Parse Callback Data ---
100
+ function parseCallbackData(data: string): { action: string; targetId: string } {
101
+ const colonIdx = data.indexOf(":");
102
+ const underscoreIdx = data.indexOf("_");
103
+
104
+ if (colonIdx !== -1) {
105
+ return {
106
+ action: data.slice(0, colonIdx),
107
+ targetId: data.slice(colonIdx + 1).replace(/_+$/, ""),
108
+ };
109
+ } else if (underscoreIdx !== -1) {
110
+ return {
111
+ action: data.slice(0, underscoreIdx),
112
+ targetId: data.slice(underscoreIdx + 1).replace(/_+$/, ""),
113
+ };
114
+ }
115
+
116
+ return { action: data, targetId: "" };
117
+ }
118
+
119
+ // --- Create Callback Payload ---
120
+ function createCallbackPayload(
121
+ type: "telegram_callback" | "line_callback",
122
+ action: string,
123
+ targetId: string,
124
+ userId: number | string,
125
+ username?: string,
126
+ messageId?: number
127
+ ): CallbackPayload {
128
+ return {
129
+ id: generateMessageId(),
130
+ type,
131
+ action,
132
+ targetId,
133
+ userId: typeof userId === "string" ? parseInt(userId, 10) || 0 : userId,
134
+ username,
135
+ messageId,
136
+ timestamp: new Date().toISOString(),
137
+ };
118
138
  }
119
139
 
120
- // --- Telegram handler ---
121
- async function handleTelegram(req: IncomingMessage, res: ServerResponse, config: Config) {
140
+ // --- Telegram Handler ---
141
+ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config: Config, relayServer: ReturnType<typeof createRelayServer>) {
122
142
  if (!config.telegram) {
123
143
  res.writeHead(404);
124
144
  res.end("Telegram not configured");
@@ -128,7 +148,6 @@ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config:
128
148
  // Verify secret token
129
149
  const secretToken = req.headers["x-telegram-bot-api-secret-token"];
130
150
  if (secretToken !== config.telegram.secret) {
131
- log("Telegram", "Unauthorized - secret mismatch");
132
151
  res.writeHead(403);
133
152
  res.end("Forbidden");
134
153
  return;
@@ -149,37 +168,27 @@ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config:
149
168
 
150
169
  // Check authorization
151
170
  if (config.telegram.allowedUsers.size > 0 && !config.telegram.allowedUsers.has(from.id)) {
152
- log("Telegram", `Unauthorized user: ${from.id}`);
153
171
  res.writeHead(403);
154
172
  res.end("Forbidden");
155
173
  return;
156
174
  }
157
175
 
158
176
  // Parse callback data
159
- const data = query.data || "";
160
- const colonIdx = data.indexOf(":");
161
- const underscoreIdx = data.indexOf("_");
162
- let action: string, targetId: string;
163
- if (colonIdx !== -1) {
164
- action = data.slice(0, colonIdx);
165
- targetId = data.slice(colonIdx + 1).replace(/_+$/, "");
166
- } else if (underscoreIdx !== -1) {
167
- action = data.slice(0, underscoreIdx);
168
- targetId = data.slice(underscoreIdx + 1).replace(/_+$/, "");
169
- } else {
170
- action = data;
171
- targetId = "";
172
- }
177
+ const { action, targetId } = parseCallbackData(query.data || "");
173
178
 
174
- // Queue the callback
175
- queueCallback({
176
- type: "telegram_callback",
179
+ // Create typed callback payload
180
+ const callback = createCallbackPayload(
181
+ "telegram_callback",
177
182
  action,
178
183
  targetId,
179
- userId: from.id,
180
- username: from.username,
181
- messageId: query.message?.message_id,
182
- });
184
+ from.id,
185
+ from.username,
186
+ query.message?.message_id
187
+ );
188
+
189
+ // Broadcast via typed streaming relay
190
+ relayServer.broadcastCallback(callback);
191
+ console.log(`[Telegram] Callback: ${action} ${targetId} from @${from.username || from.id}`);
183
192
 
184
193
  // Acknowledge immediately
185
194
  await fetch(`https://api.telegram.org/bot${config.telegram.botToken}/answerCallbackQuery`, {
@@ -187,21 +196,19 @@ async function handleTelegram(req: IncomingMessage, res: ServerResponse, config:
187
196
  headers: { "Content-Type": "application/json" },
188
197
  body: JSON.stringify({ callback_query_id: query.id }),
189
198
  });
190
-
191
- log("Telegram", `Callback: ${action} ${targetId} from @${from.username || from.id}`);
192
199
  }
193
200
 
194
201
  res.writeHead(200);
195
202
  res.end("OK");
196
203
  } catch (err) {
197
- log("Telegram", "Error:", err);
204
+ console.error("[Telegram] Error:", err);
198
205
  res.writeHead(500);
199
206
  res.end("Error");
200
207
  }
201
208
  }
202
209
 
203
- // --- LINE handler ---
204
- async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Config) {
210
+ // --- LINE Handler ---
211
+ async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Config, relayServer: ReturnType<typeof createRelayServer>) {
205
212
  if (!config.line) {
206
213
  res.writeHead(404);
207
214
  res.end("LINE not configured");
@@ -211,7 +218,6 @@ async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Con
211
218
  // Verify signature
212
219
  const signature = req.headers["x-line-signature"] as string;
213
220
  if (!signature) {
214
- log("LINE", "Missing signature");
215
221
  res.writeHead(400);
216
222
  res.end("Bad Request");
217
223
  return;
@@ -224,108 +230,117 @@ async function handleLINE(req: IncomingMessage, res: ServerResponse, config: Con
224
230
 
225
231
  // Verify
226
232
  if (!(await verifyLINE(body, config.line.channelSecret, signature))) {
227
- log("LINE", "Invalid signature");
228
233
  res.writeHead(403);
229
234
  res.end("Forbidden");
230
235
  return;
231
236
  }
232
237
 
233
238
  try {
234
- const events = JSON.parse(body).events || [];
239
+ const bodyJson = JSON.parse(body);
240
+ const events = bodyJson.events || [];
235
241
 
236
242
  for (const event of events) {
237
243
  if (event.type === "postback") {
238
- // Parse postback data
239
244
  const data = event.postback?.data || "";
240
- const colonIdx = data.indexOf(":");
241
- let action: string, targetId: string;
242
- if (colonIdx !== -1) {
243
- action = data.slice(0, colonIdx);
244
- targetId = data.slice(colonIdx + 1);
245
- } else {
246
- action = data;
247
- targetId = "";
248
- }
249
-
250
- queueCallback({
251
- type: "line_callback",
245
+ const { action, targetId } = parseCallbackData(data);
246
+
247
+ // Create typed callback payload
248
+ const callback = createCallbackPayload(
249
+ "line_callback",
252
250
  action,
253
251
  targetId,
254
- userId: event.source?.userId,
255
- replyToken: event.replyToken,
256
- });
257
-
258
- log("LINE", `Postback: ${action} ${targetId}`);
259
- } else if (event.type === "message" && event.message?.type === "text") {
260
- // Forward text messages to agent
261
- queueCallback({
262
- type: "line_message",
263
- text: event.message.text,
264
- userId: event.source?.userId,
265
- replyToken: event.replyToken,
266
- });
267
-
268
- log("LINE", `Message: ${event.message.text}`);
252
+ event.source?.userId || "unknown"
253
+ );
254
+
255
+ // Broadcast via typed streaming relay
256
+ relayServer.broadcastCallback(callback);
257
+ console.log(`[LINE] Postback: ${action} ${targetId}`);
269
258
  }
270
259
  }
271
260
 
272
261
  res.writeHead(200);
273
- res.end("OK");
262
+ res.end(JSON.stringify({ status: "ok" }));
274
263
  } catch (err) {
275
- log("LINE", "Error:", err);
264
+ console.error("[LINE] Error:", err);
276
265
  res.writeHead(500);
277
266
  res.end("Error");
278
267
  }
279
268
  }
280
269
 
281
- // --- Main server ---
282
- const PORT = parseInt(process.env.PORT || "3000", 10);
283
-
270
+ // --- Main ---
284
271
  async function main() {
285
- const config = getConfig();
272
+ const config = loadConfig();
286
273
 
287
- if (!config.telegram && !config.line) {
288
- console.error("No webhooks configured. Set up Telegram and/or LINE keys.");
289
- process.exit(1);
290
- }
291
-
292
- console.log("Unified Webhook Server");
293
- console.log(`Listening on port ${PORT}`);
274
+ console.log("===========================================");
275
+ console.log("Unified Webhook Server - Streaming v2");
276
+ console.log("===========================================");
294
277
  if (config.telegram) console.log(" /telegram - Telegram webhook");
295
278
  if (config.line) console.log(" /line - LINE webhook");
279
+ console.log(` HTTP: http://0.0.0.0:${PORT}`);
280
+ console.log(` WebSocket: ws://0.0.0.0:${WS_PORT}`);
281
+ console.log("===========================================");
282
+
283
+ // Create relay server with typed streaming
284
+ const relayOptions: RelayServerOptions = {
285
+ port: WS_PORT,
286
+ host: "0.0.0.0",
287
+ authToken: AUTH_TOKEN,
288
+ serverVersion: SERVER_VERSION,
289
+ onStateChange: (change: { previous: { state: string }; current: { state: string }; timestamp: string }) => {
290
+ console.log(`[Relay] State: ${change.previous.state} → ${change.current.state}`);
291
+ },
292
+ };
293
+
294
+ const relayServer = createRelayServer(relayOptions);
296
295
 
296
+ // Start WebSocket relay
297
+ await relayServer.start();
298
+ console.log("[Relay] WebSocket server started on port", WS_PORT);
299
+
300
+ // HTTP webhook server
297
301
  const server = createServer(async (req, res) => {
298
302
  const url = new URL(req.url || "/", `http://localhost:${PORT}`);
299
303
 
300
304
  try {
301
305
  if (url.pathname === "/telegram" && req.method === "POST") {
302
- await handleTelegram(req, res, config);
306
+ await handleTelegram(req, res, config, relayServer);
303
307
  } else if (url.pathname === "/line" && req.method === "POST") {
304
- await handleLINE(req, res, config);
308
+ await handleLINE(req, res, config, relayServer);
305
309
  } else if (url.pathname === "/health") {
306
310
  res.writeHead(200);
307
- res.end("OK");
311
+ res.end(JSON.stringify({
312
+ status: "ok",
313
+ clients: relayServer.getClientCount(),
314
+ running: relayServer.isRunning(),
315
+ }));
308
316
  } else {
309
317
  res.writeHead(404);
310
318
  res.end("Not Found");
311
319
  }
312
320
  } catch (err) {
313
- log("Server", "Error:", err);
321
+ console.error("[Server] Error:", err);
314
322
  res.writeHead(500);
315
323
  res.end("Error");
316
324
  }
317
325
  });
318
326
 
319
327
  server.listen(PORT, () => {
320
- console.log(`Server running on http://0.0.0.0:${PORT}`);
321
- console.log(`Configure webhooks:`);
322
- if (config.telegram) {
323
- console.log(` Telegram: https://webhook.bunchee.online/telegram`);
324
- }
325
- if (config.line) {
326
- console.log(` LINE: https://webhook.bunchee.online/line`);
327
- }
328
+ console.log(`HTTP server running on http://0.0.0.0:${PORT}`);
329
+ console.log("Configure webhooks:");
330
+ if (config.telegram) console.log(` Telegram: https://webhook.bunchee.online/telegram`);
331
+ if (config.line) console.log(` LINE: https://webhook.bunchee.online/line`);
328
332
  });
333
+
334
+ // Graceful shutdown
335
+ const shutdown = async () => {
336
+ console.log("\nShutting down...");
337
+ await relayServer.stop();
338
+ server.close();
339
+ process.exit(0);
340
+ };
341
+
342
+ process.on("SIGINT", shutdown);
343
+ process.on("SIGTERM", shutdown);
329
344
  }
330
345
 
331
- main();
346
+ main().catch(console.error);
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * WebSocket Relay Server - Two-way streaming callbacks
4
+ *
5
+ * Uses the relay package's typed streaming protocol:
6
+ * - Snapshot + Live event pattern (t3code style)
7
+ * - Typed message protocol
8
+ * - Connection state machine
9
+ * - Stream coalescing for high-frequency events
10
+ *
11
+ * Architecture:
12
+ * Telegram/LINE → Webhook → This Server → WebSocket → Local Client
13
+ */
14
+
15
+ import { createRelayServer, type RelayServerOptions } from "../packages/relay/src/server";
16
+
17
+ // Configuration from environment
18
+ const PORT = parseInt(process.env.WS_PORT || "8444", 10);
19
+ const HOST = process.env.WS_HOST || "0.0.0.0";
20
+ const AUTH_TOKEN = process.env.RELAY_AUTH_TOKEN || "change-me-in-production";
21
+ const SERVER_VERSION = process.env.SERVER_VERSION || "1.0.0";
22
+
23
+ // Create and start server
24
+ const options: RelayServerOptions = {
25
+ port: PORT,
26
+ host: HOST,
27
+ authToken: AUTH_TOKEN,
28
+ serverVersion: SERVER_VERSION,
29
+ onTunnelConnect: (tunnel) => {
30
+ console.log(`[Main] Tunnel connected: ${tunnel.id}`);
31
+ },
32
+ onTunnelDisconnect: (tunnelId) => {
33
+ console.log(`[Main] Tunnel disconnected: ${tunnelId}`);
34
+ },
35
+ onStateChange: (change) => {
36
+ console.log(`[Main] State change: ${change.previous.state} → ${change.current.state}`);
37
+ },
38
+ };
39
+
40
+ const server = createRelayServer(options);
41
+
42
+ async function main() {
43
+ console.log("===========================================");
44
+ console.log("WebSocket Relay Server - Streaming v2");
45
+ console.log("===========================================");
46
+ console.log(` Host: ${HOST}`);
47
+ console.log(` Port: ${PORT}`);
48
+ console.log(` Auth: ${AUTH_TOKEN ? "enabled" : "disabled"}`);
49
+ console.log(` Version: ${SERVER_VERSION}`);
50
+ console.log("===========================================");
51
+
52
+ try {
53
+ await server.start();
54
+ console.log("Server started successfully!");
55
+
56
+ // Log status periodically
57
+ setInterval(() => {
58
+ const clients = server.getClients();
59
+ console.log(`[Status] Clients: ${clients.length}, Running: ${server.isRunning()}`);
60
+ }, 30000);
61
+
62
+ } catch (err) {
63
+ console.error("Failed to start server:", err);
64
+ process.exit(1);
65
+ }
66
+ }
67
+
68
+ // Graceful shutdown
69
+ process.on("SIGINT", async () => {
70
+ console.log("\nShutting down...");
71
+ await server.stop();
72
+ process.exit(0);
73
+ });
74
+
75
+ process.on("SIGTERM", async () => {
76
+ console.log("\nShutting down...");
77
+ await server.stop();
78
+ process.exit(0);
79
+ });
80
+
81
+ main();