openclaw-reddit-agent-server 2026.2.2702 → 2026.2.2801
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 +94 -135
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,6 +26,8 @@ var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
|
26
26
|
var PAIRING_CODE_LENGTH = 6;
|
|
27
27
|
var MAX_RESPONSE_ID_LENGTH = 200;
|
|
28
28
|
var MAX_RESPONSE_ERROR_LENGTH = 2e3;
|
|
29
|
+
var EXTENSION_RECONNECT_WAIT_MS = 6500;
|
|
30
|
+
var EXTENSION_RECONNECT_POLL_MS = 250;
|
|
29
31
|
var ALLOWED_ACTIONS = /* @__PURE__ */ new Set([
|
|
30
32
|
"get_skill",
|
|
31
33
|
"fetch_subreddit",
|
|
@@ -69,6 +71,7 @@ var WebSocketBridge = class {
|
|
|
69
71
|
return this.extensionSocket !== null && this.extensionSocket.readyState === WebSocket.OPEN;
|
|
70
72
|
}
|
|
71
73
|
getPendingPairings() {
|
|
74
|
+
this.ensureStarted();
|
|
72
75
|
this.pruneExpiredPairings();
|
|
73
76
|
return Array.from(this.pendingPairings.values()).sort((a, b) => b.requestedAt - a.requestedAt).map((entry) => ({
|
|
74
77
|
code: entry.code,
|
|
@@ -87,6 +90,7 @@ var WebSocketBridge = class {
|
|
|
87
90
|
}));
|
|
88
91
|
}
|
|
89
92
|
approvePairing(codeInput) {
|
|
93
|
+
this.ensureStarted();
|
|
90
94
|
const code = this.normalizePairingCode(codeInput);
|
|
91
95
|
if (!code) {
|
|
92
96
|
throw new Error("Missing pairing code.");
|
|
@@ -183,6 +187,7 @@ var WebSocketBridge = class {
|
|
|
183
187
|
this.logger.warn(
|
|
184
188
|
`Received response for unknown request id: ${message.id}`
|
|
185
189
|
);
|
|
190
|
+
this.closeProtocolViolation(socket, "Unknown request id");
|
|
186
191
|
return;
|
|
187
192
|
}
|
|
188
193
|
this.pendingRequests.delete(message.id);
|
|
@@ -225,6 +230,11 @@ var WebSocketBridge = class {
|
|
|
225
230
|
});
|
|
226
231
|
});
|
|
227
232
|
}
|
|
233
|
+
ensureStarted() {
|
|
234
|
+
if (!this.wss) {
|
|
235
|
+
this.start();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
228
238
|
stop() {
|
|
229
239
|
if (this.idleTimer) {
|
|
230
240
|
clearTimeout(this.idleTimer);
|
|
@@ -251,10 +261,14 @@ var WebSocketBridge = class {
|
|
|
251
261
|
* Send an action to the connected Chrome extension and wait for a response.
|
|
252
262
|
* Returns the `data` field from the success response.
|
|
253
263
|
*/
|
|
254
|
-
sendAction(action, params, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
264
|
+
async sendAction(action, params, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
255
265
|
if (!ALLOWED_ACTIONS.has(action)) {
|
|
256
266
|
return Promise.reject(new Error(`Unsupported bridge action: ${action}`));
|
|
257
267
|
}
|
|
268
|
+
this.ensureStarted();
|
|
269
|
+
if (!this.isExtensionConnected) {
|
|
270
|
+
await this.waitForExtensionConnection(EXTENSION_RECONNECT_WAIT_MS);
|
|
271
|
+
}
|
|
258
272
|
if (!this.isExtensionConnected) {
|
|
259
273
|
const pendingCodes = this.getPendingPairings().map((pairing) => pairing.code);
|
|
260
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.";
|
|
@@ -268,6 +282,11 @@ var WebSocketBridge = class {
|
|
|
268
282
|
const id = randomUUID();
|
|
269
283
|
const request = { id, action, params };
|
|
270
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
|
+
}
|
|
271
290
|
const timer = setTimeout(() => {
|
|
272
291
|
this.pendingRequests.delete(id);
|
|
273
292
|
reject(
|
|
@@ -277,7 +296,13 @@ var WebSocketBridge = class {
|
|
|
277
296
|
);
|
|
278
297
|
}, timeoutMs);
|
|
279
298
|
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
280
|
-
|
|
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
|
+
}
|
|
281
306
|
});
|
|
282
307
|
}
|
|
283
308
|
resetIdleTimer() {
|
|
@@ -292,9 +317,20 @@ var WebSocketBridge = class {
|
|
|
292
317
|
this.onIdleShutdown?.();
|
|
293
318
|
}, this.idleTimeoutMs);
|
|
294
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
|
+
}
|
|
295
330
|
handleIdentify(socket, message) {
|
|
296
331
|
if (message.role !== "extension") {
|
|
297
332
|
this.logger.warn(`Ignoring identify with unsupported role: ${message.role}`);
|
|
333
|
+
this.closeProtocolViolation(socket, "Unsupported role");
|
|
298
334
|
return;
|
|
299
335
|
}
|
|
300
336
|
const deviceId = this.sanitizeDeviceId(message.deviceId);
|
|
@@ -582,7 +618,31 @@ var WebSocketBridge = class {
|
|
|
582
618
|
|
|
583
619
|
// src/tools.ts
|
|
584
620
|
import { Type } from "@sinclair/typebox";
|
|
585
|
-
var
|
|
621
|
+
var MAX_ACTION_TIMEOUT_MS = 5 * 6e4;
|
|
622
|
+
function asJsonText(data) {
|
|
623
|
+
return {
|
|
624
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function asPlainText(text) {
|
|
628
|
+
return {
|
|
629
|
+
content: [{ type: "text", text }]
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function toPlainObject(value) {
|
|
633
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
634
|
+
return {};
|
|
635
|
+
}
|
|
636
|
+
return value;
|
|
637
|
+
}
|
|
638
|
+
function resolveTimeoutMs(raw) {
|
|
639
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) {
|
|
640
|
+
return void 0;
|
|
641
|
+
}
|
|
642
|
+
const normalized = Math.floor(raw);
|
|
643
|
+
if (normalized <= 0) return void 0;
|
|
644
|
+
return Math.min(normalized, MAX_ACTION_TIMEOUT_MS);
|
|
645
|
+
}
|
|
586
646
|
function registerRedditTools(registerTool, bridge) {
|
|
587
647
|
registerTool({
|
|
588
648
|
name: "reddit_list_pairing_requests",
|
|
@@ -590,9 +650,7 @@ function registerRedditTools(registerTool, bridge) {
|
|
|
590
650
|
parameters: Type.Object({}),
|
|
591
651
|
async execute() {
|
|
592
652
|
const data = bridge.getPendingPairings();
|
|
593
|
-
return
|
|
594
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
595
|
-
};
|
|
653
|
+
return asJsonText(data);
|
|
596
654
|
}
|
|
597
655
|
});
|
|
598
656
|
registerTool({
|
|
@@ -606,9 +664,7 @@ function registerRedditTools(registerTool, bridge) {
|
|
|
606
664
|
async execute(_id, params) {
|
|
607
665
|
const code = String(params.code ?? "").trim();
|
|
608
666
|
const data = bridge.approvePairing(code);
|
|
609
|
-
return
|
|
610
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
611
|
-
};
|
|
667
|
+
return asJsonText(data);
|
|
612
668
|
}
|
|
613
669
|
});
|
|
614
670
|
registerTool({
|
|
@@ -617,146 +673,49 @@ function registerRedditTools(registerTool, bridge) {
|
|
|
617
673
|
parameters: Type.Object({}),
|
|
618
674
|
async execute() {
|
|
619
675
|
const data = bridge.getPairedDevices();
|
|
620
|
-
return
|
|
621
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
622
|
-
};
|
|
676
|
+
return asJsonText(data);
|
|
623
677
|
}
|
|
624
678
|
});
|
|
625
679
|
registerTool({
|
|
626
|
-
name: "
|
|
627
|
-
description: "
|
|
628
|
-
parameters: Type.Object({
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
enum: ["hot", "new", "top", "rising", "best"],
|
|
636
|
-
description: "Sort order. Default: hot."
|
|
637
|
-
})
|
|
638
|
-
)
|
|
639
|
-
}),
|
|
640
|
-
async execute(_id, params) {
|
|
641
|
-
const data = await bridge.sendAction("fetch_subreddit", {
|
|
642
|
-
subreddit: params.subreddit,
|
|
643
|
-
sort: params.sort
|
|
644
|
-
});
|
|
645
|
-
return {
|
|
646
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
registerTool({
|
|
651
|
-
name: "reddit_search",
|
|
652
|
-
description: "Search Reddit for posts matching a query. Optionally scope to a specific subreddit. Supports sort (relevance, top, new, comments) and time filters (hour, day, week, month, year).",
|
|
653
|
-
parameters: Type.Object({
|
|
654
|
-
query: Type.String({ description: "Search query string." }),
|
|
655
|
-
subreddit: Type.Optional(
|
|
656
|
-
Type.String({
|
|
657
|
-
description: 'Subreddit to search within. Omit to search all of Reddit. Accepts same formats as reddit_fetch_subreddit (e.g. "supplements").'
|
|
658
|
-
})
|
|
659
|
-
),
|
|
660
|
-
sort: Type.Optional(
|
|
661
|
-
Type.Unsafe({
|
|
662
|
-
type: "string",
|
|
663
|
-
enum: ["relevance", "top", "new", "comments"],
|
|
664
|
-
description: "Sort order for results. Default: relevance."
|
|
665
|
-
})
|
|
666
|
-
),
|
|
667
|
-
time: Type.Optional(
|
|
668
|
-
Type.Unsafe({
|
|
669
|
-
type: "string",
|
|
670
|
-
enum: ["hour", "day", "week", "month", "year"],
|
|
671
|
-
description: "Time filter. Default: all time."
|
|
672
|
-
})
|
|
673
|
-
)
|
|
674
|
-
}),
|
|
675
|
-
async execute(_id, params) {
|
|
676
|
-
const data = await bridge.sendAction("search_reddit", {
|
|
677
|
-
query: params.query,
|
|
678
|
-
subreddit: params.subreddit,
|
|
679
|
-
sort: params.sort,
|
|
680
|
-
time: params.time
|
|
681
|
-
});
|
|
682
|
-
return {
|
|
683
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
684
|
-
};
|
|
680
|
+
name: "reddit_extension_get_schema",
|
|
681
|
+
description: "Return the extension API schema markdown from the `get_skill` action. Call this before executing other extension actions.",
|
|
682
|
+
parameters: Type.Object({}),
|
|
683
|
+
async execute() {
|
|
684
|
+
const data = await bridge.sendAction("get_skill", {});
|
|
685
|
+
if (typeof data === "string") {
|
|
686
|
+
return asPlainText(data);
|
|
687
|
+
}
|
|
688
|
+
return asJsonText(data);
|
|
685
689
|
}
|
|
686
690
|
});
|
|
687
691
|
registerTool({
|
|
688
|
-
name: "
|
|
689
|
-
description: "
|
|
692
|
+
name: "reddit_extension_call",
|
|
693
|
+
description: "Call a browser-extension action through the Reddit bridge using the extension's action schema. Use `reddit_extension_get_schema` first.",
|
|
690
694
|
parameters: Type.Object({
|
|
691
|
-
|
|
692
|
-
description:
|
|
695
|
+
action: Type.String({
|
|
696
|
+
description: "Extension action name from the schema (for example: fetch_subreddit, search_reddit, fetch_post, reply_to_comment)."
|
|
693
697
|
}),
|
|
694
|
-
|
|
695
|
-
Type.
|
|
696
|
-
|
|
697
|
-
enum: ["hot", "new", "top", "controversial"],
|
|
698
|
-
description: "Sort order. Default: hot."
|
|
698
|
+
params: Type.Optional(
|
|
699
|
+
Type.Record(Type.String(), Type.Unknown(), {
|
|
700
|
+
description: "Action parameters as an object. Shape depends on the selected action."
|
|
699
701
|
})
|
|
700
702
|
),
|
|
701
|
-
|
|
702
|
-
Type.
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
description: "Time filter (applies when sort is top or controversial). Default: all time."
|
|
703
|
+
timeoutMs: Type.Optional(
|
|
704
|
+
Type.Number({
|
|
705
|
+
description: "Optional timeout in milliseconds (max 300000). If omitted, bridge default is used.",
|
|
706
|
+
minimum: 1
|
|
706
707
|
})
|
|
707
708
|
)
|
|
708
709
|
}),
|
|
709
710
|
async execute(_id, params) {
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
});
|
|
720
|
-
registerTool({
|
|
721
|
-
name: "reddit_fetch_post",
|
|
722
|
-
description: "Fetch a single Reddit post and its full comment tree. Provide the full post URL.",
|
|
723
|
-
parameters: Type.Object({
|
|
724
|
-
url: Type.String({
|
|
725
|
-
description: "Full Reddit post URL (e.g. https://www.reddit.com/r/supplements/comments/abc123/some_post/)."
|
|
726
|
-
})
|
|
727
|
-
}),
|
|
728
|
-
async execute(_id, params) {
|
|
729
|
-
const data = await bridge.sendAction("fetch_post", {
|
|
730
|
-
url: params.url
|
|
731
|
-
});
|
|
732
|
-
return {
|
|
733
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
registerTool({
|
|
738
|
-
name: "reddit_reply",
|
|
739
|
-
description: "Reply to a Reddit comment or post using the user's logged-in session in Chrome. Requires the Reddit Agent Chrome extension to be connected and the user to be logged into Reddit. Takes ~7-10 seconds due to DOM interaction.",
|
|
740
|
-
parameters: Type.Object({
|
|
741
|
-
commentUrl: Type.String({
|
|
742
|
-
description: "Direct URL to the comment or post to reply to."
|
|
743
|
-
}),
|
|
744
|
-
replyText: Type.String({
|
|
745
|
-
description: "The reply text to post."
|
|
746
|
-
})
|
|
747
|
-
}),
|
|
748
|
-
async execute(_id, params) {
|
|
749
|
-
const data = await bridge.sendAction(
|
|
750
|
-
"reply_to_comment",
|
|
751
|
-
{
|
|
752
|
-
commentUrl: params.commentUrl,
|
|
753
|
-
replyText: params.replyText
|
|
754
|
-
},
|
|
755
|
-
REPLY_TIMEOUT_MS
|
|
756
|
-
);
|
|
757
|
-
return {
|
|
758
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
759
|
-
};
|
|
711
|
+
const action = String(params.action ?? "").trim();
|
|
712
|
+
if (!action) {
|
|
713
|
+
throw new Error("Missing required parameter: action");
|
|
714
|
+
}
|
|
715
|
+
const actionParams = toPlainObject(params.params);
|
|
716
|
+
const timeoutMs = resolveTimeoutMs(params.timeoutMs);
|
|
717
|
+
const data = await bridge.sendAction(action, actionParams, timeoutMs);
|
|
718
|
+
return asJsonText(data);
|
|
760
719
|
}
|
|
761
720
|
});
|
|
762
721
|
}
|
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 { 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 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.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 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 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 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"]}
|
|
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 MAX_ACTION_TIMEOUT_MS = 5 * 60_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\nfunction asJsonText(data: unknown): { content: { type: \"text\"; text: string }[] } {\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(data, null, 2) }],\n };\n}\n\nfunction asPlainText(text: string): { content: { type: \"text\"; text: string }[] } {\n return {\n content: [{ type: \"text\" as const, text }],\n };\n}\n\nfunction toPlainObject(value: unknown): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return {};\n }\n return value as Record<string, unknown>;\n}\n\nfunction resolveTimeoutMs(raw: unknown): number | undefined {\n if (typeof raw !== \"number\" || !Number.isFinite(raw)) {\n return undefined;\n }\n const normalized = Math.floor(raw);\n if (normalized <= 0) return undefined;\n return Math.min(normalized, MAX_ACTION_TIMEOUT_MS);\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 asJsonText(data);\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 asJsonText(data);\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 asJsonText(data);\n },\n });\n\n registerTool({\n name: \"reddit_extension_get_schema\",\n description:\n \"Return the extension API schema markdown from the `get_skill` action. \" +\n \"Call this before executing other extension actions.\",\n parameters: Type.Object({}),\n async execute() {\n const data = await bridge.sendAction(\"get_skill\", {});\n if (typeof data === \"string\") {\n return asPlainText(data);\n }\n return asJsonText(data);\n },\n });\n\n registerTool({\n name: \"reddit_extension_call\",\n description:\n \"Call a browser-extension action through the Reddit bridge using \" +\n \"the extension's action schema. Use `reddit_extension_get_schema` first.\",\n parameters: Type.Object({\n action: Type.String({\n description:\n \"Extension action name from the schema (for example: fetch_subreddit, search_reddit, fetch_post, reply_to_comment).\",\n }),\n params: Type.Optional(\n Type.Record(Type.String(), Type.Unknown(), {\n description:\n \"Action parameters as an object. Shape depends on the selected action.\",\n })\n ),\n timeoutMs: Type.Optional(\n Type.Number({\n description:\n \"Optional timeout in milliseconds (max 300000). If omitted, bridge default is used.\",\n minimum: 1,\n })\n ),\n }),\n async execute(_id: string, params: Record<string, unknown>) {\n const action = String(params.action ?? \"\").trim();\n if (!action) {\n throw new Error(\"Missing required parameter: action\");\n }\n\n const actionParams = toPlainObject(params.params);\n const timeoutMs = resolveTimeoutMs(params.timeoutMs);\n const data = await bridge.sendAction(action, actionParams, timeoutMs);\n return asJsonText(data);\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"]}
|