@abndnce/pulsar-client 0.0.7 → 0.0.8
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 +74 -2
- package/dist/index.mjs +149 -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,75 @@ 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
|
+
* Rejects if it closes or errors before opening.
|
|
43
|
+
*/
|
|
44
|
+
declare function waitForDataChannelOpen(channel: RTCDataChannel, timeoutMs?: number): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Create a data channel on the given `pc` with the Pulsar socket label
|
|
47
|
+
* convention (`socket/<hostname>:<port>`) and wait for it to open.
|
|
48
|
+
*
|
|
49
|
+
* This is the low-level primitive used by `connect()` and
|
|
50
|
+
* `libcurlTransport()`.
|
|
51
|
+
*/
|
|
52
|
+
declare function openSocketChannel(pc: RTCPeerConnection, hostname: string, port: number, timeoutMs?: number): Promise<RTCDataChannel>;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region lib/tunnel.d.ts
|
|
55
|
+
/**
|
|
56
|
+
* A minimal WebSocket-like wrapper around an RTCDataChannel.
|
|
57
|
+
*
|
|
58
|
+
* libcurl.js expects its transport factory to return objects
|
|
59
|
+
* with `send()`, `close()`, `onopen`, `onmessage`, `onclose`,
|
|
60
|
+
* and `onerror` — which is exactly the RTCDataChannel API, so
|
|
61
|
+
* the wrapper is thin.
|
|
62
|
+
*/
|
|
63
|
+
declare class DataChannelSocket {
|
|
64
|
+
private _channel;
|
|
65
|
+
private _closed;
|
|
66
|
+
onopen: (() => void) | null;
|
|
67
|
+
onmessage: ((event: {
|
|
68
|
+
data: ArrayBuffer | string;
|
|
69
|
+
}) => void) | null;
|
|
70
|
+
onclose: (() => void) | null;
|
|
71
|
+
onerror: ((event: {
|
|
72
|
+
error?: string;
|
|
73
|
+
}) => void) | null;
|
|
74
|
+
constructor(channel: RTCDataChannel);
|
|
75
|
+
get readyState(): string;
|
|
76
|
+
get binaryType(): string;
|
|
77
|
+
set binaryType(_: string);
|
|
78
|
+
send(data: ArrayBuffer | string | ArrayBufferView): void;
|
|
79
|
+
close(): void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create a libcurl.js transport factory from an existing WebRTC
|
|
83
|
+
* peer connection.
|
|
84
|
+
*
|
|
85
|
+
* Usage:
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { connectDirect, libcurlTransport } from '@abndnce/pulsar-client';
|
|
88
|
+
* import { libcurl } from 'libcurl.js';
|
|
89
|
+
*
|
|
90
|
+
* const tunnel = await connectDirect('216.250.119.217', 42069);
|
|
91
|
+
* libcurl.transport = libcurlTransport(tunnel.pc);
|
|
92
|
+
* libcurl.set_websocket('wss://pulsar-tunnel.local/');
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* libcurl.js calls the factory with URLs like:
|
|
96
|
+
* `wss://pulsar-tunnel.local/example.com:80`
|
|
97
|
+
* `wss://pulsar-tunnel.local/216.250.119.217:443`
|
|
98
|
+
*
|
|
99
|
+
* The factory parses `<hostname>:<port>` from the URL path, opens a
|
|
100
|
+
* Pulsar socket data channel, and returns a WebSocket-like adapter.
|
|
101
|
+
*/
|
|
102
|
+
declare function libcurlTransport(pc: RTCPeerConnection): (url: string) => DataChannelSocket;
|
|
103
|
+
//#endregion
|
|
104
|
+
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,144 @@ 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
|
+
* Rejects if it closes or errors before opening.
|
|
95
|
+
*/
|
|
96
|
+
function waitForDataChannelOpen(channel, timeoutMs = 1e4) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
if (channel.readyState === "open") {
|
|
99
|
+
resolve();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const timeout = setTimeout(() => {
|
|
103
|
+
cleanup();
|
|
104
|
+
reject(/* @__PURE__ */ new Error(`DataChannel open timed out after ${timeoutMs}ms`));
|
|
105
|
+
}, timeoutMs);
|
|
106
|
+
const cleanup = () => {
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
channel.onopen = null;
|
|
109
|
+
channel.onclose = null;
|
|
110
|
+
channel.onerror = null;
|
|
111
|
+
};
|
|
112
|
+
channel.onopen = () => {
|
|
113
|
+
cleanup();
|
|
114
|
+
resolve();
|
|
115
|
+
};
|
|
116
|
+
channel.onclose = () => {
|
|
117
|
+
cleanup();
|
|
118
|
+
reject(/* @__PURE__ */ new Error("DataChannel closed before opening"));
|
|
119
|
+
};
|
|
120
|
+
channel.onerror = (e) => {
|
|
121
|
+
cleanup();
|
|
122
|
+
reject(/* @__PURE__ */ new Error(`DataChannel error before opening: ${e}`));
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Create a data channel on the given `pc` with the Pulsar socket label
|
|
128
|
+
* convention (`socket/<hostname>:<port>`) and wait for it to open.
|
|
129
|
+
*
|
|
130
|
+
* This is the low-level primitive used by `connect()` and
|
|
131
|
+
* `libcurlTransport()`.
|
|
132
|
+
*/
|
|
133
|
+
function openSocketChannel(pc, hostname, port, timeoutMs) {
|
|
134
|
+
const label = `${SOCKET_PREFIX}${hostname}:${port}`;
|
|
135
|
+
const channel = pc.createDataChannel(label, { ordered: true });
|
|
136
|
+
return waitForDataChannelOpen(channel, timeoutMs).then(() => channel);
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region lib/tunnel.ts
|
|
140
|
+
/**
|
|
141
|
+
* A minimal WebSocket-like wrapper around an RTCDataChannel.
|
|
142
|
+
*
|
|
143
|
+
* libcurl.js expects its transport factory to return objects
|
|
144
|
+
* with `send()`, `close()`, `onopen`, `onmessage`, `onclose`,
|
|
145
|
+
* and `onerror` — which is exactly the RTCDataChannel API, so
|
|
146
|
+
* the wrapper is thin.
|
|
147
|
+
*/
|
|
148
|
+
var DataChannelSocket = class {
|
|
149
|
+
_channel;
|
|
150
|
+
_closed = false;
|
|
151
|
+
onopen = null;
|
|
152
|
+
onmessage = null;
|
|
153
|
+
onclose = null;
|
|
154
|
+
onerror = null;
|
|
155
|
+
constructor(channel) {
|
|
156
|
+
this._channel = channel;
|
|
157
|
+
channel.onopen = () => {
|
|
158
|
+
if (!this._closed) this.onopen?.();
|
|
159
|
+
};
|
|
160
|
+
channel.onmessage = (event) => {
|
|
161
|
+
if (!this._closed) this.onmessage?.({ data: event.data });
|
|
162
|
+
};
|
|
163
|
+
channel.onclose = () => {
|
|
164
|
+
this._closed = true;
|
|
165
|
+
this.onclose?.();
|
|
166
|
+
};
|
|
167
|
+
channel.onerror = (event) => {
|
|
168
|
+
this.onerror?.({ error: String(event) });
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
get readyState() {
|
|
172
|
+
return this._channel.readyState;
|
|
173
|
+
}
|
|
174
|
+
get binaryType() {
|
|
175
|
+
return "arraybuffer";
|
|
176
|
+
}
|
|
177
|
+
set binaryType(_) {}
|
|
178
|
+
send(data) {
|
|
179
|
+
if (this._closed || this._channel.readyState !== "open") return;
|
|
180
|
+
this._channel.send(data);
|
|
181
|
+
}
|
|
182
|
+
close() {
|
|
183
|
+
this._closed = true;
|
|
184
|
+
try {
|
|
185
|
+
this._channel.close();
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Create a libcurl.js transport factory from an existing WebRTC
|
|
191
|
+
* peer connection.
|
|
192
|
+
*
|
|
193
|
+
* Usage:
|
|
194
|
+
* ```ts
|
|
195
|
+
* import { connectDirect, libcurlTransport } from '@abndnce/pulsar-client';
|
|
196
|
+
* import { libcurl } from 'libcurl.js';
|
|
197
|
+
*
|
|
198
|
+
* const tunnel = await connectDirect('216.250.119.217', 42069);
|
|
199
|
+
* libcurl.transport = libcurlTransport(tunnel.pc);
|
|
200
|
+
* libcurl.set_websocket('wss://pulsar-tunnel.local/');
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* libcurl.js calls the factory with URLs like:
|
|
204
|
+
* `wss://pulsar-tunnel.local/example.com:80`
|
|
205
|
+
* `wss://pulsar-tunnel.local/216.250.119.217:443`
|
|
206
|
+
*
|
|
207
|
+
* The factory parses `<hostname>:<port>` from the URL path, opens a
|
|
208
|
+
* Pulsar socket data channel, and returns a WebSocket-like adapter.
|
|
209
|
+
*/
|
|
210
|
+
function libcurlTransport(pc) {
|
|
211
|
+
return (url) => {
|
|
212
|
+
let dest;
|
|
213
|
+
try {
|
|
214
|
+
dest = new URL(url).pathname.replace(/^\//, "").replace(/\/$/, "");
|
|
215
|
+
} catch {
|
|
216
|
+
const slash = url.indexOf("/", url.indexOf("//") + 2);
|
|
217
|
+
dest = slash === -1 ? url : url.slice(slash + 1);
|
|
218
|
+
}
|
|
219
|
+
if (!dest) throw new Error(`libcurl transport: no destination found in URL "${url}"`);
|
|
220
|
+
if (dest.lastIndexOf(":") === -1) throw new Error(`libcurl transport: invalid destination "${dest}" — expected "hostname:port"`);
|
|
221
|
+
return new DataChannelSocket(pc.createDataChannel(`${SOCKET_PREFIX}${dest}`, { ordered: true }));
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
export { connectDirect, connectNostr, libcurlTransport, openSocketChannel, waitForDataChannelOpen };
|