@abndnce/pulsar-client 0.0.7 → 0.0.9
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.mts +92 -2
- package/dist/index.mjs +226 -4
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -16,7 +16,8 @@ interface PulsarClientConnection {
|
|
|
16
16
|
*
|
|
17
17
|
* @param host Server IP address
|
|
18
18
|
* @param port Server UDP port
|
|
19
|
-
* @returns A connected PulsarClientConnection with an open keepalive channel
|
|
19
|
+
* @returns A connected PulsarClientConnection with an open keepalive channel
|
|
20
|
+
* and a `connect()` helper for opening socket tunnels.
|
|
20
21
|
*/
|
|
21
22
|
declare function connectDirect(host: string, port: number): Promise<PulsarClientConnection>;
|
|
22
23
|
//#endregion
|
|
@@ -29,4 +30,93 @@ declare function connectDirect(host: string, port: number): Promise<PulsarClient
|
|
|
29
30
|
*/
|
|
30
31
|
declare function connectNostr(_relay: string, _pubkey: string): Promise<PulsarClientConnection>;
|
|
31
32
|
//#endregion
|
|
32
|
-
|
|
33
|
+
//#region lib/socket-channel.d.ts
|
|
34
|
+
/**
|
|
35
|
+
* Shared utilities for opening Pulsar socket tunnel data channels.
|
|
36
|
+
*
|
|
37
|
+
* These work with any RTCPeerConnection, regardless of how it was
|
|
38
|
+
* established (direct, nostr, etc.).
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Wait for an RTCDataChannel to enter the "open" state.
|
|
42
|
+
*
|
|
43
|
+
* Also monitors the RTCPeerConnection for failure/closure, and
|
|
44
|
+
* accepts an optional AbortSignal for cancellation.
|
|
45
|
+
*
|
|
46
|
+
* Rejects if the channel closes, the peer connection fails, or
|
|
47
|
+
* the timeout elapses before the channel opens.
|
|
48
|
+
*/
|
|
49
|
+
declare function waitForDataChannelOpen(channel: RTCDataChannel, pc: RTCPeerConnection, timeoutMs?: number, signal?: AbortSignal): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Create a data channel on the given `pc` with the Pulsar socket label
|
|
52
|
+
* convention (`socket/<hostname>:<port>`) and wait for it to open.
|
|
53
|
+
*
|
|
54
|
+
* This is the low-level primitive used by `connect()` and
|
|
55
|
+
* `libcurlTransport()`.
|
|
56
|
+
*/
|
|
57
|
+
declare function openSocketChannel(pc: RTCPeerConnection, hostname: string, port: number, timeoutMs?: number, signal?: AbortSignal): Promise<RTCDataChannel>;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region lib/tunnel.d.ts
|
|
60
|
+
/**
|
|
61
|
+
* WebSocket-compatible wrapper around an RTCDataChannel.
|
|
62
|
+
*
|
|
63
|
+
* libcurl.js compares `readyState` against `WebSocket.OPEN` (numeric 1)
|
|
64
|
+
* and expects static constants (`CONNECTING`, `OPEN`, `CLOSING`, `CLOSED`).
|
|
65
|
+
* Using `EventTarget` ensures `addEventListener` / `removeEventListener`
|
|
66
|
+
* work, which libcurl's poll loop depends on.
|
|
67
|
+
*/
|
|
68
|
+
declare class DataChannelSocket extends EventTarget {
|
|
69
|
+
static readonly CONNECTING = 0;
|
|
70
|
+
static readonly OPEN = 1;
|
|
71
|
+
static readonly CLOSING = 2;
|
|
72
|
+
static readonly CLOSED = 3;
|
|
73
|
+
readonly CONNECTING = 0;
|
|
74
|
+
readonly OPEN = 1;
|
|
75
|
+
readonly CLOSING = 2;
|
|
76
|
+
readonly CLOSED = 3;
|
|
77
|
+
readonly url: string;
|
|
78
|
+
readonly protocol = "";
|
|
79
|
+
readonly extensions = "";
|
|
80
|
+
binaryType: string;
|
|
81
|
+
onopen: ((event: Event) => void) | null;
|
|
82
|
+
onclose: ((event: CloseEvent) => void) | null;
|
|
83
|
+
onerror: ((event: Event) => void) | null;
|
|
84
|
+
onmessage: ((event: MessageEvent) => void) | null;
|
|
85
|
+
private _channel;
|
|
86
|
+
private _closed;
|
|
87
|
+
private _closeDispatched;
|
|
88
|
+
private _readyState;
|
|
89
|
+
constructor(pc: RTCPeerConnection, destination: string);
|
|
90
|
+
private _open;
|
|
91
|
+
get readyState(): number;
|
|
92
|
+
get bufferedAmount(): number;
|
|
93
|
+
send(data: string | ArrayBufferLike | ArrayBufferView): void;
|
|
94
|
+
close(): void;
|
|
95
|
+
private _dispatch;
|
|
96
|
+
private _dispatchError;
|
|
97
|
+
private _dispatchClose;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Create a libcurl.js transport factory from an existing WebRTC
|
|
101
|
+
* peer connection.
|
|
102
|
+
*
|
|
103
|
+
* Usage:
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { connectDirect, libcurlTransport } from '@abndnce/pulsar-client';
|
|
106
|
+
* import { libcurl } from 'libcurl.js';
|
|
107
|
+
*
|
|
108
|
+
* const tunnel = await connectDirect('216.250.119.217', 42069);
|
|
109
|
+
* libcurl.transport = libcurlTransport(tunnel.pc);
|
|
110
|
+
* libcurl.set_websocket('wss://pulsar-tunnel.local/');
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* libcurl.js calls the factory with URLs like:
|
|
114
|
+
* `wss://pulsar-tunnel.local/example.com:80`
|
|
115
|
+
* `wss://pulsar-tunnel.local/216.250.119.217:443`
|
|
116
|
+
*
|
|
117
|
+
* The factory parses `<hostname>:<port>` from the URL path, opens a
|
|
118
|
+
* Pulsar socket data channel, and returns a WebSocket-like adapter.
|
|
119
|
+
*/
|
|
120
|
+
declare function libcurlTransport(pc: RTCPeerConnection): (url: string) => DataChannelSocket;
|
|
121
|
+
//#endregion
|
|
122
|
+
export { type PulsarClientConnection, connectDirect, connectNostr, libcurlTransport, openSocketChannel, waitForDataChannelOpen };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
//#region ../core/
|
|
1
|
+
//#region ../core/constants.ts
|
|
2
2
|
const PULSAR_UFRAG = "pulsar";
|
|
3
3
|
const PULSAR_PWD = "pulsarpulsarpulsarpuls";
|
|
4
4
|
const PULSAR_FINGERPRINT = "F1:85:10:8F:36:FF:58:D8:D0:4B:52:D7:ED:DC:5C:28:AE:7D:DB:54:0E:2A:DD:C7:C3:94:EA:A1:27:D0:4E:78";
|
|
5
|
+
/** Label prefix for socket tunnel data channels. */
|
|
6
|
+
const SOCKET_PREFIX = "socket/";
|
|
7
|
+
/** Label for the mandatory keepalive data channel. */
|
|
8
|
+
const KEEPALIVE_LABEL = "keepalive";
|
|
5
9
|
//#endregion
|
|
6
10
|
//#region lib/connection/direct.ts
|
|
7
11
|
/**
|
|
@@ -11,11 +15,12 @@ const PULSAR_FINGERPRINT = "F1:85:10:8F:36:FF:58:D8:D0:4B:52:D7:ED:DC:5C:28:AE:7
|
|
|
11
15
|
*
|
|
12
16
|
* @param host Server IP address
|
|
13
17
|
* @param port Server UDP port
|
|
14
|
-
* @returns A connected PulsarClientConnection with an open keepalive channel
|
|
18
|
+
* @returns A connected PulsarClientConnection with an open keepalive channel
|
|
19
|
+
* and a `connect()` helper for opening socket tunnels.
|
|
15
20
|
*/
|
|
16
21
|
async function connectDirect(host, port) {
|
|
17
22
|
const pc = new RTCPeerConnection();
|
|
18
|
-
const keepalive = pc.createDataChannel(
|
|
23
|
+
const keepalive = pc.createDataChannel(KEEPALIVE_LABEL, { ordered: true });
|
|
19
24
|
const offer = await pc.createOffer();
|
|
20
25
|
await pc.setLocalDescription(offer);
|
|
21
26
|
const remoteSdp = [
|
|
@@ -77,4 +82,221 @@ async function connectNostr(_relay, _pubkey) {
|
|
|
77
82
|
throw new Error("Nostr mode not yet implemented");
|
|
78
83
|
}
|
|
79
84
|
//#endregion
|
|
80
|
-
|
|
85
|
+
//#region lib/socket-channel.ts
|
|
86
|
+
/**
|
|
87
|
+
* Shared utilities for opening Pulsar socket tunnel data channels.
|
|
88
|
+
*
|
|
89
|
+
* These work with any RTCPeerConnection, regardless of how it was
|
|
90
|
+
* established (direct, nostr, etc.).
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* Wait for an RTCDataChannel to enter the "open" state.
|
|
94
|
+
*
|
|
95
|
+
* Also monitors the RTCPeerConnection for failure/closure, and
|
|
96
|
+
* accepts an optional AbortSignal for cancellation.
|
|
97
|
+
*
|
|
98
|
+
* Rejects if the channel closes, the peer connection fails, or
|
|
99
|
+
* the timeout elapses before the channel opens.
|
|
100
|
+
*/
|
|
101
|
+
function waitForDataChannelOpen(channel, pc, timeoutMs = 1e4, signal) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
if (signal?.aborted) {
|
|
104
|
+
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (channel.readyState === "open") {
|
|
108
|
+
resolve();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const timeout = setTimeout(() => {
|
|
112
|
+
cleanup();
|
|
113
|
+
reject(/* @__PURE__ */ new Error(`DataChannel open timed out after ${timeoutMs}ms`));
|
|
114
|
+
}, timeoutMs);
|
|
115
|
+
const cleanup = () => {
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
channel.removeEventListener("open", onOpen);
|
|
118
|
+
channel.removeEventListener("close", onClose);
|
|
119
|
+
channel.removeEventListener("error", onError);
|
|
120
|
+
pc.removeEventListener("connectionstatechange", onStateChange);
|
|
121
|
+
signal?.removeEventListener("abort", onAbort);
|
|
122
|
+
};
|
|
123
|
+
const onOpen = () => {
|
|
124
|
+
cleanup();
|
|
125
|
+
resolve();
|
|
126
|
+
};
|
|
127
|
+
const onClose = () => {
|
|
128
|
+
cleanup();
|
|
129
|
+
reject(/* @__PURE__ */ new Error("DataChannel closed before opening"));
|
|
130
|
+
};
|
|
131
|
+
const onError = () => {
|
|
132
|
+
cleanup();
|
|
133
|
+
reject(/* @__PURE__ */ new Error("DataChannel error before opening"));
|
|
134
|
+
};
|
|
135
|
+
const onStateChange = () => {
|
|
136
|
+
if (pc.connectionState === "failed" || pc.connectionState === "closed") {
|
|
137
|
+
cleanup();
|
|
138
|
+
reject(/* @__PURE__ */ new Error(`Peer connection ${pc.connectionState}`));
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const onAbort = () => {
|
|
142
|
+
cleanup();
|
|
143
|
+
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
144
|
+
};
|
|
145
|
+
channel.addEventListener("open", onOpen, { once: true });
|
|
146
|
+
channel.addEventListener("close", onClose, { once: true });
|
|
147
|
+
channel.addEventListener("error", onError, { once: true });
|
|
148
|
+
pc.addEventListener("connectionstatechange", onStateChange);
|
|
149
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Create a data channel on the given `pc` with the Pulsar socket label
|
|
154
|
+
* convention (`socket/<hostname>:<port>`) and wait for it to open.
|
|
155
|
+
*
|
|
156
|
+
* This is the low-level primitive used by `connect()` and
|
|
157
|
+
* `libcurlTransport()`.
|
|
158
|
+
*/
|
|
159
|
+
function openSocketChannel(pc, hostname, port, timeoutMs, signal) {
|
|
160
|
+
const label = `${SOCKET_PREFIX}${hostname}:${port}`;
|
|
161
|
+
const channel = pc.createDataChannel(label, { ordered: true });
|
|
162
|
+
return waitForDataChannelOpen(channel, pc, timeoutMs, signal).then(() => channel);
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region lib/tunnel.ts
|
|
166
|
+
/**
|
|
167
|
+
* WebSocket-compatible wrapper around an RTCDataChannel.
|
|
168
|
+
*
|
|
169
|
+
* libcurl.js compares `readyState` against `WebSocket.OPEN` (numeric 1)
|
|
170
|
+
* and expects static constants (`CONNECTING`, `OPEN`, `CLOSING`, `CLOSED`).
|
|
171
|
+
* Using `EventTarget` ensures `addEventListener` / `removeEventListener`
|
|
172
|
+
* work, which libcurl's poll loop depends on.
|
|
173
|
+
*/
|
|
174
|
+
var DataChannelSocket = class DataChannelSocket extends EventTarget {
|
|
175
|
+
static CONNECTING = 0;
|
|
176
|
+
static OPEN = 1;
|
|
177
|
+
static CLOSING = 2;
|
|
178
|
+
static CLOSED = 3;
|
|
179
|
+
CONNECTING = 0;
|
|
180
|
+
OPEN = 1;
|
|
181
|
+
CLOSING = 2;
|
|
182
|
+
CLOSED = 3;
|
|
183
|
+
url;
|
|
184
|
+
protocol = "";
|
|
185
|
+
extensions = "";
|
|
186
|
+
binaryType = "arraybuffer";
|
|
187
|
+
onopen = null;
|
|
188
|
+
onclose = null;
|
|
189
|
+
onerror = null;
|
|
190
|
+
onmessage = null;
|
|
191
|
+
_channel = null;
|
|
192
|
+
_closed = false;
|
|
193
|
+
_closeDispatched = false;
|
|
194
|
+
_readyState = DataChannelSocket.CONNECTING;
|
|
195
|
+
constructor(pc, destination) {
|
|
196
|
+
super();
|
|
197
|
+
this.url = `wss://pulsar-tunnel.local/${destination}`;
|
|
198
|
+
const channel = pc.createDataChannel(`${SOCKET_PREFIX}${destination}`, { ordered: true });
|
|
199
|
+
channel.binaryType = "arraybuffer";
|
|
200
|
+
this._channel = channel;
|
|
201
|
+
this._open(channel, pc);
|
|
202
|
+
}
|
|
203
|
+
async _open(channel, pc) {
|
|
204
|
+
try {
|
|
205
|
+
await waitForDataChannelOpen(channel, pc);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (!this._closed) {
|
|
208
|
+
console.error(`[DataChannelSocket] Failed to open channel: ${error instanceof Error ? error.message : String(error)}`);
|
|
209
|
+
this._dispatchError();
|
|
210
|
+
}
|
|
211
|
+
this._dispatchClose();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (this._closed) {
|
|
215
|
+
channel.close();
|
|
216
|
+
this._dispatchClose();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this._channel = channel;
|
|
220
|
+
this._readyState = DataChannelSocket.OPEN;
|
|
221
|
+
channel.onmessage = (event) => {
|
|
222
|
+
this._dispatch(new MessageEvent("message", { data: event.data }));
|
|
223
|
+
};
|
|
224
|
+
channel.onclose = () => {
|
|
225
|
+
this._readyState = DataChannelSocket.CLOSED;
|
|
226
|
+
this._dispatchClose();
|
|
227
|
+
};
|
|
228
|
+
channel.onerror = () => {
|
|
229
|
+
this._dispatchError();
|
|
230
|
+
};
|
|
231
|
+
this._dispatch(new Event("open"));
|
|
232
|
+
}
|
|
233
|
+
get readyState() {
|
|
234
|
+
return this._readyState;
|
|
235
|
+
}
|
|
236
|
+
get bufferedAmount() {
|
|
237
|
+
return this._channel?.bufferedAmount ?? 0;
|
|
238
|
+
}
|
|
239
|
+
send(data) {
|
|
240
|
+
if (this._readyState !== DataChannelSocket.OPEN || !this._channel) throw new Error("DataChannelSocket is not open");
|
|
241
|
+
if (typeof data === "string") this._channel.send(data);
|
|
242
|
+
else this._channel.send(data);
|
|
243
|
+
}
|
|
244
|
+
close() {
|
|
245
|
+
if (this._closed) return;
|
|
246
|
+
this._closed = true;
|
|
247
|
+
if (this._channel && this._channel.readyState !== "closed") this._channel.close();
|
|
248
|
+
else this._dispatchClose();
|
|
249
|
+
}
|
|
250
|
+
_dispatch(event) {
|
|
251
|
+
const handlerName = `on${event.type}`;
|
|
252
|
+
const handler = this[handlerName];
|
|
253
|
+
if (handler) handler(event);
|
|
254
|
+
this.dispatchEvent(event);
|
|
255
|
+
}
|
|
256
|
+
_dispatchError() {
|
|
257
|
+
this._dispatch(new Event("error"));
|
|
258
|
+
}
|
|
259
|
+
_dispatchClose() {
|
|
260
|
+
if (this._closeDispatched) return;
|
|
261
|
+
this._closeDispatched = true;
|
|
262
|
+
this._readyState = DataChannelSocket.CLOSED;
|
|
263
|
+
this._dispatch(new CloseEvent("close"));
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* Create a libcurl.js transport factory from an existing WebRTC
|
|
268
|
+
* peer connection.
|
|
269
|
+
*
|
|
270
|
+
* Usage:
|
|
271
|
+
* ```ts
|
|
272
|
+
* import { connectDirect, libcurlTransport } from '@abndnce/pulsar-client';
|
|
273
|
+
* import { libcurl } from 'libcurl.js';
|
|
274
|
+
*
|
|
275
|
+
* const tunnel = await connectDirect('216.250.119.217', 42069);
|
|
276
|
+
* libcurl.transport = libcurlTransport(tunnel.pc);
|
|
277
|
+
* libcurl.set_websocket('wss://pulsar-tunnel.local/');
|
|
278
|
+
* ```
|
|
279
|
+
*
|
|
280
|
+
* libcurl.js calls the factory with URLs like:
|
|
281
|
+
* `wss://pulsar-tunnel.local/example.com:80`
|
|
282
|
+
* `wss://pulsar-tunnel.local/216.250.119.217:443`
|
|
283
|
+
*
|
|
284
|
+
* The factory parses `<hostname>:<port>` from the URL path, opens a
|
|
285
|
+
* Pulsar socket data channel, and returns a WebSocket-like adapter.
|
|
286
|
+
*/
|
|
287
|
+
function libcurlTransport(pc) {
|
|
288
|
+
return (url) => {
|
|
289
|
+
let dest;
|
|
290
|
+
try {
|
|
291
|
+
dest = new URL(url).pathname.replace(/^\//, "").replace(/\/$/, "");
|
|
292
|
+
} catch {
|
|
293
|
+
const slash = url.indexOf("/", url.indexOf("//") + 2);
|
|
294
|
+
dest = slash === -1 ? url : url.slice(slash + 1);
|
|
295
|
+
}
|
|
296
|
+
if (!dest) throw new Error(`libcurl transport: no destination found in URL "${url}"`);
|
|
297
|
+
if (dest.lastIndexOf(":") === -1) throw new Error(`libcurl transport: invalid destination "${dest}" — expected "hostname:port"`);
|
|
298
|
+
return new DataChannelSocket(pc, dest);
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
export { connectDirect, connectNostr, libcurlTransport, openSocketChannel, waitForDataChannelOpen };
|