@clawchatsai/connector 0.0.22 → 0.0.24

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.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * Spec: specs/multitenant-p2p.md sections 6.1-6.2
11
11
  */
12
12
  export declare const PLUGIN_ID = "connector";
13
- export declare const PLUGIN_VERSION = "0.0.22";
13
+ export declare const PLUGIN_VERSION = "0.0.24";
14
14
  interface PluginServiceContext {
15
15
  stateDir: string;
16
16
  logger: {
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import { generateSessionSecret } from './session-token.js';
22
22
  // Inline from shared/api-version.ts to avoid rootDir conflict
23
23
  const CURRENT_API_VERSION = 1;
24
24
  export const PLUGIN_ID = 'connector';
25
- export const PLUGIN_VERSION = '0.0.22';
25
+ export const PLUGIN_VERSION = '0.0.24';
26
26
  /** Max DataChannel message size (~256KB, leave room for envelope) */
27
27
  const MAX_DC_MESSAGE_SIZE = 256 * 1024;
28
28
  /** Active DataChannel connections: connectionId → send function */
@@ -62,24 +62,30 @@ function saveConfig(config) {
62
62
  // ---------------------------------------------------------------------------
63
63
  async function ensureNativeModules(ctx) {
64
64
  // OpenClaw installs plugins with --ignore-scripts, which skips native module compilation.
65
- // Check if better-sqlite3 is usable; if not, rebuild it automatically.
65
+ // Check if native modules are usable; if not, rebuild them automatically.
66
66
  const pluginDir = path.resolve(__dirname, '..');
67
- const bindingPath = path.join(pluginDir, 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node');
68
- if (fs.existsSync(bindingPath))
69
- return; // already built
70
- ctx.logger.info('Building native modules (first run)...');
67
+ const modules = [
68
+ { name: 'better-sqlite3', binding: 'build/Release/better_sqlite3.node' },
69
+ { name: 'node-datachannel', binding: 'build/Release/node_datachannel.node' },
70
+ ];
71
+ const missing = modules.filter(m => !fs.existsSync(path.join(pluginDir, 'node_modules', m.name, m.binding)));
72
+ if (missing.length === 0)
73
+ return; // all built
74
+ ctx.logger.info(`Building native modules (first run): ${missing.map(m => m.name).join(', ')}...`);
71
75
  const { execFileSync } = await import('node:child_process');
72
- try {
73
- execFileSync('npm', ['rebuild', 'better-sqlite3'], {
74
- cwd: pluginDir,
75
- stdio: 'pipe',
76
- timeout: 120_000,
77
- });
78
- ctx.logger.info('Native modules ready.');
79
- }
80
- catch (e) {
81
- ctx.logger.error(`Failed to build native modules: ${e.message}`);
82
- ctx.logger.error('Try running manually: cd ~/.openclaw/extensions/connector && npm rebuild better-sqlite3');
76
+ for (const mod of missing) {
77
+ try {
78
+ execFileSync('npm', ['rebuild', mod.name], {
79
+ cwd: pluginDir,
80
+ stdio: 'pipe',
81
+ timeout: 120_000,
82
+ });
83
+ ctx.logger.info(`${mod.name} ready.`);
84
+ }
85
+ catch (e) {
86
+ ctx.logger.error(`Failed to build ${mod.name}: ${e.message}`);
87
+ ctx.logger.error(`Try manually: cd ~/.openclaw/extensions/connector && npm rebuild ${mod.name}`);
88
+ }
83
89
  }
84
90
  }
85
91
  async function startClawChats(ctx, api) {
@@ -1,127 +1,45 @@
1
1
  /**
2
2
  * WebRTCPeerManager — manages incoming WebRTC connections from browsers.
3
3
  *
4
- * Responsibilities:
5
- * 1. Store ICE server config received from the signaling server before an
6
- * offer arrives (the signaling server sends ICE servers and offer separately).
7
- * 2. Handle an ice-offer from a browser: create an RTCPeerConnection, set the
8
- * remote description, create an answer, and return it to the caller.
9
- * 3. Wrap werift's DataChannel in a DataChannelLike interface and emit a
10
- * 'datachannel' event when the channel opens.
11
- * 4. Clean up peer connections on explicit closeAll() or individual channel close.
4
+ * Uses node-datachannel (libdatachannel C++ bindings) for production-grade
5
+ * SCTP/DTLS/WebRTC. The W3C polyfill layer provides standard browser-like APIs.
12
6
  *
13
- * Spec: specs/multitenant-p2p.md sections 6.2–6.3 (plugin WebRTC peer endpoint)
14
- *
15
- * werift is a pure-JavaScript WebRTC implementation for Node.js with no
16
- * native bindings, satisfying the "no native deps" constraint in the spec.
17
- *
18
- * Key werift API notes (actual types, not W3C spec):
19
- * - RTCDataChannel.onMessage is an rx.mini Event<[string | Buffer]> (.subscribe())
20
- * - RTCDataChannel.onclose is a plain optional callback (() => void)
21
- * - RTCPeerConnection.ondatachannel is a plain callback (RTCDataChannelEvent) => void
22
- * - RTCPeerConnection.onDataChannel is an rx.mini Event<[RTCDataChannel]>
23
- * - pc.addIceCandidate() accepts werift's RTCIceCandidate class
24
- * - pc.close() returns Promise<void>
25
- * - RTCSessionDescription constructor: (sdp: string, type: "offer" | "answer")
7
+ * Replaces werift (pure-JS WebRTC) which had unreliable SCTP message delivery
8
+ * under real network conditions (silent message drops on rapid sends).
26
9
  */
27
10
  import { EventEmitter } from 'node:events';
28
- /**
29
- * An ICE offer sent by a browser via the signaling server.
30
- * Mirrors the signaling protocol message (spec section 5.3).
31
- */
32
11
  export interface IceOffer {
33
12
  connectionId: string;
34
13
  sdp: string;
35
14
  candidates: unknown[];
36
15
  }
37
- /**
38
- * ICE server configuration delivered by the signaling server before the offer.
39
- * Contains STUN/TURN servers with optional credentials (Cloudflare TURN).
40
- */
41
16
  export interface IceServers {
42
17
  connectionId: string;
43
18
  iceServers: Array<{
44
- urls: string;
19
+ urls: string | string[];
45
20
  username?: string;
46
21
  credential?: string;
47
22
  }>;
48
23
  }
49
- /**
50
- * Stable interface for a DataChannel, independent of werift internals.
51
- * Callers (index.ts) use this for message routing, not the raw werift object.
52
- */
53
24
  export interface DataChannelLike {
54
- /** Send a UTF-8 string through the DataChannel. */
55
25
  send(data: string): void;
56
- /** Close the DataChannel. */
57
26
  close(): void;
58
- /** Register a handler for incoming string messages. */
59
27
  onMessage(handler: (data: string) => void): void;
60
- /** Register a handler invoked when the channel closes. */
61
28
  onClosed(handler: () => void): void;
62
29
  }
63
30
  export declare class WebRTCPeerManager extends EventEmitter {
64
- /**
65
- * Pending ICE server configs keyed by connectionId.
66
- * The signaling server delivers ICE servers before the offer arrives, so we
67
- * buffer them here and consume them in handleOffer().
68
- */
69
31
  private pendingIceServers;
70
- /**
71
- * Active RTCPeerConnection instances, keyed by connectionId.
72
- * Used for cleanup in closeAll() and trickle-ICE in handleIceCandidate().
73
- */
74
32
  private peerConnections;
75
- /**
76
- * Open DataChannel wrappers, keyed by connectionId.
77
- * Used to compute activeCount and for bulk close in closeAll().
78
- */
79
33
  private activeChannels;
80
34
  constructor();
81
- /**
82
- * Store ICE server configuration for a given connectionId.
83
- * Called by index.ts when the signaling client emits 'ice-servers'.
84
- * Must be called before handleOffer() for the same connectionId.
85
- */
86
35
  setIceServers(data: IceServers): void;
87
- /**
88
- * Handle an incoming ICE offer from a browser (forwarded by the signaling server).
89
- *
90
- * Process:
91
- * 1. Create RTCPeerConnection with buffered ICE servers (falls back to Google STUN).
92
- * 2. Set the remote description from the offer SDP.
93
- * 3. Add any trickle ICE candidates bundled in the offer payload.
94
- * 4. Register ondatachannel before creating the answer.
95
- * 5. Create answer, set as local description.
96
- * 6. Return the answer for the signaling client to relay back to the browser.
97
- *
98
- * @returns The ICE answer payload to forward to the browser via signaling.
99
- */
100
36
  handleOffer(offer: IceOffer): Promise<{
101
37
  connectionId: string;
102
38
  sdp: string;
103
39
  candidates: unknown[];
104
40
  }>;
105
- /**
106
- * Add a trickle ICE candidate for an existing peer connection.
107
- * Called when the signaling server relays a late-arriving candidate from the browser.
108
- */
109
41
  handleIceCandidate(connectionId: string, candidate: unknown): void;
110
- /**
111
- * Close all active peer connections and DataChannels.
112
- * Called during plugin shutdown (stopClawChats).
113
- */
114
42
  closeAll(): void;
115
- /**
116
- * The number of DataChannels that are currently open.
117
- * Reported to the signaling server via connection-count messages so it can
118
- * enforce per-user device limits (spec section 5.4).
119
- */
120
43
  get activeCount(): number;
121
- /**
122
- * Called when the browser opens a DataChannel on the peer connection.
123
- * Wraps the raw werift DataChannel, registers lifecycle handlers, and
124
- * emits 'datachannel' so index.ts can wire up the message/gateway routing.
125
- */
126
44
  private _handleDataChannel;
127
45
  }
@@ -1,44 +1,34 @@
1
1
  /**
2
2
  * WebRTCPeerManager — manages incoming WebRTC connections from browsers.
3
3
  *
4
- * Responsibilities:
5
- * 1. Store ICE server config received from the signaling server before an
6
- * offer arrives (the signaling server sends ICE servers and offer separately).
7
- * 2. Handle an ice-offer from a browser: create an RTCPeerConnection, set the
8
- * remote description, create an answer, and return it to the caller.
9
- * 3. Wrap werift's DataChannel in a DataChannelLike interface and emit a
10
- * 'datachannel' event when the channel opens.
11
- * 4. Clean up peer connections on explicit closeAll() or individual channel close.
4
+ * Uses node-datachannel (libdatachannel C++ bindings) for production-grade
5
+ * SCTP/DTLS/WebRTC. The W3C polyfill layer provides standard browser-like APIs.
12
6
  *
13
- * Spec: specs/multitenant-p2p.md sections 6.2–6.3 (plugin WebRTC peer endpoint)
14
- *
15
- * werift is a pure-JavaScript WebRTC implementation for Node.js with no
16
- * native bindings, satisfying the "no native deps" constraint in the spec.
17
- *
18
- * Key werift API notes (actual types, not W3C spec):
19
- * - RTCDataChannel.onMessage is an rx.mini Event<[string | Buffer]> (.subscribe())
20
- * - RTCDataChannel.onclose is a plain optional callback (() => void)
21
- * - RTCPeerConnection.ondatachannel is a plain callback (RTCDataChannelEvent) => void
22
- * - RTCPeerConnection.onDataChannel is an rx.mini Event<[RTCDataChannel]>
23
- * - pc.addIceCandidate() accepts werift's RTCIceCandidate class
24
- * - pc.close() returns Promise<void>
25
- * - RTCSessionDescription constructor: (sdp: string, type: "offer" | "answer")
7
+ * Replaces werift (pure-JS WebRTC) which had unreliable SCTP message delivery
8
+ * under real network conditions (silent message drops on rapid sends).
26
9
  */
27
10
  import { EventEmitter } from 'node:events';
28
- import { RTCIceCandidate, RTCPeerConnection, RTCSessionDescription, } from 'werift';
11
+ import { RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, } from 'node-datachannel/polyfill';
29
12
  // ---------------------------------------------------------------------------
30
13
  // Internal helpers
31
14
  // ---------------------------------------------------------------------------
32
- /**
33
- * Wraps a werift RTCDataChannel in the DataChannelLike interface.
34
- *
35
- * Actual werift RTCDataChannel API used here:
36
- * dc.onMessage — rx.mini Event<[string | Buffer]>, use .subscribe()
37
- * dc.onclose — plain optional callback (() => void), assign directly
38
- * dc.send(data) — sends string or Buffer
39
- * dc.close() — closes the channel
40
- */
41
15
  function wrapDataChannel(dc) {
16
+ const messageHandlers = [];
17
+ const closedHandlers = [];
18
+ // W3C-standard event handlers
19
+ dc.onmessage = (event) => {
20
+ const str = typeof event.data === 'string'
21
+ ? event.data
22
+ : Buffer.isBuffer(event.data)
23
+ ? event.data.toString('utf8')
24
+ : String(event.data);
25
+ for (const h of messageHandlers)
26
+ h(str);
27
+ };
28
+ dc.onclose = () => {
29
+ for (const h of closedHandlers)
30
+ h();
31
+ };
42
32
  return {
43
33
  send(data) {
44
34
  try {
@@ -53,19 +43,14 @@ function wrapDataChannel(dc) {
53
43
  dc.close();
54
44
  }
55
45
  catch {
56
- // Channel may already be closed — swallow the error.
46
+ // Channel may already be closed
57
47
  }
58
48
  },
59
49
  onMessage(handler) {
60
- // onMessage is an rx.mini Event<[string | Buffer]>.
61
- dc.onMessage.subscribe((data) => {
62
- const str = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
63
- handler(str);
64
- });
50
+ messageHandlers.push(handler);
65
51
  },
66
52
  onClosed(handler) {
67
- // onclose is a plain callback property on RTCDataChannel.
68
- dc.onclose = handler;
53
+ closedHandlers.push(handler);
69
54
  },
70
55
  };
71
56
  }
@@ -73,61 +58,24 @@ function wrapDataChannel(dc) {
73
58
  // WebRTCPeerManager
74
59
  // ---------------------------------------------------------------------------
75
60
  export class WebRTCPeerManager extends EventEmitter {
76
- /**
77
- * Pending ICE server configs keyed by connectionId.
78
- * The signaling server delivers ICE servers before the offer arrives, so we
79
- * buffer them here and consume them in handleOffer().
80
- */
81
61
  pendingIceServers = new Map();
82
- /**
83
- * Active RTCPeerConnection instances, keyed by connectionId.
84
- * Used for cleanup in closeAll() and trickle-ICE in handleIceCandidate().
85
- */
86
62
  peerConnections = new Map();
87
- /**
88
- * Open DataChannel wrappers, keyed by connectionId.
89
- * Used to compute activeCount and for bulk close in closeAll().
90
- */
91
63
  activeChannels = new Map();
92
64
  constructor() {
93
65
  super();
94
66
  }
95
- // -------------------------------------------------------------------------
96
- // Public API
97
- // -------------------------------------------------------------------------
98
- /**
99
- * Store ICE server configuration for a given connectionId.
100
- * Called by index.ts when the signaling client emits 'ice-servers'.
101
- * Must be called before handleOffer() for the same connectionId.
102
- */
103
67
  setIceServers(data) {
104
68
  console.log(`[WebRTCPeerManager] Storing ICE servers for connection ${data.connectionId}`);
105
69
  this.pendingIceServers.set(data.connectionId, data.iceServers);
106
70
  }
107
- /**
108
- * Handle an incoming ICE offer from a browser (forwarded by the signaling server).
109
- *
110
- * Process:
111
- * 1. Create RTCPeerConnection with buffered ICE servers (falls back to Google STUN).
112
- * 2. Set the remote description from the offer SDP.
113
- * 3. Add any trickle ICE candidates bundled in the offer payload.
114
- * 4. Register ondatachannel before creating the answer.
115
- * 5. Create answer, set as local description.
116
- * 6. Return the answer for the signaling client to relay back to the browser.
117
- *
118
- * @returns The ICE answer payload to forward to the browser via signaling.
119
- */
120
71
  async handleOffer(offer) {
121
72
  const { connectionId, sdp, candidates } = offer;
122
73
  console.log(`[WebRTCPeerManager] Handling ICE offer for connection ${connectionId}`);
123
- // Retrieve buffered ICE servers; fall back to Google's public STUN if none arrived.
124
74
  const iceServers = this.pendingIceServers.get(connectionId) ?? [
125
75
  { urls: 'stun:stun.l.google.com:19302' },
126
76
  ];
127
77
  this.pendingIceServers.delete(connectionId);
128
- // Normalize iceServers: werift expects `urls` as a string, but Cloudflare
129
- // (and the W3C spec) returns `urls` as an array. Expand each array entry
130
- // into individual objects so werift's String.includes() checks work.
78
+ // Normalize: ensure urls is always a string (not array) per entry
131
79
  const normalized = iceServers.flatMap((s) => {
132
80
  const urls = Array.isArray(s.urls) ? s.urls : [s.urls];
133
81
  return urls.map((url) => ({
@@ -136,66 +84,53 @@ export class WebRTCPeerManager extends EventEmitter {
136
84
  ...(s.credential && { credential: s.credential }),
137
85
  }));
138
86
  });
139
- // Create the peer connection with the resolved ICE servers.
140
87
  const pc = new RTCPeerConnection({ iceServers: normalized });
141
88
  this.peerConnections.set(connectionId, pc);
142
- // Register ondatachannel before creating the answer so the browser's
143
- // createDataChannel() call fires against an already-listening peer.
144
- // werift's ondatachannel is a plain callback (CallbackWithValue<RTCDataChannelEvent>).
145
- pc.ondatachannel = ({ channel }) => {
89
+ // W3C-standard ondatachannel
90
+ pc.ondatachannel = (event) => {
91
+ const channel = event.channel;
146
92
  this._handleDataChannel(channel, connectionId);
147
93
  };
148
- // Trickle ICE candidates as they're discovered (STUN/TURN/host).
149
- // werift gathers asynchronously after setLocalDescription — we must
150
- // forward each candidate to the browser via signaling.
151
- pc.onicecandidate = ({ candidate }) => {
152
- if (candidate) {
153
- this.emit('ice-candidate-local', { connectionId, candidate: candidate.toJSON() });
94
+ // Forward local ICE candidates to browser via signaling
95
+ pc.onicecandidate = (event) => {
96
+ if (event.candidate) {
97
+ this.emit('ice-candidate-local', {
98
+ connectionId,
99
+ candidate: event.candidate.toJSON ? event.candidate.toJSON() : event.candidate,
100
+ });
154
101
  }
155
102
  };
156
- // Set the browser's offer as the remote description.
157
- await pc.setRemoteDescription(new RTCSessionDescription(sdp, 'offer'));
158
- // Add any trickle ICE candidates bundled in the offer payload.
103
+ // Set remote description (browser's offer)
104
+ await pc.setRemoteDescription(new RTCSessionDescription({ sdp, type: 'offer' }));
105
+ // Add bundled trickle ICE candidates
159
106
  for (const rawCandidate of candidates) {
160
107
  try {
161
- // werift's addIceCandidate() expects its own RTCIceCandidate class.
162
- const candidate = new RTCIceCandidate(rawCandidate);
163
- await pc.addIceCandidate(candidate);
108
+ await pc.addIceCandidate(new RTCIceCandidate(rawCandidate));
164
109
  }
165
110
  catch (err) {
166
111
  console.warn(`[WebRTCPeerManager] Failed to add ICE candidate for ${connectionId}:`, err);
167
112
  }
168
113
  }
169
- // Create answer and commit it as the local description.
114
+ // Create and set local answer
170
115
  const answer = await pc.createAnswer();
171
116
  await pc.setLocalDescription(answer);
172
117
  console.log(`[WebRTCPeerManager] Answer created for connection ${connectionId}`);
173
- // answer is werift's RTCSessionDescription — answer.sdp is the SDP string.
174
118
  return {
175
119
  connectionId,
176
120
  sdp: answer.sdp,
177
- candidates: [], // werift gathers candidates into the SDP (non-trickle mode)
121
+ candidates: [],
178
122
  };
179
123
  }
180
- /**
181
- * Add a trickle ICE candidate for an existing peer connection.
182
- * Called when the signaling server relays a late-arriving candidate from the browser.
183
- */
184
124
  handleIceCandidate(connectionId, candidate) {
185
125
  const pc = this.peerConnections.get(connectionId);
186
126
  if (!pc) {
187
127
  console.warn(`[WebRTCPeerManager] handleIceCandidate: no peer connection for ${connectionId}`);
188
128
  return;
189
129
  }
190
- const rtcCandidate = new RTCIceCandidate(candidate);
191
- pc.addIceCandidate(rtcCandidate).catch((err) => {
130
+ pc.addIceCandidate(new RTCIceCandidate(candidate)).catch((err) => {
192
131
  console.warn(`[WebRTCPeerManager] Failed to add trickle ICE candidate for ${connectionId}:`, err);
193
132
  });
194
133
  }
195
- /**
196
- * Close all active peer connections and DataChannels.
197
- * Called during plugin shutdown (stopClawChats).
198
- */
199
134
  closeAll() {
200
135
  console.log(`[WebRTCPeerManager] Closing all connections (${this.peerConnections.size} peers, ${this.activeChannels.size} channels)`);
201
136
  for (const [, channel] of this.activeChannels) {
@@ -207,51 +142,33 @@ export class WebRTCPeerManager extends EventEmitter {
207
142
  this.activeChannels.clear();
208
143
  for (const [, pc] of this.peerConnections) {
209
144
  try {
210
- // werift's pc.close() returns Promise<void>; fire-and-forget on shutdown.
211
- void pc.close();
145
+ pc.close();
212
146
  }
213
147
  catch { /* already closed */ }
214
148
  }
215
149
  this.peerConnections.clear();
216
150
  this.pendingIceServers.clear();
217
151
  }
218
- /**
219
- * The number of DataChannels that are currently open.
220
- * Reported to the signaling server via connection-count messages so it can
221
- * enforce per-user device limits (spec section 5.4).
222
- */
223
152
  get activeCount() {
224
153
  return this.activeChannels.size;
225
154
  }
226
- // -------------------------------------------------------------------------
227
- // Private helpers
228
- // -------------------------------------------------------------------------
229
- /**
230
- * Called when the browser opens a DataChannel on the peer connection.
231
- * Wraps the raw werift DataChannel, registers lifecycle handlers, and
232
- * emits 'datachannel' so index.ts can wire up the message/gateway routing.
233
- */
234
155
  _handleDataChannel(dc, connectionId) {
235
156
  console.log(`[WebRTCPeerManager] DataChannel opened for connection ${connectionId}`);
236
157
  const channel = wrapDataChannel(dc);
237
158
  this.activeChannels.set(connectionId, channel);
238
- // Register the closed handler before emitting 'datachannel' so consumers
239
- // can rely on onClosed firing for any channel they receive.
240
159
  channel.onClosed(() => {
241
160
  console.log(`[WebRTCPeerManager] DataChannel closed for connection ${connectionId}`);
242
161
  this.activeChannels.delete(connectionId);
243
- // Clean up the peer connection — no DataChannel means the peer is done.
244
162
  const pc = this.peerConnections.get(connectionId);
245
163
  if (pc) {
246
164
  try {
247
- void pc.close();
165
+ pc.close();
248
166
  }
249
167
  catch { /* already closed */ }
250
168
  this.peerConnections.delete(connectionId);
251
169
  }
252
170
  this.emit('datachannel-closed', connectionId);
253
171
  });
254
- // Emit 'datachannel' so index.ts can call setupDataChannelHandler().
255
172
  this.emit('datachannel', channel, connectionId);
256
173
  }
257
174
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "type": "module",
5
5
  "description": "ClawChats OpenClaw plugin — P2P tunnel + local API bridge",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "better-sqlite3": ">=9.0.0",
28
28
  "jose": "^5.10.0",
29
- "werift": "^0.19.9",
29
+ "node-datachannel": "^0.32.1",
30
30
  "ws": "^8.0.0"
31
31
  },
32
32
  "openclaw": {