@abndnce/pulsar-client 0.0.9 → 0.0.10
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/LICENSE +21 -0
- package/dist/index.d.mts +14 -6
- package/dist/index.mjs +341 -7
- package/package.json +4 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.mts
CHANGED
|
@@ -23,12 +23,21 @@ declare function connectDirect(host: string, port: number): Promise<PulsarClient
|
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region lib/connection/nostr.d.ts
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Establish a Pulsar tunnel via Nostr relay signaling.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* 1. Connects to a Nostr relay (tries nostr.data.haus first,
|
|
29
|
+
* then kotukonostr.onrender.com)
|
|
30
|
+
* 2. Looks up the tunnel's discovery event (Kind 38000, d="pulsar-tunnel").
|
|
31
|
+
* If `tunnelCode` is given (e.g. "a3f2"), filters to the tunnel
|
|
32
|
+
* whose pubkey begins with those 4 hex characters.
|
|
33
|
+
* 3. Generates an ephemeral client keypair
|
|
34
|
+
* 4. Creates a WebRTC offer
|
|
35
|
+
* 5. Encrypts the offer and sends via a Kind 28000 ephemeral event
|
|
36
|
+
* 6. Waits for the encrypted answer
|
|
37
|
+
* 7. Sets the answer as remote description
|
|
38
|
+
* 8. Returns the connected PulsarClientConnection
|
|
30
39
|
*/
|
|
31
|
-
declare function connectNostr(
|
|
40
|
+
declare function connectNostr(tunnelCode?: string): Promise<PulsarClientConnection>;
|
|
32
41
|
//#endregion
|
|
33
42
|
//#region lib/socket-channel.d.ts
|
|
34
43
|
/**
|
|
@@ -105,14 +114,13 @@ declare class DataChannelSocket extends EventTarget {
|
|
|
105
114
|
* import { connectDirect, libcurlTransport } from '@abndnce/pulsar-client';
|
|
106
115
|
* import { libcurl } from 'libcurl.js';
|
|
107
116
|
*
|
|
108
|
-
* const tunnel = await connectDirect('
|
|
117
|
+
* const tunnel = await connectDirect('1.2.3.4', 1234);
|
|
109
118
|
* libcurl.transport = libcurlTransport(tunnel.pc);
|
|
110
119
|
* libcurl.set_websocket('wss://pulsar-tunnel.local/');
|
|
111
120
|
* ```
|
|
112
121
|
*
|
|
113
122
|
* libcurl.js calls the factory with URLs like:
|
|
114
123
|
* `wss://pulsar-tunnel.local/example.com:80`
|
|
115
|
-
* `wss://pulsar-tunnel.local/216.250.119.217:443`
|
|
116
124
|
*
|
|
117
125
|
* The factory parses `<hostname>:<port>` from the URL path, opens a
|
|
118
126
|
* Pulsar socket data channel, and returns a WebSocket-like adapter.
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
|
|
1
2
|
//#region ../core/constants.ts
|
|
2
3
|
const PULSAR_UFRAG = "pulsar";
|
|
3
4
|
const PULSAR_PWD = "pulsarpulsarpulsarpuls";
|
|
@@ -71,15 +72,349 @@ async function connectDirect(host, port) {
|
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
74
|
//#endregion
|
|
75
|
+
//#region ../core/nostr.ts
|
|
76
|
+
const NOSTR_RELAYS = ["wss://nostr.data.haus", "wss://kotukonostr.onrender.com"];
|
|
77
|
+
/** Ephemeral event kind used for encrypted WebRTC signaling. */
|
|
78
|
+
const SIGNALING_KIND = 28e3;
|
|
79
|
+
/** Parameterized replaceable event kind used for server discovery. */
|
|
80
|
+
const DISCOVERY_KIND = 38e3;
|
|
81
|
+
/** The `d` tag identifier for the Pulsar tunnel discovery event. */
|
|
82
|
+
const D_TAG_ID = "pulsar-tunnel";
|
|
83
|
+
//#endregion
|
|
74
84
|
//#region lib/connection/nostr.ts
|
|
85
|
+
function hexToBytes(hex) {
|
|
86
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
87
|
+
for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
88
|
+
return bytes;
|
|
89
|
+
}
|
|
90
|
+
function bytesToHex(bytes) {
|
|
91
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
92
|
+
}
|
|
93
|
+
function generateKeypair() {
|
|
94
|
+
const seckey = secp256k1.utils.randomPrivateKey();
|
|
95
|
+
const pubkey = secp256k1.getPublicKey(seckey, true).slice(1);
|
|
96
|
+
return {
|
|
97
|
+
seckey: bytesToHex(seckey),
|
|
98
|
+
pubkey: bytesToHex(pubkey)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
75
101
|
/**
|
|
76
|
-
*
|
|
102
|
+
* Derive a shared ECDH secret between a private key and a peer's
|
|
103
|
+
* 32-byte x-only public key (Nostr format).
|
|
77
104
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
105
|
+
* Schnorr public keys always have even y, so we reconstruct the
|
|
106
|
+
* full point via ProjectivePoint and do scalar multiplication.
|
|
80
107
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
108
|
+
function getSharedXOnly(seckey, pubkeyXOnly) {
|
|
109
|
+
const compressed = new Uint8Array(33);
|
|
110
|
+
compressed[0] = 2;
|
|
111
|
+
compressed.set(pubkeyXOnly, 1);
|
|
112
|
+
const pubPoint = secp256k1.ProjectivePoint.fromHex(compressed);
|
|
113
|
+
const scalar = bytesToBigInt(seckey);
|
|
114
|
+
const shared = pubPoint.multiply(scalar);
|
|
115
|
+
return new Uint8Array(shared.toRawBytes(false).slice(1, 33));
|
|
116
|
+
}
|
|
117
|
+
function bytesToBigInt(bytes) {
|
|
118
|
+
let hex = "";
|
|
119
|
+
for (const b of bytes) hex += b.toString(16).padStart(2, "0");
|
|
120
|
+
return BigInt("0x" + hex);
|
|
121
|
+
}
|
|
122
|
+
async function getConversationKey(seckeyBytes, pubkeyBytes) {
|
|
123
|
+
const sharedSecret = getSharedXOnly(seckeyBytes, pubkeyBytes);
|
|
124
|
+
const hkdfKey = await crypto.subtle.importKey("raw", sharedSecret, "HKDF", false, ["deriveBits", "deriveKey"]);
|
|
125
|
+
return crypto.subtle.deriveKey({
|
|
126
|
+
name: "HKDF",
|
|
127
|
+
hash: "SHA-256",
|
|
128
|
+
salt: new Uint8Array(0),
|
|
129
|
+
info: new TextEncoder().encode("nip44-v2")
|
|
130
|
+
}, hkdfKey, {
|
|
131
|
+
name: "AES-GCM",
|
|
132
|
+
length: 256
|
|
133
|
+
}, false, ["encrypt", "decrypt"]);
|
|
134
|
+
}
|
|
135
|
+
async function nip44Encrypt(plaintext, seckeyHex, pubkeyHex) {
|
|
136
|
+
const convKey = await getConversationKey(hexToBytes(seckeyHex), hexToBytes(pubkeyHex));
|
|
137
|
+
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
|
138
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
139
|
+
const encrypted = await crypto.subtle.encrypt({
|
|
140
|
+
name: "AES-GCM",
|
|
141
|
+
iv: nonce,
|
|
142
|
+
tagLength: 128
|
|
143
|
+
}, convKey, encoded);
|
|
144
|
+
const result = new Uint8Array(13 + encrypted.byteLength);
|
|
145
|
+
result[0] = 2;
|
|
146
|
+
result.set(nonce, 1);
|
|
147
|
+
result.set(new Uint8Array(encrypted), 13);
|
|
148
|
+
return btoa(String.fromCharCode(...result));
|
|
149
|
+
}
|
|
150
|
+
async function nip44Decrypt(ciphertextB64, seckeyHex, pubkeyHex) {
|
|
151
|
+
const convKey = await getConversationKey(hexToBytes(seckeyHex), hexToBytes(pubkeyHex));
|
|
152
|
+
const raw = Uint8Array.from(atob(ciphertextB64), (c) => c.charCodeAt(0));
|
|
153
|
+
if (raw.length < 13) throw new Error("Ciphertext too short");
|
|
154
|
+
const nonce = raw.slice(1, 13);
|
|
155
|
+
const ciphertext = raw.slice(13);
|
|
156
|
+
const decrypted = await crypto.subtle.decrypt({
|
|
157
|
+
name: "AES-GCM",
|
|
158
|
+
iv: nonce,
|
|
159
|
+
tagLength: 128
|
|
160
|
+
}, convKey, ciphertext);
|
|
161
|
+
return new TextDecoder().decode(decrypted);
|
|
162
|
+
}
|
|
163
|
+
function serializeEvent(ev) {
|
|
164
|
+
return JSON.stringify([
|
|
165
|
+
0,
|
|
166
|
+
ev.pubkey,
|
|
167
|
+
ev.created_at,
|
|
168
|
+
ev.kind,
|
|
169
|
+
ev.tags,
|
|
170
|
+
ev.content
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
async function computeEventId(ev) {
|
|
174
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serializeEvent(ev)));
|
|
175
|
+
return bytesToHex(new Uint8Array(hash));
|
|
176
|
+
}
|
|
177
|
+
async function signEvent(ev, seckey) {
|
|
178
|
+
const id = await computeEventId(ev);
|
|
179
|
+
const sig = schnorr.sign(hexToBytes(id), seckey);
|
|
180
|
+
return {
|
|
181
|
+
...ev,
|
|
182
|
+
id,
|
|
183
|
+
sig: bytesToHex(sig)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function connectRelay(url, timeoutMs = 1e4) {
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
const ws = new WebSocket(url);
|
|
189
|
+
const timeout = setTimeout(() => {
|
|
190
|
+
ws.close();
|
|
191
|
+
reject(/* @__PURE__ */ new Error(`Connection to ${url} timed out`));
|
|
192
|
+
}, timeoutMs);
|
|
193
|
+
ws.addEventListener("open", () => {
|
|
194
|
+
clearTimeout(timeout);
|
|
195
|
+
resolve(ws);
|
|
196
|
+
});
|
|
197
|
+
ws.addEventListener("error", () => {
|
|
198
|
+
clearTimeout(timeout);
|
|
199
|
+
reject(/* @__PURE__ */ new Error(`Failed to connect to ${url}`));
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Try each relay in order until one connects successfully.
|
|
205
|
+
*/
|
|
206
|
+
async function connectToAnyRelay() {
|
|
207
|
+
const errors = [];
|
|
208
|
+
for (const relayUrl of NOSTR_RELAYS) try {
|
|
209
|
+
const ws = await connectRelay(relayUrl);
|
|
210
|
+
console.log(`[nostr] Connected to ${relayUrl}`);
|
|
211
|
+
return ws;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
214
|
+
errors.push(msg);
|
|
215
|
+
console.warn(`[nostr] ${relayUrl}: ${msg}`);
|
|
216
|
+
}
|
|
217
|
+
throw new Error(`Failed to connect to any Nostr relay: ${errors.join("; ")}`);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Wait for a single event matching a filter, with timeout.
|
|
221
|
+
*/
|
|
222
|
+
function waitForEvent(ws, subId, filter, timeoutMs = 15e3) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const timeout = setTimeout(() => {
|
|
225
|
+
ws.send(JSON.stringify(["CLOSE", subId]));
|
|
226
|
+
reject(/* @__PURE__ */ new Error("Timed out waiting for Nostr event"));
|
|
227
|
+
}, timeoutMs);
|
|
228
|
+
ws.send(JSON.stringify([
|
|
229
|
+
"REQ",
|
|
230
|
+
subId,
|
|
231
|
+
filter
|
|
232
|
+
]));
|
|
233
|
+
const onMessage = (event) => {
|
|
234
|
+
let msg;
|
|
235
|
+
try {
|
|
236
|
+
msg = JSON.parse(event.data);
|
|
237
|
+
} catch {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (msg[0] === "EVENT" && msg[1] === subId) {
|
|
241
|
+
clearTimeout(timeout);
|
|
242
|
+
ws.removeEventListener("message", onMessage);
|
|
243
|
+
ws.send(JSON.stringify(["CLOSE", subId]));
|
|
244
|
+
resolve(msg[2]);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
ws.addEventListener("message", onMessage);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const defaultIceServers = [{ urls: "stun:stun.l.google.com:19302" }, { urls: "stun:global.stun.twilio.com:3478" }];
|
|
251
|
+
/**
|
|
252
|
+
* Represents a Pulsar tunnel connection established via Nostr signaling.
|
|
253
|
+
*/
|
|
254
|
+
var NostrClientConnection = class {
|
|
255
|
+
keepalive;
|
|
256
|
+
pc;
|
|
257
|
+
_ws;
|
|
258
|
+
_seckey;
|
|
259
|
+
_serverPubkey;
|
|
260
|
+
constructor(keepalive, pc, _ws, _seckey, _serverPubkey) {
|
|
261
|
+
this.keepalive = keepalive;
|
|
262
|
+
this.pc = pc;
|
|
263
|
+
this._ws = _ws;
|
|
264
|
+
this._seckey = _seckey;
|
|
265
|
+
this._serverPubkey = _serverPubkey;
|
|
266
|
+
}
|
|
267
|
+
async close() {
|
|
268
|
+
try {
|
|
269
|
+
this.keepalive.close();
|
|
270
|
+
} catch {}
|
|
271
|
+
try {
|
|
272
|
+
this.pc.close();
|
|
273
|
+
} catch {}
|
|
274
|
+
try {
|
|
275
|
+
this._ws.close();
|
|
276
|
+
} catch {}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* Find a Pulsar server via discovery events.
|
|
281
|
+
*
|
|
282
|
+
* If `pubkeyPrefix` is given (4 hex chars), returns the first server
|
|
283
|
+
* whose pubkey starts with that prefix. Otherwise returns any server.
|
|
284
|
+
*/
|
|
285
|
+
async function findServer(ws, pubkeyPrefix, timeoutMs = 15e3) {
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
const timeout = setTimeout(() => {
|
|
288
|
+
ws.send(JSON.stringify(["CLOSE", "pulsar-discover"]));
|
|
289
|
+
reject(/* @__PURE__ */ new Error(pubkeyPrefix ? `No Pulsar server with tunnel code "pulsar${pubkeyPrefix}" found` : "No Pulsar server found on Nostr relay"));
|
|
290
|
+
}, timeoutMs);
|
|
291
|
+
const subId = "pulsar-discover";
|
|
292
|
+
ws.send(JSON.stringify([
|
|
293
|
+
"REQ",
|
|
294
|
+
subId,
|
|
295
|
+
{
|
|
296
|
+
kinds: [DISCOVERY_KIND],
|
|
297
|
+
"#d": [D_TAG_ID],
|
|
298
|
+
limit: 0
|
|
299
|
+
}
|
|
300
|
+
]));
|
|
301
|
+
const onMessage = (event) => {
|
|
302
|
+
let msg;
|
|
303
|
+
try {
|
|
304
|
+
msg = JSON.parse(event.data);
|
|
305
|
+
} catch {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (msg[0] === "EVENT" && msg[1] === subId) {
|
|
309
|
+
const ev = msg[2];
|
|
310
|
+
if (pubkeyPrefix) {
|
|
311
|
+
if (ev.pubkey.startsWith(pubkeyPrefix)) {
|
|
312
|
+
clearTimeout(timeout);
|
|
313
|
+
ws.removeEventListener("message", onMessage);
|
|
314
|
+
ws.send(JSON.stringify(["CLOSE", subId]));
|
|
315
|
+
resolve(ev.pubkey);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
clearTimeout(timeout);
|
|
319
|
+
ws.removeEventListener("message", onMessage);
|
|
320
|
+
ws.send(JSON.stringify(["CLOSE", subId]));
|
|
321
|
+
resolve(ev.pubkey);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
ws.addEventListener("message", onMessage);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Establish a Pulsar tunnel via Nostr relay signaling.
|
|
330
|
+
*
|
|
331
|
+
* 1. Connects to a Nostr relay (tries nostr.data.haus first,
|
|
332
|
+
* then kotukonostr.onrender.com)
|
|
333
|
+
* 2. Looks up the tunnel's discovery event (Kind 38000, d="pulsar-tunnel").
|
|
334
|
+
* If `tunnelCode` is given (e.g. "a3f2"), filters to the tunnel
|
|
335
|
+
* whose pubkey begins with those 4 hex characters.
|
|
336
|
+
* 3. Generates an ephemeral client keypair
|
|
337
|
+
* 4. Creates a WebRTC offer
|
|
338
|
+
* 5. Encrypts the offer and sends via a Kind 28000 ephemeral event
|
|
339
|
+
* 6. Waits for the encrypted answer
|
|
340
|
+
* 7. Sets the answer as remote description
|
|
341
|
+
* 8. Returns the connected PulsarClientConnection
|
|
342
|
+
*/
|
|
343
|
+
async function connectNostr(tunnelCode) {
|
|
344
|
+
const ws = await connectToAnyRelay();
|
|
345
|
+
const pubkeyPrefix = tunnelCode ? tunnelCode.replace(/^pulsar/, "").slice(0, 4) : void 0;
|
|
346
|
+
console.log("[nostr] Looking up Pulsar tunnel" + (pubkeyPrefix ? ` (code ${tunnelCode})` : "") + "...");
|
|
347
|
+
const serverPubkey = await findServer(ws, pubkeyPrefix);
|
|
348
|
+
console.log(`[nostr] Found server: ${serverPubkey.slice(0, 16)}...`);
|
|
349
|
+
const clientKeys = generateKeypair();
|
|
350
|
+
console.log(`[nostr] Client pubkey: ${clientKeys.pubkey.slice(0, 16)}...`);
|
|
351
|
+
const pc = new RTCPeerConnection({ iceServers: defaultIceServers });
|
|
352
|
+
const keepalive = pc.createDataChannel(KEEPALIVE_LABEL, { ordered: true });
|
|
353
|
+
keepalive.binaryType = "arraybuffer";
|
|
354
|
+
const offer = await pc.createOffer();
|
|
355
|
+
await pc.setLocalDescription(offer);
|
|
356
|
+
await new Promise((resolve) => {
|
|
357
|
+
const timeout = setTimeout(() => resolve(), 2e3);
|
|
358
|
+
const onStateChange = () => {
|
|
359
|
+
if (pc.iceGatheringState === "complete") {
|
|
360
|
+
clearTimeout(timeout);
|
|
361
|
+
resolve();
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const onCandidate = (event) => {
|
|
365
|
+
if (!event.candidate) {
|
|
366
|
+
clearTimeout(timeout);
|
|
367
|
+
resolve();
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
pc.addEventListener("icegatheringstatechange", onStateChange);
|
|
371
|
+
pc.addEventListener("icecandidate", onCandidate);
|
|
372
|
+
});
|
|
373
|
+
const localDesc = pc.localDescription;
|
|
374
|
+
if (!localDesc) throw new Error("Failed to create local offer");
|
|
375
|
+
const offerPayload = {
|
|
376
|
+
type: "offer",
|
|
377
|
+
sdp: localDesc.sdp
|
|
378
|
+
};
|
|
379
|
+
const encryptedOffer = await nip44Encrypt(JSON.stringify(offerPayload), clientKeys.seckey, serverPubkey);
|
|
380
|
+
const offerEvent = await signEvent({
|
|
381
|
+
pubkey: clientKeys.pubkey,
|
|
382
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
383
|
+
kind: SIGNALING_KIND,
|
|
384
|
+
tags: [["p", serverPubkey]],
|
|
385
|
+
content: encryptedOffer
|
|
386
|
+
}, hexToBytes(clientKeys.seckey));
|
|
387
|
+
const answerPromise = waitForEvent(ws, "pulsar-answer", {
|
|
388
|
+
kinds: [SIGNALING_KIND],
|
|
389
|
+
"#p": [clientKeys.pubkey],
|
|
390
|
+
limit: 1
|
|
391
|
+
}, 3e4);
|
|
392
|
+
ws.send(JSON.stringify(["EVENT", offerEvent]));
|
|
393
|
+
console.log("[nostr] Sent encrypted offer, waiting for answer...");
|
|
394
|
+
const answerPlaintext = await nip44Decrypt((await answerPromise).content, clientKeys.seckey, serverPubkey);
|
|
395
|
+
const answerPayload = JSON.parse(answerPlaintext);
|
|
396
|
+
if (answerPayload.type !== "answer" || !answerPayload.sdp) throw new Error("Invalid answer from server");
|
|
397
|
+
console.log("[nostr] Received answer, connecting WebRTC...");
|
|
398
|
+
await pc.setRemoteDescription({
|
|
399
|
+
type: "answer",
|
|
400
|
+
sdp: answerPayload.sdp
|
|
401
|
+
});
|
|
402
|
+
await new Promise((resolve, reject) => {
|
|
403
|
+
const timeout = setTimeout(() => {
|
|
404
|
+
reject(/* @__PURE__ */ new Error("WebRTC connection timed out after 30s"));
|
|
405
|
+
}, 3e4);
|
|
406
|
+
pc.addEventListener("connectionstatechange", () => {
|
|
407
|
+
if (pc.connectionState === "connected") {
|
|
408
|
+
clearTimeout(timeout);
|
|
409
|
+
resolve();
|
|
410
|
+
} else if (pc.connectionState === "failed" || pc.connectionState === "disconnected") {
|
|
411
|
+
clearTimeout(timeout);
|
|
412
|
+
reject(/* @__PURE__ */ new Error(`Connection failed: ${pc.connectionState}`));
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
console.log("[nostr] WebRTC connected!");
|
|
417
|
+
return new NostrClientConnection(keepalive, pc, ws, clientKeys.seckey, serverPubkey);
|
|
83
418
|
}
|
|
84
419
|
//#endregion
|
|
85
420
|
//#region lib/socket-channel.ts
|
|
@@ -272,14 +607,13 @@ var DataChannelSocket = class DataChannelSocket extends EventTarget {
|
|
|
272
607
|
* import { connectDirect, libcurlTransport } from '@abndnce/pulsar-client';
|
|
273
608
|
* import { libcurl } from 'libcurl.js';
|
|
274
609
|
*
|
|
275
|
-
* const tunnel = await connectDirect('
|
|
610
|
+
* const tunnel = await connectDirect('1.2.3.4', 1234);
|
|
276
611
|
* libcurl.transport = libcurlTransport(tunnel.pc);
|
|
277
612
|
* libcurl.set_websocket('wss://pulsar-tunnel.local/');
|
|
278
613
|
* ```
|
|
279
614
|
*
|
|
280
615
|
* libcurl.js calls the factory with URLs like:
|
|
281
616
|
* `wss://pulsar-tunnel.local/example.com:80`
|
|
282
|
-
* `wss://pulsar-tunnel.local/216.250.119.217:443`
|
|
283
617
|
*
|
|
284
618
|
* The factory parses `<hostname>:<port>` from the URL path, opens a
|
|
285
619
|
* Pulsar socket data channel, and returns a WebSocket-like adapter.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abndnce/pulsar-client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.10",
|
|
5
5
|
"homepage": "https://github.com/abndnce/pulsar",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"dist"
|
|
13
13
|
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@noble/curves": "^1.8.1"
|
|
16
|
+
},
|
|
14
17
|
"devDependencies": {
|
|
15
18
|
"tsdown": "^0.22.0",
|
|
16
19
|
"typescript": "^6.0.3"
|