wireweave 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AnEntrypoint
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/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # wireweave
2
+
3
+ > probably emerging 🌀
4
+
5
+ nostr + webrtc voice SDK. the networking layer for 247420 projects.
6
+
7
+ ```
8
+ npm i wireweave
9
+ ```
10
+
11
+ ## what it is
12
+
13
+ serverless nostr relay pool + auth, designed for voice/chat apps that run on public relays with zero backend. extracted from [zellous](https://github.com/AnEntrypoint/zellous). buildless, esm-only, no framework lock-in.
14
+
15
+ ## what's in this release
16
+
17
+ **SDK-shaped (ES modules, zero window.*):**
18
+ - `RelayPool` — multi-relay connect, subscribe, publish, reconnect with backoff, event dedup
19
+ - `NostrAuth` — local-key gen, nsec/hex import, NIP-07 extension login, event signing
20
+
21
+ **`magicwand/legacy/` — full browser-globals port (17 files, ~94kb):**
22
+ nostr voice (perfect-negotiation, SFU hub election, camera), chat, channels, servers, roles, bans, media (blossom), pages, settings. copied verbatim from zellous. expects `state` + `XState` + `NostrTools` + `ui` on window. load order in `src/legacy/README.md`.
23
+
24
+ SDK-shaping the voice/chat/channels/servers into proper ES modules lands in 0.3+. legacy files are the honest "move everything" — they work today exactly as they did in zellous.
25
+
26
+ ## usage
27
+
28
+ ```js
29
+ import { RelayPool, NostrAuth } from 'wireweave';
30
+ import * as NostrTools from 'nostr-tools';
31
+
32
+ const auth = new NostrAuth({ nostrTools: NostrTools, storage: localStorage });
33
+ auth.generateKey();
34
+
35
+ const pool = new RelayPool({
36
+ relays: ['wss://relay.damus.io', 'wss://nos.lol'],
37
+ verifyEvent: NostrTools.verifyEvent
38
+ });
39
+ pool.connect();
40
+
41
+ pool.subscribe('mysub', [{ kinds: [1], limit: 10 }], (event) => {
42
+ console.log('got event', event.id);
43
+ });
44
+
45
+ const signed = await auth.sign({
46
+ kind: 1, created_at: Math.floor(Date.now()/1000), tags: [], content: 'gm'
47
+ });
48
+ pool.publish(signed);
49
+ ```
50
+
51
+ ## node usage
52
+
53
+ ```js
54
+ import WebSocket from 'ws';
55
+ const pool = new RelayPool({ relays: [...], WebSocketImpl: WebSocket });
56
+ ```
57
+
58
+ ## test
59
+
60
+ ```
61
+ npm test
62
+ ```
63
+
64
+ hits `wss://relay.damus.io` for a real publish → subscribe round-trip. no mocks.
65
+
66
+ ## peer deps
67
+
68
+ - `nostr-tools` ^2.7
69
+
70
+ ## license
71
+
72
+ MIT © AnEntrypoint — read the source.
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "wireweave",
3
+ "version": "0.1.1",
4
+ "description": "nostr + webrtc voice SDK. networking layer for 247420 projects.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "module": "./src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js",
10
+ "./relay-pool": "./src/relay-pool.js",
11
+ "./auth": "./src/auth.js",
12
+ "./debug": "./src/debug.js",
13
+ "./legacy": "./src/legacy/index.js",
14
+ "./legacy/*": "./src/legacy/*"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "test": "node test.js"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/AnEntrypoint/wireweave.git"
27
+ },
28
+ "keywords": [
29
+ "nostr",
30
+ "webrtc",
31
+ "voice",
32
+ "p2p",
33
+ "serverless",
34
+ "247420"
35
+ ],
36
+ "author": "AnEntrypoint",
37
+ "license": "MIT",
38
+ "bugs": {
39
+ "url": "https://github.com/AnEntrypoint/wireweave/issues"
40
+ },
41
+ "homepage": "https://github.com/AnEntrypoint/wireweave#readme",
42
+ "peerDependencies": {
43
+ "nostr-tools": "^2.7.0"
44
+ },
45
+ "devDependencies": {
46
+ "nostr-tools": "^2.7.0",
47
+ "ws": "^8.18.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }
package/src/auth.js ADDED
@@ -0,0 +1,97 @@
1
+ const b2hex = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
2
+ const hex2b = (hex) => {
3
+ const a = new Uint8Array(hex.length / 2);
4
+ for (let i = 0; i < a.length; i++) a[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
5
+ return a;
6
+ };
7
+
8
+ export class NostrAuth extends EventTarget {
9
+ constructor({ nostrTools, storage = null, extension = null } = {}) {
10
+ super();
11
+ if (!nostrTools) throw new Error('nostrTools required');
12
+ this.NT = nostrTools;
13
+ this.storage = storage;
14
+ this.extension = extension;
15
+ this.pubkey = '';
16
+ this.privkey = null;
17
+ this.profile = null;
18
+ }
19
+
20
+ loadFromStorage() {
21
+ if (!this.storage) return false;
22
+ const skHex = this.storage.getItem('zn_sk');
23
+ const pkHex = this.storage.getItem('zn_pk');
24
+ if (!skHex || !pkHex) return false;
25
+ try {
26
+ this.privkey = hex2b(skHex);
27
+ this.pubkey = pkHex;
28
+ this._emit('login', { pubkey: pkHex });
29
+ return true;
30
+ } catch { return false; }
31
+ }
32
+
33
+ generateKey() {
34
+ const sk = this.NT.generateSecretKey();
35
+ const pk = this.NT.getPublicKey(sk);
36
+ this._persist(sk, pk);
37
+ this._emit('login', { pubkey: pk });
38
+ return { pubkey: pk, privkey: sk };
39
+ }
40
+
41
+ importKey(input) {
42
+ const sk = input.startsWith('nsec')
43
+ ? (() => { const d = this.NT.nip19.decode(input); if (d.type !== 'nsec') throw new Error('not nsec'); return d.data; })()
44
+ : hex2b(input);
45
+ const pk = this.NT.getPublicKey(sk);
46
+ this._persist(sk, pk);
47
+ this._emit('login', { pubkey: pk });
48
+ return { pubkey: pk, privkey: sk };
49
+ }
50
+
51
+ async loginWithExtension() {
52
+ if (!this.extension) throw new Error('No extension provided');
53
+ const pk = await this.extension.getPublicKey();
54
+ this.pubkey = pk;
55
+ this.privkey = null;
56
+ this._emit('login', { pubkey: pk });
57
+ return pk;
58
+ }
59
+
60
+ async sign(eventTemplate) {
61
+ if (this.privkey) return this.NT.finalizeEvent(eventTemplate, this.privkey);
62
+ if (this.extension) return this.extension.signEvent(eventTemplate);
63
+ throw new Error('No signing key available');
64
+ }
65
+
66
+ logout() {
67
+ this.storage?.removeItem('zn_sk');
68
+ this.storage?.removeItem('zn_pk');
69
+ this.pubkey = '';
70
+ this.privkey = null;
71
+ this.profile = null;
72
+ this._emit('logout', {});
73
+ }
74
+
75
+ isLoggedIn() { return !!this.pubkey; }
76
+
77
+ npubShort(pubkey = this.pubkey) {
78
+ if (!pubkey) return '';
79
+ const npub = this.NT.nip19.npubEncode(pubkey);
80
+ return npub.slice(0, 8) + '...' + npub.slice(-4);
81
+ }
82
+
83
+ npubEncode(pubkey = this.pubkey) {
84
+ return pubkey ? this.NT.nip19.npubEncode(pubkey) : '';
85
+ }
86
+
87
+ _persist(sk, pk) {
88
+ this.privkey = sk;
89
+ this.pubkey = pk;
90
+ this.storage?.setItem('zn_sk', b2hex(sk));
91
+ this.storage?.setItem('zn_pk', pk);
92
+ }
93
+
94
+ _emit(type, detail) { this.dispatchEvent(new CustomEvent(type, { detail })); }
95
+ }
96
+
97
+ export const createAuth = (opts) => new NostrAuth(opts);
package/src/debug.js ADDED
@@ -0,0 +1,20 @@
1
+ const registry = new Map();
2
+
3
+ export const register = (key, obj) => {
4
+ registry.set(key, obj);
5
+ if (typeof window !== 'undefined') {
6
+ window.__magicwand = window.__magicwand || {};
7
+ window.__magicwand[key] = obj;
8
+ }
9
+ };
10
+
11
+ export const deregister = (key) => {
12
+ registry.delete(key);
13
+ if (typeof window !== 'undefined' && window.__magicwand) {
14
+ delete window.__magicwand[key];
15
+ }
16
+ };
17
+
18
+ export const get = (key) => registry.get(key);
19
+
20
+ export const all = () => Object.fromEntries(registry);
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { RelayPool, createRelayPool } from './relay-pool.js';
2
+ export { NostrAuth, createAuth } from './auth.js';
3
+ export * as debug from './debug.js';
@@ -0,0 +1,71 @@
1
+ # legacy/
2
+
3
+ browser-globals port of the full zellous networking + voice stack. these files are the original scripts copied verbatim from `zellous/docs/js/`. they assign to `window.*` (`window.nostrNet`, `window.nostrVoice`, `window.auth`, etc.) and expect co-resident globals (`state`, `XState`, `NostrTools`, `ui`, `auth`, `message`).
4
+
5
+ this is the honest "move everything" checkpoint. the SDK-shaped ES-module exports in `../` are the forward path — `relay-pool.js` + `auth.js` today, voice / chat / channels / servers to follow.
6
+
7
+ ## load order
8
+
9
+ scripts must load in this order (same order zellous uses):
10
+
11
+ ```
12
+ nostr-state-patch.js
13
+ nostr-fsm.js
14
+ nostr-network.js
15
+ nostr-auth.js
16
+ nostr-message.js
17
+ nostr-bans.js
18
+ nostr-roles.js
19
+ nostr-settings.js
20
+ nostr-voice.js
21
+ nostr-voice-rtc.js
22
+ nostr-voice-sfu.js
23
+ nostr-voice-camera.js
24
+ nostr-chat.js
25
+ nostr-channels.js
26
+ nostr-servers.js
27
+ nostr-media.js
28
+ nostr-pages.js
29
+ ```
30
+
31
+ ## prerequisites already on window
32
+
33
+ - `window.state`, `window.stateSignals`, `window.config` (zellous/state.js or equivalent)
34
+ - `window.XState` — `{ createMachine, createActor }` from xstate v5
35
+ - `window.NostrTools` — `nostr-tools` (generateSecretKey, getPublicKey, nip19, finalizeEvent, verifyEvent)
36
+ - preact signals for reactive state
37
+ - `window.ui` bindings (optional for headless)
38
+
39
+ ## files
40
+
41
+ | file | bytes | assigns |
42
+ |---|---|---|
43
+ | nostr-state-patch.js | 507 | state signal patches |
44
+ | nostr-fsm.js | 1018 | `voiceMachine`, `peerMachine` |
45
+ | nostr-network.js | 7595 | `nostrNet` relay pool |
46
+ | nostr-auth.js | 8501 | `auth` key mgmt |
47
+ | nostr-message.js | 529 | `message` system msg dispatch |
48
+ | nostr-bans.js | 2422 | `nostrBans` moderation |
49
+ | nostr-roles.js | 3077 | `serverRoles` |
50
+ | nostr-settings.js | 4231 | `serverSettings` encoder config |
51
+ | nostr-voice.js | 13458 | `nostrVoice` session |
52
+ | nostr-voice-rtc.js | 11636 | `nostrVoiceRtc` perfect-negotiation |
53
+ | nostr-voice-sfu.js | 4773 | `nostrVoiceSfu` hub election |
54
+ | nostr-voice-camera.js | 2355 | `nostrVoiceCamera` |
55
+ | nostr-chat.js | 6513 | `chat` kind-28 messaging |
56
+ | nostr-channels.js | 5369 | `channelManager` |
57
+ | nostr-servers.js | 10718 | `serverManager` communities |
58
+ | nostr-media.js | 3524 | `nostrMedia` blossom uploads |
59
+ | nostr-pages.js | 7909 | `serverPages` |
60
+
61
+ ## CDN load (via jsDelivr)
62
+
63
+ ```html
64
+ <script src="https://cdn.jsdelivr.net/npm/wireweave@latest/src/legacy/nostr-fsm.js"></script>
65
+ <script src="https://cdn.jsdelivr.net/npm/wireweave@latest/src/legacy/nostr-network.js"></script>
66
+ <!-- etc in order above -->
67
+ ```
68
+
69
+ ## status
70
+
71
+ probably emerging 🌀
@@ -0,0 +1,49 @@
1
+ export const LEGACY_LOAD_ORDER = [
2
+ 'nostr-state-patch.js',
3
+ 'nostr-fsm.js',
4
+ 'nostr-network.js',
5
+ 'nostr-auth.js',
6
+ 'nostr-message.js',
7
+ 'nostr-bans.js',
8
+ 'nostr-roles.js',
9
+ 'nostr-settings.js',
10
+ 'nostr-voice.js',
11
+ 'nostr-voice-rtc.js',
12
+ 'nostr-voice-sfu.js',
13
+ 'nostr-voice-camera.js',
14
+ 'nostr-chat.js',
15
+ 'nostr-channels.js',
16
+ 'nostr-servers.js',
17
+ 'nostr-media.js',
18
+ 'nostr-pages.js'
19
+ ];
20
+
21
+ export const loadLegacy = async ({ base = 'https://cdn.jsdelivr.net/npm/wireweave@latest/src/legacy/', prereqsReady = () => true } = {}) => {
22
+ if (!prereqsReady()) throw new Error('wireweave/legacy: prereqs not ready (state, XState, NostrTools must be on window)');
23
+ for (const f of LEGACY_LOAD_ORDER) {
24
+ await new Promise((resolve, reject) => {
25
+ const s = document.createElement('script');
26
+ s.src = base + f;
27
+ s.onload = resolve;
28
+ s.onerror = () => reject(new Error('failed: ' + f));
29
+ document.head.appendChild(s);
30
+ });
31
+ }
32
+ return {
33
+ net: window.nostrNet,
34
+ auth: window.auth,
35
+ voice: window.nostrVoice,
36
+ chat: window.chat,
37
+ channels: window.channelManager,
38
+ servers: window.serverManager,
39
+ message: window.message,
40
+ bans: window.nostrBans,
41
+ roles: window.serverRoles,
42
+ settings: window.serverSettings,
43
+ pages: window.serverPages,
44
+ media: window.nostrMedia,
45
+ voiceRtc: window.nostrVoiceRtc,
46
+ voiceSfu: window.nostrVoiceSfu,
47
+ voiceCamera: window.nostrVoiceCamera
48
+ };
49
+ };
@@ -0,0 +1,217 @@
1
+ const auth = {
2
+ get user() {
3
+ const pk = state.nostrPubkey;
4
+ if (!pk) return null;
5
+ const short = auth.npubShort(pk);
6
+ return { id: pk, username: short, displayName: state.nostrProfile?.name || short };
7
+ },
8
+
9
+ init() {
10
+ const skHex = localStorage.getItem('zn_sk');
11
+ const pkHex = localStorage.getItem('zn_pk');
12
+ if (!skHex || !pkHex) return false;
13
+ try {
14
+ state.nostrPrivkey = auth._hex2b(skHex);
15
+ state.nostrPubkey = pkHex;
16
+ const short = auth.npubShort(pkHex);
17
+ const nameEl = document.getElementById('userPanelName');
18
+ const tagEl = document.getElementById('userPanelTag');
19
+ const avatarEl = document.getElementById('userPanelAvatar');
20
+ if (nameEl) nameEl.textContent = short;
21
+ if (tagEl) tagEl.textContent = short;
22
+ if (avatarEl) { const n = avatarEl.childNodes[0]; if (n?.nodeType === 3) n.textContent = short[0].toUpperCase(); }
23
+ document.getElementById('userStatusDot')?.classList.add('online');
24
+ return true;
25
+ } catch { return false; }
26
+ },
27
+
28
+ generateKey() {
29
+ const NT = window.NostrTools;
30
+ const sk = NT.generateSecretKey();
31
+ const pk = NT.getPublicKey(sk);
32
+ localStorage.setItem('zn_sk', auth._b2hex(sk));
33
+ localStorage.setItem('zn_pk', pk);
34
+ state.nostrPrivkey = sk;
35
+ state.nostrPubkey = pk;
36
+ return { pubkey: pk, privkey: sk };
37
+ },
38
+
39
+ importKey(input) {
40
+ try {
41
+ const NT = window.NostrTools;
42
+ const sk = input.startsWith('nsec')
43
+ ? (() => { const d = NT.nip19.decode(input); if (d.type !== 'nsec') throw 0; return d.data; })()
44
+ : auth._hex2b(input);
45
+ const pk = NT.getPublicKey(sk);
46
+ localStorage.setItem('zn_sk', auth._b2hex(sk));
47
+ localStorage.setItem('zn_pk', pk);
48
+ state.nostrPrivkey = sk;
49
+ state.nostrPubkey = pk;
50
+ return true;
51
+ } catch { return false; }
52
+ },
53
+
54
+ async loginWithExtension() {
55
+ const pk = await window.nostr.getPublicKey();
56
+ state.nostrPubkey = pk;
57
+ state.nostrPrivkey = null;
58
+ return pk;
59
+ },
60
+
61
+ async sign(eventTemplate) {
62
+ if (state.nostrPrivkey) return window.NostrTools.finalizeEvent(eventTemplate, state.nostrPrivkey);
63
+ return window.nostr.signEvent(eventTemplate);
64
+ },
65
+
66
+ async setDisplayName(name) {
67
+ if (!state.nostrPubkey) throw new Error('Not logged in');
68
+ if (!name || typeof name !== 'string') throw new Error('Invalid display name');
69
+ const template = {
70
+ kind: 0,
71
+ created_at: Math.floor(Date.now() / 1000),
72
+ tags: [],
73
+ content: JSON.stringify({ name: name.trim(), ...(state.nostrProfile || {}) })
74
+ };
75
+ const signed = await auth.sign(template);
76
+ if (window.nostrNet && window.nostrNet.publish) {
77
+ await window.nostrNet.publish(signed);
78
+ }
79
+ state.nostrProfile = { ...(state.nostrProfile || {}), name: name.trim() };
80
+ const nameEl = document.getElementById('userPanelName');
81
+ const avatarEl = document.getElementById('userPanelAvatar');
82
+ if (nameEl) nameEl.textContent = state.nostrProfile.name;
83
+ if (avatarEl) { const n = avatarEl.childNodes[0]; if (n?.nodeType === 3) n.textContent = state.nostrProfile.name[0].toUpperCase(); }
84
+ if (window.chat) chat.updateProfile(state.nostrPubkey, state.nostrProfile);
85
+ },
86
+
87
+ logout() {
88
+ localStorage.removeItem('zn_sk');
89
+ localStorage.removeItem('zn_pk');
90
+ state.nostrPubkey = '';
91
+ state.nostrPrivkey = null;
92
+ state.nostrProfile = null;
93
+ const nameEl = document.getElementById('userPanelName');
94
+ const tagEl = document.getElementById('userPanelTag');
95
+ const avatarEl = document.getElementById('userPanelAvatar');
96
+ if (nameEl) nameEl.textContent = 'Not logged in';
97
+ if (tagEl) tagEl.textContent = 'Click to login';
98
+ if (avatarEl) { const n = avatarEl.childNodes[0]; if (n?.nodeType === 3) n.textContent = '?'; }
99
+ document.getElementById('userStatusDot')?.classList.remove('online');
100
+ if (window.nostrNet?.disconnect) nostrNet.disconnect();
101
+ },
102
+
103
+ getToken() { return state.nostrPubkey || null; },
104
+ isLoggedIn() { return !!state.nostrPubkey; },
105
+
106
+ npubShort(pubkey) {
107
+ if (!pubkey) return '';
108
+ const npub = window.NostrTools.nip19.npubEncode(pubkey);
109
+ return npub.slice(0, 8) + '...' + npub.slice(-4);
110
+ },
111
+
112
+ showModal() {
113
+ const modal = document.getElementById('authModal');
114
+ if (!modal) return;
115
+ modal.style.display = 'flex';
116
+ const cv = document.getElementById('nostrConnectView');
117
+ const lv = document.getElementById('nostrLoggedInView');
118
+ const loggedIn = auth.isLoggedIn();
119
+ if (cv) cv.style.display = loggedIn ? 'none' : 'flex';
120
+ if (lv) lv.style.display = loggedIn ? 'flex' : 'none';
121
+ if (loggedIn) {
122
+ const d = document.getElementById('nostrNpubDisplay');
123
+ if (d) d.textContent = auth.npubShort(state.nostrPubkey);
124
+ const inp = document.getElementById('displayNameInput');
125
+ if (inp) inp.value = state.nostrProfile?.name || '';
126
+ }
127
+ },
128
+
129
+ hideModal() {
130
+ const modal = document.getElementById('authModal');
131
+ if (modal) modal.style.display = 'none';
132
+ },
133
+
134
+ _afterLogin() {
135
+ const d = document.getElementById('nostrNpubDisplay');
136
+ if (d) d.textContent = auth.npubShort(state.nostrPubkey);
137
+ const cv = document.getElementById('nostrConnectView');
138
+ const lv = document.getElementById('nostrLoggedInView');
139
+ if (cv) cv.style.display = 'none';
140
+ if (lv) lv.style.display = 'flex';
141
+ const err = document.getElementById('nostrAuthError');
142
+ if (err) err.textContent = '';
143
+ const short = auth.npubShort(state.nostrPubkey);
144
+ const nameEl = document.getElementById('userPanelName');
145
+ const tagEl = document.getElementById('userPanelTag');
146
+ const avatarEl = document.getElementById('userPanelAvatar');
147
+ if (nameEl) nameEl.textContent = state.nostrProfile?.name || short;
148
+ if (tagEl) tagEl.textContent = short;
149
+ if (avatarEl) { const n = avatarEl.childNodes[0]; if (n?.nodeType === 3) n.textContent = (state.nostrProfile?.name || short)[0].toUpperCase(); }
150
+ document.getElementById('userStatusDot')?.classList.add('online');
151
+ document.dispatchEvent(new CustomEvent('nostr:login'));
152
+ setTimeout(() => auth.hideModal(), 1000);
153
+ },
154
+
155
+ _err(msg) {
156
+ const el = document.getElementById('nostrAuthError');
157
+ if (el) el.textContent = msg;
158
+ },
159
+
160
+ bindUI() {
161
+ const $ = id => document.getElementById(id);
162
+ const on = (id, fn) => { const el = $(id); if (el) el.addEventListener('click', fn); };
163
+
164
+ on('connectExtensionBtn', async () => {
165
+ try {
166
+ if (!window.nostr) throw new Error('No Nostr extension found');
167
+ await auth.loginWithExtension();
168
+ auth._afterLogin();
169
+ } catch (e) { auth._err(e.message); }
170
+ });
171
+
172
+ on('generateKeyBtn', () => {
173
+ try { auth.generateKey(); auth._afterLogin(); }
174
+ catch (e) { auth._err(e.message); }
175
+ });
176
+
177
+ on('importKeyBtn', () => {
178
+ const inp = $('importKeyInput');
179
+ const val = inp ? inp.value.trim() : '';
180
+ if (!val) { auth._err('Enter a key'); return; }
181
+ auth.importKey(val) ? auth._afterLogin() : auth._err('Invalid key');
182
+ });
183
+
184
+ on('copyNpubBtn', () => {
185
+ const pk = state.nostrPubkey;
186
+ if (pk) navigator.clipboard.writeText(window.NostrTools.nip19.npubEncode(pk)).catch(() => {});
187
+ });
188
+
189
+ on('saveDisplayNameBtn', async () => {
190
+ const inp = $('displayNameInput');
191
+ const val = inp ? inp.value.trim() : '';
192
+ if (!val) { auth._err('Enter a display name'); return; }
193
+ try {
194
+ await auth.setDisplayName(val);
195
+ if (inp) inp.value = val;
196
+ } catch (e) { auth._err(e.message); }
197
+ });
198
+
199
+ on('nostrLogoutBtn', () => { auth.logout(); auth.showModal(); });
200
+
201
+ const modal = document.getElementById('authModal');
202
+ if (modal) modal.addEventListener('click', (e) => { if (e.target === modal) auth.hideModal(); });
203
+
204
+ const avatarArea = document.querySelector('.user-avatar, .username-area, [data-action="show-auth"]');
205
+ if (avatarArea) avatarArea.addEventListener('click', () => auth.showModal());
206
+ },
207
+
208
+ _b2hex(bytes) { return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); },
209
+ _hex2b(hex) {
210
+ const a = new Uint8Array(hex.length / 2);
211
+ for (let i = 0; i < a.length; i++) a[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
212
+ return a;
213
+ }
214
+ };
215
+
216
+ window.__zellous.auth = auth;
217
+ window.auth = auth;
@@ -0,0 +1,59 @@
1
+ var nostrBans = {
2
+ _store: new Map(),
3
+ _sub: null,
4
+
5
+ isBanned(serverId, pubkey) {
6
+ var bans = nostrBans._store.get(serverId) || {};
7
+ return !!(bans.banned || []).includes(pubkey);
8
+ },
9
+
10
+ isTimedOut(serverId, pubkey) {
11
+ var bans = nostrBans._store.get(serverId) || {};
12
+ var timeout = bans.timeouts && bans.timeouts[pubkey];
13
+ if (!timeout) return false;
14
+ return timeout.expiry > Math.floor(Date.now() / 1000);
15
+ },
16
+
17
+ subscribe(serverId) {
18
+ if (nostrBans._sub) { nostrNet.unsubscribe(nostrBans._sub); nostrBans._sub = null; }
19
+ if (!serverId) return;
20
+ var creator = serverId.split(':')[0];
21
+ if (!creator) return;
22
+ nostrBans._sub = 'bans-' + serverId;
23
+ nostrNet.subscribe(nostrBans._sub,
24
+ [{ kinds: [30078], authors: [creator], '#d': ['zellous-ban:' + serverId, 'zellous-timeout:' + serverId] }],
25
+ function(event) {
26
+ if (event.pubkey !== creator) return;
27
+ try {
28
+ var dTag = event.tags.find(function(t) { return t[0] === 'd'; });
29
+ if (!dTag || !dTag[1]) return;
30
+ var prefix = dTag[1].split(':')[0];
31
+ var banData = nostrBans._store.get(serverId) || { banned: [], timeouts: {} };
32
+ if (prefix === 'zellous-ban') {
33
+ var pubkey = dTag[1].split(':')[2];
34
+ if (pubkey && !banData.banned.includes(pubkey)) banData.banned.push(pubkey);
35
+ } else if (prefix === 'zellous-timeout') {
36
+ var pubkey = dTag[1].split(':')[2];
37
+ if (pubkey) {
38
+ var data = JSON.parse(event.content);
39
+ if (data.expiry > Math.floor(Date.now() / 1000)) {
40
+ if (!banData.timeouts) banData.timeouts = {};
41
+ banData.timeouts[pubkey] = { expiry: data.expiry };
42
+ } else if (banData.timeouts && banData.timeouts[pubkey]) {
43
+ delete banData.timeouts[pubkey];
44
+ }
45
+ }
46
+ }
47
+ nostrBans._store.set(serverId, banData);
48
+ if (window.__debug && window.__debug.bans) window.__debug.bans = Object.fromEntries(nostrBans._store);
49
+ } catch(e) {}
50
+ },
51
+ function() {}
52
+ );
53
+ }
54
+ };
55
+
56
+ window.__zellous.bans = nostrBans;
57
+ window.nostrBans = nostrBans;
58
+ if (!window.__debug) window.__debug = {};
59
+ Object.defineProperty(window.__debug, 'bans', { get: function() { return Object.fromEntries(nostrBans._store); }, configurable: true });