wireweave 0.3.19 → 0.3.21

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/README.md CHANGED
@@ -27,10 +27,26 @@ ww.auth.loadFromStorage() || ww.auth.generateKey();
27
27
  ww.servers.init();
28
28
  ```
29
29
 
30
- `ww` exposes: `pool`, `auth`, `fsm`, `message`, `bans`, `roles`, `settings`, `pages`, `media`, `channels`, `servers`, `chat`, `voice` (lazy via `ensureVoice()`), `setCurrentChannel()`.
30
+ `ww` exposes: `pool`, `auth`, `fsm`, `message`, `bans`, `roles`, `settings`, `pages`, `media`, `channels`, `servers`, `chat`, `voice` (lazy via `ensureVoice()`), `dm` (lazy via `ensureDM()`), `setCurrentChannel()`, `currentChannelId`, `currentServerId`.
31
31
 
32
32
  every submodule is an `EventTarget`. subscribe with `addEventListener('event', ...)`.
33
33
 
34
+ `message` is a standalone in-memory `MessageBus` (bounded ring + typed handler dispatch). It is provided for apps that want a local message store; no other wireweave module depends on it, so ignore it if you don't need it.
35
+
36
+ ### node / non-browser environments
37
+
38
+ `createWireweave` and `NostrAuth`/`Servers` need a `storage` adapter outside the browser — pass `{ getItem, setItem, removeItem }`. Without it `createWireweave` throws `wireweave: storage required` up front (in the browser it defaults to `localStorage`). On-relay `d`-tags use a frozen `zellous-` prefix and storage keys use `zn_*`; these are published wire/storage contracts kept stable across the rename to wireweave — do not change them without a migration path.
39
+
40
+ ## direct messages (encrypted)
41
+
42
+ ```js
43
+ const dm = ww.ensureDM(); // lazy: needs nostr-tools built with nip44
44
+ dm.subscribe(({ peer, plaintext }) => console.log(peer, plaintext));
45
+ await dm.send(peerPubkey, 'hello');
46
+ ```
47
+
48
+ **Caveat:** nip44 encryption derives a conversation key from your **private key**, so DM requires a privkey-backed signer (`generateKey`/`importKey`). It does **not** work with extension signing (NIP-07), which never exposes the privkey — `dm.send` throws `DM: privkey required` in that case.
49
+
34
50
  ## modules
35
51
 
36
52
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wireweave",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
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/bans.js CHANGED
@@ -69,8 +69,8 @@ export class Bans extends EventTarget {
69
69
  const data = this.store.get(serverId) || { banned: [], timeouts: {} };
70
70
  if (parsed.ns === 'ban' && pubkey && !data.banned.includes(pubkey)) data.banned.push(pubkey);
71
71
  else if (parsed.ns === 'timeout' && pubkey) {
72
- const parsed = JSON.parse(event.content);
73
- if (parsed.expiry > Math.floor(Date.now() / 1000)) (data.timeouts = data.timeouts || {})[pubkey] = { expiry: parsed.expiry };
72
+ const body = JSON.parse(event.content);
73
+ if (body.expiry > Math.floor(Date.now() / 1000)) (data.timeouts = data.timeouts || {})[pubkey] = { expiry: body.expiry };
74
74
  else if (data.timeouts?.[pubkey]) delete data.timeouts[pubkey];
75
75
  }
76
76
  this.store.set(serverId, data);
package/src/channels.js CHANGED
@@ -72,6 +72,14 @@ export class Channels extends EventTarget {
72
72
  await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
73
73
  }
74
74
 
75
+ // Per-channel metadata patch — used for topic, voiceMode, and any future
76
+ // server-published channel-scoped configuration. Owner-only.
77
+ async update(id, patch) {
78
+ if (!patch || typeof patch !== 'object') return;
79
+ this.channels = this.channels.map(c => c.id === id ? { ...c, ...patch } : c);
80
+ await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
81
+ }
82
+
75
83
  async remove(id) {
76
84
  this.channels = this.channels.filter(c => c.id !== id);
77
85
  await this._publish(); this._emit('updated', { channels: this.channels, categories: this.categories });
package/src/data.js CHANGED
@@ -72,6 +72,7 @@ export class DataSession extends EventTarget {
72
72
  this._stopHeartbeat();
73
73
  for (const pk of Array.from(this.peers.keys())) this._closePeer(pk);
74
74
  this.peers.clear();
75
+ for (const pk of Object.keys(this.retrySchedule)) this._cancelReconnect(pk);
75
76
  if (this.roomId) {
76
77
  this.pool.unsubscribe('data-presence-' + this.roomId);
77
78
  this.pool.unsubscribe('data-signals-' + this.roomId);
package/src/dtag.js CHANGED
@@ -1,13 +1,19 @@
1
1
  const NAMESPACES = new Set(['ban', 'timeout', 'kick', 'page', 'channels', 'roles', 'settings']);
2
2
 
3
+ // Frozen on-relay wire prefix. The brand is now wireweave but deployed events
4
+ // carry 'zellous-' d-tags, so this is a published-contract constant — do not
5
+ // rename it without a migration path. parseDtag derives its slice offset from
6
+ // PREFIX.length so the literal and the offset can never drift apart.
7
+ const PREFIX = 'zellous-';
8
+
3
9
  export const dtag = (ns, ...parts) => {
4
10
  if (!NAMESPACES.has(ns)) throw new Error('dtag: unknown namespace ' + ns);
5
- return ['zellous-' + ns, ...parts].join(':');
11
+ return [PREFIX + ns, ...parts].join(':');
6
12
  };
7
13
 
8
14
  export const parseDtag = (s) => {
9
- if (typeof s !== 'string' || !s.startsWith('zellous-')) return null;
10
- const parts = s.slice(8).split(':');
15
+ if (typeof s !== 'string' || !s.startsWith(PREFIX)) return null;
16
+ const parts = s.slice(PREFIX.length).split(':');
11
17
  const ns = parts.shift();
12
18
  if (!NAMESPACES.has(ns)) return null;
13
19
  return { ns, parts };
package/src/voice.js CHANGED
@@ -103,6 +103,7 @@ export class VoiceSession extends EventTarget {
103
103
  this._sfuStop();
104
104
  for (const pk of Array.from(this.peers.keys())) this._closePeer(pk);
105
105
  this.peers.clear();
106
+ for (const pk of Object.keys(this.retrySchedule)) this._cancelReconnect(pk);
106
107
  if (this._activityTimer) { clearInterval(this._activityTimer); this._activityTimer = null; }
107
108
  if (this._activeAnalyzers) { for (const k of Array.from(this._activeAnalyzers.keys())) this._detachAnalyzer(k); }
108
109
  if (this._outboundRec) { try { this._outboundRec.rec.stop(); } catch {} this._outboundRec = null; }
@@ -453,14 +454,17 @@ export class VoiceSession extends EventTarget {
453
454
  }
454
455
  if (data.action === 'leave') {
455
456
  this.participants.delete(shortId);
457
+ this.sfu.pubkeyByShortId?.delete(shortId);
456
458
  this._closePeer(event.pubkey);
457
459
  if (this.sfu.hub === event.pubkey) this._sfuOnHubLost();
458
460
  }
459
461
  else if (!this.participants.has(shortId)) {
460
462
  this.participants.set(shortId, { identity: data.name || event.pubkey.slice(0, 8), isSpeaking: false, isMuted: false, isLocal: false, hasVideo: false, connectionQuality: 'connecting' });
463
+ if (this.sfu.pubkeyByShortId) this.sfu.pubkeyByShortId.set(shortId, event.pubkey);
461
464
  this._sfuMaybeElect();
462
465
  this._maybeConnect(event.pubkey);
463
466
  } else {
467
+ if (this.sfu.pubkeyByShortId && !this.sfu.pubkeyByShortId.has(shortId)) this.sfu.pubkeyByShortId.set(shortId, event.pubkey);
464
468
  this._sfuMaybeElect();
465
469
  if (this._sfuShouldHaveConnectionTo(event.pubkey) && !this.peers.has(event.pubkey)) this._maybeConnect(event.pubkey);
466
470
  }
@@ -690,6 +694,7 @@ export class VoiceSession extends EventTarget {
690
694
  this.sfu.lastSwitch = 0;
691
695
  this.sfu.capacityMatrix = new Map();
692
696
  this.sfu.reflexiveByPeer = new Map();
697
+ this.sfu.pubkeyByShortId = new Map();
693
698
  this._sfuStopStats();
694
699
  this.sfu.statsInterval = setInterval(() => this._sfuPoll(), 2500);
695
700
  }
@@ -818,6 +823,7 @@ export class VoiceSession extends EventTarget {
818
823
  this.sfu.hub = top.pubkey;
819
824
  this.sfu.warmBackup = (runnerUp && runnerUp.pubkey !== top.pubkey) ? runnerUp.pubkey : null;
820
825
  this.sfu.lastSwitch = Date.now();
826
+ try { this.sfu.actor?.send({ type: 'elect' }); } catch {}
821
827
  this.sfu.actor?.send({ type: 'elected' });
822
828
  this._emit('hub-changed', { hub: top.pubkey, previous: previousHub, score: top.score, uplink: top.uplink });
823
829
 
@@ -832,11 +838,13 @@ export class VoiceSession extends EventTarget {
832
838
  // - Else: keep PCs only to hub + warm backup.
833
839
  _sfuApplyTopology(hubPk) {
834
840
  if (hubPk === this.auth.pubkey) {
835
- for (const peerPk of this.participants.keys()) {
836
- // participants keys are shortIds; iterate via known peer pubkeys from rttMatrix instead.
837
- }
838
- // Use capacityMatrix + presence-known pubkeys as the participant list.
841
+ // Hub connects to every known participant. Build the pubkey set from
842
+ // every available source pubkeyByShortId (presence-derived), peers,
843
+ // capacityMatrix, rttMatrix — because at the moment of the very first
844
+ // election the matrices may not be populated yet.
839
845
  const known = new Set();
846
+ if (this.sfu.pubkeyByShortId) for (const pk of this.sfu.pubkeyByShortId.values()) known.add(pk);
847
+ for (const pk of this.peers.keys()) known.add(pk);
840
848
  for (const pk of this.sfu.capacityMatrix?.keys() || []) known.add(pk);
841
849
  for (const pk of this.sfu.rttMatrix.keys()) known.add(pk);
842
850
  for (const pk of known) {
@@ -908,6 +916,7 @@ export class VoiceSession extends EventTarget {
908
916
  this.sfu.hub = promote;
909
917
  this.sfu.warmBackup = null;
910
918
  this.sfu.lastSwitch = Date.now();
919
+ try { this.sfu.actor?.send({ type: 'elect' }); } catch {}
911
920
  this.sfu.actor?.send({ type: 'elected' });
912
921
  this._emit('hub-changed', { hub: promote, previous: lost, reason: 'failover' });
913
922
  if (promote === this.auth.pubkey) this._sfuBecomeHub();
package/src/wireweave.js CHANGED
@@ -11,6 +11,7 @@ import { createRoles } from './roles.js';
11
11
  import { createSettings } from './settings.js';
12
12
  import { createMedia } from './media.js';
13
13
  import { createPages } from './pages.js';
14
+ import { createDM } from './dm.js';
14
15
  import { register } from './debug.js';
15
16
 
16
17
  export const createWireweave = ({
@@ -76,10 +77,21 @@ export const createWireweave = ({
76
77
 
77
78
  const setCurrentChannel = (id) => { currentChannelId = id; if (id) chat.loadHistory(id); };
78
79
 
80
+ // DM is lazy: nip44 encryption requires a privkey-backed signer (not extension)
81
+ // and nostr-tools built with nip44. Constructing it eagerly would throw for
82
+ // builds without nip44, so we defer to first use — mirrors ensureVoice.
83
+ let dm = null;
84
+ const ensureDM = () => {
85
+ if (!dm) dm = createDM({ relayPool: pool, auth, nostrTools });
86
+ return dm;
87
+ };
88
+
79
89
  const api = {
80
90
  pool, auth, fsm, message, bans, roles, settings, pages, media, channels, servers, chat,
81
91
  get voice() { return voice; },
82
92
  ensureVoice,
93
+ get dm() { return dm; },
94
+ ensureDM,
83
95
  setCurrentChannel,
84
96
  get currentChannelId() { return currentChannelId; },
85
97
  get currentServerId() { return servers.currentServerId; }