wireweave 0.3.43 → 0.3.45

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/data.js +30 -1
  3. package/src/dm.js +39 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wireweave",
3
- "version": "0.3.43",
3
+ "version": "0.3.45",
4
4
  "description": "nostr + webrtc voice + data SDK. networking layer for 247420 projects.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/data.js CHANGED
@@ -16,9 +16,37 @@ const DEFAULT_ICE_SERVERS = [
16
16
  { urls: 'turn:openrelay.metered.ca:443?transport=tcp', username: 'openrelayproject', credential: 'openrelayproject' }
17
17
  ];
18
18
  let ICE_SERVERS = DEFAULT_ICE_SERVERS;
19
- export const setIceServers = (list) => { if (Array.isArray(list) && list.length) ICE_SERVERS = list; };
19
+ let iceServersOverridden = false;
20
+ export const setIceServers = (list) => { if (Array.isArray(list) && list.length) { ICE_SERVERS = list; iceServersOverridden = true; } };
20
21
  export const getIceServers = () => ICE_SERVERS.slice();
21
22
 
23
+ // The bundled TURN entries use the openrelayproject public demo credentials
24
+ // (metered.ca's shared, rate-limited, no-SLA relay). Fine for local dev/testing,
25
+ // but a real hosted deployment that never calls setIceServers() is silently
26
+ // depending on a third party's free-tier relay for every NAT-restricted peer —
27
+ // warn once, at first connect, so that's discoverable instead of a mystery
28
+ // "some players can never connect" bug report. Never fires on localhost/loopback
29
+ // dev servers, and never fires once the host has provided its own iceServers.
30
+ let defaultTurnWarned = false;
31
+ const isHostedDeployment = () => {
32
+ try {
33
+ const host = typeof location !== 'undefined' && location.hostname;
34
+ if (!host) return false;
35
+ if (host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host === '') return false;
36
+ if (host.endsWith('.local')) return false;
37
+ return true;
38
+ } catch { return false; }
39
+ };
40
+ const warnDefaultTurnCredentialsOnce = () => {
41
+ if (defaultTurnWarned || iceServersOverridden) return;
42
+ if (!isHostedDeployment()) return;
43
+ defaultTurnWarned = true;
44
+ console.warn('[wireweave] Using the bundled default TURN servers (openrelayproject shared public credentials) ' +
45
+ 'on a non-localhost deployment. These are a free, rate-limited, no-SLA relay meant for local dev/testing — ' +
46
+ 'NAT-restricted peers on this deployment may fail to connect or get throttled. Call setIceServers() with your ' +
47
+ 'own TURN credentials before connecting.');
48
+ };
49
+
22
50
  const PRESENCE_EXPIRY = 300000;
23
51
  const HEARTBEAT = 30000;
24
52
  const DISCONNECT_GRACE = 8000;
@@ -64,6 +92,7 @@ export class DataSession extends EventTarget {
64
92
  }
65
93
 
66
94
  async connect(room, { displayName = 'Guest' } = {}) {
95
+ warnDefaultTurnCredentialsOnce();
67
96
  if (!this.actor) this._initActor();
68
97
  if (!this.actor.getSnapshot().can({ type: 'connect' })) await this.disconnect();
69
98
  this.actor.send({ type: 'connect' });
package/src/dm.js CHANGED
@@ -1,41 +1,53 @@
1
+ const GIFT_WRAP_KIND = 1059;
2
+
1
3
  export class DM extends EventTarget {
2
4
  constructor({ relayPool, auth, nostrTools }) {
3
5
  super();
4
6
  if (!relayPool || !auth || !nostrTools) throw new Error('DM: deps required');
5
7
  if (!nostrTools.nip44) throw new Error('nostr-tools nip44 missing');
8
+ if (!nostrTools.nip59) throw new Error('nostr-tools nip59 missing');
6
9
  this.pool = relayPool;
7
10
  this.auth = auth;
8
11
  this.NT = nostrTools;
9
12
  this.subId = null;
10
13
  }
11
14
 
12
- _convKey(peerPubkey) {
13
- if (!this.auth.privkey) throw new Error('DM: privkey required (extension signing not supported for nip44)');
14
- return this.NT.nip44.v2.utils.getConversationKey
15
- ? this.NT.nip44.v2.utils.getConversationKey(this.auth.privkey, peerPubkey)
16
- : this.NT.nip44.getConversationKey(this.auth.privkey, peerPubkey);
17
- }
18
-
15
+ // NIP-17: send is a real kind:14 rumor, gift-wrapped (NIP-59) once per
16
+ // recipient and once more for ourselves (self-copy), each wrap signed by
17
+ // a fresh single-use random key so no relay observer can attribute the
18
+ // wrap's outer envelope to either the sender or the recipient — only the
19
+ // holder of the recipient's (or our own) private key can even see it's a
20
+ // DM at all, let alone read it or learn who sent it.
19
21
  async send(peerPubkey, plaintext) {
20
- const key = this._convKey(peerPubkey);
21
- const ciphertext = this.NT.nip44.v2.encrypt(plaintext, key);
22
- const signed = await this.auth.sign({
22
+ if (!this.auth.privkey) throw new Error('DM: privkey required (extension signing not supported for nip17)');
23
+ const rumor = {
23
24
  kind: 14,
24
25
  created_at: Math.floor(Date.now() / 1000),
25
26
  tags: [['p', peerPubkey]],
26
- content: ciphertext
27
- });
28
- this.pool.publish(signed);
29
- return signed;
27
+ content: plaintext
28
+ };
29
+ const wrapForPeer = this.NT.nip59.wrapEvent(rumor, this.auth.privkey, peerPubkey);
30
+ const wrapForSelf = this.NT.nip59.wrapEvent(rumor, this.auth.privkey, this.auth.pubkey);
31
+ this.pool.publish(wrapForPeer);
32
+ this.pool.publish(wrapForSelf);
33
+ return wrapForPeer;
34
+ }
35
+
36
+ // Unwrap a gift-wrap (kind 1059) down to its rumor and return the
37
+ // plaintext. Requires our own privkey — only the wrap's addressed
38
+ // recipient (the 'p' tag on the outer event) can unwrap it.
39
+ decrypt(wrap) {
40
+ if (!this.auth.privkey) throw new Error('DM: privkey required (extension signing not supported for nip17)');
41
+ const rumor = this.NT.nip59.unwrapEvent(wrap, this.auth.privkey);
42
+ return rumor.content;
30
43
  }
31
44
 
32
- decrypt(event) {
33
- const peer = event.pubkey === this.auth.pubkey
34
- ? (event.tags.find(t => t[0] === 'p')?.[1] || '')
35
- : event.pubkey;
36
- if (!peer) throw new Error('DM: cannot resolve peer');
37
- const key = this._convKey(peer);
38
- return this.NT.nip44.v2.decrypt(event.content, key);
45
+ // Same as decrypt() but returns the full unwrapped rumor (sender pubkey,
46
+ // created_at, tags) alongside the plaintext, for callers that need the
47
+ // real (rumor-level) sender identity rather than the wrap's throwaway key.
48
+ unwrap(wrap) {
49
+ if (!this.auth.privkey) throw new Error('DM: privkey required (extension signing not supported for nip17)');
50
+ return this.NT.nip59.unwrapEvent(wrap, this.auth.privkey);
39
51
  }
40
52
 
41
53
  subscribe(onMessage) {
@@ -43,14 +55,14 @@ export class DM extends EventTarget {
43
55
  const subId = 'dm-' + this.auth.pubkey.slice(0, 16);
44
56
  this.subId = subId;
45
57
  this.pool.subscribe(subId, [
46
- { kinds: [14], '#p': [this.auth.pubkey] },
47
- { kinds: [14], authors: [this.auth.pubkey] }
58
+ { kinds: [GIFT_WRAP_KIND], '#p': [this.auth.pubkey] }
48
59
  ], (event) => {
49
60
  try {
50
- const plaintext = this.decrypt(event);
51
- onMessage({ event, plaintext, peer: event.pubkey === this.auth.pubkey
52
- ? (event.tags.find(t => t[0] === 'p')?.[1] || '')
53
- : event.pubkey });
61
+ const rumor = this.unwrap(event);
62
+ const peer = rumor.pubkey === this.auth.pubkey
63
+ ? (rumor.tags.find(t => t[0] === 'p')?.[1] || '')
64
+ : rumor.pubkey;
65
+ onMessage({ event, rumor, plaintext: rumor.content, peer });
54
66
  } catch (e) {
55
67
  this._emit('error', { event, error: e.message });
56
68
  }