wireweave 0.1.2 → 0.1.4

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/src/servers.js ADDED
@@ -0,0 +1,112 @@
1
+ export class Servers extends EventTarget {
2
+ constructor({ relayPool, auth, storage, onSwitch = null }) {
3
+ super();
4
+ if (!relayPool || !auth || !storage) throw new Error('Servers: deps required');
5
+ this.pool = relayPool; this.auth = auth; this.storage = storage; this.onSwitch = onSwitch;
6
+ this.servers = []; this.currentServerId = null;
7
+ }
8
+
9
+ load() {
10
+ try { this.servers = JSON.parse(this.storage.getItem('zn_servers') || '[]'); } catch { this.servers = []; }
11
+ if (this.auth.pubkey) {
12
+ this.pool.subscribe('my-servers', [{ kinds: [34550], authors: [this.auth.pubkey] }], (ev) => this._handleEvent(ev));
13
+ }
14
+ try {
15
+ const joined = JSON.parse(this.storage.getItem('zn_joined_servers') || '[]');
16
+ joined.forEach(sid => { if (!this.servers.find(s => s.id === sid)) this.servers = [...this.servers, { id: sid, name: sid.slice(0, 8), iconColor: '#5865F2' }]; });
17
+ const unresolved = joined.filter(sid => { const p = sid.split(':'); return p.length === 2 && p[0] !== this.auth.pubkey; });
18
+ const byAuthor = {};
19
+ unresolved.forEach(sid => { const [author, dTag] = sid.split(':'); (byAuthor[author] = byAuthor[author] || []).push(dTag); });
20
+ Object.keys(byAuthor).forEach(author => {
21
+ this.pool.subscribe('joined-server-' + author.slice(0, 8),
22
+ [{ kinds: [34550], authors: [author], '#d': byAuthor[author] }],
23
+ (ev) => this._handleEvent(ev));
24
+ });
25
+ } catch {}
26
+ this._emit('updated', { servers: this.servers });
27
+ }
28
+
29
+ _handleEvent(event) {
30
+ const nameTag = event.tags.find(t => t[0] === 'name');
31
+ const colorTag = event.tags.find(t => t[0] === 'color');
32
+ const dTag = event.tags.find(t => t[0] === 'd');
33
+ if (!dTag) return;
34
+ const serverId = event.pubkey + ':' + dTag[1];
35
+ const name = nameTag ? nameTag[1] : serverId.slice(0, 8);
36
+ const iconColor = colorTag ? colorTag[1] : '#5865F2';
37
+ const existing = this.servers.find(s => s.id === serverId);
38
+ if (existing) { existing.name = name; existing.iconColor = iconColor; existing.ownerId = event.pubkey; this.servers = [...this.servers]; }
39
+ else this.servers = [...this.servers, { id: serverId, name, iconColor, ownerId: event.pubkey }];
40
+ this._persist();
41
+ this._emit('updated', { servers: this.servers });
42
+ }
43
+
44
+ async rename(serverId, name, iconColor = '#5865F2') {
45
+ if (!serverId?.startsWith(this.auth.pubkey + ':')) throw new Error('Only owner can rename');
46
+ const dTag = serverId.split(':')[1];
47
+ const signed = await this.auth.sign({ kind: 34550, created_at: Math.floor(Date.now() / 1000), tags: [['d', dTag], ['name', name], ['color', iconColor]], content: '' });
48
+ this.pool.publish(signed);
49
+ const s = this.servers.find(x => x.id === serverId);
50
+ if (s) { s.name = name; s.iconColor = iconColor; this.servers = [...this.servers]; this._persist(); this._emit('updated', { servers: this.servers }); }
51
+ }
52
+
53
+ async create(name, iconColor = '#5865F2') {
54
+ const dTag = Math.random().toString(36).slice(2, 10);
55
+ const serverId = this.auth.pubkey + ':' + dTag;
56
+ const signed = await this.auth.sign({ kind: 34550, created_at: Math.floor(Date.now() / 1000), tags: [['d', dTag], ['name', name], ['color', iconColor]], content: '' });
57
+ this.pool.publish(signed);
58
+ this.servers = [...this.servers, { id: serverId, name, iconColor, ownerId: this.auth.pubkey }];
59
+ this._persist();
60
+ await this.switchTo(serverId);
61
+ }
62
+
63
+ async join(serverId) {
64
+ try {
65
+ const joined = JSON.parse(this.storage.getItem('zn_joined_servers') || '[]');
66
+ if (!joined.includes(serverId)) { joined.push(serverId); this.storage.setItem('zn_joined_servers', JSON.stringify(joined)); }
67
+ } catch {}
68
+ if (!this.servers.find(s => s.id === serverId)) { this.servers = [...this.servers, { id: serverId, name: serverId.slice(0, 8), iconColor: '#5865F2' }]; this._persist(); }
69
+ await this.switchTo(serverId);
70
+ }
71
+
72
+ async delete(serverId) {
73
+ this.servers = this.servers.filter(s => s.id !== serverId);
74
+ this._persist();
75
+ try {
76
+ const joined = JSON.parse(this.storage.getItem('zn_joined_servers') || '[]');
77
+ this.storage.setItem('zn_joined_servers', JSON.stringify(joined.filter(id => id !== serverId)));
78
+ } catch {}
79
+ if (this.currentServerId === serverId) await this.switchTo(null);
80
+ else this._emit('updated', { servers: this.servers });
81
+ }
82
+
83
+ async leave(serverId) { return this.delete(serverId); }
84
+
85
+ async switchTo(serverId) {
86
+ this.currentServerId = serverId;
87
+ if (serverId) this.storage.setItem('zn_lastServer', serverId); else this.storage.removeItem('zn_lastServer');
88
+ this._emit('switched', { serverId });
89
+ await this.onSwitch?.(serverId);
90
+ }
91
+
92
+ init() {
93
+ this.load();
94
+ const last = this.storage.getItem('zn_lastServer');
95
+ if (last && this.servers.find(s => s.id === last)) this.switchTo(last);
96
+ else if (this.servers.length) this.switchTo(this.servers[0].id);
97
+ }
98
+
99
+ getOrder() { try { return JSON.parse(this.storage.getItem('zn_serverOrder') || '[]'); } catch { return []; } }
100
+ saveOrder(ids) { this.storage.setItem('zn_serverOrder', JSON.stringify(ids)); this._emit('updated', { servers: this.servers }); }
101
+ sorted() {
102
+ const order = this.getOrder();
103
+ if (!order.length) return this.servers;
104
+ const idx = {}; order.forEach((id, i) => idx[id] = i);
105
+ return this.servers.slice().sort((a, b) => (idx[a.id] ?? Infinity) - (idx[b.id] ?? Infinity));
106
+ }
107
+
108
+ _persist() { this.storage.setItem('zn_servers', JSON.stringify(this.servers)); }
109
+ _emit(t, d) { this.dispatchEvent(new CustomEvent(t, { detail: d })); }
110
+ }
111
+
112
+ export const createServers = (opts) => new Servers(opts);
@@ -0,0 +1,72 @@
1
+ const VALID_BITRATES = [8000, 16000, 24000, 48000, 96000];
2
+ const clampBitrate = (v) => VALID_BITRATES.reduce((prev, cur) => Math.abs(cur - v) < Math.abs(prev - v) ? cur : prev);
3
+
4
+ export class Settings extends EventTarget {
5
+ constructor({ relayPool, auth, roles }) {
6
+ super();
7
+ if (!relayPool || !auth || !roles) throw new Error('Settings: deps required');
8
+ this.pool = relayPool; this.auth = auth; this.roles = roles;
9
+ this.store = new Map();
10
+ this.sub = null;
11
+ }
12
+
13
+ getBitrate(serverId) { return this.store.get(serverId)?.opusBitrate || 24000; }
14
+
15
+ async setBitrate(serverId, bitrate) {
16
+ if (!this.roles.isOwner(serverId) && !this.roles.isAdmin(serverId)) throw new Error('Insufficient permissions');
17
+ const clamped = clampBitrate(Number(bitrate));
18
+ const existing = this.store.get(serverId) || {};
19
+ const next = { ...existing, opusBitrate: clamped };
20
+ this.store.set(serverId, next);
21
+ const signed = await this.auth.sign({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', 'zellous-settings:' + serverId]], content: JSON.stringify(next) });
22
+ this.pool.publish(signed);
23
+ this.dispatchEvent(new CustomEvent('updated', { detail: { serverId, next } }));
24
+ return clamped;
25
+ }
26
+
27
+ getEmbedAllowlist(serverId) {
28
+ const s = this.store.get(serverId);
29
+ if (!s?.embedAllowlist) return [];
30
+ return typeof s.embedAllowlist === 'string' ? s.embedAllowlist.split(',').map(d => d.trim()).filter(Boolean) : s.embedAllowlist;
31
+ }
32
+
33
+ async setEmbedAllowlist(serverId, domainsStr) {
34
+ if (!this.roles.isOwner(serverId) && !this.roles.isAdmin(serverId)) throw new Error('Insufficient permissions');
35
+ const domains = domainsStr.split(',').map(d => d.trim()).filter(Boolean);
36
+ const existing = this.store.get(serverId) || {};
37
+ const next = { ...existing, embedAllowlist: domains };
38
+ this.store.set(serverId, next);
39
+ const signed = await this.auth.sign({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', 'zellous-settings:' + serverId]], content: JSON.stringify(next) });
40
+ this.pool.publish(signed);
41
+ this.dispatchEvent(new CustomEvent('updated', { detail: { serverId, next } }));
42
+ return domains;
43
+ }
44
+
45
+ isOriginAllowed(serverId, origin) {
46
+ if (!origin) return false;
47
+ const allowlist = this.getEmbedAllowlist(serverId);
48
+ if (!allowlist.length) return true;
49
+ let url; try { url = new URL(origin); } catch { return false; }
50
+ return allowlist.some(p => {
51
+ if (p === '*') return true;
52
+ if (p.startsWith('*.')) { const s = p.slice(2); return url.hostname === s || url.hostname.endsWith('.' + s); }
53
+ return url.hostname === p || url.hostname === p.replace(/^https?:\/\//, '');
54
+ });
55
+ }
56
+
57
+ subscribe(serverId) {
58
+ if (this.sub) { this.pool.unsubscribe(this.sub); this.sub = null; }
59
+ if (!serverId) return;
60
+ const creator = serverId.split(':')[0];
61
+ if (!creator) return;
62
+ this.sub = 'settings-' + serverId;
63
+ this.pool.subscribe(this.sub,
64
+ [{ kinds: [30078], authors: [creator], '#d': ['zellous-settings:' + serverId] }],
65
+ (event) => {
66
+ if (event.pubkey !== creator) return;
67
+ try { this.store.set(serverId, JSON.parse(event.content)); this.dispatchEvent(new CustomEvent('updated', { detail: { serverId, next: this.store.get(serverId) } })); } catch {}
68
+ });
69
+ }
70
+ }
71
+
72
+ export const createSettings = (opts) => new Settings(opts);
@@ -0,0 +1,89 @@
1
+ import { createRelayPool } from './relay-pool.js';
2
+ import { createAuth } from './auth.js';
3
+ import { createFSM } from './fsm.js';
4
+ import { createVoiceSession } from './voice.js';
5
+ import { createChat } from './chat.js';
6
+ import { createChannels } from './channels.js';
7
+ import { createServers } from './servers.js';
8
+ import { createMessageBus } from './message.js';
9
+ import { createBans } from './bans.js';
10
+ import { createRoles } from './roles.js';
11
+ import { createSettings } from './settings.js';
12
+ import { createMedia } from './media.js';
13
+ import { createPages } from './pages.js';
14
+ import { register } from './debug.js';
15
+
16
+ export const createWireweave = ({
17
+ nostrTools,
18
+ xstate,
19
+ storage = (typeof localStorage !== 'undefined' ? localStorage : null),
20
+ extension = (typeof window !== 'undefined' ? window.nostr : null),
21
+ relays = ['wss://relay.damus.io', 'wss://relay.primal.net', 'wss://nos.lol', 'wss://relay.snort.social'],
22
+ mediaDevices = (typeof navigator !== 'undefined' ? navigator.mediaDevices : null),
23
+ WebSocketImpl = (typeof WebSocket !== 'undefined' ? WebSocket : null)
24
+ } = {}) => {
25
+ if (!nostrTools) throw new Error('wireweave: nostrTools required');
26
+ if (!xstate) throw new Error('wireweave: xstate required');
27
+
28
+ const fsm = createFSM(xstate);
29
+ const pool = createRelayPool({ relays, verifyEvent: nostrTools.verifyEvent, WebSocketImpl });
30
+ const auth = createAuth({ nostrTools, storage, extension });
31
+ const message = createMessageBus();
32
+ const roles = createRoles({ relayPool: pool, auth });
33
+ const bans = createBans({ relayPool: pool, auth, roles });
34
+ const settings = createSettings({ relayPool: pool, auth, roles });
35
+ const pages = createPages({ relayPool: pool, auth, roles });
36
+ const media = createMedia({ relayPool: pool, auth });
37
+ const channels = createChannels({ relayPool: pool, auth });
38
+
39
+ let currentChannelId = null;
40
+ const chat = createChat({
41
+ relayPool: pool, auth,
42
+ getChannelContext: () => ({ channelId: currentChannelId, serverId: servers.currentServerId || '' }),
43
+ isAdmin: (sid) => roles.isAdmin(sid)
44
+ });
45
+
46
+ const servers = createServers({
47
+ relayPool: pool, auth, storage,
48
+ onSwitch: async (serverId) => {
49
+ roles.subscribe(serverId);
50
+ settings.subscribe(serverId);
51
+ bans.subscribe(serverId);
52
+ pages.subscribe(serverId);
53
+ if (serverId) {
54
+ await new Promise((resolve) => {
55
+ let done = false;
56
+ const finish = () => { if (done) return; done = true; resolve(); };
57
+ channels.load(serverId, finish);
58
+ setTimeout(finish, 3000);
59
+ });
60
+ }
61
+ const firstText = channels.channels.find(c => c.type === 'text');
62
+ if (firstText) {
63
+ currentChannelId = firstText.id;
64
+ chat.loadHistory(firstText.id);
65
+ }
66
+ }
67
+ });
68
+
69
+ let voice = null;
70
+ const ensureVoice = ({ serverId, displayName, onAudioTrack, onVideoTrack }) => {
71
+ if (!voice) voice = createVoiceSession({ fsm, xstate, relayPool: pool, auth, mediaDevices, bans, serverId, onAudioTrack, onVideoTrack });
72
+ else voice.serverId = serverId;
73
+ return voice;
74
+ };
75
+
76
+ const setCurrentChannel = (id) => { currentChannelId = id; if (id) chat.loadHistory(id); };
77
+
78
+ const api = {
79
+ pool, auth, fsm, message, bans, roles, settings, pages, media, channels, servers, chat,
80
+ get voice() { return voice; },
81
+ ensureVoice,
82
+ setCurrentChannel,
83
+ get currentChannelId() { return currentChannelId; },
84
+ get currentServerId() { return servers.currentServerId; }
85
+ };
86
+
87
+ register('wireweave', api);
88
+ return api;
89
+ };
@@ -1,71 +0,0 @@
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 🌀
@@ -1,49 +0,0 @@
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
- };
@@ -1,217 +0,0 @@
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;
@@ -1,59 +0,0 @@
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 });