@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 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
- export { type PulsarClientConnection, connectDirect, connectNostr };
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/credentials.ts
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("keepalive", { ordered: true });
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
- export { connectDirect, connectNostr };
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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@abndnce/pulsar-client",
3
3
  "type": "module",
4
- "version": "0.0.7",
4
+ "version": "0.0.9",
5
5
  "homepage": "https://github.com/abndnce/pulsar",
6
6
  "license": "MIT",
7
7
  "exports": {