@clawchatsai/connector 0.0.21 → 0.0.23
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 +1 -1
- package/dist/index.js +1 -1
- package/dist/shim.d.ts +0 -5
- package/dist/shim.js +2 -39
- package/dist/webrtc-peer.d.ts +5 -87
- package/dist/webrtc-peer.js +44 -127
- package/package.json +2 -2
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.
|
|
13
|
+
export declare const PLUGIN_VERSION = "0.0.23";
|
|
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.
|
|
25
|
+
export const PLUGIN_VERSION = '0.0.23';
|
|
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 */
|
package/dist/shim.d.ts
CHANGED
|
@@ -56,11 +56,6 @@ declare class FakeRes extends Writable {
|
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* Dispatch an RPC request through handleRequest and return the response.
|
|
59
|
-
*
|
|
60
|
-
* Safety nets:
|
|
61
|
-
* 1. If handleRequest() throws, we return a 500 with the error message.
|
|
62
|
-
* 2. If res.finished never resolves (handleRequest forgot to call res.end()),
|
|
63
|
-
* we time out after RPC_TIMEOUT_MS and return a 504.
|
|
64
59
|
*/
|
|
65
60
|
export declare function dispatchRpc(rpc: RpcRequest, handleRequest: HandleRequestFn): Promise<RpcResponse>;
|
|
66
61
|
export {};
|
package/dist/shim.js
CHANGED
|
@@ -137,51 +137,14 @@ function tryJsonParse(buf) {
|
|
|
137
137
|
return buf.toString('base64');
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
/** RPC dispatch timeout — if handleRequest doesn't call res.end() within
|
|
141
|
-
* this window, we return a 504 so the browser isn't left hanging. */
|
|
142
|
-
const RPC_TIMEOUT_MS = 30_000;
|
|
143
140
|
/**
|
|
144
141
|
* Dispatch an RPC request through handleRequest and return the response.
|
|
145
|
-
*
|
|
146
|
-
* Safety nets:
|
|
147
|
-
* 1. If handleRequest() throws, we return a 500 with the error message.
|
|
148
|
-
* 2. If res.finished never resolves (handleRequest forgot to call res.end()),
|
|
149
|
-
* we time out after RPC_TIMEOUT_MS and return a 504.
|
|
150
142
|
*/
|
|
151
143
|
export async function dispatchRpc(rpc, handleRequest) {
|
|
152
144
|
const req = new FakeReq(rpc);
|
|
153
145
|
const res = new FakeRes();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
159
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
160
|
-
console.error(`[RPC] handleRequest threw for ${rpc.method} ${rpc.url}: ${message}`);
|
|
161
|
-
if (stack)
|
|
162
|
-
console.error(stack);
|
|
163
|
-
return {
|
|
164
|
-
id: rpc.id,
|
|
165
|
-
status: 500,
|
|
166
|
-
headers: {},
|
|
167
|
-
body: JSON.stringify({ error: 'Internal server error', detail: message }),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
// Wait for res.finished, but give up after RPC_TIMEOUT_MS
|
|
171
|
-
const result = await Promise.race([
|
|
172
|
-
res.finished,
|
|
173
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('RPC_TIMEOUT')), RPC_TIMEOUT_MS)),
|
|
174
|
-
]).catch((err) => {
|
|
175
|
-
console.error(`[RPC] Response never completed for ${rpc.method} ${rpc.url}: ${err.message}`);
|
|
176
|
-
return {
|
|
177
|
-
status: 504,
|
|
178
|
-
headers: {},
|
|
179
|
-
body: Buffer.from(JSON.stringify({
|
|
180
|
-
error: 'Gateway timeout',
|
|
181
|
-
detail: `Plugin did not respond within ${RPC_TIMEOUT_MS / 1000}s for ${rpc.method} ${rpc.url}`,
|
|
182
|
-
})),
|
|
183
|
-
};
|
|
184
|
-
});
|
|
146
|
+
await handleRequest(req, res);
|
|
147
|
+
const result = await res.finished;
|
|
185
148
|
return {
|
|
186
149
|
id: rpc.id,
|
|
187
150
|
status: result.status,
|
package/dist/webrtc-peer.d.ts
CHANGED
|
@@ -1,127 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebRTCPeerManager — manages incoming WebRTC connections from browsers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
}
|
package/dist/webrtc-peer.js
CHANGED
|
@@ -1,44 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebRTCPeerManager — manages incoming WebRTC connections from browsers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
46
|
+
// Channel may already be closed
|
|
57
47
|
}
|
|
58
48
|
},
|
|
59
49
|
onMessage(handler) {
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
157
|
-
await pc.setRemoteDescription(new RTCSessionDescription(sdp, 'offer'));
|
|
158
|
-
// Add
|
|
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
|
-
|
|
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
|
|
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: [],
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.23",
|
|
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
|
-
"
|
|
29
|
+
"node-datachannel": "^0.32.1",
|
|
30
30
|
"ws": "^8.0.0"
|
|
31
31
|
},
|
|
32
32
|
"openclaw": {
|