openclaw-reddit-agent-server 2026.2.2701 → 2026.2.2703

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.
package/dist/index.js CHANGED
@@ -7,14 +7,16 @@ import {
7
7
  readFileSync,
8
8
  writeFileSync
9
9
  } from "fs";
10
+ import { isIP } from "net";
10
11
  import { homedir } from "os";
11
12
  import path from "path";
12
13
  import { WebSocket, WebSocketServer } from "ws";
13
14
  var REQUEST_TIMEOUT_MS = 6e4;
14
15
  var DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
15
- var DEFAULT_BIND_HOST = "localhost";
16
+ var DEFAULT_BIND_HOST = "0.0.0.0";
16
17
  var DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1e3;
17
18
  var DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
19
+ var DEFAULT_NETWORK_POLICY = "private";
18
20
  var DEFAULT_PAIRING_STORE_PATH = path.join(
19
21
  homedir(),
20
22
  ".openclaw",
@@ -24,6 +26,8 @@ var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
24
26
  var PAIRING_CODE_LENGTH = 6;
25
27
  var MAX_RESPONSE_ID_LENGTH = 200;
26
28
  var MAX_RESPONSE_ERROR_LENGTH = 2e3;
29
+ var EXTENSION_RECONNECT_WAIT_MS = 6500;
30
+ var EXTENSION_RECONNECT_POLL_MS = 250;
27
31
  var ALLOWED_ACTIONS = /* @__PURE__ */ new Set([
28
32
  "get_skill",
29
33
  "fetch_subreddit",
@@ -44,6 +48,7 @@ var WebSocketBridge = class {
44
48
  logger;
45
49
  pairingStorePath;
46
50
  pairingCodeTtlMs;
51
+ networkPolicy;
47
52
  idleTimeoutMs;
48
53
  onIdleShutdown;
49
54
  idleTimer = null;
@@ -57,6 +62,7 @@ var WebSocketBridge = class {
57
62
  };
58
63
  this.pairingStorePath = options?.pairingStorePath?.trim() || DEFAULT_PAIRING_STORE_PATH;
59
64
  this.pairingCodeTtlMs = options?.pairingCodeTtlMs ?? DEFAULT_PAIRING_TTL_MS;
65
+ this.networkPolicy = options?.networkPolicy ?? DEFAULT_NETWORK_POLICY;
60
66
  this.idleTimeoutMs = options?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
61
67
  this.onIdleShutdown = options?.onIdleShutdown;
62
68
  this.loadPairedDevices();
@@ -65,6 +71,7 @@ var WebSocketBridge = class {
65
71
  return this.extensionSocket !== null && this.extensionSocket.readyState === WebSocket.OPEN;
66
72
  }
67
73
  getPendingPairings() {
74
+ this.ensureStarted();
68
75
  this.pruneExpiredPairings();
69
76
  return Array.from(this.pendingPairings.values()).sort((a, b) => b.requestedAt - a.requestedAt).map((entry) => ({
70
77
  code: entry.code,
@@ -83,6 +90,7 @@ var WebSocketBridge = class {
83
90
  }));
84
91
  }
85
92
  approvePairing(codeInput) {
93
+ this.ensureStarted();
86
94
  const code = this.normalizePairingCode(codeInput);
87
95
  if (!code) {
88
96
  throw new Error("Missing pairing code.");
@@ -140,8 +148,16 @@ var WebSocketBridge = class {
140
148
  `WebSocket bridge listening on ws://${this.host}:${this.port}/ws`
141
149
  );
142
150
  this.wss.on("connection", (socket, request) => {
151
+ const remoteAddress = request.socket.remoteAddress ?? "unknown";
152
+ if (!this.isClientAddressAllowed(remoteAddress)) {
153
+ this.logger.warn(
154
+ `Rejected bridge connection from ${remoteAddress} due to network policy "${this.networkPolicy}"`
155
+ );
156
+ this.closeProtocolViolation(socket, "Client network not allowed");
157
+ return;
158
+ }
143
159
  this.logger.info(
144
- `New WebSocket connection from ${request.socket.remoteAddress ?? "unknown"}`
160
+ `New WebSocket connection from ${remoteAddress}`
145
161
  );
146
162
  socket.on("message", (raw) => {
147
163
  const message = this.parseIncomingMessage(raw);
@@ -171,6 +187,7 @@ var WebSocketBridge = class {
171
187
  this.logger.warn(
172
188
  `Received response for unknown request id: ${message.id}`
173
189
  );
190
+ this.closeProtocolViolation(socket, "Unknown request id");
174
191
  return;
175
192
  }
176
193
  this.pendingRequests.delete(message.id);
@@ -213,6 +230,11 @@ var WebSocketBridge = class {
213
230
  });
214
231
  });
215
232
  }
233
+ ensureStarted() {
234
+ if (!this.wss) {
235
+ this.start();
236
+ }
237
+ }
216
238
  stop() {
217
239
  if (this.idleTimer) {
218
240
  clearTimeout(this.idleTimer);
@@ -239,10 +261,14 @@ var WebSocketBridge = class {
239
261
  * Send an action to the connected Chrome extension and wait for a response.
240
262
  * Returns the `data` field from the success response.
241
263
  */
242
- sendAction(action, params, timeoutMs = REQUEST_TIMEOUT_MS) {
264
+ async sendAction(action, params, timeoutMs = REQUEST_TIMEOUT_MS) {
243
265
  if (!ALLOWED_ACTIONS.has(action)) {
244
266
  return Promise.reject(new Error(`Unsupported bridge action: ${action}`));
245
267
  }
268
+ this.ensureStarted();
269
+ if (!this.isExtensionConnected) {
270
+ await this.waitForExtensionConnection(EXTENSION_RECONNECT_WAIT_MS);
271
+ }
246
272
  if (!this.isExtensionConnected) {
247
273
  const pendingCodes = this.getPendingPairings().map((pairing) => pairing.code);
248
274
  const pairingHint = pendingCodes.length > 0 ? `Pending pairing code(s): ${pendingCodes.join(", ")}. Approve one with reddit_approve_pairing.` : "Open the extension popup and request pairing, then approve with reddit_approve_pairing.";
@@ -256,6 +282,11 @@ var WebSocketBridge = class {
256
282
  const id = randomUUID();
257
283
  const request = { id, action, params };
258
284
  return new Promise((resolve, reject) => {
285
+ const activeSocket = this.extensionSocket;
286
+ if (!activeSocket || activeSocket.readyState !== WebSocket.OPEN) {
287
+ reject(new Error("Extension socket is not open"));
288
+ return;
289
+ }
259
290
  const timer = setTimeout(() => {
260
291
  this.pendingRequests.delete(id);
261
292
  reject(
@@ -265,7 +296,13 @@ var WebSocketBridge = class {
265
296
  );
266
297
  }, timeoutMs);
267
298
  this.pendingRequests.set(id, { resolve, reject, timer });
268
- this.extensionSocket.send(JSON.stringify(request));
299
+ try {
300
+ activeSocket.send(JSON.stringify(request));
301
+ } catch (error) {
302
+ clearTimeout(timer);
303
+ this.pendingRequests.delete(id);
304
+ reject(error instanceof Error ? error : new Error(String(error)));
305
+ }
269
306
  });
270
307
  }
271
308
  resetIdleTimer() {
@@ -280,9 +317,20 @@ var WebSocketBridge = class {
280
317
  this.onIdleShutdown?.();
281
318
  }, this.idleTimeoutMs);
282
319
  }
320
+ async waitForExtensionConnection(timeoutMs) {
321
+ if (this.isExtensionConnected) return;
322
+ const deadline = Date.now() + timeoutMs;
323
+ while (Date.now() < deadline) {
324
+ if (this.isExtensionConnected) return;
325
+ await new Promise(
326
+ (resolve) => setTimeout(resolve, EXTENSION_RECONNECT_POLL_MS)
327
+ );
328
+ }
329
+ }
283
330
  handleIdentify(socket, message) {
284
331
  if (message.role !== "extension") {
285
332
  this.logger.warn(`Ignoring identify with unsupported role: ${message.role}`);
333
+ this.closeProtocolViolation(socket, "Unsupported role");
286
334
  return;
287
335
  }
288
336
  const deviceId = this.sanitizeDeviceId(message.deviceId);
@@ -519,6 +567,53 @@ var WebSocketBridge = class {
519
567
  describeSocket(socket) {
520
568
  return socket === this.extensionSocket ? "authenticated" : "unauthenticated";
521
569
  }
570
+ isClientAddressAllowed(remoteAddress) {
571
+ const normalized = this.normalizeRemoteAddress(remoteAddress);
572
+ if (!normalized) return false;
573
+ if (this.isLoopbackAddress(normalized)) {
574
+ return true;
575
+ }
576
+ if (this.networkPolicy === "loopback") {
577
+ return false;
578
+ }
579
+ if (this.networkPolicy === "any") {
580
+ return true;
581
+ }
582
+ return this.isPrivateOrTailscaleAddress(normalized);
583
+ }
584
+ normalizeRemoteAddress(address) {
585
+ const value = address.trim();
586
+ if (!value) return null;
587
+ if (value.startsWith("::ffff:")) {
588
+ return value.slice("::ffff:".length);
589
+ }
590
+ return value;
591
+ }
592
+ isLoopbackAddress(address) {
593
+ return address === "::1" || address === "127.0.0.1";
594
+ }
595
+ isPrivateOrTailscaleAddress(address) {
596
+ if (this.isTailscaleIPv6(address)) {
597
+ return true;
598
+ }
599
+ if (isIP(address) !== 4) {
600
+ return false;
601
+ }
602
+ const parts = address.split(".").map((part) => Number(part));
603
+ if (parts.length !== 4 || parts.some((part) => Number.isNaN(part))) {
604
+ return false;
605
+ }
606
+ const [a, b] = parts;
607
+ if (a === 10) return true;
608
+ if (a === 172 && b >= 16 && b <= 31) return true;
609
+ if (a === 192 && b === 168) return true;
610
+ if (a === 100 && b >= 64 && b <= 127) return true;
611
+ return false;
612
+ }
613
+ isTailscaleIPv6(address) {
614
+ const normalized = address.toLowerCase();
615
+ return normalized.startsWith("fd7a:115c:a1e0:");
616
+ }
522
617
  };
523
618
 
524
619
  // src/tools.ts
@@ -704,8 +799,9 @@ function registerRedditTools(registerTool, bridge) {
704
799
 
705
800
  // index.ts
706
801
  var BRIDGE_PORT = 7071;
707
- var DEFAULT_HOST = "localhost";
802
+ var DEFAULT_HOST = "0.0.0.0";
708
803
  var DEFAULT_PAIRING_CODE_TTL_SECONDS = 600;
804
+ var DEFAULT_NETWORK_POLICY2 = "private";
709
805
  var redditAgentPlugin = {
710
806
  id: "openclaw-reddit-agent-server",
711
807
  name: "OpenClaw Reddit Agent",
@@ -714,6 +810,7 @@ var redditAgentPlugin = {
714
810
  const host = typeof api.pluginConfig?.host === "string" && api.pluginConfig.host.trim().length > 0 ? api.pluginConfig.host.trim() : DEFAULT_HOST;
715
811
  const pairingCodeTtlSeconds = typeof api.pluginConfig?.pairingCodeTtlSeconds === "number" && api.pluginConfig.pairingCodeTtlSeconds > 0 ? api.pluginConfig.pairingCodeTtlSeconds : DEFAULT_PAIRING_CODE_TTL_SECONDS;
716
812
  const pairingStorePath = typeof api.pluginConfig?.pairingStorePath === "string" && api.pluginConfig.pairingStorePath.trim().length > 0 ? api.pluginConfig.pairingStorePath.trim() : void 0;
813
+ const networkPolicy = api.pluginConfig?.networkPolicy === "loopback" || api.pluginConfig?.networkPolicy === "private" || api.pluginConfig?.networkPolicy === "any" ? api.pluginConfig.networkPolicy : DEFAULT_NETWORK_POLICY2;
717
814
  const bridge = new WebSocketBridge(BRIDGE_PORT, {
718
815
  logger: {
719
816
  info: (msg) => api.logger.info(msg),
@@ -722,7 +819,8 @@ var redditAgentPlugin = {
722
819
  },
723
820
  host,
724
821
  pairingCodeTtlMs: pairingCodeTtlSeconds * 1e3,
725
- pairingStorePath
822
+ pairingStorePath,
823
+ networkPolicy
726
824
  });
727
825
  api.registerService({
728
826
  id: "reddit-agent-bridge",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ws-bridge.ts","../src/tools.ts","../index.ts"],"sourcesContent":["import { createHash, randomInt, randomUUID } from \"node:crypto\";\nimport {\n chmodSync,\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport type {\n BridgeRequest,\n BridgeResponse,\n IdentifyMessage,\n PairedDevice,\n PendingPairing,\n PendingRequest,\n} from \"./types.js\";\n\nconst REQUEST_TIMEOUT_MS = 60_000;\nconst DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\nconst DEFAULT_BIND_HOST = \"localhost\";\nconst DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1000; // 10 minutes\nconst DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024; // 8 MiB\nconst DEFAULT_PAIRING_STORE_PATH = path.join(\n homedir(),\n \".openclaw\",\n \"reddit-agent-pairings.json\"\n);\nconst PAIRING_CODE_ALPHABET = \"ABCDEFGHJKLMNPQRSTUVWXYZ23456789\";\nconst PAIRING_CODE_LENGTH = 6;\nconst MAX_RESPONSE_ID_LENGTH = 200;\nconst MAX_RESPONSE_ERROR_LENGTH = 2_000;\nconst ALLOWED_ACTIONS = new Set([\n \"get_skill\",\n \"fetch_subreddit\",\n \"search_reddit\",\n \"fetch_user_posts\",\n \"fetch_post\",\n \"reply_to_comment\",\n]);\n\ninterface PairedDeviceRecord {\n deviceId: string;\n deviceName: string;\n authTokenHash: string;\n approvedAt: string;\n lastSeenAt: string;\n}\n\ninterface PairingStoreFile {\n version: number;\n devices: Array<\n Omit<PairedDeviceRecord, \"authTokenHash\"> & {\n authTokenHash?: string;\n authToken?: string;\n }\n >;\n}\n\ninterface PendingPairingRecord {\n code: string;\n deviceId: string;\n deviceName: string;\n requestedAt: number;\n expiresAt: number;\n socket: WebSocket;\n}\n\nexport interface BridgeLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n}\n\nexport interface BridgeOptions {\n logger?: BridgeLogger;\n host?: string;\n pairingStorePath?: string;\n pairingCodeTtlMs?: number;\n /** Shut down after this many ms of inactivity (0 to disable). Defaults to 5 minutes. */\n idleTimeoutMs?: number;\n /** Called after the bridge stops itself due to inactivity. */\n onIdleShutdown?: () => void;\n}\n\nexport class WebSocketBridge {\n private wss: WebSocketServer | null = null;\n private extensionSocket: WebSocket | null = null;\n private pendingRequests = new Map<string, PendingRequest>();\n private pendingPairings = new Map<string, PendingPairingRecord>();\n private pendingPairingCodeBySocket = new Map<WebSocket, string>();\n private pairedDevices = new Map<string, PairedDeviceRecord>();\n private port: number;\n private host: string;\n private logger: BridgeLogger;\n private pairingStorePath: string;\n private pairingCodeTtlMs: number;\n private idleTimeoutMs: number;\n private onIdleShutdown?: () => void;\n private idleTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(port: number, options?: BridgeOptions) {\n this.port = port;\n this.host = options?.host?.trim() || DEFAULT_BIND_HOST;\n this.logger = options?.logger ?? {\n info: (msg: string) => console.log(`[openclaw-reddit-agent-server] ${msg}`),\n warn: (msg: string) => console.warn(`[openclaw-reddit-agent-server] ${msg}`),\n error: (msg: string) =>\n console.error(`[openclaw-reddit-agent-server] ${msg}`),\n };\n this.pairingStorePath =\n options?.pairingStorePath?.trim() || DEFAULT_PAIRING_STORE_PATH;\n this.pairingCodeTtlMs = options?.pairingCodeTtlMs ?? DEFAULT_PAIRING_TTL_MS;\n this.idleTimeoutMs = options?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;\n this.onIdleShutdown = options?.onIdleShutdown;\n\n this.loadPairedDevices();\n }\n\n get isExtensionConnected(): boolean {\n return (\n this.extensionSocket !== null &&\n this.extensionSocket.readyState === WebSocket.OPEN\n );\n }\n\n getPendingPairings(): PendingPairing[] {\n this.pruneExpiredPairings();\n return Array.from(this.pendingPairings.values())\n .sort((a, b) => b.requestedAt - a.requestedAt)\n .map((entry) => ({\n code: entry.code,\n deviceId: entry.deviceId,\n deviceName: entry.deviceName,\n requestedAt: new Date(entry.requestedAt).toISOString(),\n expiresAt: new Date(entry.expiresAt).toISOString(),\n }));\n }\n\n getPairedDevices(): PairedDevice[] {\n return Array.from(this.pairedDevices.values())\n .sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))\n .map((device) => ({\n deviceId: device.deviceId,\n deviceName: device.deviceName,\n approvedAt: device.approvedAt,\n lastSeenAt: device.lastSeenAt,\n }));\n }\n\n approvePairing(codeInput: string): PairedDevice {\n const code = this.normalizePairingCode(codeInput);\n if (!code) {\n throw new Error(\"Missing pairing code.\");\n }\n\n this.pruneExpiredPairings();\n const pairing = this.pendingPairings.get(code);\n if (!pairing) {\n throw new Error(\n `Pairing code \"${code}\" is invalid or expired. Request a new code in the extension.`\n );\n }\n\n if (pairing.socket.readyState !== WebSocket.OPEN) {\n this.deletePendingPairing(code);\n throw new Error(\"Pairing request is no longer active. Request a new code.\");\n }\n\n const now = new Date().toISOString();\n const authToken = randomUUID();\n const paired: PairedDeviceRecord = {\n deviceId: pairing.deviceId,\n deviceName: pairing.deviceName,\n authTokenHash: this.hashAuthToken(authToken),\n approvedAt: now,\n lastSeenAt: now,\n };\n\n this.pairedDevices.set(paired.deviceId, paired);\n this.persistPairedDevices();\n\n this.sendJson(pairing.socket, {\n type: \"pairing_approved\",\n deviceId: paired.deviceId,\n deviceName: paired.deviceName,\n authToken,\n });\n\n this.deletePendingPairing(code);\n this.markSocketAsExtension(pairing.socket, paired);\n\n this.logger.info(\n `Approved extension pairing for \"${paired.deviceName}\" (${paired.deviceId})`\n );\n return {\n deviceId: paired.deviceId,\n deviceName: paired.deviceName,\n approvedAt: paired.approvedAt,\n lastSeenAt: paired.lastSeenAt,\n };\n }\n\n start(): void {\n if (this.wss) return;\n\n this.resetIdleTimer();\n this.wss = new WebSocketServer({\n port: this.port,\n host: this.host,\n path: \"/ws\",\n maxPayload: DEFAULT_MAX_PAYLOAD_BYTES,\n });\n this.logger.info(\n `WebSocket bridge listening on ws://${this.host}:${this.port}/ws`\n );\n\n this.wss.on(\"connection\", (socket, request) => {\n this.logger.info(\n `New WebSocket connection from ${request.socket.remoteAddress ?? \"unknown\"}`\n );\n\n socket.on(\"message\", (raw) => {\n const message = this.parseIncomingMessage(raw);\n if (!message) {\n this.logger.warn(\"Failed to parse incoming WebSocket message\");\n this.closeProtocolViolation(socket, \"Invalid JSON message\");\n return;\n }\n\n if (this.isPingMessage(message)) {\n this.sendJson(socket, { type: \"pong\" });\n return;\n }\n\n if (this.isIdentifyMessage(message)) {\n this.handleIdentify(socket, message);\n return;\n }\n\n if (this.isBridgeResponseMessage(message)) {\n if (socket !== this.extensionSocket) {\n this.logger.warn(\n `Dropping response from unauthenticated socket id: ${message.id}`\n );\n this.closeProtocolViolation(socket, \"Unauthenticated response\");\n return;\n }\n\n const pending = this.pendingRequests.get(message.id);\n if (!pending) {\n this.logger.warn(\n `Received response for unknown request id: ${message.id}`\n );\n return;\n }\n\n this.pendingRequests.delete(message.id);\n clearTimeout(pending.timer);\n this.resetIdleTimer();\n\n if (message.success) {\n pending.resolve(message.data);\n } else {\n pending.reject(new Error(message.error));\n }\n return;\n }\n\n if (this.isObject(message) && \"action\" in message) {\n this.logger.warn(\n `Rejected illegal inbound action from extension socket ${this.describeSocket(socket)}`\n );\n this.closeProtocolViolation(\n socket,\n \"Inbound actions are not allowed from extension\"\n );\n return;\n }\n\n this.logger.warn(\n `Ignoring unexpected bridge message from ${this.describeSocket(socket)}`\n );\n this.closeProtocolViolation(socket, \"Unexpected message shape\");\n });\n\n socket.on(\"close\", () => {\n const code = this.pendingPairingCodeBySocket.get(socket);\n if (code) {\n this.deletePendingPairing(code);\n }\n\n if (socket === this.extensionSocket) {\n this.logger.info(\"Extension disconnected\");\n this.extensionSocket = null;\n }\n });\n\n socket.on(\"error\", (err) => {\n this.logger.error(`WebSocket error: ${err.message}`);\n });\n });\n }\n\n stop(): void {\n if (this.idleTimer) {\n clearTimeout(this.idleTimer);\n this.idleTimer = null;\n }\n\n // Reject all pending requests\n for (const [id, pending] of this.pendingRequests) {\n clearTimeout(pending.timer);\n pending.reject(new Error(\"WebSocket bridge shutting down\"));\n this.pendingRequests.delete(id);\n }\n\n this.pendingPairings.clear();\n this.pendingPairingCodeBySocket.clear();\n\n if (this.extensionSocket) {\n this.extensionSocket.close();\n this.extensionSocket = null;\n }\n\n if (this.wss) {\n this.wss.close();\n this.wss = null;\n }\n\n this.logger.info(\"WebSocket bridge stopped\");\n }\n\n /**\n * Send an action to the connected Chrome extension and wait for a response.\n * Returns the `data` field from the success response.\n */\n sendAction(\n action: string,\n params: Record<string, unknown>,\n timeoutMs = REQUEST_TIMEOUT_MS\n ): Promise<unknown> {\n if (!ALLOWED_ACTIONS.has(action)) {\n return Promise.reject(new Error(`Unsupported bridge action: ${action}`));\n }\n\n if (!this.isExtensionConnected) {\n const pendingCodes = this.getPendingPairings().map((pairing) => pairing.code);\n const pairingHint =\n pendingCodes.length > 0\n ? `Pending pairing code(s): ${pendingCodes.join(\", \")}. Approve one with reddit_approve_pairing.`\n : \"Open the extension popup and request pairing, then approve with reddit_approve_pairing.\";\n return Promise.reject(\n new Error(\n \"Reddit Agent Chrome extension is not connected. \" +\n `Ensure the extension points to ws://<your-openclaw-host>:${this.port}/ws and is approved. ` +\n pairingHint\n )\n );\n }\n\n this.resetIdleTimer();\n\n const id = randomUUID();\n const request: BridgeRequest = { id, action, params };\n\n return new Promise<unknown>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pendingRequests.delete(id);\n reject(\n new Error(\n `Request timed out after ${timeoutMs}ms for action: ${action}`\n )\n );\n }, timeoutMs);\n\n this.pendingRequests.set(id, { resolve, reject, timer });\n this.extensionSocket!.send(JSON.stringify(request));\n });\n }\n\n private resetIdleTimer(): void {\n if (this.idleTimeoutMs <= 0) return;\n if (this.idleTimer) clearTimeout(this.idleTimer);\n this.idleTimer = setTimeout(() => {\n this.idleTimer = null;\n this.logger.info(\n `No activity for ${this.idleTimeoutMs / 1000}s — idle shutdown`\n );\n this.stop();\n this.onIdleShutdown?.();\n }, this.idleTimeoutMs);\n }\n\n private handleIdentify(\n socket: WebSocket,\n message: IdentifyMessage\n ): void {\n if (message.role !== \"extension\") {\n this.logger.warn(`Ignoring identify with unsupported role: ${message.role}`);\n return;\n }\n\n const deviceId = this.sanitizeDeviceId(message.deviceId);\n if (!deviceId) {\n this.logger.warn(\"Identify message has an invalid deviceId\");\n this.closeProtocolViolation(socket, \"Invalid deviceId\");\n return;\n }\n\n const deviceName = this.sanitizeDeviceName(message.deviceName);\n const paired = this.pairedDevices.get(deviceId);\n const providedToken = message.authToken?.trim();\n const providedTokenHash = providedToken\n ? this.hashAuthToken(providedToken)\n : null;\n\n if (paired && providedTokenHash && providedTokenHash === paired.authTokenHash) {\n this.deletePendingPairingBySocket(socket);\n this.markSocketAsExtension(socket, {\n ...paired,\n deviceName,\n });\n this.logger.info(`Extension authenticated (${deviceName}, ${deviceId})`);\n return;\n }\n\n const pairing = this.createOrRefreshPairing(socket, deviceId, deviceName);\n this.logger.info(\n `Pairing required for \"${pairing.deviceName}\" (${pairing.deviceId}). Awaiting user approval.`\n );\n this.sendJson(socket, {\n type: \"pairing_required\",\n code: pairing.code,\n deviceId: pairing.deviceId,\n deviceName: pairing.deviceName,\n requestedAt: new Date(pairing.requestedAt).toISOString(),\n expiresAt: new Date(pairing.expiresAt).toISOString(),\n });\n }\n\n private markSocketAsExtension(\n socket: WebSocket,\n paired: PairedDeviceRecord\n ): void {\n if (this.extensionSocket && this.extensionSocket !== socket) {\n this.extensionSocket.close();\n }\n\n const now = new Date().toISOString();\n const updated: PairedDeviceRecord = {\n ...paired,\n lastSeenAt: now,\n };\n this.pairedDevices.set(updated.deviceId, updated);\n this.persistPairedDevices();\n\n this.extensionSocket = socket;\n this.resetIdleTimer();\n this.sendJson(socket, {\n type: \"identified\",\n deviceId: updated.deviceId,\n deviceName: updated.deviceName,\n });\n }\n\n private createOrRefreshPairing(\n socket: WebSocket,\n deviceId: string,\n deviceName: string\n ): PendingPairingRecord {\n this.pruneExpiredPairings();\n this.deletePendingPairingBySocket(socket);\n\n for (const pairing of this.pendingPairings.values()) {\n if (pairing.deviceId === deviceId && pairing.socket.readyState === WebSocket.OPEN) {\n this.pendingPairingCodeBySocket.delete(pairing.socket);\n this.pendingPairingCodeBySocket.set(socket, pairing.code);\n pairing.socket = socket;\n pairing.deviceName = deviceName;\n pairing.requestedAt = Date.now();\n pairing.expiresAt = pairing.requestedAt + this.pairingCodeTtlMs;\n return pairing;\n }\n }\n\n let code = this.generatePairingCode();\n while (this.pendingPairings.has(code)) {\n code = this.generatePairingCode();\n }\n\n const requestedAt = Date.now();\n const pairing: PendingPairingRecord = {\n code,\n deviceId,\n deviceName,\n requestedAt,\n expiresAt: requestedAt + this.pairingCodeTtlMs,\n socket,\n };\n this.pendingPairings.set(code, pairing);\n this.pendingPairingCodeBySocket.set(socket, code);\n return pairing;\n }\n\n private pruneExpiredPairings(): void {\n const now = Date.now();\n for (const [code, pairing] of this.pendingPairings) {\n if (pairing.expiresAt <= now || pairing.socket.readyState !== WebSocket.OPEN) {\n this.deletePendingPairing(code);\n }\n }\n }\n\n private deletePendingPairing(code: string): void {\n const pairing = this.pendingPairings.get(code);\n if (!pairing) return;\n this.pendingPairings.delete(code);\n this.pendingPairingCodeBySocket.delete(pairing.socket);\n }\n\n private deletePendingPairingBySocket(socket: WebSocket): void {\n const code = this.pendingPairingCodeBySocket.get(socket);\n if (!code) return;\n this.deletePendingPairing(code);\n }\n\n private normalizePairingCode(input: string): string {\n return input.trim().toUpperCase().replace(/[^A-Z0-9]/g, \"\");\n }\n\n private generatePairingCode(): string {\n let code = \"\";\n for (let index = 0; index < PAIRING_CODE_LENGTH; index += 1) {\n const pos = randomInt(0, PAIRING_CODE_ALPHABET.length);\n code += PAIRING_CODE_ALPHABET[pos];\n }\n return code;\n }\n\n private sanitizeDeviceName(raw?: string): string {\n const value = raw?.trim();\n return value ? value.slice(0, 120) : \"Reddit Agent Extension\";\n }\n\n private sanitizeDeviceId(raw?: string): string | null {\n const value = raw?.trim();\n if (!value || value.length > 120) return null;\n return /^[A-Za-z0-9._:-]+$/.test(value) ? value : null;\n }\n\n private hashAuthToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n }\n\n private loadPairedDevices(): void {\n try {\n if (!existsSync(this.pairingStorePath)) {\n return;\n }\n const raw = readFileSync(this.pairingStorePath, \"utf8\");\n const parsed = JSON.parse(raw) as Partial<PairingStoreFile>;\n const devices = Array.isArray(parsed.devices) ? parsed.devices : [];\n let needsMigration = false;\n\n for (const device of devices) {\n const authTokenHash =\n typeof device.authTokenHash === \"string\"\n ? device.authTokenHash\n : typeof device.authToken === \"string\"\n ? this.hashAuthToken(device.authToken)\n : null;\n if (\n typeof device.deviceId === \"string\" &&\n typeof device.deviceName === \"string\" &&\n typeof authTokenHash === \"string\" &&\n authTokenHash.length > 0 &&\n typeof device.approvedAt === \"string\" &&\n typeof device.lastSeenAt === \"string\"\n ) {\n if (typeof device.authToken === \"string\" && !device.authTokenHash) {\n needsMigration = true;\n }\n this.pairedDevices.set(device.deviceId, {\n deviceId: device.deviceId,\n deviceName: device.deviceName,\n authTokenHash,\n approvedAt: device.approvedAt,\n lastSeenAt: device.lastSeenAt,\n });\n }\n }\n\n if (needsMigration) {\n this.persistPairedDevices();\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n this.logger.warn(\n `Failed to load pairing store at ${this.pairingStorePath}: ${message}`\n );\n }\n }\n\n private persistPairedDevices(): void {\n try {\n const storeDir = path.dirname(this.pairingStorePath);\n mkdirSync(storeDir, { recursive: true, mode: 0o700 });\n const payload: PairingStoreFile = {\n version: 2,\n devices: Array.from(this.pairedDevices.values()),\n };\n writeFileSync(this.pairingStorePath, JSON.stringify(payload, null, 2), {\n encoding: \"utf8\",\n mode: 0o600,\n });\n try {\n chmodSync(storeDir, 0o700);\n chmodSync(this.pairingStorePath, 0o600);\n } catch {\n // Best-effort on platforms that don't support POSIX permissions.\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n this.logger.error(\n `Failed to persist pairing store at ${this.pairingStorePath}: ${message}`\n );\n }\n }\n\n private sendJson(socket: WebSocket, payload: object): void {\n if (socket.readyState !== WebSocket.OPEN) return;\n socket.send(JSON.stringify(payload));\n }\n\n private parseIncomingMessage(raw: unknown): unknown | null {\n try {\n let text: string;\n if (typeof raw === \"string\") {\n text = raw;\n } else if (raw instanceof Buffer) {\n text = raw.toString(\"utf8\");\n } else if (raw instanceof ArrayBuffer) {\n text = Buffer.from(raw).toString(\"utf8\");\n } else if (Array.isArray(raw)) {\n text = Buffer.concat(raw).toString(\"utf8\");\n } else {\n return null;\n }\n return JSON.parse(text);\n } catch {\n return null;\n }\n }\n\n private isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n }\n\n private isPingMessage(message: unknown): message is { type: \"ping\" } {\n return this.isObject(message) && message.type === \"ping\";\n }\n\n private isIdentifyMessage(message: unknown): message is IdentifyMessage {\n return (\n this.isObject(message) &&\n message.type === \"identify\" &&\n message.role === \"extension\" &&\n typeof message.deviceId === \"string\" &&\n (typeof message.deviceName === \"undefined\" ||\n typeof message.deviceName === \"string\") &&\n (typeof message.authToken === \"undefined\" ||\n typeof message.authToken === \"string\")\n );\n }\n\n private isBridgeResponseMessage(message: unknown): message is BridgeResponse {\n if (!this.isObject(message)) return false;\n if (typeof message.id !== \"string\" || message.id.length === 0) return false;\n if (message.id.length > MAX_RESPONSE_ID_LENGTH) return false;\n if (typeof message.success !== \"boolean\") return false;\n\n if (message.success) {\n return \"data\" in message;\n }\n\n return (\n typeof message.error === \"string\" &&\n message.error.length <= MAX_RESPONSE_ERROR_LENGTH\n );\n }\n\n private closeProtocolViolation(socket: WebSocket, reason: string): void {\n try {\n socket.close(1008, reason.slice(0, 123));\n } catch {\n socket.close();\n }\n }\n\n private describeSocket(socket: WebSocket): string {\n return socket === this.extensionSocket ? \"authenticated\" : \"unauthenticated\";\n }\n}\n","import { Type } from \"@sinclair/typebox\";\nimport type { WebSocketBridge } from \"./ws-bridge.js\";\n\nconst REPLY_TIMEOUT_MS = 90_000;\n\ninterface ToolConfig {\n name: string;\n description: string;\n parameters: unknown;\n execute: (\n id: string,\n params: Record<string, unknown>\n ) => Promise<{ content: { type: \"text\"; text: string }[] }>;\n}\n\nexport function registerRedditTools(\n registerTool: (config: ToolConfig) => void,\n bridge: WebSocketBridge\n) {\n registerTool({\n name: \"reddit_list_pairing_requests\",\n description:\n \"List pending browser-extension pairing requests that need user approval.\",\n parameters: Type.Object({}),\n async execute() {\n const data = bridge.getPendingPairings();\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_approve_pairing\",\n description:\n \"Approve a pending browser-extension pairing request by its pairing code.\",\n parameters: Type.Object({\n code: Type.String({\n description:\n \"Pairing code shown in the extension popup (for example: ABC123).\",\n }),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const code = String(params.code ?? \"\").trim();\n const data = bridge.approvePairing(code);\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_list_paired_devices\",\n description:\n \"List previously approved extension devices that can auto-connect.\",\n parameters: Type.Object({}),\n async execute() {\n const data = bridge.getPairedDevices();\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_fetch_subreddit\",\n description:\n \"Fetch a subreddit's listing from Reddit. Accepts subreddit name, r/name, \" +\n \"or full URL. Optionally specify sort order (hot, new, top, rising, best).\",\n parameters: Type.Object({\n subreddit: Type.String({\n description:\n 'Subreddit name or URL. Accepts: \"supplements\", \"r/supplements\", ' +\n '\"/r/supplements\", or \"https://www.reddit.com/r/supplements\".',\n }),\n sort: Type.Optional(\n Type.Unsafe<\"hot\" | \"new\" | \"top\" | \"rising\" | \"best\">({\n type: \"string\",\n enum: [\"hot\", \"new\", \"top\", \"rising\", \"best\"],\n description: \"Sort order. Default: hot.\",\n })\n ),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"fetch_subreddit\", {\n subreddit: params.subreddit,\n sort: params.sort,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_search\",\n description:\n \"Search Reddit for posts matching a query. Optionally scope to a specific \" +\n \"subreddit. Supports sort (relevance, top, new, comments) and time \" +\n \"filters (hour, day, week, month, year).\",\n parameters: Type.Object({\n query: Type.String({ description: \"Search query string.\" }),\n subreddit: Type.Optional(\n Type.String({\n description:\n \"Subreddit to search within. Omit to search all of Reddit. \" +\n 'Accepts same formats as reddit_fetch_subreddit (e.g. \"supplements\").',\n })\n ),\n sort: Type.Optional(\n Type.Unsafe<\"relevance\" | \"top\" | \"new\" | \"comments\">({\n type: \"string\",\n enum: [\"relevance\", \"top\", \"new\", \"comments\"],\n description: \"Sort order for results. Default: relevance.\",\n })\n ),\n time: Type.Optional(\n Type.Unsafe<\"hour\" | \"day\" | \"week\" | \"month\" | \"year\">({\n type: \"string\",\n enum: [\"hour\", \"day\", \"week\", \"month\", \"year\"],\n description: \"Time filter. Default: all time.\",\n })\n ),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"search_reddit\", {\n query: params.query,\n subreddit: params.subreddit,\n sort: params.sort,\n time: params.time,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_fetch_user_posts\",\n description:\n \"Fetch posts submitted by a Reddit user. Provide the username. \" +\n \"Supports sort (hot, new, top, controversial) and time filters \" +\n \"(hour, day, week, month, year).\",\n parameters: Type.Object({\n username: Type.String({\n description:\n 'Reddit username (e.g. \"richo-s\"). Do not include u/ or /u/ prefix.',\n }),\n sort: Type.Optional(\n Type.Unsafe<\"hot\" | \"new\" | \"top\" | \"controversial\">({\n type: \"string\",\n enum: [\"hot\", \"new\", \"top\", \"controversial\"],\n description: \"Sort order. Default: hot.\",\n })\n ),\n time: Type.Optional(\n Type.Unsafe<\"hour\" | \"day\" | \"week\" | \"month\" | \"year\">({\n type: \"string\",\n enum: [\"hour\", \"day\", \"week\", \"month\", \"year\"],\n description: \"Time filter (applies when sort is top or controversial). Default: all time.\",\n })\n ),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"fetch_user_posts\", {\n username: params.username,\n sort: params.sort,\n time: params.time,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_fetch_post\",\n description:\n \"Fetch a single Reddit post and its full comment tree. Provide the full post URL.\",\n parameters: Type.Object({\n url: Type.String({\n description:\n \"Full Reddit post URL (e.g. https://www.reddit.com/r/supplements/comments/abc123/some_post/).\",\n }),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"fetch_post\", {\n url: params.url,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_reply\",\n description:\n \"Reply to a Reddit comment or post using the user's logged-in session in Chrome. \" +\n \"Requires the Reddit Agent Chrome extension to be connected and the user to be \" +\n \"logged into Reddit. Takes ~7-10 seconds due to DOM interaction.\",\n parameters: Type.Object({\n commentUrl: Type.String({\n description: \"Direct URL to the comment or post to reply to.\",\n }),\n replyText: Type.String({\n description: \"The reply text to post.\",\n }),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\n \"reply_to_comment\",\n {\n commentUrl: params.commentUrl,\n replyText: params.replyText,\n },\n REPLY_TIMEOUT_MS\n );\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n}\n","import { WebSocketBridge } from \"./src/ws-bridge.js\";\nimport { registerRedditTools } from \"./src/tools.js\";\n\nconst BRIDGE_PORT = 7071;\nconst DEFAULT_HOST = \"localhost\";\nconst DEFAULT_PAIRING_CODE_TTL_SECONDS = 600;\n\ninterface PluginLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n}\n\ninterface PluginApi {\n pluginConfig: Record<string, unknown> | undefined;\n logger: PluginLogger;\n registerTool: (config: unknown) => void;\n registerService: (config: {\n id: string;\n start: () => void | Promise<void>;\n stop: () => void | Promise<void>;\n }) => void;\n}\n\nconst redditAgentPlugin = {\n id: \"openclaw-reddit-agent-server\",\n name: \"OpenClaw Reddit Agent\",\n description:\n \"Bridge to Reddit Chrome extension for browsing, searching, and commenting.\",\n\n register(api: PluginApi) {\n const host =\n typeof api.pluginConfig?.host === \"string\" &&\n api.pluginConfig.host.trim().length > 0\n ? api.pluginConfig.host.trim()\n : DEFAULT_HOST;\n const pairingCodeTtlSeconds =\n typeof api.pluginConfig?.pairingCodeTtlSeconds === \"number\" &&\n api.pluginConfig.pairingCodeTtlSeconds > 0\n ? api.pluginConfig.pairingCodeTtlSeconds\n : DEFAULT_PAIRING_CODE_TTL_SECONDS;\n const pairingStorePath =\n typeof api.pluginConfig?.pairingStorePath === \"string\" &&\n api.pluginConfig.pairingStorePath.trim().length > 0\n ? api.pluginConfig.pairingStorePath.trim()\n : undefined;\n\n const bridge = new WebSocketBridge(BRIDGE_PORT, {\n logger: {\n info: (msg) => api.logger.info(msg),\n warn: (msg) => api.logger.warn(msg),\n error: (msg) => api.logger.error(msg),\n },\n host,\n pairingCodeTtlMs: pairingCodeTtlSeconds * 1000,\n pairingStorePath,\n });\n\n api.registerService({\n id: \"reddit-agent-bridge\",\n start: () => {\n bridge.start();\n },\n stop: () => {\n bridge.stop();\n },\n });\n\n registerRedditTools(\n (config) => api.registerTool(config),\n bridge\n );\n },\n};\n\nexport default redditAgentPlugin;\n"],"mappings":"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","names":["pairing"]}
1
+ {"version":3,"sources":["../src/ws-bridge.ts","../src/tools.ts","../index.ts"],"sourcesContent":["import { createHash, randomInt, randomUUID } from \"node:crypto\";\nimport {\n chmodSync,\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { isIP } from \"node:net\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport type {\n BridgeRequest,\n BridgeResponse,\n IdentifyMessage,\n PairedDevice,\n PendingPairing,\n PendingRequest,\n} from \"./types.js\";\n\nconst REQUEST_TIMEOUT_MS = 60_000;\nconst DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\nconst DEFAULT_BIND_HOST = \"0.0.0.0\";\nconst DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1000; // 10 minutes\nconst DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024; // 8 MiB\nconst DEFAULT_NETWORK_POLICY: BridgeNetworkPolicy = \"private\";\nconst DEFAULT_PAIRING_STORE_PATH = path.join(\n homedir(),\n \".openclaw\",\n \"reddit-agent-pairings.json\"\n);\nconst PAIRING_CODE_ALPHABET = \"ABCDEFGHJKLMNPQRSTUVWXYZ23456789\";\nconst PAIRING_CODE_LENGTH = 6;\nconst MAX_RESPONSE_ID_LENGTH = 200;\nconst MAX_RESPONSE_ERROR_LENGTH = 2_000;\nconst EXTENSION_RECONNECT_WAIT_MS = 6_500;\nconst EXTENSION_RECONNECT_POLL_MS = 250;\nconst ALLOWED_ACTIONS = new Set([\n \"get_skill\",\n \"fetch_subreddit\",\n \"search_reddit\",\n \"fetch_user_posts\",\n \"fetch_post\",\n \"reply_to_comment\",\n]);\n\ninterface PairedDeviceRecord {\n deviceId: string;\n deviceName: string;\n authTokenHash: string;\n approvedAt: string;\n lastSeenAt: string;\n}\n\ninterface PairingStoreFile {\n version: number;\n devices: Array<\n Omit<PairedDeviceRecord, \"authTokenHash\"> & {\n authTokenHash?: string;\n authToken?: string;\n }\n >;\n}\n\ninterface PendingPairingRecord {\n code: string;\n deviceId: string;\n deviceName: string;\n requestedAt: number;\n expiresAt: number;\n socket: WebSocket;\n}\n\nexport interface BridgeLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n}\n\nexport interface BridgeOptions {\n logger?: BridgeLogger;\n host?: string;\n pairingStorePath?: string;\n pairingCodeTtlMs?: number;\n networkPolicy?: BridgeNetworkPolicy;\n /** Shut down after this many ms of inactivity (0 to disable). Defaults to 5 minutes. */\n idleTimeoutMs?: number;\n /** Called after the bridge stops itself due to inactivity. */\n onIdleShutdown?: () => void;\n}\n\nexport type BridgeNetworkPolicy = \"loopback\" | \"private\" | \"any\";\n\nexport class WebSocketBridge {\n private wss: WebSocketServer | null = null;\n private extensionSocket: WebSocket | null = null;\n private pendingRequests = new Map<string, PendingRequest>();\n private pendingPairings = new Map<string, PendingPairingRecord>();\n private pendingPairingCodeBySocket = new Map<WebSocket, string>();\n private pairedDevices = new Map<string, PairedDeviceRecord>();\n private port: number;\n private host: string;\n private logger: BridgeLogger;\n private pairingStorePath: string;\n private pairingCodeTtlMs: number;\n private networkPolicy: BridgeNetworkPolicy;\n private idleTimeoutMs: number;\n private onIdleShutdown?: () => void;\n private idleTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(port: number, options?: BridgeOptions) {\n this.port = port;\n this.host = options?.host?.trim() || DEFAULT_BIND_HOST;\n this.logger = options?.logger ?? {\n info: (msg: string) => console.log(`[openclaw-reddit-agent-server] ${msg}`),\n warn: (msg: string) => console.warn(`[openclaw-reddit-agent-server] ${msg}`),\n error: (msg: string) =>\n console.error(`[openclaw-reddit-agent-server] ${msg}`),\n };\n this.pairingStorePath =\n options?.pairingStorePath?.trim() || DEFAULT_PAIRING_STORE_PATH;\n this.pairingCodeTtlMs = options?.pairingCodeTtlMs ?? DEFAULT_PAIRING_TTL_MS;\n this.networkPolicy = options?.networkPolicy ?? DEFAULT_NETWORK_POLICY;\n this.idleTimeoutMs = options?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;\n this.onIdleShutdown = options?.onIdleShutdown;\n\n this.loadPairedDevices();\n }\n\n get isExtensionConnected(): boolean {\n return (\n this.extensionSocket !== null &&\n this.extensionSocket.readyState === WebSocket.OPEN\n );\n }\n\n getPendingPairings(): PendingPairing[] {\n this.ensureStarted();\n this.pruneExpiredPairings();\n return Array.from(this.pendingPairings.values())\n .sort((a, b) => b.requestedAt - a.requestedAt)\n .map((entry) => ({\n code: entry.code,\n deviceId: entry.deviceId,\n deviceName: entry.deviceName,\n requestedAt: new Date(entry.requestedAt).toISOString(),\n expiresAt: new Date(entry.expiresAt).toISOString(),\n }));\n }\n\n getPairedDevices(): PairedDevice[] {\n return Array.from(this.pairedDevices.values())\n .sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))\n .map((device) => ({\n deviceId: device.deviceId,\n deviceName: device.deviceName,\n approvedAt: device.approvedAt,\n lastSeenAt: device.lastSeenAt,\n }));\n }\n\n approvePairing(codeInput: string): PairedDevice {\n this.ensureStarted();\n const code = this.normalizePairingCode(codeInput);\n if (!code) {\n throw new Error(\"Missing pairing code.\");\n }\n\n this.pruneExpiredPairings();\n const pairing = this.pendingPairings.get(code);\n if (!pairing) {\n throw new Error(\n `Pairing code \"${code}\" is invalid or expired. Request a new code in the extension.`\n );\n }\n\n if (pairing.socket.readyState !== WebSocket.OPEN) {\n this.deletePendingPairing(code);\n throw new Error(\"Pairing request is no longer active. Request a new code.\");\n }\n\n const now = new Date().toISOString();\n const authToken = randomUUID();\n const paired: PairedDeviceRecord = {\n deviceId: pairing.deviceId,\n deviceName: pairing.deviceName,\n authTokenHash: this.hashAuthToken(authToken),\n approvedAt: now,\n lastSeenAt: now,\n };\n\n this.pairedDevices.set(paired.deviceId, paired);\n this.persistPairedDevices();\n\n this.sendJson(pairing.socket, {\n type: \"pairing_approved\",\n deviceId: paired.deviceId,\n deviceName: paired.deviceName,\n authToken,\n });\n\n this.deletePendingPairing(code);\n this.markSocketAsExtension(pairing.socket, paired);\n\n this.logger.info(\n `Approved extension pairing for \"${paired.deviceName}\" (${paired.deviceId})`\n );\n return {\n deviceId: paired.deviceId,\n deviceName: paired.deviceName,\n approvedAt: paired.approvedAt,\n lastSeenAt: paired.lastSeenAt,\n };\n }\n\n start(): void {\n if (this.wss) return;\n\n this.resetIdleTimer();\n this.wss = new WebSocketServer({\n port: this.port,\n host: this.host,\n path: \"/ws\",\n maxPayload: DEFAULT_MAX_PAYLOAD_BYTES,\n });\n this.logger.info(\n `WebSocket bridge listening on ws://${this.host}:${this.port}/ws`\n );\n\n this.wss.on(\"connection\", (socket, request) => {\n const remoteAddress = request.socket.remoteAddress ?? \"unknown\";\n if (!this.isClientAddressAllowed(remoteAddress)) {\n this.logger.warn(\n `Rejected bridge connection from ${remoteAddress} due to network policy \"${this.networkPolicy}\"`\n );\n this.closeProtocolViolation(socket, \"Client network not allowed\");\n return;\n }\n this.logger.info(\n `New WebSocket connection from ${remoteAddress}`\n );\n\n socket.on(\"message\", (raw) => {\n const message = this.parseIncomingMessage(raw);\n if (!message) {\n this.logger.warn(\"Failed to parse incoming WebSocket message\");\n this.closeProtocolViolation(socket, \"Invalid JSON message\");\n return;\n }\n\n if (this.isPingMessage(message)) {\n this.sendJson(socket, { type: \"pong\" });\n return;\n }\n\n if (this.isIdentifyMessage(message)) {\n this.handleIdentify(socket, message);\n return;\n }\n\n if (this.isBridgeResponseMessage(message)) {\n if (socket !== this.extensionSocket) {\n this.logger.warn(\n `Dropping response from unauthenticated socket id: ${message.id}`\n );\n this.closeProtocolViolation(socket, \"Unauthenticated response\");\n return;\n }\n\n const pending = this.pendingRequests.get(message.id);\n if (!pending) {\n this.logger.warn(\n `Received response for unknown request id: ${message.id}`\n );\n this.closeProtocolViolation(socket, \"Unknown request id\");\n return;\n }\n\n this.pendingRequests.delete(message.id);\n clearTimeout(pending.timer);\n this.resetIdleTimer();\n\n if (message.success) {\n pending.resolve(message.data);\n } else {\n pending.reject(new Error(message.error));\n }\n return;\n }\n\n if (this.isObject(message) && \"action\" in message) {\n this.logger.warn(\n `Rejected illegal inbound action from extension socket ${this.describeSocket(socket)}`\n );\n this.closeProtocolViolation(\n socket,\n \"Inbound actions are not allowed from extension\"\n );\n return;\n }\n\n this.logger.warn(\n `Ignoring unexpected bridge message from ${this.describeSocket(socket)}`\n );\n this.closeProtocolViolation(socket, \"Unexpected message shape\");\n });\n\n socket.on(\"close\", () => {\n const code = this.pendingPairingCodeBySocket.get(socket);\n if (code) {\n this.deletePendingPairing(code);\n }\n\n if (socket === this.extensionSocket) {\n this.logger.info(\"Extension disconnected\");\n this.extensionSocket = null;\n }\n });\n\n socket.on(\"error\", (err) => {\n this.logger.error(`WebSocket error: ${err.message}`);\n });\n });\n }\n\n ensureStarted(): void {\n if (!this.wss) {\n this.start();\n }\n }\n\n stop(): void {\n if (this.idleTimer) {\n clearTimeout(this.idleTimer);\n this.idleTimer = null;\n }\n\n // Reject all pending requests\n for (const [id, pending] of this.pendingRequests) {\n clearTimeout(pending.timer);\n pending.reject(new Error(\"WebSocket bridge shutting down\"));\n this.pendingRequests.delete(id);\n }\n\n this.pendingPairings.clear();\n this.pendingPairingCodeBySocket.clear();\n\n if (this.extensionSocket) {\n this.extensionSocket.close();\n this.extensionSocket = null;\n }\n\n if (this.wss) {\n this.wss.close();\n this.wss = null;\n }\n\n this.logger.info(\"WebSocket bridge stopped\");\n }\n\n /**\n * Send an action to the connected Chrome extension and wait for a response.\n * Returns the `data` field from the success response.\n */\n async sendAction(\n action: string,\n params: Record<string, unknown>,\n timeoutMs = REQUEST_TIMEOUT_MS\n ): Promise<unknown> {\n if (!ALLOWED_ACTIONS.has(action)) {\n return Promise.reject(new Error(`Unsupported bridge action: ${action}`));\n }\n\n this.ensureStarted();\n if (!this.isExtensionConnected) {\n await this.waitForExtensionConnection(EXTENSION_RECONNECT_WAIT_MS);\n }\n\n if (!this.isExtensionConnected) {\n const pendingCodes = this.getPendingPairings().map((pairing) => pairing.code);\n const pairingHint =\n pendingCodes.length > 0\n ? `Pending pairing code(s): ${pendingCodes.join(\", \")}. Approve one with reddit_approve_pairing.`\n : \"Open the extension popup and request pairing, then approve with reddit_approve_pairing.\";\n return Promise.reject(\n new Error(\n \"Reddit Agent Chrome extension is not connected. \" +\n `Ensure the extension points to ws://<your-openclaw-host>:${this.port}/ws and is approved. ` +\n pairingHint\n )\n );\n }\n\n this.resetIdleTimer();\n\n const id = randomUUID();\n const request: BridgeRequest = { id, action, params };\n\n return new Promise<unknown>((resolve, reject) => {\n const activeSocket = this.extensionSocket;\n if (!activeSocket || activeSocket.readyState !== WebSocket.OPEN) {\n reject(new Error(\"Extension socket is not open\"));\n return;\n }\n\n const timer = setTimeout(() => {\n this.pendingRequests.delete(id);\n reject(\n new Error(\n `Request timed out after ${timeoutMs}ms for action: ${action}`\n )\n );\n }, timeoutMs);\n\n this.pendingRequests.set(id, { resolve, reject, timer });\n try {\n activeSocket.send(JSON.stringify(request));\n } catch (error) {\n clearTimeout(timer);\n this.pendingRequests.delete(id);\n reject(error instanceof Error ? error : new Error(String(error)));\n }\n });\n }\n\n private resetIdleTimer(): void {\n if (this.idleTimeoutMs <= 0) return;\n if (this.idleTimer) clearTimeout(this.idleTimer);\n this.idleTimer = setTimeout(() => {\n this.idleTimer = null;\n this.logger.info(\n `No activity for ${this.idleTimeoutMs / 1000}s — idle shutdown`\n );\n this.stop();\n this.onIdleShutdown?.();\n }, this.idleTimeoutMs);\n }\n\n private async waitForExtensionConnection(timeoutMs: number): Promise<void> {\n if (this.isExtensionConnected) return;\n\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (this.isExtensionConnected) return;\n await new Promise((resolve) =>\n setTimeout(resolve, EXTENSION_RECONNECT_POLL_MS)\n );\n }\n }\n\n private handleIdentify(\n socket: WebSocket,\n message: IdentifyMessage\n ): void {\n if (message.role !== \"extension\") {\n this.logger.warn(`Ignoring identify with unsupported role: ${message.role}`);\n this.closeProtocolViolation(socket, \"Unsupported role\");\n return;\n }\n\n const deviceId = this.sanitizeDeviceId(message.deviceId);\n if (!deviceId) {\n this.logger.warn(\"Identify message has an invalid deviceId\");\n this.closeProtocolViolation(socket, \"Invalid deviceId\");\n return;\n }\n\n const deviceName = this.sanitizeDeviceName(message.deviceName);\n const paired = this.pairedDevices.get(deviceId);\n const providedToken = message.authToken?.trim();\n const providedTokenHash = providedToken\n ? this.hashAuthToken(providedToken)\n : null;\n\n if (paired && providedTokenHash && providedTokenHash === paired.authTokenHash) {\n this.deletePendingPairingBySocket(socket);\n this.markSocketAsExtension(socket, {\n ...paired,\n deviceName,\n });\n this.logger.info(`Extension authenticated (${deviceName}, ${deviceId})`);\n return;\n }\n\n const pairing = this.createOrRefreshPairing(socket, deviceId, deviceName);\n this.logger.info(\n `Pairing required for \"${pairing.deviceName}\" (${pairing.deviceId}). Awaiting user approval.`\n );\n this.sendJson(socket, {\n type: \"pairing_required\",\n code: pairing.code,\n deviceId: pairing.deviceId,\n deviceName: pairing.deviceName,\n requestedAt: new Date(pairing.requestedAt).toISOString(),\n expiresAt: new Date(pairing.expiresAt).toISOString(),\n });\n }\n\n private markSocketAsExtension(\n socket: WebSocket,\n paired: PairedDeviceRecord\n ): void {\n if (this.extensionSocket && this.extensionSocket !== socket) {\n this.extensionSocket.close();\n }\n\n const now = new Date().toISOString();\n const updated: PairedDeviceRecord = {\n ...paired,\n lastSeenAt: now,\n };\n this.pairedDevices.set(updated.deviceId, updated);\n this.persistPairedDevices();\n\n this.extensionSocket = socket;\n this.resetIdleTimer();\n this.sendJson(socket, {\n type: \"identified\",\n deviceId: updated.deviceId,\n deviceName: updated.deviceName,\n });\n }\n\n private createOrRefreshPairing(\n socket: WebSocket,\n deviceId: string,\n deviceName: string\n ): PendingPairingRecord {\n this.pruneExpiredPairings();\n this.deletePendingPairingBySocket(socket);\n\n for (const pairing of this.pendingPairings.values()) {\n if (pairing.deviceId === deviceId && pairing.socket.readyState === WebSocket.OPEN) {\n this.pendingPairingCodeBySocket.delete(pairing.socket);\n this.pendingPairingCodeBySocket.set(socket, pairing.code);\n pairing.socket = socket;\n pairing.deviceName = deviceName;\n pairing.requestedAt = Date.now();\n pairing.expiresAt = pairing.requestedAt + this.pairingCodeTtlMs;\n return pairing;\n }\n }\n\n let code = this.generatePairingCode();\n while (this.pendingPairings.has(code)) {\n code = this.generatePairingCode();\n }\n\n const requestedAt = Date.now();\n const pairing: PendingPairingRecord = {\n code,\n deviceId,\n deviceName,\n requestedAt,\n expiresAt: requestedAt + this.pairingCodeTtlMs,\n socket,\n };\n this.pendingPairings.set(code, pairing);\n this.pendingPairingCodeBySocket.set(socket, code);\n return pairing;\n }\n\n private pruneExpiredPairings(): void {\n const now = Date.now();\n for (const [code, pairing] of this.pendingPairings) {\n if (pairing.expiresAt <= now || pairing.socket.readyState !== WebSocket.OPEN) {\n this.deletePendingPairing(code);\n }\n }\n }\n\n private deletePendingPairing(code: string): void {\n const pairing = this.pendingPairings.get(code);\n if (!pairing) return;\n this.pendingPairings.delete(code);\n this.pendingPairingCodeBySocket.delete(pairing.socket);\n }\n\n private deletePendingPairingBySocket(socket: WebSocket): void {\n const code = this.pendingPairingCodeBySocket.get(socket);\n if (!code) return;\n this.deletePendingPairing(code);\n }\n\n private normalizePairingCode(input: string): string {\n return input.trim().toUpperCase().replace(/[^A-Z0-9]/g, \"\");\n }\n\n private generatePairingCode(): string {\n let code = \"\";\n for (let index = 0; index < PAIRING_CODE_LENGTH; index += 1) {\n const pos = randomInt(0, PAIRING_CODE_ALPHABET.length);\n code += PAIRING_CODE_ALPHABET[pos];\n }\n return code;\n }\n\n private sanitizeDeviceName(raw?: string): string {\n const value = raw?.trim();\n return value ? value.slice(0, 120) : \"Reddit Agent Extension\";\n }\n\n private sanitizeDeviceId(raw?: string): string | null {\n const value = raw?.trim();\n if (!value || value.length > 120) return null;\n return /^[A-Za-z0-9._:-]+$/.test(value) ? value : null;\n }\n\n private hashAuthToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n }\n\n private loadPairedDevices(): void {\n try {\n if (!existsSync(this.pairingStorePath)) {\n return;\n }\n const raw = readFileSync(this.pairingStorePath, \"utf8\");\n const parsed = JSON.parse(raw) as Partial<PairingStoreFile>;\n const devices = Array.isArray(parsed.devices) ? parsed.devices : [];\n let needsMigration = false;\n\n for (const device of devices) {\n const authTokenHash =\n typeof device.authTokenHash === \"string\"\n ? device.authTokenHash\n : typeof device.authToken === \"string\"\n ? this.hashAuthToken(device.authToken)\n : null;\n if (\n typeof device.deviceId === \"string\" &&\n typeof device.deviceName === \"string\" &&\n typeof authTokenHash === \"string\" &&\n authTokenHash.length > 0 &&\n typeof device.approvedAt === \"string\" &&\n typeof device.lastSeenAt === \"string\"\n ) {\n if (typeof device.authToken === \"string\" && !device.authTokenHash) {\n needsMigration = true;\n }\n this.pairedDevices.set(device.deviceId, {\n deviceId: device.deviceId,\n deviceName: device.deviceName,\n authTokenHash,\n approvedAt: device.approvedAt,\n lastSeenAt: device.lastSeenAt,\n });\n }\n }\n\n if (needsMigration) {\n this.persistPairedDevices();\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n this.logger.warn(\n `Failed to load pairing store at ${this.pairingStorePath}: ${message}`\n );\n }\n }\n\n private persistPairedDevices(): void {\n try {\n const storeDir = path.dirname(this.pairingStorePath);\n mkdirSync(storeDir, { recursive: true, mode: 0o700 });\n const payload: PairingStoreFile = {\n version: 2,\n devices: Array.from(this.pairedDevices.values()),\n };\n writeFileSync(this.pairingStorePath, JSON.stringify(payload, null, 2), {\n encoding: \"utf8\",\n mode: 0o600,\n });\n try {\n chmodSync(storeDir, 0o700);\n chmodSync(this.pairingStorePath, 0o600);\n } catch {\n // Best-effort on platforms that don't support POSIX permissions.\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n this.logger.error(\n `Failed to persist pairing store at ${this.pairingStorePath}: ${message}`\n );\n }\n }\n\n private sendJson(socket: WebSocket, payload: object): void {\n if (socket.readyState !== WebSocket.OPEN) return;\n socket.send(JSON.stringify(payload));\n }\n\n private parseIncomingMessage(raw: unknown): unknown | null {\n try {\n let text: string;\n if (typeof raw === \"string\") {\n text = raw;\n } else if (raw instanceof Buffer) {\n text = raw.toString(\"utf8\");\n } else if (raw instanceof ArrayBuffer) {\n text = Buffer.from(raw).toString(\"utf8\");\n } else if (Array.isArray(raw)) {\n text = Buffer.concat(raw).toString(\"utf8\");\n } else {\n return null;\n }\n return JSON.parse(text);\n } catch {\n return null;\n }\n }\n\n private isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n }\n\n private isPingMessage(message: unknown): message is { type: \"ping\" } {\n return this.isObject(message) && message.type === \"ping\";\n }\n\n private isIdentifyMessage(message: unknown): message is IdentifyMessage {\n return (\n this.isObject(message) &&\n message.type === \"identify\" &&\n message.role === \"extension\" &&\n typeof message.deviceId === \"string\" &&\n (typeof message.deviceName === \"undefined\" ||\n typeof message.deviceName === \"string\") &&\n (typeof message.authToken === \"undefined\" ||\n typeof message.authToken === \"string\")\n );\n }\n\n private isBridgeResponseMessage(message: unknown): message is BridgeResponse {\n if (!this.isObject(message)) return false;\n if (typeof message.id !== \"string\" || message.id.length === 0) return false;\n if (message.id.length > MAX_RESPONSE_ID_LENGTH) return false;\n if (typeof message.success !== \"boolean\") return false;\n\n if (message.success) {\n return \"data\" in message;\n }\n\n return (\n typeof message.error === \"string\" &&\n message.error.length <= MAX_RESPONSE_ERROR_LENGTH\n );\n }\n\n private closeProtocolViolation(socket: WebSocket, reason: string): void {\n try {\n socket.close(1008, reason.slice(0, 123));\n } catch {\n socket.close();\n }\n }\n\n private describeSocket(socket: WebSocket): string {\n return socket === this.extensionSocket ? \"authenticated\" : \"unauthenticated\";\n }\n\n private isClientAddressAllowed(remoteAddress: string): boolean {\n const normalized = this.normalizeRemoteAddress(remoteAddress);\n if (!normalized) return false;\n\n if (this.isLoopbackAddress(normalized)) {\n return true;\n }\n\n if (this.networkPolicy === \"loopback\") {\n return false;\n }\n\n if (this.networkPolicy === \"any\") {\n return true;\n }\n\n return this.isPrivateOrTailscaleAddress(normalized);\n }\n\n private normalizeRemoteAddress(address: string): string | null {\n const value = address.trim();\n if (!value) return null;\n if (value.startsWith(\"::ffff:\")) {\n return value.slice(\"::ffff:\".length);\n }\n return value;\n }\n\n private isLoopbackAddress(address: string): boolean {\n return address === \"::1\" || address === \"127.0.0.1\";\n }\n\n private isPrivateOrTailscaleAddress(address: string): boolean {\n if (this.isTailscaleIPv6(address)) {\n return true;\n }\n\n if (isIP(address) !== 4) {\n return false;\n }\n\n const parts = address.split(\".\").map((part) => Number(part));\n if (parts.length !== 4 || parts.some((part) => Number.isNaN(part))) {\n return false;\n }\n\n const [a, b] = parts;\n\n if (a === 10) return true;\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 168) return true;\n // Tailscale CGNAT range 100.64.0.0/10\n if (a === 100 && b >= 64 && b <= 127) return true;\n\n return false;\n }\n\n private isTailscaleIPv6(address: string): boolean {\n const normalized = address.toLowerCase();\n // Tailscale ULA range: fd7a:115c:a1e0::/48\n return normalized.startsWith(\"fd7a:115c:a1e0:\");\n }\n}\n","import { Type } from \"@sinclair/typebox\";\nimport type { WebSocketBridge } from \"./ws-bridge.js\";\n\nconst REPLY_TIMEOUT_MS = 90_000;\n\ninterface ToolConfig {\n name: string;\n description: string;\n parameters: unknown;\n execute: (\n id: string,\n params: Record<string, unknown>\n ) => Promise<{ content: { type: \"text\"; text: string }[] }>;\n}\n\nexport function registerRedditTools(\n registerTool: (config: ToolConfig) => void,\n bridge: WebSocketBridge\n) {\n registerTool({\n name: \"reddit_list_pairing_requests\",\n description:\n \"List pending browser-extension pairing requests that need user approval.\",\n parameters: Type.Object({}),\n async execute() {\n const data = bridge.getPendingPairings();\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_approve_pairing\",\n description:\n \"Approve a pending browser-extension pairing request by its pairing code.\",\n parameters: Type.Object({\n code: Type.String({\n description:\n \"Pairing code shown in the extension popup (for example: ABC123).\",\n }),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const code = String(params.code ?? \"\").trim();\n const data = bridge.approvePairing(code);\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_list_paired_devices\",\n description:\n \"List previously approved extension devices that can auto-connect.\",\n parameters: Type.Object({}),\n async execute() {\n const data = bridge.getPairedDevices();\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_fetch_subreddit\",\n description:\n \"Fetch a subreddit's listing from Reddit. Accepts subreddit name, r/name, \" +\n \"or full URL. Optionally specify sort order (hot, new, top, rising, best).\",\n parameters: Type.Object({\n subreddit: Type.String({\n description:\n 'Subreddit name or URL. Accepts: \"supplements\", \"r/supplements\", ' +\n '\"/r/supplements\", or \"https://www.reddit.com/r/supplements\".',\n }),\n sort: Type.Optional(\n Type.Unsafe<\"hot\" | \"new\" | \"top\" | \"rising\" | \"best\">({\n type: \"string\",\n enum: [\"hot\", \"new\", \"top\", \"rising\", \"best\"],\n description: \"Sort order. Default: hot.\",\n })\n ),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"fetch_subreddit\", {\n subreddit: params.subreddit,\n sort: params.sort,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_search\",\n description:\n \"Search Reddit for posts matching a query. Optionally scope to a specific \" +\n \"subreddit. Supports sort (relevance, top, new, comments) and time \" +\n \"filters (hour, day, week, month, year).\",\n parameters: Type.Object({\n query: Type.String({ description: \"Search query string.\" }),\n subreddit: Type.Optional(\n Type.String({\n description:\n \"Subreddit to search within. Omit to search all of Reddit. \" +\n 'Accepts same formats as reddit_fetch_subreddit (e.g. \"supplements\").',\n })\n ),\n sort: Type.Optional(\n Type.Unsafe<\"relevance\" | \"top\" | \"new\" | \"comments\">({\n type: \"string\",\n enum: [\"relevance\", \"top\", \"new\", \"comments\"],\n description: \"Sort order for results. Default: relevance.\",\n })\n ),\n time: Type.Optional(\n Type.Unsafe<\"hour\" | \"day\" | \"week\" | \"month\" | \"year\">({\n type: \"string\",\n enum: [\"hour\", \"day\", \"week\", \"month\", \"year\"],\n description: \"Time filter. Default: all time.\",\n })\n ),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"search_reddit\", {\n query: params.query,\n subreddit: params.subreddit,\n sort: params.sort,\n time: params.time,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_fetch_user_posts\",\n description:\n \"Fetch posts submitted by a Reddit user. Provide the username. \" +\n \"Supports sort (hot, new, top, controversial) and time filters \" +\n \"(hour, day, week, month, year).\",\n parameters: Type.Object({\n username: Type.String({\n description:\n 'Reddit username (e.g. \"richo-s\"). Do not include u/ or /u/ prefix.',\n }),\n sort: Type.Optional(\n Type.Unsafe<\"hot\" | \"new\" | \"top\" | \"controversial\">({\n type: \"string\",\n enum: [\"hot\", \"new\", \"top\", \"controversial\"],\n description: \"Sort order. Default: hot.\",\n })\n ),\n time: Type.Optional(\n Type.Unsafe<\"hour\" | \"day\" | \"week\" | \"month\" | \"year\">({\n type: \"string\",\n enum: [\"hour\", \"day\", \"week\", \"month\", \"year\"],\n description: \"Time filter (applies when sort is top or controversial). Default: all time.\",\n })\n ),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"fetch_user_posts\", {\n username: params.username,\n sort: params.sort,\n time: params.time,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_fetch_post\",\n description:\n \"Fetch a single Reddit post and its full comment tree. Provide the full post URL.\",\n parameters: Type.Object({\n url: Type.String({\n description:\n \"Full Reddit post URL (e.g. https://www.reddit.com/r/supplements/comments/abc123/some_post/).\",\n }),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\"fetch_post\", {\n url: params.url,\n });\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n\n registerTool({\n name: \"reddit_reply\",\n description:\n \"Reply to a Reddit comment or post using the user's logged-in session in Chrome. \" +\n \"Requires the Reddit Agent Chrome extension to be connected and the user to be \" +\n \"logged into Reddit. Takes ~7-10 seconds due to DOM interaction.\",\n parameters: Type.Object({\n commentUrl: Type.String({\n description: \"Direct URL to the comment or post to reply to.\",\n }),\n replyText: Type.String({\n description: \"The reply text to post.\",\n }),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const data = await bridge.sendAction(\n \"reply_to_comment\",\n {\n commentUrl: params.commentUrl,\n replyText: params.replyText,\n },\n REPLY_TIMEOUT_MS\n );\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n },\n });\n}\n","import { WebSocketBridge } from \"./src/ws-bridge.js\";\nimport { registerRedditTools } from \"./src/tools.js\";\n\nconst BRIDGE_PORT = 7071;\nconst DEFAULT_HOST = \"0.0.0.0\";\nconst DEFAULT_PAIRING_CODE_TTL_SECONDS = 600;\nconst DEFAULT_NETWORK_POLICY = \"private\";\n\ninterface PluginLogger {\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n}\n\ninterface PluginApi {\n pluginConfig: Record<string, unknown> | undefined;\n logger: PluginLogger;\n registerTool: (config: unknown) => void;\n registerService: (config: {\n id: string;\n start: () => void | Promise<void>;\n stop: () => void | Promise<void>;\n }) => void;\n}\n\nconst redditAgentPlugin = {\n id: \"openclaw-reddit-agent-server\",\n name: \"OpenClaw Reddit Agent\",\n description:\n \"Bridge to Reddit Chrome extension for browsing, searching, and commenting.\",\n\n register(api: PluginApi) {\n const host =\n typeof api.pluginConfig?.host === \"string\" &&\n api.pluginConfig.host.trim().length > 0\n ? api.pluginConfig.host.trim()\n : DEFAULT_HOST;\n const pairingCodeTtlSeconds =\n typeof api.pluginConfig?.pairingCodeTtlSeconds === \"number\" &&\n api.pluginConfig.pairingCodeTtlSeconds > 0\n ? api.pluginConfig.pairingCodeTtlSeconds\n : DEFAULT_PAIRING_CODE_TTL_SECONDS;\n const pairingStorePath =\n typeof api.pluginConfig?.pairingStorePath === \"string\" &&\n api.pluginConfig.pairingStorePath.trim().length > 0\n ? api.pluginConfig.pairingStorePath.trim()\n : undefined;\n const networkPolicy =\n api.pluginConfig?.networkPolicy === \"loopback\" ||\n api.pluginConfig?.networkPolicy === \"private\" ||\n api.pluginConfig?.networkPolicy === \"any\"\n ? api.pluginConfig.networkPolicy\n : DEFAULT_NETWORK_POLICY;\n\n const bridge = new WebSocketBridge(BRIDGE_PORT, {\n logger: {\n info: (msg) => api.logger.info(msg),\n warn: (msg) => api.logger.warn(msg),\n error: (msg) => api.logger.error(msg),\n },\n host,\n pairingCodeTtlMs: pairingCodeTtlSeconds * 1000,\n pairingStorePath,\n networkPolicy,\n });\n\n api.registerService({\n id: \"reddit-agent-bridge\",\n start: () => {\n bridge.start();\n },\n stop: () => {\n bridge.stop();\n },\n });\n\n registerRedditTools(\n (config) => api.registerTool(config),\n bridge\n );\n },\n};\n\nexport default redditAgentPlugin;\n"],"mappings":"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","names":["pairing","DEFAULT_NETWORK_POLICY"]}
@@ -8,7 +8,12 @@
8
8
  "properties": {
9
9
  "host": {
10
10
  "type": "string",
11
- "default": "localhost"
11
+ "default": "0.0.0.0"
12
+ },
13
+ "networkPolicy": {
14
+ "type": "string",
15
+ "enum": ["loopback", "private", "any"],
16
+ "default": "private"
12
17
  },
13
18
  "pairingCodeTtlSeconds": {
14
19
  "type": "integer",
@@ -24,8 +29,13 @@
24
29
  "uiHints": {
25
30
  "host": {
26
31
  "label": "Bind Host",
27
- "placeholder": "localhost",
28
- "help": "Network interface to bind the WebSocket bridge to. localhost is safest for same-machine use. Use 0.0.0.0 only when you explicitly need LAN/Tailscale access."
32
+ "placeholder": "0.0.0.0",
33
+ "help": "Network interface to bind the WebSocket bridge to. Default 0.0.0.0 allows remote extension connections (for example over Tailscale)."
34
+ },
35
+ "networkPolicy": {
36
+ "label": "Client Network Policy",
37
+ "placeholder": "private",
38
+ "help": "Allowed client source networks: loopback (same machine only), private (loopback + RFC1918 + Tailscale ranges), any (no source-network restriction)."
29
39
  },
30
40
  "pairingCodeTtlSeconds": {
31
41
  "label": "Pairing Code TTL (seconds)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-reddit-agent-server",
3
- "version": "2026.2.2701",
3
+ "version": "2026.2.2703",
4
4
  "description": "OpenClaw plugin for Reddit automation via Chrome extension bridge",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",