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 +21 -0
- package/README.md +72 -0
- package/package.json +52 -0
- package/src/auth.js +97 -0
- package/src/debug.js +20 -0
- package/src/index.js +3 -0
- package/src/legacy/README.md +71 -0
- package/src/legacy/index.js +49 -0
- package/src/legacy/nostr-auth.js +217 -0
- package/src/legacy/nostr-bans.js +59 -0
- package/src/legacy/nostr-channels.js +145 -0
- package/src/legacy/nostr-chat.js +185 -0
- package/src/legacy/nostr-fsm.js +5 -0
- package/src/legacy/nostr-media.js +97 -0
- package/src/legacy/nostr-message.js +12 -0
- package/src/legacy/nostr-network.js +182 -0
- package/src/legacy/nostr-pages.js +152 -0
- package/src/legacy/nostr-roles.js +74 -0
- package/src/legacy/nostr-servers.js +223 -0
- package/src/legacy/nostr-settings.js +97 -0
- package/src/legacy/nostr-state-patch.js +19 -0
- package/src/legacy/nostr-voice-camera.js +63 -0
- package/src/legacy/nostr-voice-rtc.js +199 -0
- package/src/legacy/nostr-voice-sfu.js +128 -0
- package/src/legacy/nostr-voice.js +257 -0
- package/src/relay-pool.js +170 -0
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,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 });
|