Initial commit: P2P counter app (nostr discovery + WebRTC mesh)
15 changed files+2713−0
.gitignoreadded+3−0View file
@@ -0,0 +1,3 @@
1+node_modules
2+dist
3+*.log
README.mdadded+47−0View file
@@ -0,0 +1,47 @@
1+# CommonView
2+
3+A minimal peer-to-peer web app. Everyone who opens the page becomes a peer in a
4+single shared network. Peers discover each other over **nostr** relays and
5+communicate over **WebRTC** (full mesh). It borrows the discovery/signaling
6+approach from [trystero](https://github.com/dmotz/trystero)'s nostr strategy,
7+but only the parts needed here.
8+
9+For now the shared state is just a counter — a stand-in for the richer state
10+(including binary blobs) planned later.
11+
12+## How it works
13+
14+- **Identity** ([src/p2p/identity.ts](src/p2p/identity.ts)) — each peer has a
15+ secp256k1 / BIP340 (schnorr) keypair, persisted in `localStorage`. The x-only
16+ public key **is** the peer ID. The same key signs every nostr event and every
17+ application message.
18+- **Discovery/signaling** ([src/p2p/nostr.ts](src/p2p/nostr.ts)) — peers
19+ announce on a hashed "root topic" and listen there to find each other. WebRTC
20+ offers/answers/ICE are delivered to a per-peer topic. Only ephemeral nostr
21+ event kinds are used, so nothing is stored on relays.
22+- **WebRTC** ([src/p2p/peer.ts](src/p2p/peer.ts)) — a trimmed data-channel
23+ wrapper. Glare is avoided by letting only the peer with the smaller ID make
24+ the offer.
25+- **Mesh + state** ([src/p2p/network.ts](src/p2p/network.ts)) — every peer
26+ connects to every other peer. On connect, peers exchange a self-reported
27+ timestamp of when they joined. The **oldest** peer (earliest timestamp, ties
28+ broken by ID) is the **central** peer and holds the authoritative state.
29+ - Commands (`+`/`−`) are sent to the central peer, which applies them and
30+ broadcasts the new state to everyone.
31+ - Every message is a signed envelope (`{data, from, sig}`) and is verified on
32+ receipt.
33+ - If the central peer leaves, the next-oldest peer becomes central and its
34+ last-known state becomes the source of truth.
35+ - A page reload, or losing and regaining the network, is a **reconnect** (new
36+ timestamp). State is never persisted, so once all peers leave it resets.
37+
38+## Run
39+
40+```
41+npm install
42+npm run dev
43+```
44+
45+Open the printed URL in **two different browsers or profiles** (two tabs in the
46+same profile share the same `localStorage` key, so they'd be the *same* peer).
47+Watch the roster populate and the counter stay in sync.
index.htmladded+12−0View file
@@ -0,0 +1,12 @@
1+<!doctype html>
2+<html lang="en">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+ <title>CommonView</title>
7+ </head>
8+ <body>
9+ <div id="root"></div>
10+ <script type="module" src="/src/main.tsx"></script>
11+ </body>
12+</html>
package-lock.jsonadded+1781−0View file
@@ -0,0 +1,1781 @@
1+{
2+ "name": "commonview",
3+ "version": "0.0.0",
4+ "lockfileVersion": 3,
5+ "requires": true,
6+ "packages": {
7+ "": {
8+ "name": "commonview",
9+ "version": "0.0.0",
10+ "dependencies": {
11+ "@noble/secp256k1": "^3.1.0",
12+ "react": "^18.3.1",
13+ "react-dom": "^18.3.1"
14+ },
15+ "devDependencies": {
16+ "@types/react": "^18.3.12",
17+ "@types/react-dom": "^18.3.1",
18+ "@vitejs/plugin-react": "^4.3.4",
19+ "typescript": "^5.6.3",
20+ "vite": "^5.4.11"
21+ }
22+ },
23+ "node_modules/@babel/code-frame": {
24+ "version": "7.29.7",
25+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
26+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
27+ "dev": true,
28+ "license": "MIT",
29+ "dependencies": {
30+ "@babel/helper-validator-identifier": "^7.29.7",
31+ "js-tokens": "^4.0.0",
32+ "picocolors": "^1.1.1"
33+ },
34+ "engines": {
35+ "node": ">=6.9.0"
36+ }
37+ },
38+ "node_modules/@babel/compat-data": {
39+ "version": "7.29.7",
40+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
41+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
42+ "dev": true,
43+ "license": "MIT",
44+ "engines": {
45+ "node": ">=6.9.0"
46+ }
47+ },
48+ "node_modules/@babel/core": {
49+ "version": "7.29.7",
50+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
51+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
52+ "dev": true,
53+ "license": "MIT",
54+ "dependencies": {
55+ "@babel/code-frame": "^7.29.7",
56+ "@babel/generator": "^7.29.7",
57+ "@babel/helper-compilation-targets": "^7.29.7",
58+ "@babel/helper-module-transforms": "^7.29.7",
59+ "@babel/helpers": "^7.29.7",
60+ "@babel/parser": "^7.29.7",
61+ "@babel/template": "^7.29.7",
62+ "@babel/traverse": "^7.29.7",
63+ "@babel/types": "^7.29.7",
64+ "@jridgewell/remapping": "^2.3.5",
65+ "convert-source-map": "^2.0.0",
66+ "debug": "^4.1.0",
67+ "gensync": "^1.0.0-beta.2",
68+ "json5": "^2.2.3",
69+ "semver": "^6.3.1"
70+ },
71+ "engines": {
72+ "node": ">=6.9.0"
73+ },
74+ "funding": {
75+ "type": "opencollective",
76+ "url": "https://opencollective.com/babel"
77+ }
78+ },
79+ "node_modules/@babel/generator": {
80+ "version": "7.29.7",
81+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
82+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
83+ "dev": true,
84+ "license": "MIT",
85+ "dependencies": {
86+ "@babel/parser": "^7.29.7",
87+ "@babel/types": "^7.29.7",
88+ "@jridgewell/gen-mapping": "^0.3.12",
89+ "@jridgewell/trace-mapping": "^0.3.28",
90+ "jsesc": "^3.0.2"
91+ },
92+ "engines": {
93+ "node": ">=6.9.0"
94+ }
95+ },
96+ "node_modules/@babel/helper-compilation-targets": {
97+ "version": "7.29.7",
98+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
99+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
100+ "dev": true,
101+ "license": "MIT",
102+ "dependencies": {
103+ "@babel/compat-data": "^7.29.7",
104+ "@babel/helper-validator-option": "^7.29.7",
105+ "browserslist": "^4.24.0",
106+ "lru-cache": "^5.1.1",
107+ "semver": "^6.3.1"
108+ },
109+ "engines": {
110+ "node": ">=6.9.0"
111+ }
112+ },
113+ "node_modules/@babel/helper-globals": {
114+ "version": "7.29.7",
115+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
116+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
117+ "dev": true,
118+ "license": "MIT",
119+ "engines": {
120+ "node": ">=6.9.0"
121+ }
122+ },
123+ "node_modules/@babel/helper-module-imports": {
124+ "version": "7.29.7",
125+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
126+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
127+ "dev": true,
128+ "license": "MIT",
129+ "dependencies": {
130+ "@babel/traverse": "^7.29.7",
131+ "@babel/types": "^7.29.7"
132+ },
133+ "engines": {
134+ "node": ">=6.9.0"
135+ }
136+ },
137+ "node_modules/@babel/helper-module-transforms": {
138+ "version": "7.29.7",
139+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
140+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
141+ "dev": true,
142+ "license": "MIT",
143+ "dependencies": {
144+ "@babel/helper-module-imports": "^7.29.7",
145+ "@babel/helper-validator-identifier": "^7.29.7",
146+ "@babel/traverse": "^7.29.7"
147+ },
148+ "engines": {
149+ "node": ">=6.9.0"
150+ },
151+ "peerDependencies": {
152+ "@babel/core": "^7.0.0"
153+ }
154+ },
155+ "node_modules/@babel/helper-plugin-utils": {
156+ "version": "7.29.7",
157+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
158+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
159+ "dev": true,
160+ "license": "MIT",
161+ "engines": {
162+ "node": ">=6.9.0"
163+ }
164+ },
165+ "node_modules/@babel/helper-string-parser": {
166+ "version": "7.29.7",
167+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
168+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
169+ "dev": true,
170+ "license": "MIT",
171+ "engines": {
172+ "node": ">=6.9.0"
173+ }
174+ },
175+ "node_modules/@babel/helper-validator-identifier": {
176+ "version": "7.29.7",
177+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
178+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
179+ "dev": true,
180+ "license": "MIT",
181+ "engines": {
182+ "node": ">=6.9.0"
183+ }
184+ },
185+ "node_modules/@babel/helper-validator-option": {
186+ "version": "7.29.7",
187+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
188+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
189+ "dev": true,
190+ "license": "MIT",
191+ "engines": {
192+ "node": ">=6.9.0"
193+ }
194+ },
195+ "node_modules/@babel/helpers": {
196+ "version": "7.29.7",
197+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
198+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
199+ "dev": true,
200+ "license": "MIT",
201+ "dependencies": {
202+ "@babel/template": "^7.29.7",
203+ "@babel/types": "^7.29.7"
204+ },
205+ "engines": {
206+ "node": ">=6.9.0"
207+ }
208+ },
209+ "node_modules/@babel/parser": {
210+ "version": "7.29.7",
211+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
212+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
213+ "dev": true,
214+ "license": "MIT",
215+ "dependencies": {
216+ "@babel/types": "^7.29.7"
217+ },
218+ "bin": {
219+ "parser": "bin/babel-parser.js"
220+ },
221+ "engines": {
222+ "node": ">=6.0.0"
223+ }
224+ },
225+ "node_modules/@babel/plugin-transform-react-jsx-self": {
226+ "version": "7.29.7",
227+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
228+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
229+ "dev": true,
230+ "license": "MIT",
231+ "dependencies": {
232+ "@babel/helper-plugin-utils": "^7.29.7"
233+ },
234+ "engines": {
235+ "node": ">=6.9.0"
236+ },
237+ "peerDependencies": {
238+ "@babel/core": "^7.0.0-0"
239+ }
240+ },
241+ "node_modules/@babel/plugin-transform-react-jsx-source": {
242+ "version": "7.29.7",
243+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
244+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
245+ "dev": true,
246+ "license": "MIT",
247+ "dependencies": {
248+ "@babel/helper-plugin-utils": "^7.29.7"
249+ },
250+ "engines": {
251+ "node": ">=6.9.0"
252+ },
253+ "peerDependencies": {
254+ "@babel/core": "^7.0.0-0"
255+ }
256+ },
257+ "node_modules/@babel/template": {
258+ "version": "7.29.7",
259+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
260+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
261+ "dev": true,
262+ "license": "MIT",
263+ "dependencies": {
264+ "@babel/code-frame": "^7.29.7",
265+ "@babel/parser": "^7.29.7",
266+ "@babel/types": "^7.29.7"
267+ },
268+ "engines": {
269+ "node": ">=6.9.0"
270+ }
271+ },
272+ "node_modules/@babel/traverse": {
273+ "version": "7.29.7",
274+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
275+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
276+ "dev": true,
277+ "license": "MIT",
278+ "dependencies": {
279+ "@babel/code-frame": "^7.29.7",
280+ "@babel/generator": "^7.29.7",
281+ "@babel/helper-globals": "^7.29.7",
282+ "@babel/parser": "^7.29.7",
283+ "@babel/template": "^7.29.7",
284+ "@babel/types": "^7.29.7",
285+ "debug": "^4.3.1"
286+ },
287+ "engines": {
288+ "node": ">=6.9.0"
289+ }
290+ },
291+ "node_modules/@babel/types": {
292+ "version": "7.29.7",
293+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
294+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
295+ "dev": true,
296+ "license": "MIT",
297+ "dependencies": {
298+ "@babel/helper-string-parser": "^7.29.7",
299+ "@babel/helper-validator-identifier": "^7.29.7"
300+ },
301+ "engines": {
302+ "node": ">=6.9.0"
303+ }
304+ },
305+ "node_modules/@esbuild/aix-ppc64": {
306+ "version": "0.21.5",
307+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
308+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
309+ "cpu": [
310+ "ppc64"
311+ ],
312+ "dev": true,
313+ "license": "MIT",
314+ "optional": true,
315+ "os": [
316+ "aix"
317+ ],
318+ "engines": {
319+ "node": ">=12"
320+ }
321+ },
322+ "node_modules/@esbuild/android-arm": {
323+ "version": "0.21.5",
324+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
325+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
326+ "cpu": [
327+ "arm"
328+ ],
329+ "dev": true,
330+ "license": "MIT",
331+ "optional": true,
332+ "os": [
333+ "android"
334+ ],
335+ "engines": {
336+ "node": ">=12"
337+ }
338+ },
339+ "node_modules/@esbuild/android-arm64": {
340+ "version": "0.21.5",
341+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
342+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
343+ "cpu": [
344+ "arm64"
345+ ],
346+ "dev": true,
347+ "license": "MIT",
348+ "optional": true,
349+ "os": [
350+ "android"
351+ ],
352+ "engines": {
353+ "node": ">=12"
354+ }
355+ },
356+ "node_modules/@esbuild/android-x64": {
357+ "version": "0.21.5",
358+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
359+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
360+ "cpu": [
361+ "x64"
362+ ],
363+ "dev": true,
364+ "license": "MIT",
365+ "optional": true,
366+ "os": [
367+ "android"
368+ ],
369+ "engines": {
370+ "node": ">=12"
371+ }
372+ },
373+ "node_modules/@esbuild/darwin-arm64": {
374+ "version": "0.21.5",
375+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
376+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
377+ "cpu": [
378+ "arm64"
379+ ],
380+ "dev": true,
381+ "license": "MIT",
382+ "optional": true,
383+ "os": [
384+ "darwin"
385+ ],
386+ "engines": {
387+ "node": ">=12"
388+ }
389+ },
390+ "node_modules/@esbuild/darwin-x64": {
391+ "version": "0.21.5",
392+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
393+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
394+ "cpu": [
395+ "x64"
396+ ],
397+ "dev": true,
398+ "license": "MIT",
399+ "optional": true,
400+ "os": [
401+ "darwin"
402+ ],
403+ "engines": {
404+ "node": ">=12"
405+ }
406+ },
407+ "node_modules/@esbuild/freebsd-arm64": {
408+ "version": "0.21.5",
409+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
410+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
411+ "cpu": [
412+ "arm64"
413+ ],
414+ "dev": true,
415+ "license": "MIT",
416+ "optional": true,
417+ "os": [
418+ "freebsd"
419+ ],
420+ "engines": {
421+ "node": ">=12"
422+ }
423+ },
424+ "node_modules/@esbuild/freebsd-x64": {
425+ "version": "0.21.5",
426+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
427+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
428+ "cpu": [
429+ "x64"
430+ ],
431+ "dev": true,
432+ "license": "MIT",
433+ "optional": true,
434+ "os": [
435+ "freebsd"
436+ ],
437+ "engines": {
438+ "node": ">=12"
439+ }
440+ },
441+ "node_modules/@esbuild/linux-arm": {
442+ "version": "0.21.5",
443+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
444+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
445+ "cpu": [
446+ "arm"
447+ ],
448+ "dev": true,
449+ "license": "MIT",
450+ "optional": true,
451+ "os": [
452+ "linux"
453+ ],
454+ "engines": {
455+ "node": ">=12"
456+ }
457+ },
458+ "node_modules/@esbuild/linux-arm64": {
459+ "version": "0.21.5",
460+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
461+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
462+ "cpu": [
463+ "arm64"
464+ ],
465+ "dev": true,
466+ "license": "MIT",
467+ "optional": true,
468+ "os": [
469+ "linux"
470+ ],
471+ "engines": {
472+ "node": ">=12"
473+ }
474+ },
475+ "node_modules/@esbuild/linux-ia32": {
476+ "version": "0.21.5",
477+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
478+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
479+ "cpu": [
480+ "ia32"
481+ ],
482+ "dev": true,
483+ "license": "MIT",
484+ "optional": true,
485+ "os": [
486+ "linux"
487+ ],
488+ "engines": {
489+ "node": ">=12"
490+ }
491+ },
492+ "node_modules/@esbuild/linux-loong64": {
493+ "version": "0.21.5",
494+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
495+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
496+ "cpu": [
497+ "loong64"
498+ ],
499+ "dev": true,
500+ "license": "MIT",
501+ "optional": true,
502+ "os": [
503+ "linux"
504+ ],
505+ "engines": {
506+ "node": ">=12"
507+ }
508+ },
509+ "node_modules/@esbuild/linux-mips64el": {
510+ "version": "0.21.5",
511+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
512+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
513+ "cpu": [
514+ "mips64el"
515+ ],
516+ "dev": true,
517+ "license": "MIT",
518+ "optional": true,
519+ "os": [
520+ "linux"
521+ ],
522+ "engines": {
523+ "node": ">=12"
524+ }
525+ },
526+ "node_modules/@esbuild/linux-ppc64": {
527+ "version": "0.21.5",
528+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
529+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
530+ "cpu": [
531+ "ppc64"
532+ ],
533+ "dev": true,
534+ "license": "MIT",
535+ "optional": true,
536+ "os": [
537+ "linux"
538+ ],
539+ "engines": {
540+ "node": ">=12"
541+ }
542+ },
543+ "node_modules/@esbuild/linux-riscv64": {
544+ "version": "0.21.5",
545+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
546+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
547+ "cpu": [
548+ "riscv64"
549+ ],
550+ "dev": true,
551+ "license": "MIT",
552+ "optional": true,
553+ "os": [
554+ "linux"
555+ ],
556+ "engines": {
557+ "node": ">=12"
558+ }
559+ },
560+ "node_modules/@esbuild/linux-s390x": {
561+ "version": "0.21.5",
562+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
563+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
564+ "cpu": [
565+ "s390x"
566+ ],
567+ "dev": true,
568+ "license": "MIT",
569+ "optional": true,
570+ "os": [
571+ "linux"
572+ ],
573+ "engines": {
574+ "node": ">=12"
575+ }
576+ },
577+ "node_modules/@esbuild/linux-x64": {
578+ "version": "0.21.5",
579+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
580+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
581+ "cpu": [
582+ "x64"
583+ ],
584+ "dev": true,
585+ "license": "MIT",
586+ "optional": true,
587+ "os": [
588+ "linux"
589+ ],
590+ "engines": {
591+ "node": ">=12"
592+ }
593+ },
594+ "node_modules/@esbuild/netbsd-x64": {
595+ "version": "0.21.5",
596+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
597+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
598+ "cpu": [
599+ "x64"
600+ ],
601+ "dev": true,
602+ "license": "MIT",
603+ "optional": true,
604+ "os": [
605+ "netbsd"
606+ ],
607+ "engines": {
608+ "node": ">=12"
609+ }
610+ },
611+ "node_modules/@esbuild/openbsd-x64": {
612+ "version": "0.21.5",
613+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
614+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
615+ "cpu": [
616+ "x64"
617+ ],
618+ "dev": true,
619+ "license": "MIT",
620+ "optional": true,
621+ "os": [
622+ "openbsd"
623+ ],
624+ "engines": {
625+ "node": ">=12"
626+ }
627+ },
628+ "node_modules/@esbuild/sunos-x64": {
629+ "version": "0.21.5",
630+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
631+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
632+ "cpu": [
633+ "x64"
634+ ],
635+ "dev": true,
636+ "license": "MIT",
637+ "optional": true,
638+ "os": [
639+ "sunos"
640+ ],
641+ "engines": {
642+ "node": ">=12"
643+ }
644+ },
645+ "node_modules/@esbuild/win32-arm64": {
646+ "version": "0.21.5",
647+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
648+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
649+ "cpu": [
650+ "arm64"
651+ ],
652+ "dev": true,
653+ "license": "MIT",
654+ "optional": true,
655+ "os": [
656+ "win32"
657+ ],
658+ "engines": {
659+ "node": ">=12"
660+ }
661+ },
662+ "node_modules/@esbuild/win32-ia32": {
663+ "version": "0.21.5",
664+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
665+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
666+ "cpu": [
667+ "ia32"
668+ ],
669+ "dev": true,
670+ "license": "MIT",
671+ "optional": true,
672+ "os": [
673+ "win32"
674+ ],
675+ "engines": {
676+ "node": ">=12"
677+ }
678+ },
679+ "node_modules/@esbuild/win32-x64": {
680+ "version": "0.21.5",
681+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
682+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
683+ "cpu": [
684+ "x64"
685+ ],
686+ "dev": true,
687+ "license": "MIT",
688+ "optional": true,
689+ "os": [
690+ "win32"
691+ ],
692+ "engines": {
693+ "node": ">=12"
694+ }
695+ },
696+ "node_modules/@jridgewell/gen-mapping": {
697+ "version": "0.3.13",
698+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
699+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
700+ "dev": true,
701+ "license": "MIT",
702+ "dependencies": {
703+ "@jridgewell/sourcemap-codec": "^1.5.0",
704+ "@jridgewell/trace-mapping": "^0.3.24"
705+ }
706+ },
707+ "node_modules/@jridgewell/remapping": {
708+ "version": "2.3.5",
709+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
710+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
711+ "dev": true,
712+ "license": "MIT",
713+ "dependencies": {
714+ "@jridgewell/gen-mapping": "^0.3.5",
715+ "@jridgewell/trace-mapping": "^0.3.24"
716+ }
717+ },
718+ "node_modules/@jridgewell/resolve-uri": {
719+ "version": "3.1.2",
720+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
721+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
722+ "dev": true,
723+ "license": "MIT",
724+ "engines": {
725+ "node": ">=6.0.0"
726+ }
727+ },
728+ "node_modules/@jridgewell/sourcemap-codec": {
729+ "version": "1.5.5",
730+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
731+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
732+ "dev": true,
733+ "license": "MIT"
734+ },
735+ "node_modules/@jridgewell/trace-mapping": {
736+ "version": "0.3.31",
737+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
738+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
739+ "dev": true,
740+ "license": "MIT",
741+ "dependencies": {
742+ "@jridgewell/resolve-uri": "^3.1.0",
743+ "@jridgewell/sourcemap-codec": "^1.4.14"
744+ }
745+ },
746+ "node_modules/@noble/secp256k1": {
747+ "version": "3.1.0",
748+ "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.1.0.tgz",
749+ "integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==",
750+ "license": "MIT",
751+ "funding": {
752+ "url": "https://paulmillr.com/funding/"
753+ }
754+ },
755+ "node_modules/@rolldown/pluginutils": {
756+ "version": "1.0.0-beta.27",
757+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
758+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
759+ "dev": true,
760+ "license": "MIT"
761+ },
762+ "node_modules/@rollup/rollup-android-arm-eabi": {
763+ "version": "4.62.2",
764+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
765+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
766+ "cpu": [
767+ "arm"
768+ ],
769+ "dev": true,
770+ "license": "MIT",
771+ "optional": true,
772+ "os": [
773+ "android"
774+ ]
775+ },
776+ "node_modules/@rollup/rollup-android-arm64": {
777+ "version": "4.62.2",
778+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
779+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
780+ "cpu": [
781+ "arm64"
782+ ],
783+ "dev": true,
784+ "license": "MIT",
785+ "optional": true,
786+ "os": [
787+ "android"
788+ ]
789+ },
790+ "node_modules/@rollup/rollup-darwin-arm64": {
791+ "version": "4.62.2",
792+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
793+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
794+ "cpu": [
795+ "arm64"
796+ ],
797+ "dev": true,
798+ "license": "MIT",
799+ "optional": true,
800+ "os": [
801+ "darwin"
802+ ]
803+ },
804+ "node_modules/@rollup/rollup-darwin-x64": {
805+ "version": "4.62.2",
806+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
807+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
808+ "cpu": [
809+ "x64"
810+ ],
811+ "dev": true,
812+ "license": "MIT",
813+ "optional": true,
814+ "os": [
815+ "darwin"
816+ ]
817+ },
818+ "node_modules/@rollup/rollup-freebsd-arm64": {
819+ "version": "4.62.2",
820+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
821+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
822+ "cpu": [
823+ "arm64"
824+ ],
825+ "dev": true,
826+ "license": "MIT",
827+ "optional": true,
828+ "os": [
829+ "freebsd"
830+ ]
831+ },
832+ "node_modules/@rollup/rollup-freebsd-x64": {
833+ "version": "4.62.2",
834+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
835+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
836+ "cpu": [
837+ "x64"
838+ ],
839+ "dev": true,
840+ "license": "MIT",
841+ "optional": true,
842+ "os": [
843+ "freebsd"
844+ ]
845+ },
846+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
847+ "version": "4.62.2",
848+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
849+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
850+ "cpu": [
851+ "arm"
852+ ],
853+ "dev": true,
854+ "libc": [
855+ "glibc"
856+ ],
857+ "license": "MIT",
858+ "optional": true,
859+ "os": [
860+ "linux"
861+ ]
862+ },
863+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
864+ "version": "4.62.2",
865+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
866+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
867+ "cpu": [
868+ "arm"
869+ ],
870+ "dev": true,
871+ "libc": [
872+ "musl"
873+ ],
874+ "license": "MIT",
875+ "optional": true,
876+ "os": [
877+ "linux"
878+ ]
879+ },
880+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
881+ "version": "4.62.2",
882+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
883+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
884+ "cpu": [
885+ "arm64"
886+ ],
887+ "dev": true,
888+ "libc": [
889+ "glibc"
890+ ],
891+ "license": "MIT",
892+ "optional": true,
893+ "os": [
894+ "linux"
895+ ]
896+ },
897+ "node_modules/@rollup/rollup-linux-arm64-musl": {
898+ "version": "4.62.2",
899+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
900+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
901+ "cpu": [
902+ "arm64"
903+ ],
904+ "dev": true,
905+ "libc": [
906+ "musl"
907+ ],
908+ "license": "MIT",
909+ "optional": true,
910+ "os": [
911+ "linux"
912+ ]
913+ },
914+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
915+ "version": "4.62.2",
916+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
917+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
918+ "cpu": [
919+ "loong64"
920+ ],
921+ "dev": true,
922+ "libc": [
923+ "glibc"
924+ ],
925+ "license": "MIT",
926+ "optional": true,
927+ "os": [
928+ "linux"
929+ ]
930+ },
931+ "node_modules/@rollup/rollup-linux-loong64-musl": {
932+ "version": "4.62.2",
933+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
934+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
935+ "cpu": [
936+ "loong64"
937+ ],
938+ "dev": true,
939+ "libc": [
940+ "musl"
941+ ],
942+ "license": "MIT",
943+ "optional": true,
944+ "os": [
945+ "linux"
946+ ]
947+ },
948+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
949+ "version": "4.62.2",
950+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
951+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
952+ "cpu": [
953+ "ppc64"
954+ ],
955+ "dev": true,
956+ "libc": [
957+ "glibc"
958+ ],
959+ "license": "MIT",
960+ "optional": true,
961+ "os": [
962+ "linux"
963+ ]
964+ },
965+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
966+ "version": "4.62.2",
967+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
968+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
969+ "cpu": [
970+ "ppc64"
971+ ],
972+ "dev": true,
973+ "libc": [
974+ "musl"
975+ ],
976+ "license": "MIT",
977+ "optional": true,
978+ "os": [
979+ "linux"
980+ ]
981+ },
982+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
983+ "version": "4.62.2",
984+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
985+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
986+ "cpu": [
987+ "riscv64"
988+ ],
989+ "dev": true,
990+ "libc": [
991+ "glibc"
992+ ],
993+ "license": "MIT",
994+ "optional": true,
995+ "os": [
996+ "linux"
997+ ]
998+ },
999+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1000+ "version": "4.62.2",
1001+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
1002+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
1003+ "cpu": [
1004+ "riscv64"
1005+ ],
1006+ "dev": true,
1007+ "libc": [
1008+ "musl"
1009+ ],
1010+ "license": "MIT",
1011+ "optional": true,
1012+ "os": [
1013+ "linux"
1014+ ]
1015+ },
1016+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1017+ "version": "4.62.2",
1018+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
1019+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
1020+ "cpu": [
1021+ "s390x"
1022+ ],
1023+ "dev": true,
1024+ "libc": [
1025+ "glibc"
1026+ ],
1027+ "license": "MIT",
1028+ "optional": true,
1029+ "os": [
1030+ "linux"
1031+ ]
1032+ },
1033+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1034+ "version": "4.62.2",
1035+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
1036+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
1037+ "cpu": [
1038+ "x64"
1039+ ],
1040+ "dev": true,
1041+ "libc": [
1042+ "glibc"
1043+ ],
1044+ "license": "MIT",
1045+ "optional": true,
1046+ "os": [
1047+ "linux"
1048+ ]
1049+ },
1050+ "node_modules/@rollup/rollup-linux-x64-musl": {
1051+ "version": "4.62.2",
1052+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
1053+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
1054+ "cpu": [
1055+ "x64"
1056+ ],
1057+ "dev": true,
1058+ "libc": [
1059+ "musl"
1060+ ],
1061+ "license": "MIT",
1062+ "optional": true,
1063+ "os": [
1064+ "linux"
1065+ ]
1066+ },
1067+ "node_modules/@rollup/rollup-openbsd-x64": {
1068+ "version": "4.62.2",
1069+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
1070+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
1071+ "cpu": [
1072+ "x64"
1073+ ],
1074+ "dev": true,
1075+ "license": "MIT",
1076+ "optional": true,
1077+ "os": [
1078+ "openbsd"
1079+ ]
1080+ },
1081+ "node_modules/@rollup/rollup-openharmony-arm64": {
1082+ "version": "4.62.2",
1083+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
1084+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
1085+ "cpu": [
1086+ "arm64"
1087+ ],
1088+ "dev": true,
1089+ "license": "MIT",
1090+ "optional": true,
1091+ "os": [
1092+ "openharmony"
1093+ ]
1094+ },
1095+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1096+ "version": "4.62.2",
1097+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
1098+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
1099+ "cpu": [
1100+ "arm64"
1101+ ],
1102+ "dev": true,
1103+ "license": "MIT",
1104+ "optional": true,
1105+ "os": [
1106+ "win32"
1107+ ]
1108+ },
1109+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1110+ "version": "4.62.2",
1111+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
1112+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
1113+ "cpu": [
1114+ "ia32"
1115+ ],
1116+ "dev": true,
1117+ "license": "MIT",
1118+ "optional": true,
1119+ "os": [
1120+ "win32"
1121+ ]
1122+ },
1123+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1124+ "version": "4.62.2",
1125+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
1126+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
1127+ "cpu": [
1128+ "x64"
1129+ ],
1130+ "dev": true,
1131+ "license": "MIT",
1132+ "optional": true,
1133+ "os": [
1134+ "win32"
1135+ ]
1136+ },
1137+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1138+ "version": "4.62.2",
1139+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
1140+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
1141+ "cpu": [
1142+ "x64"
1143+ ],
1144+ "dev": true,
1145+ "license": "MIT",
1146+ "optional": true,
1147+ "os": [
1148+ "win32"
1149+ ]
1150+ },
1151+ "node_modules/@types/babel__core": {
1152+ "version": "7.20.5",
1153+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1154+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1155+ "dev": true,
1156+ "license": "MIT",
1157+ "dependencies": {
1158+ "@babel/parser": "^7.20.7",
1159+ "@babel/types": "^7.20.7",
1160+ "@types/babel__generator": "*",
1161+ "@types/babel__template": "*",
1162+ "@types/babel__traverse": "*"
1163+ }
1164+ },
1165+ "node_modules/@types/babel__generator": {
1166+ "version": "7.27.0",
1167+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1168+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1169+ "dev": true,
1170+ "license": "MIT",
1171+ "dependencies": {
1172+ "@babel/types": "^7.0.0"
1173+ }
1174+ },
1175+ "node_modules/@types/babel__template": {
1176+ "version": "7.4.4",
1177+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1178+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1179+ "dev": true,
1180+ "license": "MIT",
1181+ "dependencies": {
1182+ "@babel/parser": "^7.1.0",
1183+ "@babel/types": "^7.0.0"
1184+ }
1185+ },
1186+ "node_modules/@types/babel__traverse": {
1187+ "version": "7.28.0",
1188+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1189+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1190+ "dev": true,
1191+ "license": "MIT",
1192+ "dependencies": {
1193+ "@babel/types": "^7.28.2"
1194+ }
1195+ },
1196+ "node_modules/@types/estree": {
1197+ "version": "1.0.9",
1198+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1199+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1200+ "dev": true,
1201+ "license": "MIT"
1202+ },
1203+ "node_modules/@types/prop-types": {
1204+ "version": "15.7.15",
1205+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
1206+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1207+ "dev": true,
1208+ "license": "MIT"
1209+ },
1210+ "node_modules/@types/react": {
1211+ "version": "18.3.31",
1212+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
1213+ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
1214+ "dev": true,
1215+ "license": "MIT",
1216+ "dependencies": {
1217+ "@types/prop-types": "*",
1218+ "csstype": "^3.2.2"
1219+ }
1220+ },
1221+ "node_modules/@types/react-dom": {
1222+ "version": "18.3.7",
1223+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
1224+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
1225+ "dev": true,
1226+ "license": "MIT",
1227+ "peerDependencies": {
1228+ "@types/react": "^18.0.0"
1229+ }
1230+ },
1231+ "node_modules/@vitejs/plugin-react": {
1232+ "version": "4.7.0",
1233+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1234+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1235+ "dev": true,
1236+ "license": "MIT",
1237+ "dependencies": {
1238+ "@babel/core": "^7.28.0",
1239+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1240+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1241+ "@rolldown/pluginutils": "1.0.0-beta.27",
1242+ "@types/babel__core": "^7.20.5",
1243+ "react-refresh": "^0.17.0"
1244+ },
1245+ "engines": {
1246+ "node": "^14.18.0 || >=16.0.0"
1247+ },
1248+ "peerDependencies": {
1249+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1250+ }
1251+ },
1252+ "node_modules/baseline-browser-mapping": {
1253+ "version": "2.10.40",
1254+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz",
1255+ "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==",
1256+ "dev": true,
1257+ "license": "Apache-2.0",
1258+ "bin": {
1259+ "baseline-browser-mapping": "dist/cli.cjs"
1260+ },
1261+ "engines": {
1262+ "node": ">=6.0.0"
1263+ }
1264+ },
1265+ "node_modules/browserslist": {
1266+ "version": "4.28.4",
1267+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
1268+ "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
1269+ "dev": true,
1270+ "funding": [
1271+ {
1272+ "type": "opencollective",
1273+ "url": "https://opencollective.com/browserslist"
1274+ },
1275+ {
1276+ "type": "tidelift",
1277+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1278+ },
1279+ {
1280+ "type": "github",
1281+ "url": "https://github.com/sponsors/ai"
1282+ }
1283+ ],
1284+ "license": "MIT",
1285+ "dependencies": {
1286+ "baseline-browser-mapping": "^2.10.38",
1287+ "caniuse-lite": "^1.0.30001799",
1288+ "electron-to-chromium": "^1.5.376",
1289+ "node-releases": "^2.0.48",
1290+ "update-browserslist-db": "^1.2.3"
1291+ },
1292+ "bin": {
1293+ "browserslist": "cli.js"
1294+ },
1295+ "engines": {
1296+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1297+ }
1298+ },
1299+ "node_modules/caniuse-lite": {
1300+ "version": "1.0.30001800",
1301+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz",
1302+ "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==",
1303+ "dev": true,
1304+ "funding": [
1305+ {
1306+ "type": "opencollective",
1307+ "url": "https://opencollective.com/browserslist"
1308+ },
1309+ {
1310+ "type": "tidelift",
1311+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1312+ },
1313+ {
1314+ "type": "github",
1315+ "url": "https://github.com/sponsors/ai"
1316+ }
1317+ ],
1318+ "license": "CC-BY-4.0"
1319+ },
1320+ "node_modules/convert-source-map": {
1321+ "version": "2.0.0",
1322+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1323+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1324+ "dev": true,
1325+ "license": "MIT"
1326+ },
1327+ "node_modules/csstype": {
1328+ "version": "3.2.3",
1329+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1330+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1331+ "dev": true,
1332+ "license": "MIT"
1333+ },
1334+ "node_modules/debug": {
1335+ "version": "4.4.3",
1336+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1337+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1338+ "dev": true,
1339+ "license": "MIT",
1340+ "dependencies": {
1341+ "ms": "^2.1.3"
1342+ },
1343+ "engines": {
1344+ "node": ">=6.0"
1345+ },
1346+ "peerDependenciesMeta": {
1347+ "supports-color": {
1348+ "optional": true
1349+ }
1350+ }
1351+ },
1352+ "node_modules/electron-to-chromium": {
1353+ "version": "1.5.383",
1354+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz",
1355+ "integrity": "sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==",
1356+ "dev": true,
1357+ "license": "ISC"
1358+ },
1359+ "node_modules/esbuild": {
1360+ "version": "0.21.5",
1361+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1362+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1363+ "dev": true,
1364+ "hasInstallScript": true,
1365+ "license": "MIT",
1366+ "bin": {
1367+ "esbuild": "bin/esbuild"
1368+ },
1369+ "engines": {
1370+ "node": ">=12"
1371+ },
1372+ "optionalDependencies": {
1373+ "@esbuild/aix-ppc64": "0.21.5",
1374+ "@esbuild/android-arm": "0.21.5",
1375+ "@esbuild/android-arm64": "0.21.5",
1376+ "@esbuild/android-x64": "0.21.5",
1377+ "@esbuild/darwin-arm64": "0.21.5",
1378+ "@esbuild/darwin-x64": "0.21.5",
1379+ "@esbuild/freebsd-arm64": "0.21.5",
1380+ "@esbuild/freebsd-x64": "0.21.5",
1381+ "@esbuild/linux-arm": "0.21.5",
1382+ "@esbuild/linux-arm64": "0.21.5",
1383+ "@esbuild/linux-ia32": "0.21.5",
1384+ "@esbuild/linux-loong64": "0.21.5",
1385+ "@esbuild/linux-mips64el": "0.21.5",
1386+ "@esbuild/linux-ppc64": "0.21.5",
1387+ "@esbuild/linux-riscv64": "0.21.5",
1388+ "@esbuild/linux-s390x": "0.21.5",
1389+ "@esbuild/linux-x64": "0.21.5",
1390+ "@esbuild/netbsd-x64": "0.21.5",
1391+ "@esbuild/openbsd-x64": "0.21.5",
1392+ "@esbuild/sunos-x64": "0.21.5",
1393+ "@esbuild/win32-arm64": "0.21.5",
1394+ "@esbuild/win32-ia32": "0.21.5",
1395+ "@esbuild/win32-x64": "0.21.5"
1396+ }
1397+ },
1398+ "node_modules/escalade": {
1399+ "version": "3.2.0",
1400+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1401+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1402+ "dev": true,
1403+ "license": "MIT",
1404+ "engines": {
1405+ "node": ">=6"
1406+ }
1407+ },
1408+ "node_modules/fsevents": {
1409+ "version": "2.3.3",
1410+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1411+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1412+ "dev": true,
1413+ "hasInstallScript": true,
1414+ "license": "MIT",
1415+ "optional": true,
1416+ "os": [
1417+ "darwin"
1418+ ],
1419+ "engines": {
1420+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1421+ }
1422+ },
1423+ "node_modules/gensync": {
1424+ "version": "1.0.0-beta.2",
1425+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1426+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1427+ "dev": true,
1428+ "license": "MIT",
1429+ "engines": {
1430+ "node": ">=6.9.0"
1431+ }
1432+ },
1433+ "node_modules/js-tokens": {
1434+ "version": "4.0.0",
1435+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1436+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1437+ "license": "MIT"
1438+ },
1439+ "node_modules/jsesc": {
1440+ "version": "3.1.0",
1441+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1442+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1443+ "dev": true,
1444+ "license": "MIT",
1445+ "bin": {
1446+ "jsesc": "bin/jsesc"
1447+ },
1448+ "engines": {
1449+ "node": ">=6"
1450+ }
1451+ },
1452+ "node_modules/json5": {
1453+ "version": "2.2.3",
1454+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1455+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1456+ "dev": true,
1457+ "license": "MIT",
1458+ "bin": {
1459+ "json5": "lib/cli.js"
1460+ },
1461+ "engines": {
1462+ "node": ">=6"
1463+ }
1464+ },
1465+ "node_modules/loose-envify": {
1466+ "version": "1.4.0",
1467+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1468+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1469+ "license": "MIT",
1470+ "dependencies": {
1471+ "js-tokens": "^3.0.0 || ^4.0.0"
1472+ },
1473+ "bin": {
1474+ "loose-envify": "cli.js"
1475+ }
1476+ },
1477+ "node_modules/lru-cache": {
1478+ "version": "5.1.1",
1479+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1480+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1481+ "dev": true,
1482+ "license": "ISC",
1483+ "dependencies": {
1484+ "yallist": "^3.0.2"
1485+ }
1486+ },
1487+ "node_modules/ms": {
1488+ "version": "2.1.3",
1489+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1490+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1491+ "dev": true,
1492+ "license": "MIT"
1493+ },
1494+ "node_modules/nanoid": {
1495+ "version": "3.3.15",
1496+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
1497+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
1498+ "dev": true,
1499+ "funding": [
1500+ {
1501+ "type": "github",
1502+ "url": "https://github.com/sponsors/ai"
1503+ }
1504+ ],
1505+ "license": "MIT",
1506+ "bin": {
1507+ "nanoid": "bin/nanoid.cjs"
1508+ },
1509+ "engines": {
1510+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1511+ }
1512+ },
1513+ "node_modules/node-releases": {
1514+ "version": "2.0.50",
1515+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
1516+ "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
1517+ "dev": true,
1518+ "license": "MIT",
1519+ "engines": {
1520+ "node": ">=18"
1521+ }
1522+ },
1523+ "node_modules/picocolors": {
1524+ "version": "1.1.1",
1525+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1526+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1527+ "dev": true,
1528+ "license": "ISC"
1529+ },
1530+ "node_modules/postcss": {
1531+ "version": "8.5.16",
1532+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
1533+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
1534+ "dev": true,
1535+ "funding": [
1536+ {
1537+ "type": "opencollective",
1538+ "url": "https://opencollective.com/postcss/"
1539+ },
1540+ {
1541+ "type": "tidelift",
1542+ "url": "https://tidelift.com/funding/github/npm/postcss"
1543+ },
1544+ {
1545+ "type": "github",
1546+ "url": "https://github.com/sponsors/ai"
1547+ }
1548+ ],
1549+ "license": "MIT",
1550+ "dependencies": {
1551+ "nanoid": "^3.3.12",
1552+ "picocolors": "^1.1.1",
1553+ "source-map-js": "^1.2.1"
1554+ },
1555+ "engines": {
1556+ "node": "^10 || ^12 || >=14"
1557+ }
1558+ },
1559+ "node_modules/react": {
1560+ "version": "18.3.1",
1561+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1562+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1563+ "license": "MIT",
1564+ "dependencies": {
1565+ "loose-envify": "^1.1.0"
1566+ },
1567+ "engines": {
1568+ "node": ">=0.10.0"
1569+ }
1570+ },
1571+ "node_modules/react-dom": {
1572+ "version": "18.3.1",
1573+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1574+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1575+ "license": "MIT",
1576+ "dependencies": {
1577+ "loose-envify": "^1.1.0",
1578+ "scheduler": "^0.23.2"
1579+ },
1580+ "peerDependencies": {
1581+ "react": "^18.3.1"
1582+ }
1583+ },
1584+ "node_modules/react-refresh": {
1585+ "version": "0.17.0",
1586+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1587+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1588+ "dev": true,
1589+ "license": "MIT",
1590+ "engines": {
1591+ "node": ">=0.10.0"
1592+ }
1593+ },
1594+ "node_modules/rollup": {
1595+ "version": "4.62.2",
1596+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
1597+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
1598+ "dev": true,
1599+ "license": "MIT",
1600+ "dependencies": {
1601+ "@types/estree": "1.0.9"
1602+ },
1603+ "bin": {
1604+ "rollup": "dist/bin/rollup"
1605+ },
1606+ "engines": {
1607+ "node": ">=18.0.0",
1608+ "npm": ">=8.0.0"
1609+ },
1610+ "optionalDependencies": {
1611+ "@rollup/rollup-android-arm-eabi": "4.62.2",
1612+ "@rollup/rollup-android-arm64": "4.62.2",
1613+ "@rollup/rollup-darwin-arm64": "4.62.2",
1614+ "@rollup/rollup-darwin-x64": "4.62.2",
1615+ "@rollup/rollup-freebsd-arm64": "4.62.2",
1616+ "@rollup/rollup-freebsd-x64": "4.62.2",
1617+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
1618+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
1619+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
1620+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
1621+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
1622+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
1623+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
1624+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
1625+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
1626+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
1627+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
1628+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
1629+ "@rollup/rollup-linux-x64-musl": "4.62.2",
1630+ "@rollup/rollup-openbsd-x64": "4.62.2",
1631+ "@rollup/rollup-openharmony-arm64": "4.62.2",
1632+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
1633+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
1634+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
1635+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
1636+ "fsevents": "~2.3.2"
1637+ }
1638+ },
1639+ "node_modules/scheduler": {
1640+ "version": "0.23.2",
1641+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1642+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1643+ "license": "MIT",
1644+ "dependencies": {
1645+ "loose-envify": "^1.1.0"
1646+ }
1647+ },
1648+ "node_modules/semver": {
1649+ "version": "6.3.1",
1650+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1651+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1652+ "dev": true,
1653+ "license": "ISC",
1654+ "bin": {
1655+ "semver": "bin/semver.js"
1656+ }
1657+ },
1658+ "node_modules/source-map-js": {
1659+ "version": "1.2.1",
1660+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1661+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1662+ "dev": true,
1663+ "license": "BSD-3-Clause",
1664+ "engines": {
1665+ "node": ">=0.10.0"
1666+ }
1667+ },
1668+ "node_modules/typescript": {
1669+ "version": "5.9.3",
1670+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1671+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1672+ "dev": true,
1673+ "license": "Apache-2.0",
1674+ "bin": {
1675+ "tsc": "bin/tsc",
1676+ "tsserver": "bin/tsserver"
1677+ },
1678+ "engines": {
1679+ "node": ">=14.17"
1680+ }
1681+ },
1682+ "node_modules/update-browserslist-db": {
1683+ "version": "1.2.3",
1684+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1685+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1686+ "dev": true,
1687+ "funding": [
1688+ {
1689+ "type": "opencollective",
1690+ "url": "https://opencollective.com/browserslist"
1691+ },
1692+ {
1693+ "type": "tidelift",
1694+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1695+ },
1696+ {
1697+ "type": "github",
1698+ "url": "https://github.com/sponsors/ai"
1699+ }
1700+ ],
1701+ "license": "MIT",
1702+ "dependencies": {
1703+ "escalade": "^3.2.0",
1704+ "picocolors": "^1.1.1"
1705+ },
1706+ "bin": {
1707+ "update-browserslist-db": "cli.js"
1708+ },
1709+ "peerDependencies": {
1710+ "browserslist": ">= 4.21.0"
1711+ }
1712+ },
1713+ "node_modules/vite": {
1714+ "version": "5.4.21",
1715+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
1716+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
1717+ "dev": true,
1718+ "license": "MIT",
1719+ "dependencies": {
1720+ "esbuild": "^0.21.3",
1721+ "postcss": "^8.4.43",
1722+ "rollup": "^4.20.0"
1723+ },
1724+ "bin": {
1725+ "vite": "bin/vite.js"
1726+ },
1727+ "engines": {
1728+ "node": "^18.0.0 || >=20.0.0"
1729+ },
1730+ "funding": {
1731+ "url": "https://github.com/vitejs/vite?sponsor=1"
1732+ },
1733+ "optionalDependencies": {
1734+ "fsevents": "~2.3.3"
1735+ },
1736+ "peerDependencies": {
1737+ "@types/node": "^18.0.0 || >=20.0.0",
1738+ "less": "*",
1739+ "lightningcss": "^1.21.0",
1740+ "sass": "*",
1741+ "sass-embedded": "*",
1742+ "stylus": "*",
1743+ "sugarss": "*",
1744+ "terser": "^5.4.0"
1745+ },
1746+ "peerDependenciesMeta": {
1747+ "@types/node": {
1748+ "optional": true
1749+ },
1750+ "less": {
1751+ "optional": true
1752+ },
1753+ "lightningcss": {
1754+ "optional": true
1755+ },
1756+ "sass": {
1757+ "optional": true
1758+ },
1759+ "sass-embedded": {
1760+ "optional": true
1761+ },
1762+ "stylus": {
1763+ "optional": true
1764+ },
1765+ "sugarss": {
1766+ "optional": true
1767+ },
1768+ "terser": {
1769+ "optional": true
1770+ }
1771+ }
1772+ },
1773+ "node_modules/yallist": {
1774+ "version": "3.1.1",
1775+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1776+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1777+ "dev": true,
1778+ "license": "ISC"
1779+ }
1780+ }
1781+}
package.jsonadded+23−0View file
@@ -0,0 +1,23 @@
1+{
2+ "name": "commonview",
3+ "private": true,
4+ "version": "0.0.0",
5+ "type": "module",
6+ "scripts": {
7+ "dev": "vite",
8+ "build": "tsc -b && vite build",
9+ "preview": "vite preview"
10+ },
11+ "dependencies": {
12+ "@noble/secp256k1": "^3.1.0",
13+ "react": "^18.3.1",
14+ "react-dom": "^18.3.1"
15+ },
16+ "devDependencies": {
17+ "@types/react": "^18.3.12",
18+ "@types/react-dom": "^18.3.1",
19+ "@vitejs/plugin-react": "^4.3.4",
20+ "typescript": "^5.6.3",
21+ "vite": "^5.4.11"
22+ }
23+}
src/App.tsxadded+72−0View file
@@ -0,0 +1,72 @@
1+import {useNetwork} from './useNetwork'
2+
3+const short = (id: string) => id.slice(0, 8) + '…' + id.slice(-4)
4+
5+const time = (ms: number) => new Date(ms).toLocaleTimeString()
6+
7+export default function App() {
8+ const {snapshot, dispatch} = useNetwork()
9+ const {selfId, connectedAt, centralId, amCentral, roster, state, version} =
10+ snapshot
11+
12+ return (
13+ <div style={{fontFamily: 'sans-serif', maxWidth: 640, margin: '2rem auto', padding: '0 1rem'}}>
14+ <h1>CommonView</h1>
15+
16+ <section>
17+ <h2>You</h2>
18+ <div>ID: <code>{short(selfId)}</code></div>
19+ <div>Connected at: {time(connectedAt)}</div>
20+ <div>
21+ Role:{' '}
22+ <strong>{amCentral ? 'CENTRAL (source of truth)' : 'peer'}</strong>
23+ </div>
24+ <div>
25+ Central peer:{' '}
26+ <code>{centralId ? short(centralId) : '(none)'}</code>
27+ </div>
28+ </section>
29+
30+ <section>
31+ <h2>Shared state</h2>
32+ <div style={{fontSize: '2rem'}}>counter = {state.counter}</div>
33+ <div style={{color: '#666', fontSize: '0.85rem'}}>version {version}</div>
34+ <div style={{marginTop: '0.5rem'}}>
35+ <button onClick={() => dispatch({op: 'decrement'})}>−</button>{' '}
36+ <button onClick={() => dispatch({op: 'increment'})}>+</button>
37+ </div>
38+ <p style={{color: '#666', fontSize: '0.85rem'}}>
39+ Commands are sent to the central peer, which updates the authoritative
40+ state and broadcasts it back to everyone.
41+ </p>
42+ </section>
43+
44+ <section>
45+ <h2>Peers in room ({roster.length})</h2>
46+ <table style={{borderCollapse: 'collapse', width: '100%'}}>
47+ <thead>
48+ <tr style={{textAlign: 'left'}}>
49+ <th style={{padding: '0.25rem'}}>ID</th>
50+ <th style={{padding: '0.25rem'}}>Connected</th>
51+ <th style={{padding: '0.25rem'}}>Role</th>
52+ </tr>
53+ </thead>
54+ <tbody>
55+ {roster.map(p => (
56+ <tr key={p.peerId} style={{background: p.isSelf ? '#f0f0f0' : undefined}}>
57+ <td style={{padding: '0.25rem'}}>
58+ <code>{short(p.peerId)}</code>
59+ {p.isSelf ? ' (you)' : ''}
60+ </td>
61+ <td style={{padding: '0.25rem'}}>{time(p.connectedAt)}</td>
62+ <td style={{padding: '0.25rem'}}>
63+ {p.isCentral ? 'central' : 'peer'}
64+ </td>
65+ </tr>
66+ ))}
67+ </tbody>
68+ </table>
69+ </section>
70+ </div>
71+ )
72+}
src/main.tsxadded+9−0View file
@@ -0,0 +1,9 @@
1+import {StrictMode} from 'react'
2+import {createRoot} from 'react-dom/client'
3+import App from './App'
4+
5+createRoot(document.getElementById('root')!).render(
6+ <StrictMode>
7+ <App />
8+ </StrictMode>
9+)
src/p2p/identity.tsadded+96−0View file
@@ -0,0 +1,96 @@
1+import * as secp from '@noble/secp256k1'
2+
3+// The peer's identity is a secp256k1 / BIP340 (schnorr) keypair.
4+// - The x-only public key (hex) IS the peer ID.
5+// - The private key is persisted in localStorage so the identity survives reloads.
6+// - The same key signs both nostr events (for relay discovery/signaling) and
7+// every application-level message sent over WebRTC.
8+
9+const STORAGE_KEY = 'commonview:privkey'
10+
11+const toHex = (bytes: Uint8Array): string =>
12+ bytes.reduce((s, b) => s + b.toString(16).padStart(2, '0'), '')
13+
14+const fromHex = (hex: string): Uint8Array => {
15+ const out = new Uint8Array(hex.length / 2)
16+ for (let i = 0; i < out.length; i++) {
17+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
18+ }
19+ return out
20+}
21+
22+const loadOrCreateSecretKey = (): Uint8Array => {
23+ const existing = localStorage.getItem(STORAGE_KEY)
24+ if (existing && existing.length === 64) {
25+ return fromHex(existing)
26+ }
27+ const {secretKey} = secp.schnorr.keygen()
28+ localStorage.setItem(STORAGE_KEY, toHex(secretKey))
29+ return secretKey
30+}
31+
32+const secretKey = loadOrCreateSecretKey()
33+const publicKey = secp.schnorr.getPublicKey(secretKey)
34+
35+/** This peer's ID = its x-only public key, as hex. */
36+export const selfId: string = toHex(publicKey)
37+
38+const sha256 = async (str: string): Promise<Uint8Array> =>
39+ new Uint8Array(
40+ await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str))
41+ )
42+
43+/** Sign an arbitrary string payload; returns hex signature. */
44+export const sign = async (payload: string): Promise<string> =>
45+ toHex(await secp.schnorr.signAsync(await sha256(payload), secretKey))
46+
47+/** Verify a hex signature over a string payload against a peer's ID (pubkey hex). */
48+export const verify = async (
49+ payload: string,
50+ sigHex: string,
51+ pubkeyHex: string
52+): Promise<boolean> => {
53+ try {
54+ return await secp.schnorr.verifyAsync(
55+ fromHex(sigHex),
56+ await sha256(payload),
57+ fromHex(pubkeyHex)
58+ )
59+ } catch {
60+ return false
61+ }
62+}
63+
64+// ---- nostr event signing (schnorr over the nostr event id) ----
65+
66+export interface NostrEvent {
67+ id: string
68+ pubkey: string
69+ created_at: number
70+ kind: number
71+ tags: string[][]
72+ content: string
73+ sig: string
74+}
75+
76+/** Build and sign a nostr event with this peer's key. */
77+export const makeNostrEvent = async (
78+ kind: number,
79+ tags: string[][],
80+ content: string
81+): Promise<NostrEvent> => {
82+ const created_at = Math.floor(Date.now() / 1000)
83+ const serialized = JSON.stringify([
84+ 0,
85+ selfId,
86+ created_at,
87+ kind,
88+ tags,
89+ content
90+ ])
91+ const id = toHex(await sha256(serialized))
92+ const sig = toHex(await secp.schnorr.signAsync(fromHex(id), secretKey))
93+ return {id, pubkey: selfId, created_at, kind, tags, content, sig}
94+}
95+
96+export {toHex, fromHex}
src/p2p/network.tsadded+346−0View file
@@ -0,0 +1,346 @@
1+import {selfId, sign, verify} from './identity'
2+import {Nostr, peerTopic, rootTopic} from './nostr'
3+import {Peer, type Signal} from './peer'
4+
5+// ---------------------------------------------------------------------------
6+// Application state + commands. For this first version the shared state is just
7+// a single counter. Commands are dispatched to the central peer, which applies
8+// them to the authoritative state and broadcasts the result.
9+// ---------------------------------------------------------------------------
10+
11+export interface AppState {
12+ counter: number
13+}
14+
15+export type Command = {op: 'increment'} | {op: 'decrement'}
16+
17+const initialState = (): AppState => ({counter: 0})
18+
19+const applyCommand = (state: AppState, cmd: Command): AppState => {
20+ switch (cmd.op) {
21+ case 'increment':
22+ return {counter: state.counter + 1}
23+ case 'decrement':
24+ return {counter: state.counter - 1}
25+ default:
26+ return state
27+ }
28+}
29+
30+// ---------------------------------------------------------------------------
31+// Wire protocol (over the WebRTC data channel). Every message is a signed
32+// envelope: `data` is the exact JSON string that was signed, `from` is the
33+// sender's peer ID (public key), and `sig` is the schnorr signature.
34+// ---------------------------------------------------------------------------
35+
36+type Message =
37+ | {t: 'hello'; connectedAt: number}
38+ | {t: 'command'; cmd: Command; forwarded?: boolean}
39+ | {t: 'state'; state: AppState; version: number}
40+
41+interface Envelope {
42+ data: string
43+ from: string
44+ sig: string
45+}
46+
47+// ---------------------------------------------------------------------------
48+
49+export interface RosterEntry {
50+ peerId: string
51+ connectedAt: number
52+ isSelf: boolean
53+ isCentral: boolean
54+}
55+
56+export interface Snapshot {
57+ selfId: string
58+ connectedAt: number
59+ centralId: string | null
60+ amCentral: boolean
61+ roster: RosterEntry[]
62+ state: AppState
63+ version: number
64+}
65+
66+const ANNOUNCE_INTERVAL_MS = 5000
67+const ROOM_ID = 'default'
68+
69+interface Connection {
70+ peer: Peer
71+ connectedAt: number | null // self-reported timestamp from the remote peer
72+}
73+
74+export class Network {
75+ private nostr = new Nostr()
76+ private root = ''
77+ private connections = new Map<string, Connection>()
78+
79+ private connectedAt = Date.now()
80+ private state: AppState = initialState()
81+ private version = 0
82+
83+ private snapshot!: Snapshot
84+ private listeners = new Set<() => void>()
85+
86+ constructor() {
87+ this.rebuildSnapshot()
88+ void this.start()
89+
90+ window.addEventListener('online', () => {
91+ // Regaining a connection counts as a reconnect: new timestamp.
92+ this.connectedAt = Date.now()
93+ this.broadcastHello()
94+ this.recompute()
95+ })
96+ }
97+
98+ private async start() {
99+ this.root = await rootTopic(ROOM_ID)
100+
101+ // Receive WebRTC signaling addressed to us.
102+ const selfSignalTopic = await peerTopic(this.root, selfId)
103+ this.nostr.subscribe(selfSignalTopic, (content, from) => {
104+ if (from === selfId) return
105+ let signal: Signal
106+ try {
107+ signal = JSON.parse(content)
108+ } catch {
109+ return
110+ }
111+ this.handleSignal(from, signal)
112+ })
113+
114+ // Discover peers via announcements on the root topic.
115+ this.nostr.subscribe(this.root, (content, from) => {
116+ if (from === selfId) return
117+ let ann: {peerId?: string}
118+ try {
119+ ann = JSON.parse(content)
120+ } catch {
121+ return
122+ }
123+ if (ann.peerId && ann.peerId === from) this.maybeConnect(from)
124+ })
125+
126+ const announce = () =>
127+ void this.nostr.publish(this.root, JSON.stringify({peerId: selfId}))
128+ announce()
129+ setInterval(announce, ANNOUNCE_INTERVAL_MS)
130+ }
131+
132+ // ---- connection setup -------------------------------------------------
133+
134+ private maybeConnect(peerId: string) {
135+ if (peerId === selfId || this.connections.has(peerId)) return
136+ // Deterministic initiator: the peer with the smaller ID makes the offer.
137+ const initiator = selfId < peerId
138+ this.createPeer(peerId, initiator)
139+ }
140+
141+ private createPeer(peerId: string, initiator: boolean): Connection {
142+ const peer = new Peer(initiator)
143+ const conn: Connection = {peer, connectedAt: null}
144+ this.connections.set(peerId, conn)
145+
146+ peer.setHandlers({
147+ signal: signal => {
148+ void this.sendSignal(peerId, signal)
149+ },
150+ connect: () => {
151+ // Tell the new peer our self-reported connect time.
152+ void this.sendTo(peerId, {t: 'hello', connectedAt: this.connectedAt})
153+ // If we're central, sync the newcomer immediately.
154+ if (this.amCentral()) void this.broadcastState()
155+ this.recompute()
156+ },
157+ data: raw => void this.handleData(peerId, raw),
158+ close: () => {
159+ if (this.connections.get(peerId)?.peer === peer) {
160+ this.connections.delete(peerId)
161+ this.recompute()
162+ }
163+ }
164+ })
165+
166+ return conn
167+ }
168+
169+ private async sendSignal(peerId: string, signal: Signal) {
170+ const topic = await peerTopic(this.root, peerId)
171+ void this.nostr.publish(topic, JSON.stringify(signal))
172+ }
173+
174+ private handleSignal(from: string, signal: Signal) {
175+ let conn = this.connections.get(from)
176+ if (!conn) {
177+ if (signal.type !== 'offer') return // nothing to attach it to yet
178+ conn = this.createPeer(from, false)
179+ }
180+ void conn.peer.signal(signal)
181+ }
182+
183+ // ---- messaging --------------------------------------------------------
184+
185+ private async sendTo(peerId: string, msg: Message) {
186+ const conn = this.connections.get(peerId)
187+ if (!conn) return
188+ const data = JSON.stringify(msg)
189+ const sig = await sign(data)
190+ const env: Envelope = {data, from: selfId, sig}
191+ conn.peer.send(JSON.stringify(env))
192+ }
193+
194+ private async broadcast(msg: Message) {
195+ const data = JSON.stringify(msg)
196+ const sig = await sign(data)
197+ const env: Envelope = {data, from: selfId, sig}
198+ const payload = JSON.stringify(env)
199+ for (const conn of this.connections.values()) conn.peer.send(payload)
200+ }
201+
202+ private broadcastHello() {
203+ void this.broadcast({t: 'hello', connectedAt: this.connectedAt})
204+ }
205+
206+ private async broadcastState() {
207+ await this.broadcast({t: 'state', state: this.state, version: this.version})
208+ }
209+
210+ private async handleData(from: string, raw: string) {
211+ let env: Envelope
212+ try {
213+ env = JSON.parse(raw)
214+ } catch {
215+ return
216+ }
217+ // The envelope must be signed by the peer we received it from.
218+ if (env.from !== from) return
219+ if (!(await verify(env.data, env.sig, env.from))) return
220+
221+ let msg: Message
222+ try {
223+ msg = JSON.parse(env.data)
224+ } catch {
225+ return
226+ }
227+
228+ switch (msg.t) {
229+ case 'hello': {
230+ const conn = this.connections.get(from)
231+ if (conn) {
232+ conn.connectedAt = msg.connectedAt
233+ this.recompute()
234+ }
235+ break
236+ }
237+ case 'command': {
238+ if (this.amCentral()) {
239+ this.state = applyCommand(this.state, msg.cmd)
240+ this.version++
241+ await this.broadcastState()
242+ this.rebuildSnapshot()
243+ } else if (!msg.forwarded) {
244+ // Not central; forward once toward the central peer.
245+ const central = this.centralId()
246+ if (central && this.connections.has(central)) {
247+ void this.sendTo(central, {...msg, forwarded: true})
248+ }
249+ }
250+ break
251+ }
252+ case 'state': {
253+ // Only trust state from the current central peer.
254+ if (from === this.centralId()) {
255+ this.state = msg.state
256+ this.version = msg.version
257+ this.rebuildSnapshot()
258+ }
259+ break
260+ }
261+ }
262+ }
263+
264+ // ---- central-peer election -------------------------------------------
265+
266+ /** All peers we know about, with a reported connect time, plus ourselves. */
267+ private participants(): {peerId: string; connectedAt: number}[] {
268+ const list = [{peerId: selfId, connectedAt: this.connectedAt}]
269+ for (const [peerId, conn] of this.connections) {
270+ if (conn.peer.isConnected && conn.connectedAt !== null) {
271+ list.push({peerId, connectedAt: conn.connectedAt})
272+ }
273+ }
274+ return list
275+ }
276+
277+ /** The oldest peer (smallest connect time; ties broken by peer ID) is central. */
278+ private centralId(): string | null {
279+ const list = this.participants()
280+ if (list.length === 0) return null
281+ return list.reduce((oldest, p) =>
282+ p.connectedAt < oldest.connectedAt ||
283+ (p.connectedAt === oldest.connectedAt && p.peerId < oldest.peerId)
284+ ? p
285+ : oldest
286+ ).peerId
287+ }
288+
289+ private amCentral(): boolean {
290+ return this.centralId() === selfId
291+ }
292+
293+ private recompute() {
294+ const wasCentral = this.snapshot.amCentral
295+ this.rebuildSnapshot()
296+ // If we just became central, our last-known state is now the source of
297+ // truth — push it to everyone.
298+ if (!wasCentral && this.snapshot.amCentral) void this.broadcastState()
299+ }
300+
301+ // ---- public API -------------------------------------------------------
302+
303+ dispatch(cmd: Command) {
304+ if (this.amCentral()) {
305+ this.state = applyCommand(this.state, cmd)
306+ this.version++
307+ void this.broadcastState()
308+ this.rebuildSnapshot()
309+ } else {
310+ const central = this.centralId()
311+ if (central && this.connections.has(central)) {
312+ void this.sendTo(central, {t: 'command', cmd})
313+ }
314+ }
315+ }
316+
317+ getSnapshot = (): Snapshot => this.snapshot
318+
319+ subscribe = (listener: () => void): (() => void) => {
320+ this.listeners.add(listener)
321+ return () => this.listeners.delete(listener)
322+ }
323+
324+ private rebuildSnapshot() {
325+ const centralId = this.centralId()
326+ const roster: RosterEntry[] = this.participants()
327+ .map(p => ({
328+ peerId: p.peerId,
329+ connectedAt: p.connectedAt,
330+ isSelf: p.peerId === selfId,
331+ isCentral: p.peerId === centralId
332+ }))
333+ .sort((a, b) => a.connectedAt - b.connectedAt)
334+
335+ this.snapshot = {
336+ selfId,
337+ connectedAt: this.connectedAt,
338+ centralId,
339+ amCentral: centralId === selfId,
340+ roster,
341+ state: this.state,
342+ version: this.version
343+ }
344+ for (const l of this.listeners) l()
345+ }
346+}
src/p2p/nostr.tsadded+137−0View file
@@ -0,0 +1,137 @@
1+import {makeNostrEvent, type NostrEvent} from './identity'
2+
3+// Minimal nostr client, modeled on trystero's nostr strategy but trimmed to
4+// only what we need: publish to a topic, and subscribe to a topic. Topics are
5+// carried in an 'x' tag; each topic maps to an ephemeral event kind (20000+)
6+// so relays don't store the messages.
7+
8+const RELAYS = [
9+ 'wss://relay.damus.io',
10+ 'wss://nos.lol',
11+ 'wss://relay.mostr.pub',
12+ 'wss://purplerelay.com'
13+]
14+
15+const TAG = 'x'
16+
17+const strToNum = (str: string, limit: number): number => {
18+ let sum = 0
19+ for (let i = 0; i < str.length; i++) sum += str.charCodeAt(i)
20+ return sum % limit
21+}
22+
23+const kindForTopic = (topic: string): number => strToNum(topic, 10000) + 20000
24+
25+const nowSec = (): number => Math.floor(Date.now() / 1000)
26+
27+const genSubId = (): string =>
28+ Array.from({length: 16}, () =>
29+ Math.floor(Math.random() * 16).toString(16)
30+ ).join('')
31+
32+type TopicHandler = (content: string, fromPubkey: string) => void
33+
34+export class Nostr {
35+ private sockets: WebSocket[] = []
36+ private subs = new Map<string, {topic: string; handler: TopicHandler}>()
37+
38+ constructor() {
39+ for (const url of RELAYS) this.connect(url)
40+ }
41+
42+ private connect(url: string) {
43+ let ws: WebSocket
44+ try {
45+ ws = new WebSocket(url)
46+ } catch {
47+ return
48+ }
49+ this.sockets.push(ws)
50+
51+ ws.onopen = () => {
52+ // (re)send all active subscriptions on this socket
53+ for (const [subId, {topic}] of this.subs) this.sendReq(ws, subId, topic)
54+ }
55+
56+ ws.onmessage = ev => {
57+ let msg: unknown
58+ try {
59+ msg = JSON.parse(ev.data as string)
60+ } catch {
61+ return
62+ }
63+ if (!Array.isArray(msg) || msg[0] !== 'EVENT') return
64+ const subId = msg[1] as string
65+ const event = msg[2] as NostrEvent
66+ const sub = this.subs.get(subId)
67+ if (sub && event && typeof event.content === 'string') {
68+ sub.handler(event.content, event.pubkey)
69+ }
70+ }
71+
72+ ws.onclose = () => {
73+ this.sockets = this.sockets.filter(s => s !== ws)
74+ // reconnect after a short delay
75+ setTimeout(() => this.connect(url), 3000)
76+ }
77+
78+ ws.onerror = () => ws.close()
79+ }
80+
81+ private sendReq(ws: WebSocket, subId: string, topic: string) {
82+ if (ws.readyState !== WebSocket.OPEN) return
83+ ws.send(
84+ JSON.stringify([
85+ 'REQ',
86+ subId,
87+ {kinds: [kindForTopic(topic)], since: nowSec(), ['#' + TAG]: [topic]}
88+ ])
89+ )
90+ }
91+
92+ /** Subscribe to a topic. Handler fires for each incoming event. */
93+ subscribe(topic: string, handler: TopicHandler): () => void {
94+ const subId = genSubId()
95+ this.subs.set(subId, {topic, handler})
96+ for (const ws of this.sockets) this.sendReq(ws, subId, topic)
97+ return () => {
98+ this.subs.delete(subId)
99+ for (const ws of this.sockets) {
100+ if (ws.readyState === WebSocket.OPEN) {
101+ ws.send(JSON.stringify(['CLOSE', subId]))
102+ }
103+ }
104+ }
105+ }
106+
107+ /** Publish a signed event to a topic. */
108+ async publish(topic: string, content: string): Promise<void> {
109+ const event = await makeNostrEvent(
110+ kindForTopic(topic),
111+ [[TAG, topic]],
112+ content
113+ )
114+ const payload = JSON.stringify(['EVENT', event])
115+ for (const ws of this.sockets) {
116+ if (ws.readyState === WebSocket.OPEN) ws.send(payload)
117+ }
118+ }
119+}
120+
121+const sha256Hex = async (str: string): Promise<string> => {
122+ const buf = await crypto.subtle.digest(
123+ 'SHA-256',
124+ new TextEncoder().encode(str)
125+ )
126+ return Array.from(new Uint8Array(buf))
127+ .map(b => b.toString(16).padStart(2, '0'))
128+ .join('')
129+}
130+
131+/** Topic everyone in a room announces on / listens to for discovery. */
132+export const rootTopic = (roomId: string): Promise<string> =>
133+ sha256Hex(`commonview:${roomId}`)
134+
135+/** Per-peer topic used to deliver WebRTC signaling to a specific peer. */
136+export const peerTopic = (root: string, peerId: string): Promise<string> =>
137+ sha256Hex(`${root}:${peerId}`)
src/p2p/peer.tsadded+146−0View file
@@ -0,0 +1,146 @@
1+// A thin WebRTC wrapper, distilled from trystero's peer.ts. We only need a
2+// reliable ordered data channel plus offer/answer/ICE signaling. To keep things
3+// simple we avoid "perfect negotiation" glare handling by ensuring only ONE
4+// side (a deterministically chosen initiator) ever creates the offer.
5+
6+export type Signal =
7+ | {type: 'offer'; sdp: string}
8+ | {type: 'answer'; sdp: string}
9+ | {type: 'candidate'; candidate: RTCIceCandidateInit}
10+
11+export interface PeerHandlers {
12+ signal: (signal: Signal) => void
13+ connect: () => void
14+ data: (data: string) => void
15+ close: () => void
16+}
17+
18+const ICE_SERVERS: RTCIceServer[] = [
19+ {urls: 'stun:stun.l.google.com:19302'},
20+ {urls: 'stun:stun1.l.google.com:19302'},
21+ {urls: 'stun:stun.cloudflare.com:3478'}
22+]
23+
24+export class Peer {
25+ private pc: RTCPeerConnection
26+ private channel: RTCDataChannel | null = null
27+ private handlers: Partial<PeerHandlers> = {}
28+ private pendingCandidates: RTCIceCandidateInit[] = []
29+ private closed = false
30+
31+ constructor(private initiator: boolean) {
32+ this.pc = new RTCPeerConnection({iceServers: ICE_SERVERS})
33+
34+ this.pc.onicecandidate = ({candidate}) => {
35+ if (candidate) {
36+ this.handlers.signal?.({type: 'candidate', candidate: candidate.toJSON()})
37+ }
38+ }
39+
40+ this.pc.onconnectionstatechange = () => {
41+ const s = this.pc.connectionState
42+ if (s === 'failed' || s === 'closed' || s === 'disconnected') {
43+ this.destroy()
44+ }
45+ }
46+
47+ if (initiator) {
48+ this.setupChannel(this.pc.createDataChannel('data'))
49+ this.pc.onnegotiationneeded = () => void this.makeOffer()
50+ } else {
51+ this.pc.ondatachannel = ({channel}) => this.setupChannel(channel)
52+ }
53+ }
54+
55+ setHandlers(handlers: Partial<PeerHandlers>) {
56+ Object.assign(this.handlers, handlers)
57+ }
58+
59+ private setupChannel(channel: RTCDataChannel) {
60+ this.channel = channel
61+ channel.onopen = () => this.handlers.connect?.()
62+ channel.onclose = () => this.destroy()
63+ channel.onmessage = e => this.handlers.data?.(e.data as string)
64+ }
65+
66+ private async makeOffer() {
67+ if (this.closed) return
68+ try {
69+ await this.pc.setLocalDescription(await this.pc.createOffer())
70+ this.handlers.signal?.({
71+ type: 'offer',
72+ sdp: this.pc.localDescription!.sdp
73+ })
74+ } catch {
75+ /* ignore */
76+ }
77+ }
78+
79+ async signal(signal: Signal) {
80+ if (this.closed) return
81+ try {
82+ if (signal.type === 'candidate') {
83+ if (this.pc.remoteDescription) {
84+ await this.pc.addIceCandidate(signal.candidate)
85+ } else {
86+ this.pendingCandidates.push(signal.candidate)
87+ }
88+ return
89+ }
90+
91+ if (signal.type === 'offer') {
92+ if (this.initiator) return // initiators never accept remote offers
93+ await this.pc.setRemoteDescription({type: 'offer', sdp: signal.sdp})
94+ await this.flushCandidates()
95+ await this.pc.setLocalDescription(await this.pc.createAnswer())
96+ this.handlers.signal?.({
97+ type: 'answer',
98+ sdp: this.pc.localDescription!.sdp
99+ })
100+ return
101+ }
102+
103+ if (signal.type === 'answer') {
104+ await this.pc.setRemoteDescription({type: 'answer', sdp: signal.sdp})
105+ await this.flushCandidates()
106+ }
107+ } catch {
108+ /* ignore transient signaling errors */
109+ }
110+ }
111+
112+ private async flushCandidates() {
113+ const queued = this.pendingCandidates.splice(0)
114+ for (const c of queued) {
115+ try {
116+ await this.pc.addIceCandidate(c)
117+ } catch {
118+ /* ignore */
119+ }
120+ }
121+ }
122+
123+ send(data: string) {
124+ if (this.channel?.readyState === 'open') this.channel.send(data)
125+ }
126+
127+ get isConnected(): boolean {
128+ return this.channel?.readyState === 'open'
129+ }
130+
131+ destroy() {
132+ if (this.closed) return
133+ this.closed = true
134+ try {
135+ this.channel?.close()
136+ } catch {
137+ /* ignore */
138+ }
139+ try {
140+ this.pc.close()
141+ } catch {
142+ /* ignore */
143+ }
144+ this.handlers.close?.()
145+ }
146+}
src/useNetwork.tsadded+14−0View file
@@ -0,0 +1,14 @@
1+import {useSyncExternalStore} from 'react'
2+import {Network, type Snapshot} from './p2p/network'
3+
4+// A single Network instance for the whole app (module-level so React StrictMode
5+// double-mounting doesn't create two peer networks).
6+const network = new Network()
7+
8+export const useNetwork = (): {
9+ snapshot: Snapshot
10+ dispatch: Network['dispatch']
11+} => {
12+ const snapshot = useSyncExternalStore(network.subscribe, network.getSnapshot)
13+ return {snapshot, dispatch: cmd => network.dispatch(cmd)}
14+}
tsconfig.jsonadded+20−0View file
@@ -0,0 +1,20 @@
1+{
2+ "compilerOptions": {
3+ "target": "ES2020",
4+ "useDefineForClassFields": true,
5+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6+ "module": "ESNext",
7+ "skipLibCheck": true,
8+ "moduleResolution": "bundler",
9+ "allowImportingTsExtensions": true,
10+ "resolveJsonModule": true,
11+ "isolatedModules": true,
12+ "noEmit": true,
13+ "jsx": "react-jsx",
14+ "strict": true,
15+ "noUnusedLocals": true,
16+ "noUnusedParameters": true,
17+ "noFallthroughCasesInSwitch": true
18+ },
19+ "include": ["src"]
20+}
tsconfig.tsbuildinfoadded+1−0View file
@@ -0,0 +1 @@
1+{"root":["./src/App.tsx","./src/main.tsx","./src/useNetwork.ts","./src/p2p/identity.ts","./src/p2p/network.ts","./src/p2p/nostr.ts","./src/p2p/peer.ts"],"version":"5.9.3"}
vite.config.tsadded+6−0View file
@@ -0,0 +1,6 @@
1+import {defineConfig} from 'vite'
2+import react from '@vitejs/plugin-react'
3+
4+export default defineConfig({
5+ plugins: [react()]
6+})