@clawchatsai/connector 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * GatewayBridge — persistent WebSocket connection to the local OpenClaw gateway.
3
+ *
4
+ * Responsibilities:
5
+ * 1. Connect to the local gateway using token-only auth (loopback connections
6
+ * with valid token skip device identity entirely).
7
+ * 2. Broadcast incoming gateway events to all connected DataChannel clients
8
+ * via the 'gateway-event' EventEmitter event.
9
+ * 3. Reconnect with exponential backoff on disconnect.
10
+ */
11
+ import { EventEmitter } from 'node:events';
12
+ export interface PluginConfig {
13
+ userId: string;
14
+ serverUrl: string;
15
+ apiKey: string;
16
+ gatewayToken?: string;
17
+ devicePrivateKey?: string;
18
+ schemaVersion: number;
19
+ installedAt: string;
20
+ }
21
+ export interface BridgeConfig {
22
+ gatewayToken: string;
23
+ }
24
+ export declare class GatewayBridge extends EventEmitter {
25
+ private readonly gatewayUrl;
26
+ private readonly config;
27
+ private ws;
28
+ private _isConnected;
29
+ private destroyed;
30
+ private reconnectTimer;
31
+ private backoffMs;
32
+ constructor(gatewayUrl: string, config: BridgeConfig);
33
+ /**
34
+ * Initiate the connection. Resolves once the WebSocket has been opened
35
+ * (not necessarily after the gateway handshake completes — listen for
36
+ * the 'connected' event for that).
37
+ */
38
+ connect(): Promise<void>;
39
+ /**
40
+ * Forward a message to the gateway (transparent proxy).
41
+ */
42
+ send(data: string): void;
43
+ /**
44
+ * Permanently close the connection and cancel any pending reconnect.
45
+ */
46
+ disconnect(): void;
47
+ get isConnected(): boolean;
48
+ private _openSocket;
49
+ private _handleMessage;
50
+ private _sendConnect;
51
+ private _scheduleReconnect;
52
+ private _cancelReconnect;
53
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * GatewayBridge — persistent WebSocket connection to the local OpenClaw gateway.
3
+ *
4
+ * Responsibilities:
5
+ * 1. Connect to the local gateway using token-only auth (loopback connections
6
+ * with valid token skip device identity entirely).
7
+ * 2. Broadcast incoming gateway events to all connected DataChannel clients
8
+ * via the 'gateway-event' EventEmitter event.
9
+ * 3. Reconnect with exponential backoff on disconnect.
10
+ */
11
+ import { EventEmitter } from 'node:events';
12
+ import { WebSocket } from 'ws';
13
+ import { PLUGIN_VERSION } from './index.js';
14
+ // Reconnect backoff constants (milliseconds)
15
+ const BACKOFF_INITIAL_MS = 1_000;
16
+ const BACKOFF_MAX_MS = 30_000;
17
+ // Fixed client identity fields
18
+ const CLIENT_ID = 'gateway-client';
19
+ const CLIENT_MODE = 'backend';
20
+ const ROLE = 'operator';
21
+ const SCOPES = ['operator.read', 'operator.write', 'operator.admin'];
22
+ export class GatewayBridge extends EventEmitter {
23
+ gatewayUrl;
24
+ config;
25
+ ws = null;
26
+ _isConnected = false;
27
+ destroyed = false;
28
+ // Reconnect state
29
+ reconnectTimer = null;
30
+ backoffMs = BACKOFF_INITIAL_MS;
31
+ constructor(gatewayUrl, config) {
32
+ super();
33
+ this.gatewayUrl = gatewayUrl;
34
+ this.config = config;
35
+ }
36
+ /**
37
+ * Initiate the connection. Resolves once the WebSocket has been opened
38
+ * (not necessarily after the gateway handshake completes — listen for
39
+ * the 'connected' event for that).
40
+ */
41
+ connect() {
42
+ return new Promise((resolve, reject) => {
43
+ if (this.destroyed) {
44
+ reject(new Error('GatewayBridge has been destroyed'));
45
+ return;
46
+ }
47
+ this._openSocket(resolve, reject);
48
+ });
49
+ }
50
+ /**
51
+ * Forward a message to the gateway (transparent proxy).
52
+ */
53
+ send(data) {
54
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
55
+ console.warn('[GatewayBridge] send() called but WebSocket is not open');
56
+ return;
57
+ }
58
+ this.ws.send(data);
59
+ }
60
+ /**
61
+ * Permanently close the connection and cancel any pending reconnect.
62
+ */
63
+ disconnect() {
64
+ this.destroyed = true;
65
+ this._cancelReconnect();
66
+ if (this.ws) {
67
+ this.ws.terminate();
68
+ this.ws = null;
69
+ }
70
+ this._isConnected = false;
71
+ }
72
+ get isConnected() {
73
+ return this._isConnected;
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // Private helpers
77
+ // ---------------------------------------------------------------------------
78
+ _openSocket(resolveConnect = null, rejectConnect = null) {
79
+ console.log(`[GatewayBridge] Connecting to ${this.gatewayUrl}`);
80
+ const ws = new WebSocket(this.gatewayUrl);
81
+ this.ws = ws;
82
+ let resolved = false;
83
+ ws.on('open', () => {
84
+ console.log('[GatewayBridge] WebSocket opened');
85
+ if (!resolved && resolveConnect) {
86
+ resolved = true;
87
+ resolveConnect();
88
+ }
89
+ });
90
+ ws.on('error', (err) => {
91
+ console.error('[GatewayBridge] WebSocket error:', err.message);
92
+ if (!resolved && rejectConnect) {
93
+ resolved = true;
94
+ rejectConnect(err);
95
+ }
96
+ });
97
+ ws.on('message', (raw) => {
98
+ this._handleMessage(typeof raw === 'string' ? raw : raw.toString('utf8'));
99
+ });
100
+ ws.on('close', (code, reason) => {
101
+ console.log(`[GatewayBridge] WebSocket closed (code=${code} reason=${reason.toString('utf8') || '(none)'})`);
102
+ this._isConnected = false;
103
+ this.ws = null;
104
+ this.emit('disconnected');
105
+ if (!this.destroyed) {
106
+ this._scheduleReconnect();
107
+ }
108
+ });
109
+ }
110
+ _handleMessage(raw) {
111
+ // Always broadcast the raw event to connected DataChannel clients first.
112
+ this.emit('gateway-event', raw);
113
+ // Parse for handshake handling.
114
+ let msg;
115
+ try {
116
+ msg = JSON.parse(raw);
117
+ }
118
+ catch {
119
+ // Non-JSON frame — forward as-is, ignore for handshake purposes.
120
+ return;
121
+ }
122
+ const type = msg['type'];
123
+ const event = msg['event'];
124
+ // connect.challenge → send token-only connect request (loopback skips nonce)
125
+ if (type === 'event' && event === 'connect.challenge') {
126
+ this._sendConnect();
127
+ return;
128
+ }
129
+ // hello-ok → handshake complete
130
+ if (type === 'res') {
131
+ const payload = msg['payload'];
132
+ if (payload?.['type'] === 'hello-ok') {
133
+ console.log('[GatewayBridge] Gateway handshake complete');
134
+ this._isConnected = true;
135
+ // Reset backoff on successful connection.
136
+ this.backoffMs = BACKOFF_INITIAL_MS;
137
+ this.emit('connected');
138
+ }
139
+ }
140
+ }
141
+ _sendConnect() {
142
+ this.send(JSON.stringify({
143
+ type: 'req',
144
+ id: 'gw-connect-1',
145
+ method: 'connect',
146
+ params: {
147
+ minProtocol: 3,
148
+ maxProtocol: 3,
149
+ client: {
150
+ id: CLIENT_ID,
151
+ version: PLUGIN_VERSION,
152
+ platform: 'node',
153
+ mode: CLIENT_MODE,
154
+ },
155
+ role: ROLE,
156
+ scopes: SCOPES,
157
+ auth: { token: this.config.gatewayToken },
158
+ caps: ['tool-events'],
159
+ },
160
+ }));
161
+ }
162
+ _scheduleReconnect() {
163
+ if (this.destroyed)
164
+ return;
165
+ if (this.reconnectTimer !== null)
166
+ return; // already scheduled
167
+ console.log(`[GatewayBridge] Reconnecting in ${this.backoffMs}ms`);
168
+ this.reconnectTimer = setTimeout(() => {
169
+ this.reconnectTimer = null;
170
+ if (!this.destroyed) {
171
+ this._openSocket();
172
+ }
173
+ }, this.backoffMs);
174
+ // Exponential backoff: double each time, cap at BACKOFF_MAX_MS.
175
+ this.backoffMs = Math.min(this.backoffMs * 2, BACKOFF_MAX_MS);
176
+ }
177
+ _cancelReconnect() {
178
+ if (this.reconnectTimer !== null) {
179
+ clearTimeout(this.reconnectTimer);
180
+ this.reconnectTimer = null;
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @shellchat/tunnel — OpenClaw plugin entry point
3
+ *
4
+ * Registers ShellChat as a gateway plugin, providing:
5
+ * - Local HTTP API bridge via createApp()
6
+ * - WebRTC DataChannel for browser connections
7
+ * - Signaling client for NAT traversal
8
+ * - Gateway bridge for local OpenClaw communication
9
+ *
10
+ * Spec: specs/multitenant-p2p.md sections 6.1-6.2
11
+ */
12
+ export declare const PLUGIN_ID = "tunnel";
13
+ export declare const PLUGIN_VERSION = "0.0.1";
14
+ interface PluginServiceContext {
15
+ stateDir: string;
16
+ logger: {
17
+ info: (msg: string) => void;
18
+ warn: (msg: string) => void;
19
+ error: (msg: string) => void;
20
+ };
21
+ _forceUpdate?: boolean;
22
+ }
23
+ interface PluginCliContext {
24
+ program: {
25
+ command: (name: string) => {
26
+ description: (desc: string) => CliCommand;
27
+ command: (nameAndArgs: string) => CliCommand;
28
+ };
29
+ };
30
+ }
31
+ interface CliCommand {
32
+ description: (desc: string) => CliCommand;
33
+ action: (handler: (...args: unknown[]) => void | Promise<void>) => CliCommand;
34
+ command: (nameAndArgs: string) => CliCommand;
35
+ }
36
+ interface PluginApi {
37
+ registerService: (opts: {
38
+ id: string;
39
+ start: (ctx: PluginServiceContext) => Promise<void>;
40
+ stop: (ctx: PluginServiceContext) => Promise<void>;
41
+ }) => void;
42
+ registerCli: (handler: (ctx: PluginCliContext) => void) => void;
43
+ registerCommand: (opts: {
44
+ name: string;
45
+ description: string;
46
+ handler: () => {
47
+ text: string;
48
+ };
49
+ }) => void;
50
+ runtime: {
51
+ requestRestart?: (reason: string) => void;
52
+ };
53
+ config?: Record<string, unknown>;
54
+ }
55
+ interface OpenClawPluginDefinition {
56
+ id: string;
57
+ name: string;
58
+ description: string;
59
+ register: (api: PluginApi) => void;
60
+ }
61
+ declare const plugin: OpenClawPluginDefinition;
62
+ export default plugin;