OverviewFilesCommitsIssuesDocsReleasesPeers

p2pbuilders commits / a3cf156

initial public release
p2pbuilders — permissionless p2p hacker news on the pear/bare stack. single feed, hypercore per identity, hashcash-gated posts, reputation-weighted voting, auto-seeded to the p2p-hiverelay network. see README.md for the live pear:// link, SPEC.md for the data model + design. 41 tests green across 11 milestones.
a3cf15608a8dec3cf755897ba7622cd1c0c70c7f
by p2pbuilders <noreply@p2pbuilders.dev> · 2026-04-19T05:58:26.000Z

Changed files (61) +11155 -0

.gitignore +31 -0
……@@ -0,0 +1,31 @@
1# deps
2node_modules/
3
4# local storage / secrets (corestore data + identity keys)
5.data/
6.hiverelay-publish/
7.hiverelay-seed/
8.hiverelay-probe/
9.p2pbuilders.key
10primary.key
11
12# iOS build artifacts
13ios-app/build/
14ios-app/P2PBuilders.xcodeproj/
15ios-app/Pods/
16ios-app/P2PBuilders/Resources/backend.bundle.mjs
17ios-app/P2PBuilders/Resources/index.html
18ios-app/P2PBuilders/Resources/app.js
19ios-app/P2PBuilders/Resources/styles.css
20ios-app/P2PBuilders/Resources/transport.js
21
22# editor / OS
23.DS_Store
24.vscode/
25.idea/
26*.swp
27*.swo
28
29# logs
30*.log
31npm-debug.log*
LICENSE +15 -0
……@@ -0,0 +1,15 @@
1 Apache License
2 Version 2.0, January 2004
3 http://www.apache.org/licenses/
4
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
README.md +126 -0
……@@ -0,0 +1,126 @@
1# p2pbuilders
2
3A permissionless, peer-to-peer Hacker News clone on the [Pear](https://docs.pears.com/) + [Bare](https://github.com/holepunchto/bare) stack. Single feed, one hypercore per identity, hashcash-gated posts, reputation-weighted voting. Posts are mirrored across the [p2p-hiverelay](https://github.com/bigdestiny2/P2P-Hiverelay) network so they stay online when authors close their laptops.
4
5## Try it
6
7Install Pear once:
8
9```
10npm i -g pear
11pear # bootstraps the runtime
12export PATH="$HOME/Library/Application Support/pear/bin:$PATH"
13```
14
15Run p2pbuilders:
16
17```
18pear run pear://gopfpwat99tcuaakasfnftrds3j6t7srdmi3qidbhm9xeizt1a5y
19```
20
21Type `help` at the prompt.
22
23## What's in here
24
25```
26src/
27├── backend/ shared Node/Bare backend (ops, PoW, indexer, reputation, antispam)
28│ ├── ops.js signed append-only op schema (post, comment, vote, edit, tombstone,
29│ │ follow, block, profile, board_create, blocklist_publish)
30│ ├── pow.js blake2b hashcash — ~80ms on post, ~20ms on comment
31│ ├── node.js Node class: wraps corestore/hypercore for a user identity
32│ ├── indexer.js local hyperbee folding all replicated cores into queryable views
33│ ├── reputation.js age + received-upvote weighted voting
34│ ├── swarm.js hyperswarm + protomux announce channel for peer discovery
35│ ├── rpc.js JSON-RPC dispatch shared by every transport
36│ └── server.js optional Node HTTP/WS dev server
37├── terminal/
38│ └── main.js the Pear terminal entry (readline UI)
39├── bare/
40│ ├── harness.js Pear/bare-kit shared backend bootstrap
41│ └── ios-entry.js bare-kit-pear iOS entry
42├── pear/
43│ └── worker.js Pear desktop worker (parked)
44└── relay/
45 └── server.js self-hosted hiverelay with @hyperswarm/dht-relay endpoint
46
47public/ HTML/CSS/JS for the browser/desktop UI (dev mode)
48ios-app/ minimal Xcode project using bare-kit-pear (parked)
49scripts/
50 publish.js publish a directory to hiverelay
51 seed-pear.js ask hiverelay to seed a pear:// key
52 probe-relays.mjs list relays currently pinning a key
53test/ 41 tests covering every layer
54```
55
56## How it works
57
58**Identity** — each user is a single hypercore keyed by a 32-byte primary key stored locally by Corestore. The public key is the user's global identity; there's no signup or central registry.
59
60**Ops** — every block in a user's core is one signed op (post/comment/vote/edit/tombstone/etc.). Hypercore's built-in block signatures authenticate every op automatically.
61
62**PoW** — posts, comments, and board creation carry a blake2b hashcash nonce. Default difficulty: 18 bits for posts (~80ms), 16 for comments (~20ms). The indexer refuses to index under-PoW ops.
63
64**Reputation** — `weight = clamp(log2(1 + ageDays) * sqrt(1 + received) / 50, 0.02, 1.0)`. New keys always get a 0.02 floor (you can vote from day one; you just don't move the needle until you stick around). Full table: see [SPEC.md](./SPEC.md) §6.2.
65
66**Discovery** — users find each other via Hyperswarm on a deterministic topic hash. A protomux "announce" channel gossips known pubkeys; anyone joining a board auto-tracks everyone else's hypercore within seconds.
67
68**Persistence** — on boot, the terminal client asks [p2p-hiverelay](https://github.com/bigdestiny2/P2P-Hiverelay) to seed its own hypercore + every peer it tracks. With 5+ relays across 3 continents, posts outlive the author's uptime.
69
70**Moderation** — an `ADMIN_PUBKEY` constant (one identity) can delete anyone's posts. Normal users can only delete their own. The indexer enforces the rule — admin-authored tombstones are honored regardless of target, others are dropped unless the author matches.
71
72## Develop
73
74```
75git clone <this-repo>
76cd p2pbuilders
77npm install
78
79# run the Node dev server (browser at http://localhost:8787)
80npm run dev
81
82# run the terminal app under standalone Bare (no Pear needed)
83./node_modules/bare/bin/bare src/terminal/main.js
84
85# run the full test suite (41 tests)
86npm test
87```
88
89## Stage a new build to Pear
90
91```
92# 1. make sure /tmp/p2pbuilders symlinks to this project (Pear chokes on paths
93# containing spaces inside pear-electron/pre)
94ln -sfn "$(pwd)" /tmp/p2pbuilders
95
96# 2. stage from the space-free path
97cd /tmp/p2pbuilders
98pear stage --no-ask dev . # prints pear://0.<n>.<key>
99
100# 3. run it
101pear run pear://<that-key>
102```
103
104## Seed an app or user to hiverelay
105
106```
107node scripts/publish.js # publish ./public as a hyperdrive
108node scripts/seed-pear.js pear://<pear-key>
109node scripts/probe-relays.mjs # see which relays are pinning it
110```
111
112## Status
113
114**Shipping now:**
115- Terminal app via `pear run pear://…`
116- Full backend verified with 41 tests
117- Auto-seeding to the public hiverelay network
118
119**Parked:**
120- Pear desktop GUI — `pear-electron/pre` keeps timing out on some installs; the scaffolding is in [src/pear/](./src/pear/) for when we revisit.
121- iOS via [bare-kit-pear](https://github.com/bigdestiny2/bare-kit-pear) — entry at [src/bare/ios-entry.js](./src/bare/ios-entry.js), Swift integration in [ios-app/](./ios-app/) ready to build in Xcode.
122- PearBrowser — would require a rewrite onto `window.pear.sync`'s Autobase model. See [SPEC.md §11.3](./SPEC.md).
123
124## License
125
126Apache 2.0. See [LICENSE](./LICENSE).
SPEC.md +328 -0
……@@ -0,0 +1,328 @@
1# P2PBuilders — Spec v0.1
2
3A permissionless, peer-to-peer, Reddit-style messageboard on the Pear/Bare stack.
4
5No servers, no central moderators, no account registration. Every user runs a node; the board is the sum of everyone's signed append-only logs.
6
7---
8
9## 1. Goals & non-goals
10
11**Goals**
12- Hacker News-shaped UX: **single feed**, title + optional URL + optional text body, threaded comments, upvote-driven ranking.
13- Permissionless posting — anyone can spin up a key and start posting.
14- Basic antispam/sybil resistance without a central authority.
15- Works offline-first; syncs when online; stays available via hiverelay when author is offline.
16- Small, auditable op schema that won't need a painful migration in 6 months.
17
18**Non-goals (for v0.1)**
19- Sub-feeds / subreddits — the data model supports boards but the UI defaults to a single `front` feed.
20- Private/DM threads.
21- Rich media (images/video) — text + external links only.
22- Federation with non-Pear systems.
23- Strong sybil resistance (e.g. proof-of-stake, social graph verification). We target "raise the cost of spam," not "prevent sybils."
24
25> **Pivot note (v0.1a):** p2pbuilders was originally designed Reddit-shaped (boards + browsable board list). We've pivoted to HN-shaped because it dramatically narrows UI scope. The **data model retains the `board` field on posts** (everything on-chain still supports multiple boards); we just always post to `front` by default and hide the board-chooser from the primary UI. Power users can still query other boards via the backend API.
26
27---
28
29## 2. Stack
30
31| Layer | Choice | Why |
32| -------------- | ------------------------- | ---------------------------------------- |
33| Runtime | Pear (desktop) + Bare | P2P-native, no browser sandbox limits |
34| Storage | Corestore + Hypercore | Signed append-only log per identity |
35| Index | Hyperbee | Ordered queries over folded state |
36| Discovery | Hyperswarm (DHT) | Topic-based peer finding |
37| Always-on | p2p-hiverelay | Keeps user cores seeded when offline |
38| Crypto | `sodium-universal` | Ed25519 sigs, blake2b hashes |
39| Serialization | `compact-encoding` (cenc) | Deterministic, versionable binary format |
40
41---
42
43## 3. Identity
44
45- On first launch, each user generates a random **32-byte primary key** and persists it locally (mode 0600).
46- The user's **personal Hypercore** is derived by Corestore from `(primaryKey, "p2pbuilders/user")`. Its **public key** is the user's global identity, referenced as `pubkey` throughout the spec.
47- No usernames, no registry — nicknames are advisory (see `profile` op).
48- All of a user's ops live in that single personal Hypercore. Hypercore signs every block with the derived keypair; op-level authenticity is implicit, no per-op signatures.
49- Secrets-to-protect: **the primary key**. If it leaks, someone else can write to your core. If you lose it, you lose your identity — there is no recovery path in v0.1.
50- Users MAY publish a `profile` op (nickname, bio, avatar blob key) as block 0 of their core.
51
52---
53
54## 4. Data model
55
56### 4.1 Ops (all encoded with `compact-encoding`)
57
58Every block in a user's core is one op. Version byte is first to allow forward-compatible migrations.
59
60```
61Op := {
62 v: uint8 // schema version, starts at 1
63 type: uint8 // 1=post 2=comment 3=vote 4=edit 5=tombstone
64 // 6=follow 7=block 8=profile
65 // 9=board_create 10=blocklist_publish
66 ts: uint64 // author-claimed ms timestamp (advisory)
67 payload: <type-specific>
68 pow?: { bits: uint8, nonce: buffer } // required on post/comment/board_create
69}
70```
71
72### 4.2 Op IDs
73
74`opId = authorPubkey ++ blockSeq` (40 bytes). Deterministic, collision-free, no hashing required. Posts, comments, votes all referenced this way.
75
76### 4.3 Type payloads
77
78```
79post: { board: string, title: string, body: string, link?: string }
80comment: { parent: opId, body: string } // parent is post OR comment
81vote: { target: opId, dir: int8 } // +1 or -1; 0 = retract
82edit: { target: opId, body: string, title?: string }
83tombstone: { target: opId }
84follow: { target: pubkey }
85block: { target: pubkey, public: bool } // public=true → gossipable
86profile: { nick: string, bio?: string, avatar?: blobKey }
87board_create: { name: string, description: string, minPowBits: uint8 }
88blocklist_publish: { list: [pubkey], version: uint32 }
89```
90
91Board names are lowercase `[a-z0-9_-]{2,32}`. First `board_create` op observed for a name wins; later ones are ignored.
92
93---
94
95## 5. Proof of work (antispam layer 1)
96
97Fast and simple. Applied to `post`, `comment`, and `board_create` ops.
98
99### 5.1 Algorithm
100
101```
102digest = blake2b(cenc.encode(opWithoutPow))
103target = 2^(256 - bits)
104find nonce such that blake2b(digest ++ nonce) < target
105```
106
107### 5.2 Parameters
108
109| Op | Default bits | Rough time on M-class laptop |
110| -------------- | ------------ | ---------------------------- |
111| `comment` | 16 | ~20 ms |
112| `post` | 18 | ~80 ms |
113| `board_create` | 22 | ~1.3 s |
114
115Boards MAY declare `minPowBits` higher than defaults (e.g. for busy boards). Peers refuse to index ops below the board's declared minimum.
116
117PoW is non-authoritative — it's an indexing policy, not a consensus rule. A peer can accept anything it wants into its local view; the default indexer rejects under-PoW ops.
118
119### 5.3 Why not heavier PoW?
120
121We want sub-second for regular use. This is a speed-bump, not a wall. Layers 2–4 do the rest.
122
123---
124
125## 6. Antispam layers 2–4
126
127### 6.1 Rate limits (layer 2)
128
129Default indexer policy (per-peer, configurable):
130
131- 10 posts/hour per pubkey
132- 60 comments/hour per pubkey
133- 600 votes/hour per pubkey
134
135Violations cause the indexer to drop that core's subsequent ops for a cooldown (1h → 24h on repeat). Core is still replicated (cheap), just not indexed.
136
137### 6.2 Reputation weighting (layer 3)
138
139```
140rep(key) = log2(1 + max(ageDays, 1/24)) * sqrt(1 + receivedUpvotes)
141voteWeight(key) = clamp(rep(key) / 50, 0.02, 1.0)
142```
143
144`ageDays` is bounded by the earliest op observed across the network — not author-claimed — to prevent backdating.
145
146Sort orders (hot/top) use weighted vote sums. Raw vote counts still shown in the UI.
147
148#### 6.2.1 Simulation results
149
150| Scenario | Weight |
151| ----------------------------- | -------- |
152| Brand new (floor) | 0.020 |
153| 1 day, lurker | 0.020 |
154| 7 days, 5 upvotes | 0.147 |
155| 30 days, 10 upvotes | 0.329 |
156| 90 days, 50 upvotes | 0.929 |
157| 365 days, 500 upvotes | 1.000 |
158| Viral day-1 post, 1000 upvts | 0.633 |
159
160The **0.02 floor** means new users can always upvote; they just don't move the needle until they stick around.
161
162#### 6.2.2 Sybil cost analysis
163
164| Attacker setup | Effective weight | Cost |
165| ------------------------------------------- | ---------------- | ------------------------------- |
166| 100 fresh keys, no cross-voting | 2.0 | ~8s PoW, rate-limited |
167| 100 keys aged 30d, 1 mutual upvote each | 14.0 | 30d patience |
168| 100 keys aged 30d, 10 mutual upvotes each | 32.9 | 30d patience + coordinated PoW |
169
170Reference: a legit 90-day user with 50 upvotes has weight 0.93. The worst attacker row above ≈ 35 legit users. **Acceptable for v0.1 as a speed bump. Not acceptable as the final answer.**
171
172#### 6.2.3 Known weakness: cross-voting rings
173
174The table in §6.2.2 shows cross-voting is the real attack. v0.1 does **not** detect it. Future work (v0.2+):
175
176- Cluster detection: if a set of keys overwhelmingly upvotes each other relative to the rest of the network, discount their mutual votes.
177- Per-curator blocklists (§6.3) already let humans flag rings manually.
178
179This limitation is called out explicitly so nobody thinks PoW + rep = sybil-proof. It isn't.
180
181### 6.3 Blocklists (layer 4)
182
183- Every user maintains a **local blocklist**. Blocked keys' ops are hidden from their UI.
184- Users MAY publish a `blocklist_publish` op. Others can **subscribe** — treating that curator's blocks as their own.
185- No forced moderation. A board is whatever your subscribed blocklists let through.
186- Sybil mitigation: a handful of widely-subscribed curators creates de-facto moderation without giving anyone board ownership.
187
188---
189
190## 7. Discovery & replication
191
192### 7.1 Topics
193
194- **Per-board topic**: `blake2b('p2pbuilders:board:v1:' + boardName)` → Hyperswarm topic.
195- **Global roster topic**: `blake2b('p2pbuilders:roster:v1')` — peers gossip known user-core keys here. Bootstraps new nodes.
196
197### 7.2 Protocol on connect
198
1991. Exchange capabilities (version, supported op types).
2002. Exchange **core-key manifests**: "I have these author cores at these lengths."
2013. Replicate cores of interest (any core active in subscribed boards).
2024. Gossip new core keys seen since last sync.
203
204### 7.3 Hiverelay
205
206- User configures one or more hiverelay endpoints.
207- Relay pins the user's own core (and optionally friends' cores) so posts stay reachable when the author is offline.
208- Relay is **dumb storage** — it does not validate, moderate, or index. Pure availability.
209
210---
211
212## 8. Local indexer
213
214Runs in a Bare worklet. Consumes ops from all replicated cores, writes to a local Hyperbee.
215
216### 8.1 Keyspaces
217
218```
219post/<board>/new/<ts>/<opId> → opId
220post/<board>/hot/<score>/<opId> → opId // periodically rescored
221comment/<parentOpId>/<ts>/<opId> → opId
222vote/<targetOpId>/<voterPubkey> → dir // last-write-wins by blockSeq
223user/<pubkey>/posts/<ts> → opId
224board/<name> → board_create op
225rep/<pubkey> → cached reputation
226```
227
228### 8.2 Scoring
229
230Hot score (reddit-like, weighted):
231```
232score = log10(max(1, |weightedVotes|)) * sign(weightedVotes)
233 + (ts - epoch) / 45000
234```
235Recomputed on a 60s cadence for posts <24h old, 10m cadence otherwise.
236
237### 8.3 Handling edits/tombstones
238
239- `edit` → indexer overlays latest edit when reading; original block stays.
240- `tombstone` → indexer hides target from views but keeps it for audit; operator can force-unindex.
241- Only the author of an op can edit or tombstone it (enforced by "op and target share the same core").
242
243---
244
245## 9. UI surfaces (v0.1 — HN-shaped)
246
247- **Front page** — single ranked feed. Default sort: `hot` (HN-style score/age decay). Alternative: `new`.
248- **Thread view** — submission + nested comments. Comments sorted by score within siblings.
249- **Submit** — title + optional URL + optional text. Shows PoW progress (fast but honest).
250- **Profile** — your pubkey, nickname, karma, key age, blocked users, configured relays.
251
252### 9.1 HN hot ranking
253
254```
255hot(post) = (max(0, weightedScore) + 1)^0.8 / ((hoursOld + 2)^1.8)
256```
257
258- `weightedScore` from §6.2 (reputation-weighted).
259- `+1` so new zero-score posts aren't invisible.
260- Age penalty dominates after ~1 day; posts fall off the front naturally.
261
262---
263
264## 10. Resolved decisions & open questions
265
266### Resolved (v0.1)
267
2681. **Board squatting** — accepted. First `board_create` wins. Boards are just names; social pressure sorts out the good ones.
2692. **Vote privacy** — votes are public and signed.
2703. **Edit history** — UI shows latest only; full history available via "view source" / raw op inspection.
2714. **Reputation formula** — simulated (see §6.2.1–§6.2.3). Floor added at 0.02. Cross-voting rings flagged as known weakness, deferred to v0.2.
2725. **Hiverelay defaults** — minimum 2 relays per user recommended in onboarding (not enforced).
273
274### Still open
275
2766. **Content deletion / "right to forget".** Tombstones hide but don't delete — signed ops persist in replicated cores forever. This is a real UX + legal problem (GDPR-style requests, CSAM, doxxing). **Deferred, but must be answered before any public launch.** Options to evaluate later:
277 - Accept the limitation; educate users that "delete" means "hide."
278 - Cryptographic shredding: encrypt payloads with per-op keys; "delete" = destroy the key. Relays keep ciphertext, readers can't decrypt.
279 - Opt-in append-only "mute at indexer" — every conforming indexer agrees to stop serving tombstoned content. Best-effort, not enforceable.
280
281---
282
283## 11. Milestones
284
285Shipped in v0.1 (all green, see `npm test` — 41 tests):
286
2871. **M1 — Core.** Identity, op encoding, PoW, single-user local write/read. No networking.
2882. **M2 — Replication.** Corestore + Hyperswarm; two nodes see each other's posts on a shared board.
2893. **M3 — Peer discovery.** Protomux announce channel; auto-trackUser on board join; transitive discovery (C learns A via B).
2904. **M4 — Indexer.** Local Hyperbee views — hot/new/top, comment threads, vote tallies, board registry.
2915. **M5 — UI.** HTTP + WebSocket RPC dev transport; HN-shaped SPA frontend (browser/mobile-safe).
2926. **M6 — Antispam.** PoW gate, per-pubkey rate limits, reputation weighting, local blocklists.
2937. **M7 — Hiverelay (self-hosted).** Relay mode seeds announced cores 24/7 for offline authors.
2948. **M8 — Hardening.** Edit/tombstone overlays (author-only enforcement), profile op indexing.
2959. **M9 — Transports.** Pear desktop worker + IPC (no HTTP bridge); `@hyperswarm/dht-relay` WebSocket endpoint on hiverelay.
29610. **M10 — Bare runtime.** `src/backend/_rt.js` shim; `src/bare/harness.js` + `src/bare/ios-entry.js` boot the full backend under raw Bare, verified by a subprocess smoke test. iOS path via bare-kit-pear uses this entry.
29711. **M11 — MVP UX.** `listCommentTree` RPC + nested comment rendering; edit/delete buttons on owned items; primary-key backup + reveal/copy on `/me`; first-launch onboarding (`/welcome`).
298
299### 11.1 Transport matrix
300
301| Runtime | How UI talks to backend | State |
302| ----------------------- | ------------------------------------------------------ | ---------------------------------- |
303| Pear desktop | `Pear.Worker.run('./src/pear/worker.js')` + pipe | Verified: backend loads under |
304| | | Bare (test/m10-bare.js). |
305| iOS via bare-kit-pear | Kit loads `src/bare/ios-entry.js`; host pipes JSON-RPC | Entry written, pipe wiring ready |
306| | | for kit integration. |
307| Node + any browser (dev)| `http://localhost:8787` + WebSocket `/rpc` | Working, tested. |
308| Raw browser (no Bare) | dht-relay + browser-built hypercore/corestore | **Blocked.** See §11.2. |
309
310### 11.2 iOS via bare-kit-pear (primary mobile path)
311
312Since bare-kit-pear ships a Bare runtime inside an iOS app, our Node-style backend runs on iOS unmodified — the whole reason we isolated `fs`/`path`/`os` through `src/backend/_rt.js`. The SDK is expected to:
313
3141. Load `src/bare/ios-entry.js` into the embedded Bare runtime.
3152. Hand us a duplex pipe (`bare-ipc` or `Bare.IPC.connect()`).
3163. Drive the UI natively (or via a webview) and send framed JSON-RPC over the pipe.
317
318The RPC surface is identical to what the dev-mode WebSocket server exposes — same method names, same shape. A native iOS view controller posts `{"id":1,"method":"listPosts","params":{}}` and gets back a feed.
319
320### 11.3 Raw browser client — still parked
321
322Without Bare, browser-only clients (regular Safari, non-Pear Chrome) still hit the corestore-7/hypercore-11 storage blocker. Three options from before remain, but they're no longer urgent because bare-kit-pear covers iOS:
323
324- **(a) Downgrade** to `corestore@5`/`hypercore@10` + `random-access-idb`.
325- **(b) Target PearBrowser's `window.pear.sync` API** (Autobase-based).
326- **(c) Wait for a Bare web-runtime.**
327
328Parked. Revisit if real demand for non-Pear-browser access shows up.
ios-app/P2PBuilders/AppDelegate.swift +19 -0
……@@ -0,0 +1,19 @@
1import UIKit
2
3@main
4class AppDelegate: UIResponder, UIApplicationDelegate {
5 func application(
6 _ application: UIApplication,
7 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
8 ) -> Bool {
9 return true
10 }
11
12 func application(
13 _ application: UIApplication,
14 configurationForConnecting connectingSceneSession: UISceneSession,
15 options: UIScene.ConnectionOptions
16 ) -> UISceneConfiguration {
17 UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
18 }
19}
ios-app/P2PBuilders/Assets.xcassets/AccentColor.colorset/Contents.json +12 -0
……@@ -0,0 +1,12 @@
1{
2 "colors": [
3 {
4 "idiom": "universal",
5 "color": {
6 "color-space": "srgb",
7 "components": { "alpha": "1.000", "red": "1.000", "green": "0.400", "blue": "0.000" }
8 }
9 }
10 ],
11 "info": { "version": 1, "author": "xcode" }
12}
ios-app/P2PBuilders/Assets.xcassets/AppIcon.appiconset/Contents.json +9 -0
……@@ -0,0 +1,9 @@
1{
2 "images": [
3 { "idiom": "universal", "platform": "ios", "size": "1024x1024" }
4 ],
5 "info": {
6 "version": 1,
7 "author": "xcode"
8 }
9}
ios-app/P2PBuilders/Assets.xcassets/Contents.json +6 -0
……@@ -0,0 +1,6 @@
1{
2 "info": {
3 "version": 1,
4 "author": "xcode"
5 }
6}
ios-app/P2PBuilders/BareBridge.swift +99 -0
……@@ -0,0 +1,99 @@
1import Foundation
2import BareKit
3
4// BareBridge wires three things together:
5// 1. A BareWorklet running our bundled JS backend (bare-pack output)
6// 2. A BareIPC for talking to that worklet
7// 3. A WKWebView on the other side — frames we receive from the webview
8// get written to IPC; frames we read from IPC get dispatched back to
9// the webview via a callback (usually `window.__bareRx(chunk)`).
10//
11// Wire format: line-delimited JSON (our backend's harness.js handles this).
12//
13// Lifecycle — call startWorklet(...) once, then sendToWorklet(...) for each
14// outgoing frame. Incoming frames are delivered via onFrameFromWorklet.
15
16final class BareBridge {
17 private var worklet: BareWorklet?
18 private var ipc: BareIPC?
19 private var readBuffer = Data()
20 private let readQueue = DispatchQueue(label: "com.p2pbuilders.bare.read")
21
22 // Called with each inbound frame (as a UTF-8 string, already de-framed
23 // from the newline-delimited stream). Set this before starting the worklet.
24 var onFrameFromWorklet: ((String) -> Void)?
25
26 // Load the bundled backend source and start the worklet + IPC.
27 // Throws if the bundle resource is missing.
28 func startWorklet(bundleResource: String = "backend.bundle", bundleExtension: String = "mjs") throws {
29 guard let url = Bundle.main.url(forResource: bundleResource, withExtension: bundleExtension) else {
30 throw NSError(
31 domain: "BareBridge",
32 code: 1,
33 userInfo: [NSLocalizedDescriptionKey: "Missing bundled resource \(bundleResource).\(bundleExtension)"]
34 )
35 }
36 let source = try String(contentsOf: url, encoding: .utf8)
37
38 let worklet = BareWorklet()
39 try worklet.start(name: "p2pbuilders", source: source, arguments: [])
40 self.worklet = worklet
41
42 // Auto-wired by bare-kit after worklet.start()
43 let ipc = BareIPC(worklet: worklet)
44 self.ipc = ipc
45
46 // Kick off the read loop.
47 readLoop()
48 }
49
50 // Push bytes from the webview to the Bare worklet. We append a newline so
51 // the JSON-RPC framer on the JS side sees a complete frame per call.
52 func sendToWorklet(_ jsonFrame: String) {
53 guard let ipc = ipc else { return }
54 var line = jsonFrame
55 if !line.hasSuffix("\n") { line += "\n" }
56 if let data = line.data(using: .utf8) {
57 ipc.write(data) { _ in /* errors surface via read loop teardown */ }
58 }
59 }
60
61 func shutdown() {
62 worklet?.terminate()
63 worklet = nil
64 ipc = nil
65 }
66
67 // MARK: - Private
68
69 private func readLoop() {
70 guard let ipc = ipc else { return }
71 ipc.read { [weak self] data, error in
72 guard let self = self else { return }
73 if let error = error {
74 NSLog("[BareBridge] read error: %@", error.localizedDescription)
75 return
76 }
77 if let data = data, !data.isEmpty {
78 self.readQueue.sync {
79 self.readBuffer.append(data)
80 self.drainFrames()
81 }
82 }
83 // Re-arm.
84 self.readLoop()
85 }
86 }
87
88 // Extract complete newline-delimited frames from the buffer, emit each as
89 // a string to the webview-facing callback.
90 private func drainFrames() {
91 while let nlRange = readBuffer.range(of: Data([0x0A])) {
92 let frameData = readBuffer.subdata(in: 0..<nlRange.lowerBound)
93 readBuffer.removeSubrange(0..<nlRange.upperBound)
94 guard let line = String(data: frameData, encoding: .utf8), !line.isEmpty else { continue }
95 let cb = onFrameFromWorklet
96 DispatchQueue.main.async { cb?(line) }
97 }
98 }
99}
ios-app/P2PBuilders/Info.plist +60 -0
……@@ -0,0 +1,60 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3<plist version="1.0">
4<dict>
5 <key>CFBundleDevelopmentRegion</key>
6 <string>$(DEVELOPMENT_LANGUAGE)</string>
7 <key>CFBundleDisplayName</key>
8 <string>P2PBuilders</string>
9 <key>CFBundleExecutable</key>
10 <string>$(EXECUTABLE_NAME)</string>
11 <key>CFBundleIdentifier</key>
12 <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
13 <key>CFBundleInfoDictionaryVersion</key>
14 <string>6.0</string>
15 <key>CFBundleName</key>
16 <string>$(PRODUCT_NAME)</string>
17 <key>CFBundlePackageType</key>
18 <string>APPL</string>
19 <key>CFBundleShortVersionString</key>
20 <string>0.1</string>
21 <key>CFBundleVersion</key>
22 <string>1</string>
23 <key>LSRequiresIPhoneOS</key>
24 <true/>
25 <key>NSAppTransportSecurity</key>
26 <dict>
27 <key>NSAllowsArbitraryLoads</key>
28 <true/>
29 </dict>
30 <key>UIApplicationSceneManifest</key>
31 <dict>
32 <key>UIApplicationSupportsMultipleScenes</key>
33 <false/>
34 <key>UISceneConfigurations</key>
35 <dict>
36 <key>UIWindowSceneSessionRoleApplication</key>
37 <array>
38 <dict>
39 <key>UISceneConfigurationName</key>
40 <string>Default Configuration</string>
41 <key>UISceneDelegateClassName</key>
42 <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
43 </dict>
44 </array>
45 </dict>
46 </dict>
47 <key>UILaunchScreen</key>
48 <dict/>
49 <key>UIRequiredDeviceCapabilities</key>
50 <array>
51 <string>arm64</string>
52 </array>
53 <key>UISupportedInterfaceOrientations</key>
54 <array>
55 <string>UIInterfaceOrientationPortrait</string>
56 <string>UIInterfaceOrientationLandscapeLeft</string>
57 <string>UIInterfaceOrientationLandscapeRight</string>
58 </array>
59</dict>
60</plist>
ios-app/P2PBuilders/SceneDelegate.swift +16 -0
……@@ -0,0 +1,16 @@
1import UIKit
2
3class SceneDelegate: UIResponder, UIWindowSceneDelegate {
4 var window: UIWindow?
5
6 func scene(
7 _ scene: UIScene,
8 willConnectTo session: UISceneSession,
9 options connectionOptions: UIScene.ConnectionOptions
10 ) {
11 guard let windowScene = (scene as? UIWindowScene) else { return }
12 window = UIWindow(windowScene: windowScene)
13 window?.rootViewController = ViewController()
14 window?.makeKeyAndVisible()
15 }
16}
ios-app/P2PBuilders/ViewController.swift +90 -0
……@@ -0,0 +1,90 @@
1import UIKit
2import WebKit
3
4// ViewController hosts a WKWebView and wires it to the Bare worklet via
5// BareBridge. Flow:
6//
7// frontend calls `window.webkit.messageHandlers.bare.postMessage(json)`
8// ↓ (WKScriptMessageHandler)
9// ViewController.userContentController(_:didReceive:)
10// ↓
11// BareBridge.sendToWorklet(json) → BareIPC.write
12//
13// BareIPC.read → BareBridge.onFrameFromWorklet
14// ↓
15// webView.evaluateJavaScript("window.__bareRx(\(safeString))")
16// ↓
17// transport.js WebKit bridge resolves the pending RPC promise
18//
19// The webview loads index.html from the app bundle's Resources/ directory.
20
21final class ViewController: UIViewController, WKScriptMessageHandler {
22 private var webView: WKWebView!
23 private let bridge = BareBridge()
24
25 override func viewDidLoad() {
26 super.viewDidLoad()
27 setupWebView()
28 startBackend()
29 }
30
31 private func setupWebView() {
32 let contentController = WKUserContentController()
33 contentController.add(self, name: "bare")
34
35 let config = WKWebViewConfiguration()
36 config.userContentController = contentController
37 config.preferences.javaScriptCanOpenWindowsAutomatically = false
38
39 let wv = WKWebView(frame: view.bounds, configuration: config)
40 wv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
41 // Useful during bring-up — remove/gate for release.
42 if #available(iOS 16.4, *) {
43 wv.isInspectable = true
44 }
45 view.addSubview(wv)
46 self.webView = wv
47
48 // Load index.html from the bundle's Resources directory.
49 guard let resourcesUrl = Bundle.main.resourceURL else {
50 NSLog("[ViewController] Bundle.main.resourceURL missing")
51 return
52 }
53 let htmlUrl = resourcesUrl.appendingPathComponent("Resources/index.html")
54 let baseUrl = resourcesUrl.appendingPathComponent("Resources")
55 wv.loadFileURL(htmlUrl, allowingReadAccessTo: baseUrl)
56 }
57
58 private func startBackend() {
59 bridge.onFrameFromWorklet = { [weak self] line in
60 self?.deliverToWebView(line)
61 }
62 do {
63 try bridge.startWorklet()
64 } catch {
65 NSLog("[ViewController] failed to start backend worklet: %@", String(describing: error))
66 }
67 }
68
69 // MARK: - WKScriptMessageHandler
70
71 func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
72 guard message.name == "bare" else { return }
73 guard let body = message.body as? String else { return }
74 bridge.sendToWorklet(body)
75 }
76
77 // MARK: - Helpers
78
79 private func deliverToWebView(_ line: String) {
80 // Safely encode the line as a JSON string literal for injection.
81 guard let data = try? JSONSerialization.data(withJSONObject: line, options: [.fragmentsAllowed]),
82 let escaped = String(data: data, encoding: .utf8) else { return }
83 let js = "window.__bareRx(\(escaped))"
84 webView.evaluateJavaScript(js, completionHandler: nil)
85 }
86
87 deinit {
88 bridge.shutdown()
89 }
90}
ios-app/README.md +161 -0
……@@ -0,0 +1,161 @@
1# P2PBuilders — iOS app shell
2
3Minimal iOS app that embeds the p2pbuilders Bare backend (via bare-kit-pear)
4alongside a WKWebView showing the frontend. Designed to run in the iOS
5Simulator first; same target should work on device after codesigning.
6
7## Architecture
8
9```
10┌──────────────────── iOS app process ────────────────────┐
11│ │
12│ ┌─────────────────┐ ┌──────────────────────┐ │
13│ │ WKWebView │ │ BareWorklet │ │
14│ │ │ │ (bare-kit-pear) │ │
15│ │ index.html │ │ │ │
16│ │ ├ app.js │ │ backend.bundle.mjs │ │
17│ │ └ transport.js│ │ └ ios-entry.js │ │
18│ │ │ │ ↓ loads │ │
19│ │ │ │ harness.js │ │
20│ │ │ │ ↓ │ │
21│ │ │ │ Node / Indexer / │ │
22│ │ │ │ Hyperswarm / ... │ │
23│ └──────┬──────────┘ └────┬─────────────────┘ │
24│ │ │ │
25│ │ webkit.bare │ BareIPC │
26│ │ postMessage │ │
27│ │ ┌─────────────┴───────────┐ │
28│ └────────→│ BareBridge │ │
29│ │ (ViewController.swift) │ │
30│ │ newline-delimited JSON │ │
31│ │ RPC │ │
32│ └─────────────────────────┘ │
33│ │
34└─────────────────────────────────────────────────────────┘
35```
36
37The frontend, backend, and RPC protocol are all unchanged from the Node /
38Pear versions. This app is just a host.
39
40## Prereqs
41
42Install these once:
43
44```bash
45brew install xcodegen xcbeautify
46xcode-select --install
47```
48
49Also make sure the monorepo's Node deps are installed:
50
51```bash
52cd .. # project root
53npm install
54```
55
56## First-time setup
57
58### 1. Generate the Xcode project
59
60```bash
61cd ios-app
62xcodegen
63open P2PBuilders.xcodeproj
64```
65
66### 2. Add bare-kit-pear as a Swift Package dependency
67
68In Xcode:
69
701. File → Add Package Dependencies…
712. Paste the bare-kit-pear URL: `https://github.com/bigdestiny2/bare-kit-pear`
723. Choose a version rule (branch `main` works)
734. Select the `BareKit` library product and add it to the `P2PBuilders` target.
74
75If the module name isn't literally `BareKit`, change the `import BareKit`
76line in `BareBridge.swift` to match.
77
78### 3. Prepare frontend + backend bundles
79
80From the project root:
81
82```bash
83./ios-app/scripts/prepare-resources.sh
84```
85
86This copies `public/*` into `ios-app/P2PBuilders/Resources/` and runs
87`bare-pack` to produce `backend.bundle.mjs`.
88
89Re-run whenever you change frontend files or anything under `src/bare` or
90`src/backend`.
91
92### 4. Build + run in the simulator
93
94From the project root:
95
96```bash
97./ios-app/scripts/build-sim.sh
98```
99
100This: prepares resources, builds the app, finds (or boots) a simulator,
101installs, and launches. When it finishes you should see the orange
102p2pbuilders header on the iOS simulator window.
103
104Alternatively, click Run in Xcode.
105
106## What to expect on first launch
107
1081. Welcome screen (first-launch onboarding)
1092. Enter a nickname, tap "get started"
1103. Front page — empty at first; submit a post
1114. PoW pill animates while the backend mints (~80ms on simulator)
1125. Thread view with upvote / reply / edit / delete
113
114The backend joins Hyperswarm on the `front` board topic, so if another
115peer is posting right now you should start seeing their posts after a
116few seconds.
117
118## Troubleshooting
119
120- **Blank webview, no errors in Xcode:** the `Resources/` folder wasn't
121 copied into the .app bundle. Re-run `prepare-resources.sh`, then rebuild.
122- **`Missing bundled resource backend.bundle.mjs`:** run
123 `prepare-resources.sh` before building.
124- **`no such module 'BareKit'`:** bare-kit-pear Swift Package isn't
125 linked. Redo step 2.
126- **Backend starts but UI never connects (`ios-bare: disconnected`):**
127 Check Xcode console for `[BareBridge] read error` lines. Likely cause
128 is a bundling issue — the bundled JS references a native addon that
129 wasn't shipped in bare-kit-pear. See the next section.
130- **Native addons missing (sodium, hyperdht UDP):** bare-kit-pear must
131 include the Bare addons our backend uses. Check bare-kit-pear's
132 `Package.swift` or `Podfile.lock` for `bare-sodium`, `bare-hyperdht`,
133 etc.
134- **UDP blocked / no peers:** check Settings → peer count. If zero,
135 your network probably doesn't allow UDP; fall back to a hosted
136 DHT-relay (see project root `src/relay/server.js` for self-hosting
137 or pair with a hiverelay).
138
139## File layout
140
141```
142ios-app/
143├── README.md ← this file
144├── project.yml ← XcodeGen spec
145├── P2PBuilders/
146│ ├── AppDelegate.swift
147│ ├── SceneDelegate.swift
148│ ├── ViewController.swift ← WKWebView + script message handler
149│ ├── BareBridge.swift ← BareKit.IPC ↔ newline-JSON frames
150│ ├── Info.plist
151│ ├── Assets.xcassets/
152│ └── Resources/ ← populated by prepare-resources.sh
153│ ├── index.html
154│ ├── app.js
155│ ├── styles.css
156│ ├── transport.js
157│ └── backend.bundle.mjs ← bare-pack output
158└── scripts/
159 ├── prepare-resources.sh
160 └── build-sim.sh
161```
ios-app/project.yml +69 -0
……@@ -0,0 +1,69 @@
1# XcodeGen spec for the P2PBuilders iOS shell.
2#
3# To generate the .xcodeproj:
4# brew install xcodegen
5# cd ios-app && xcodegen
6#
7# After generating:
8# open P2PBuilders.xcodeproj
9# Then add bare-kit-pear as a Swift Package dependency (File → Add Packages…)
10# pointing at your fork: https://github.com/bigdestiny2/bare-kit-pear
11#
12# If your SDK's module name is not "BareKit", update the `import BareKit`
13# line in BareBridge.swift accordingly.
14
15name: P2PBuilders
16options:
17 bundleIdPrefix: com.p2pbuilders
18 deploymentTarget:
19 iOS: "16.0"
20
21settings:
22 base:
23 SWIFT_VERSION: "5.9"
24 ENABLE_BITCODE: NO
25 IPHONEOS_DEPLOYMENT_TARGET: "16.0"
26 TARGETED_DEVICE_FAMILY: "1,2" # iPhone + iPad
27
28targets:
29 P2PBuilders:
30 type: application
31 platform: iOS
32 sources:
33 - path: P2PBuilders
34 excludes:
35 - "**/.DS_Store"
36 resources:
37 - path: P2PBuilders/Resources
38 optional: false
39 info:
40 path: P2PBuilders/Info.plist
41 properties:
42 CFBundleDisplayName: P2PBuilders
43 CFBundleShortVersionString: "0.1"
44 CFBundleVersion: "1"
45 LSRequiresIPhoneOS: true
46 UILaunchScreen: {}
47 UIRequiredDeviceCapabilities:
48 - arm64
49 UISupportedInterfaceOrientations:
50 - UIInterfaceOrientationPortrait
51 - UIInterfaceOrientationLandscapeLeft
52 - UIInterfaceOrientationLandscapeRight
53 UIApplicationSceneManifest:
54 UIApplicationSupportsMultipleScenes: false
55 UISceneConfigurations:
56 UIWindowSceneSessionRoleApplication:
57 - UISceneConfigurationName: Default Configuration
58 UISceneDelegateClassName: $(PRODUCT_MODULE_NAME).SceneDelegate
59 NSAppTransportSecurity:
60 NSAllowsArbitraryLoads: true
61 settings:
62 base:
63 PRODUCT_BUNDLE_IDENTIFIER: com.p2pbuilders.app
64 GENERATE_INFOPLIST_FILE: NO
65 ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
66 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
67# SPM dependencies (bare-kit-pear) are added after xcodegen runs, via
68# Xcode's File → Add Package Dependencies… (paste the repo URL, link the
69# BareKit product to the P2PBuilders target).
ios-app/scripts/build-sim.sh +69 -0
……@@ -0,0 +1,69 @@
1#!/usr/bin/env bash
2# build-sim.sh — one-shot build + install + launch in the iOS simulator.
3#
4# Prereqs:
5# - Xcode installed, simulator available
6# - xcodegen (brew install xcodegen)
7# - bare-kit-pear added as an SPM dependency inside the generated Xcode
8# project (see ios-app/README.md)
9#
10# Run from project root:
11# ./ios-app/scripts/build-sim.sh
12
13set -euo pipefail
14
15ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
16IOS="$ROOT/ios-app"
17SCHEME="P2PBuilders"
18BUNDLE_ID="com.p2pbuilders.app"
19DERIVED="$IOS/build"
20
21echo "[build] preparing resources"
22"$IOS/scripts/prepare-resources.sh"
23
24cd "$IOS"
25if [[ ! -d P2PBuilders.xcodeproj ]]; then
26 echo "[build] generating Xcode project via xcodegen"
27 xcodegen
28fi
29
30echo "[build] xcodebuild for iOS Simulator"
31xcodebuild \
32 -project P2PBuilders.xcodeproj \
33 -scheme "$SCHEME" \
34 -configuration Debug \
35 -sdk iphonesimulator \
36 -destination 'generic/platform=iOS Simulator' \
37 -derivedDataPath "$DERIVED" \
38 build \
39 | xcbeautify 2>/dev/null || true
40
41# Find the built .app
42APP_PATH="$(find "$DERIVED/Build/Products/Debug-iphonesimulator" -name "$SCHEME.app" -maxdepth 2 -type d | head -n1)"
43if [[ -z "$APP_PATH" || ! -d "$APP_PATH" ]]; then
44 echo "[build] error: built app not found under $DERIVED/Build/Products/Debug-iphonesimulator"
45 exit 1
46fi
47echo "[build] built: $APP_PATH"
48
49# Boot a simulator (pick the first booted one; otherwise boot the default)
50BOOTED="$(xcrun simctl list devices booted | awk -F '[()]' '/\(Booted\)/ { print $2; exit }' || true)"
51if [[ -z "$BOOTED" ]]; then
52 echo "[build] no simulator booted; opening Simulator.app"
53 open -a Simulator
54 # wait up to ~20s for a boot
55 for i in {1..20}; do
56 sleep 1
57 BOOTED="$(xcrun simctl list devices booted | awk -F '[()]' '/\(Booted\)/ { print $2; exit }' || true)"
58 [[ -n "$BOOTED" ]] && break
59 done
60 if [[ -z "$BOOTED" ]]; then
61 echo "[build] could not detect a booted simulator. Open Simulator, boot any iPhone, and rerun."
62 exit 1
63 fi
64fi
65echo "[build] installing on simulator $BOOTED"
66xcrun simctl install "$BOOTED" "$APP_PATH"
67echo "[build] launching"
68xcrun simctl launch "$BOOTED" "$BUNDLE_ID"
69echo "[build] done. logs: xcrun simctl spawn $BOOTED log stream --predicate 'process == \"$SCHEME\"'"
ios-app/scripts/prepare-resources.sh +38 -0
……@@ -0,0 +1,38 @@
1#!/usr/bin/env bash
2# prepare-resources.sh — copy the frontend files and produce the Bare backend
3# bundle so they're bundled into the iOS app at build time.
4#
5# Run from project root:
6# ./ios-app/scripts/prepare-resources.sh
7#
8# What it does:
9# 1. Copies public/*.{html,js,css} into ios-app/P2PBuilders/Resources/
10# 2. Runs bare-pack to produce Resources/backend.bundle.mjs (the JS blob
11# the worklet loads).
12
13set -euo pipefail
14
15ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
16RESOURCES="$ROOT/ios-app/P2PBuilders/Resources"
17
18echo "[prepare] using project root: $ROOT"
19echo "[prepare] destination: $RESOURCES"
20
21mkdir -p "$RESOURCES"
22
23# 1. Copy frontend files
24cp "$ROOT/public/index.html" "$RESOURCES/"
25cp "$ROOT/public/app.js" "$RESOURCES/"
26cp "$ROOT/public/styles.css" "$RESOURCES/"
27cp "$ROOT/public/transport.js" "$RESOURCES/"
28
29echo "[prepare] copied public/* to Resources/"
30
31# 2. Bundle the Bare backend
32cd "$ROOT"
33BUNDLE_OUT="$RESOURCES/backend.bundle.mjs"
34echo "[prepare] bundling backend → $BUNDLE_OUT"
35npx --no-install bare-pack --linked --host ios-arm64 src/bare/ios-entry.js -o "$BUNDLE_OUT"
36
37echo "[prepare] done."
38ls -lh "$RESOURCES/"
package-lock.json +4256 -0
……@@ -0,0 +1,4256 @@
1{
2 "name": "p2pbuilders",
3 "version": "0.0.1",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "p2pbuilders",
9 "version": "0.0.1",
10 "dependencies": {
11 "@hyperswarm/dht-relay": "^0.4.3",
12 "b4a": "^1.6.6",
13 "bare-env": "^3.0.0",
14 "bare-fs": "^4.7.1",
15 "bare-ipc": "^1.1.1",
16 "bare-os": "^3.8.7",
17 "bare-path": "^3.0.0",
18 "bare-process": "^4.4.1",
19 "bare-readline": "^1.3.1",
20 "bare-tty": "^5.1.0",
21 "compact-encoding": "^2.15.0",
22 "corestore": "^7.0.0",
23 "hyperbee": "^2.27.3",
24 "hypercore": "^11.0.0",
25 "hyperdht": "^6.30.0",
26 "hyperswarm": "^4.17.0",
27 "p2p-hiverelay": "^0.4.2",
28 "pear-electron": "^1.7.28",
29 "random-access-memory": "^6.2.1",
30 "sodium-universal": "^5.0.0",
31 "ws": "^8.20.0"
32 },
33 "devDependencies": {
34 "bare": "^1.28.4",
35 "bare-pack": "^2.0.1"
36 }
37 },
38 "node_modules/@grpc/grpc-js": {
39 "version": "1.14.3",
40 "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
41 "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
42 "license": "Apache-2.0",
43 "dependencies": {
44 "@grpc/proto-loader": "^0.8.0",
45 "@js-sdsl/ordered-map": "^4.4.2"
46 },
47 "engines": {
48 "node": ">=12.10.0"
49 }
50 },
51 "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
52 "version": "0.8.0",
53 "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
54 "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
55 "license": "Apache-2.0",
56 "dependencies": {
57 "lodash.camelcase": "^4.3.0",
58 "long": "^5.0.0",
59 "protobufjs": "^7.5.3",
60 "yargs": "^17.7.2"
61 },
62 "bin": {
63 "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
64 },
65 "engines": {
66 "node": ">=6"
67 }
68 },
69 "node_modules/@grpc/proto-loader": {
70 "version": "0.7.15",
71 "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
72 "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
73 "license": "Apache-2.0",
74 "dependencies": {
75 "lodash.camelcase": "^4.3.0",
76 "long": "^5.0.0",
77 "protobufjs": "^7.2.5",
78 "yargs": "^17.7.2"
79 },
80 "bin": {
81 "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
82 },
83 "engines": {
84 "node": ">=6"
85 }
86 },
87 "node_modules/@holesail/hyper-cmd-lib-net": {
88 "version": "1.1.2",
89 "resolved": "https://registry.npmjs.org/@holesail/hyper-cmd-lib-net/-/hyper-cmd-lib-net-1.1.2.tgz",
90 "integrity": "sha512-4SDQ6p+lVxap8F3RfEQPeY+bk/YQ8CxZis7AeUbDnspy9TPV/tyUrrx1elcjtAO/3b1AsVgFV8o0rouhz23/9w==",
91 "license": "Apache-2.0",
92 "dependencies": {
93 "bare-dgram": "^1.0.1",
94 "bare-net": "^2.2.0",
95 "net": "npm:bare-net@^2.2.0"
96 }
97 },
98 "node_modules/@hyperswarm/dht-relay": {
99 "version": "0.4.3",
100 "resolved": "https://registry.npmjs.org/@hyperswarm/dht-relay/-/dht-relay-0.4.3.tgz",
101 "integrity": "sha512-bjiBnVOtm/vXF7RO6CGK6fQOMmhb4LMmgOlbPrNrEvf2CWzAfam6klXQ4DgKb1GY86KGiNCnSoB6TimNgaobbg==",
102 "license": "ISC",
103 "dependencies": {
104 "@hyperswarm/secret-stream": "^6.0.0",
105 "b4a": "^1.1.4",
106 "compact-encoding": "^2.6.1",
107 "compact-encoding-net": "^1.0.1",
108 "events": "^3.3.0",
109 "graceful-goodbye": "^1.2.1",
110 "hyperdht": "^6.6.0",
111 "noise-handshake": "^3.0.3",
112 "protomux": "^3.1.1",
113 "safety-catch": "^1.0.1",
114 "sodium-universal": "^4.0.0",
115 "streamx": "^2.11.3",
116 "timeout-refresh": "^2.0.1",
117 "ws": "^8.12.1"
118 },
119 "bin": {
120 "dht-relay": "bin.js"
121 }
122 },
123 "node_modules/@hyperswarm/dht-relay/node_modules/noise-handshake": {
124 "version": "3.1.0",
125 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-3.1.0.tgz",
126 "integrity": "sha512-0S1qkUvMbTvZCfgr/vSkVT84YyvI4Q0OLwSc5BFxVmjaePrxAwVeXeJDY3A7N/7+qj95gZ15LaNoP9ZnBXH5Lw==",
127 "license": "ISC",
128 "dependencies": {
129 "b4a": "^1.1.0",
130 "nanoassert": "^2.0.0",
131 "sodium-universal": "^4.0.0"
132 }
133 },
134 "node_modules/@hyperswarm/dht-relay/node_modules/sodium-native": {
135 "version": "4.3.3",
136 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
137 "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==",
138 "license": "MIT",
139 "dependencies": {
140 "require-addon": "^1.1.0"
141 }
142 },
143 "node_modules/@hyperswarm/dht-relay/node_modules/sodium-universal": {
144 "version": "4.0.1",
145 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-4.0.1.tgz",
146 "integrity": "sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==",
147 "license": "MIT",
148 "dependencies": {
149 "sodium-native": "^4.0.0"
150 },
151 "peerDependencies": {
152 "sodium-javascript": "~0.8.0"
153 },
154 "peerDependenciesMeta": {
155 "sodium-javascript": {
156 "optional": true
157 }
158 }
159 },
160 "node_modules/@hyperswarm/secret-stream": {
161 "version": "6.9.1",
162 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
163 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
164 "license": "Apache-2.0",
165 "dependencies": {
166 "b4a": "^1.1.0",
167 "hypercore-crypto": "^3.3.1",
168 "noise-curve-ed": "^2.0.1",
169 "noise-handshake": "^4.0.0",
170 "sodium-secretstream": "^1.1.0",
171 "sodium-universal": "^5.0.0",
172 "streamx": "^2.14.0",
173 "timeout-refresh": "^2.0.0",
174 "unslab": "^1.3.0"
175 }
176 },
177 "node_modules/@inquirer/ansi": {
178 "version": "2.0.5",
179 "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz",
180 "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==",
181 "license": "MIT",
182 "engines": {
183 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
184 }
185 },
186 "node_modules/@inquirer/checkbox": {
187 "version": "5.1.3",
188 "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.3.tgz",
189 "integrity": "sha512-+G7I8CT+EHv/hasNfUl3P37DVoMoZfpA+2FXmM54dA8MxYle1YqucxbacxHalw1iAFSdKNEDTGNV7F+j1Ldqcg==",
190 "license": "MIT",
191 "dependencies": {
192 "@inquirer/ansi": "^2.0.5",
193 "@inquirer/core": "^11.1.8",
194 "@inquirer/figures": "^2.0.5",
195 "@inquirer/type": "^4.0.5"
196 },
197 "engines": {
198 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
199 },
200 "peerDependencies": {
201 "@types/node": ">=18"
202 },
203 "peerDependenciesMeta": {
204 "@types/node": {
205 "optional": true
206 }
207 }
208 },
209 "node_modules/@inquirer/confirm": {
210 "version": "6.0.11",
211 "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.11.tgz",
212 "integrity": "sha512-pTpHjg0iEIRMYV/7oCZUMf27/383E6Wyhfc/MY+AVQGEoUobffIYWOK9YLP2XFRGz/9i6WlTQh1CkFVIo2Y7XA==",
213 "license": "MIT",
214 "dependencies": {
215 "@inquirer/core": "^11.1.8",
216 "@inquirer/type": "^4.0.5"
217 },
218 "engines": {
219 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
220 },
221 "peerDependencies": {
222 "@types/node": ">=18"
223 },
224 "peerDependenciesMeta": {
225 "@types/node": {
226 "optional": true
227 }
228 }
229 },
230 "node_modules/@inquirer/core": {
231 "version": "11.1.8",
232 "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.8.tgz",
233 "integrity": "sha512-/u+yJk2pOKNDOh1ZgdUH2RQaRx6OOH4I0uwL95qPvTFTIL38YBsuSC4r1yXBB3Q6JvNqFFc202gk0Ew79rrcjA==",
234 "license": "MIT",
235 "dependencies": {
236 "@inquirer/ansi": "^2.0.5",
237 "@inquirer/figures": "^2.0.5",
238 "@inquirer/type": "^4.0.5",
239 "cli-width": "^4.1.0",
240 "fast-wrap-ansi": "^0.2.0",
241 "mute-stream": "^3.0.0",
242 "signal-exit": "^4.1.0"
243 },
244 "engines": {
245 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
246 },
247 "peerDependencies": {
248 "@types/node": ">=18"
249 },
250 "peerDependenciesMeta": {
251 "@types/node": {
252 "optional": true
253 }
254 }
255 },
256 "node_modules/@inquirer/editor": {
257 "version": "5.1.0",
258 "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.0.tgz",
259 "integrity": "sha512-6wlkYl65Qfayy48gPCfU4D7li6KCAGN79mLXa/tYHZH99OfZ820yY+HA+DgE88r8YwwgeuY6PQgNqMeK6LuMmw==",
260 "license": "MIT",
261 "dependencies": {
262 "@inquirer/core": "^11.1.8",
263 "@inquirer/external-editor": "^3.0.0",
264 "@inquirer/type": "^4.0.5"
265 },
266 "engines": {
267 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
268 },
269 "peerDependencies": {
270 "@types/node": ">=18"
271 },
272 "peerDependenciesMeta": {
273 "@types/node": {
274 "optional": true
275 }
276 }
277 },
278 "node_modules/@inquirer/expand": {
279 "version": "5.0.12",
280 "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.12.tgz",
281 "integrity": "sha512-vOfrB33b7YIZfDauXS8vNNz2Z86FozTZLIt7e+7/dCaPJ1RXZsHCuI9TlcERzEUq57vkM+UdnBgxP0rFd23JYQ==",
282 "license": "MIT",
283 "dependencies": {
284 "@inquirer/core": "^11.1.8",
285 "@inquirer/type": "^4.0.5"
286 },
287 "engines": {
288 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
289 },
290 "peerDependencies": {
291 "@types/node": ">=18"
292 },
293 "peerDependenciesMeta": {
294 "@types/node": {
295 "optional": true
296 }
297 }
298 },
299 "node_modules/@inquirer/external-editor": {
300 "version": "3.0.0",
301 "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.0.tgz",
302 "integrity": "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==",
303 "license": "MIT",
304 "dependencies": {
305 "chardet": "^2.1.1",
306 "iconv-lite": "^0.7.2"
307 },
308 "engines": {
309 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
310 },
311 "peerDependencies": {
312 "@types/node": ">=18"
313 },
314 "peerDependenciesMeta": {
315 "@types/node": {
316 "optional": true
317 }
318 }
319 },
320 "node_modules/@inquirer/figures": {
321 "version": "2.0.5",
322 "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz",
323 "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==",
324 "license": "MIT",
325 "engines": {
326 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
327 }
328 },
329 "node_modules/@inquirer/input": {
330 "version": "5.0.11",
331 "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.11.tgz",
332 "integrity": "sha512-twUWidn4ocPO8qi6fRM7tNWt7W1FOnOZqQ+/+PsfLUacMR5rFLDPK9ql0nBPwxi0oELbo8T5NhRs8B2+qQEqFQ==",
333 "license": "MIT",
334 "dependencies": {
335 "@inquirer/core": "^11.1.8",
336 "@inquirer/type": "^4.0.5"
337 },
338 "engines": {
339 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
340 },
341 "peerDependencies": {
342 "@types/node": ">=18"
343 },
344 "peerDependenciesMeta": {
345 "@types/node": {
346 "optional": true
347 }
348 }
349 },
350 "node_modules/@inquirer/number": {
351 "version": "4.0.11",
352 "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.11.tgz",
353 "integrity": "sha512-Vscmim9TCksQsfjPtka/JwPUcbLhqWYrgfPf1cHrCm24X/F2joFwnageD50yMKsaX14oNGOyKf/RNXAFkNjWpA==",
354 "license": "MIT",
355 "dependencies": {
356 "@inquirer/core": "^11.1.8",
357 "@inquirer/type": "^4.0.5"
358 },
359 "engines": {
360 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
361 },
362 "peerDependencies": {
363 "@types/node": ">=18"
364 },
365 "peerDependenciesMeta": {
366 "@types/node": {
367 "optional": true
368 }
369 }
370 },
371 "node_modules/@inquirer/password": {
372 "version": "5.0.11",
373 "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.11.tgz",
374 "integrity": "sha512-9KZFeRaNHIcejtPb0wN4ddFc7EvobVoAFa049eS3LrDZFxI8O7xUXiITEOinBzkZFAIwY5V4yzQae/QfO9cbbg==",
375 "license": "MIT",
376 "dependencies": {
377 "@inquirer/ansi": "^2.0.5",
378 "@inquirer/core": "^11.1.8",
379 "@inquirer/type": "^4.0.5"
380 },
381 "engines": {
382 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
383 },
384 "peerDependencies": {
385 "@types/node": ">=18"
386 },
387 "peerDependenciesMeta": {
388 "@types/node": {
389 "optional": true
390 }
391 }
392 },
393 "node_modules/@inquirer/prompts": {
394 "version": "8.4.1",
395 "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.1.tgz",
396 "integrity": "sha512-AH5xPQ997K7e0F0vulPlteIHke2awMkFi8F0dBemrDfmvtPmHJo82mdHbONC4F/t8d1NHwrbI5cGVI+RbLWdoQ==",
397 "license": "MIT",
398 "dependencies": {
399 "@inquirer/checkbox": "^5.1.3",
400 "@inquirer/confirm": "^6.0.11",
401 "@inquirer/editor": "^5.1.0",
402 "@inquirer/expand": "^5.0.12",
403 "@inquirer/input": "^5.0.11",
404 "@inquirer/number": "^4.0.11",
405 "@inquirer/password": "^5.0.11",
406 "@inquirer/rawlist": "^5.2.7",
407 "@inquirer/search": "^4.1.7",
408 "@inquirer/select": "^5.1.3"
409 },
410 "engines": {
411 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
412 },
413 "peerDependencies": {
414 "@types/node": ">=18"
415 },
416 "peerDependenciesMeta": {
417 "@types/node": {
418 "optional": true
419 }
420 }
421 },
422 "node_modules/@inquirer/rawlist": {
423 "version": "5.2.7",
424 "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.7.tgz",
425 "integrity": "sha512-AqRMiD9+uE1lskDPrdqHwrV/EUmxKEBLX44SR7uxK3vD2413AmVfE5EQaPeNzYf5Pq5SitHJDYUFVF0poIr09w==",
426 "license": "MIT",
427 "dependencies": {
428 "@inquirer/core": "^11.1.8",
429 "@inquirer/type": "^4.0.5"
430 },
431 "engines": {
432 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
433 },
434 "peerDependencies": {
435 "@types/node": ">=18"
436 },
437 "peerDependenciesMeta": {
438 "@types/node": {
439 "optional": true
440 }
441 }
442 },
443 "node_modules/@inquirer/search": {
444 "version": "4.1.7",
445 "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.7.tgz",
446 "integrity": "sha512-1y7+0N65AWk5RdlXH/Kn13txf3IjIQ7OEfhCEkDTU+h5wKMLq8DUF3P6z+/kLSxDGDtQT1dRBWEUC3o/VvImsQ==",
447 "license": "MIT",
448 "dependencies": {
449 "@inquirer/core": "^11.1.8",
450 "@inquirer/figures": "^2.0.5",
451 "@inquirer/type": "^4.0.5"
452 },
453 "engines": {
454 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
455 },
456 "peerDependencies": {
457 "@types/node": ">=18"
458 },
459 "peerDependenciesMeta": {
460 "@types/node": {
461 "optional": true
462 }
463 }
464 },
465 "node_modules/@inquirer/select": {
466 "version": "5.1.3",
467 "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.3.tgz",
468 "integrity": "sha512-zYyqWgGQi3NhBcNq4Isc5rB3oEdQEh1Q/EcAnOW0FK4MpnXWkvSBYgA4cYrTM4A9UB573omouZbnL9JJ74Mq3A==",
469 "license": "MIT",
470 "dependencies": {
471 "@inquirer/ansi": "^2.0.5",
472 "@inquirer/core": "^11.1.8",
473 "@inquirer/figures": "^2.0.5",
474 "@inquirer/type": "^4.0.5"
475 },
476 "engines": {
477 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
478 },
479 "peerDependencies": {
480 "@types/node": ">=18"
481 },
482 "peerDependenciesMeta": {
483 "@types/node": {
484 "optional": true
485 }
486 }
487 },
488 "node_modules/@inquirer/type": {
489 "version": "4.0.5",
490 "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz",
491 "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==",
492 "license": "MIT",
493 "engines": {
494 "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0"
495 },
496 "peerDependencies": {
497 "@types/node": ">=18"
498 },
499 "peerDependenciesMeta": {
500 "@types/node": {
501 "optional": true
502 }
503 }
504 },
505 "node_modules/@js-sdsl/ordered-map": {
506 "version": "4.4.2",
507 "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
508 "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
509 "license": "MIT",
510 "funding": {
511 "type": "opencollective",
512 "url": "https://opencollective.com/js-sdsl"
513 }
514 },
515 "node_modules/@leichtgewicht/ip-codec": {
516 "version": "2.0.5",
517 "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
518 "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
519 "license": "MIT"
520 },
521 "node_modules/@noble/hashes": {
522 "version": "2.2.0",
523 "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
524 "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
525 "license": "MIT",
526 "engines": {
527 "node": ">= 20.19.0"
528 },
529 "funding": {
530 "url": "https://paulmillr.com/funding/"
531 }
532 },
533 "node_modules/@noble/secp256k1": {
534 "version": "3.1.0",
535 "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.1.0.tgz",
536 "integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==",
537 "license": "MIT",
538 "funding": {
539 "url": "https://paulmillr.com/funding/"
540 }
541 },
542 "node_modules/@pinojs/redact": {
543 "version": "0.4.0",
544 "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
545 "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
546 "license": "MIT"
547 },
548 "node_modules/@protobufjs/aspromise": {
549 "version": "1.1.2",
550 "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
551 "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
552 "license": "BSD-3-Clause"
553 },
554 "node_modules/@protobufjs/base64": {
555 "version": "1.1.2",
556 "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
557 "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
558 "license": "BSD-3-Clause"
559 },
560 "node_modules/@protobufjs/codegen": {
561 "version": "2.0.4",
562 "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
563 "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
564 "license": "BSD-3-Clause"
565 },
566 "node_modules/@protobufjs/eventemitter": {
567 "version": "1.1.0",
568 "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
569 "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
570 "license": "BSD-3-Clause"
571 },
572 "node_modules/@protobufjs/fetch": {
573 "version": "1.1.0",
574 "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
575 "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
576 "license": "BSD-3-Clause",
577 "dependencies": {
578 "@protobufjs/aspromise": "^1.1.1",
579 "@protobufjs/inquire": "^1.1.0"
580 }
581 },
582 "node_modules/@protobufjs/float": {
583 "version": "1.0.2",
584 "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
585 "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
586 "license": "BSD-3-Clause"
587 },
588 "node_modules/@protobufjs/inquire": {
589 "version": "1.1.0",
590 "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
591 "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
592 "license": "BSD-3-Clause"
593 },
594 "node_modules/@protobufjs/path": {
595 "version": "1.1.2",
596 "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
597 "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
598 "license": "BSD-3-Clause"
599 },
600 "node_modules/@protobufjs/pool": {
601 "version": "1.1.0",
602 "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
603 "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
604 "license": "BSD-3-Clause"
605 },
606 "node_modules/@protobufjs/utf8": {
607 "version": "1.1.0",
608 "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
609 "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
610 "license": "BSD-3-Clause"
611 },
612 "node_modules/@types/node": {
613 "version": "25.6.0",
614 "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
615 "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
616 "license": "MIT",
617 "dependencies": {
618 "undici-types": "~7.19.0"
619 }
620 },
621 "node_modules/acorn": {
622 "version": "8.16.0",
623 "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
624 "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
625 "license": "MIT",
626 "bin": {
627 "acorn": "bin/acorn"
628 },
629 "engines": {
630 "node": ">=0.4.0"
631 }
632 },
633 "node_modules/adaptive-timeout": {
634 "version": "1.0.1",
635 "resolved": "https://registry.npmjs.org/adaptive-timeout/-/adaptive-timeout-1.0.1.tgz",
636 "integrity": "sha512-+seGiBtHqbnyZ0Quq/ZGxxwP66153WBABawa07/QeyuPFzbduPBjiGQAyCiriiLDzt3dkbMBskSlfqtRwblK8Q==",
637 "license": "Apache-2.0",
638 "dependencies": {
639 "xache": "^1.2.1"
640 }
641 },
642 "node_modules/ansi-parser": {
643 "version": "3.2.11",
644 "resolved": "https://registry.npmjs.org/ansi-parser/-/ansi-parser-3.2.11.tgz",
645 "integrity": "sha512-HeEwCVf/pI9qQUVaTkoDBl9U/QU4RvWJ+Trg1jNSNvDzFsBAId6fHv6VLiC7HzwrOiL8uOEkO5T1eTgpnTFQfQ==",
646 "license": "MIT"
647 },
648 "node_modules/ansi-regex": {
649 "version": "5.0.1",
650 "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
651 "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
652 "license": "MIT",
653 "engines": {
654 "node": ">=8"
655 }
656 },
657 "node_modules/ansi-styles": {
658 "version": "4.3.0",
659 "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
660 "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
661 "license": "MIT",
662 "dependencies": {
663 "color-convert": "^2.0.1"
664 },
665 "engines": {
666 "node": ">=8"
667 },
668 "funding": {
669 "url": "https://github.com/chalk/ansi-styles?sponsor=1"
670 }
671 },
672 "node_modules/atomic-sleep": {
673 "version": "1.0.0",
674 "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
675 "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
676 "license": "MIT",
677 "engines": {
678 "node": ">=8.0.0"
679 }
680 },
681 "node_modules/b4a": {
682 "version": "1.8.0",
683 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
684 "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
685 "license": "Apache-2.0",
686 "peerDependencies": {
687 "react-native-b4a": "*"
688 },
689 "peerDependenciesMeta": {
690 "react-native-b4a": {
691 "optional": true
692 }
693 }
694 },
695 "node_modules/bare": {
696 "version": "1.28.4",
697 "resolved": "https://registry.npmjs.org/bare/-/bare-1.28.4.tgz",
698 "integrity": "sha512-4gTeh78rs1CjgZdSkAQEbryDTMl6qr643ArdQCtJfNMZxWbE77onPHYpEaV+Rk+v+tOg7RpunAHsOCN8WDRPcg==",
699 "dev": true,
700 "license": "Apache-2.0",
701 "bin": {
702 "bare": "bin/bare"
703 },
704 "peerDependencies": {
705 "bare-buffer": "*",
706 "bare-console": "*",
707 "bare-events": "*",
708 "bare-queue-microtask": "*",
709 "bare-runtime": "*",
710 "bare-structured-clone": "*",
711 "bare-timers": "*",
712 "bare-url": "*"
713 },
714 "peerDependenciesMeta": {
715 "bare-buffer": {
716 "optional": true
717 },
718 "bare-console": {
719 "optional": true
720 },
721 "bare-events": {
722 "optional": true
723 },
724 "bare-queue-microtask": {
725 "optional": true
726 },
727 "bare-structured-clone": {
728 "optional": true
729 },
730 "bare-timers": {
731 "optional": true
732 },
733 "bare-url": {
734 "optional": true
735 }
736 }
737 },
738 "node_modules/bare-abort": {
739 "version": "2.0.13",
740 "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz",
741 "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
742 "license": "Apache-2.0"
743 },
744 "node_modules/bare-addon-resolve": {
745 "version": "1.10.0",
746 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
747 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
748 "license": "Apache-2.0",
749 "dependencies": {
750 "bare-module-resolve": "^1.10.0",
751 "bare-semver": "^1.0.0"
752 },
753 "peerDependencies": {
754 "bare-url": "*"
755 },
756 "peerDependenciesMeta": {
757 "bare-url": {
758 "optional": true
759 }
760 }
761 },
762 "node_modules/bare-ansi-escapes": {
763 "version": "2.2.3",
764 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
765 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
766 "license": "Apache-2.0",
767 "dependencies": {
768 "bare-stream": "^2.6.5"
769 },
770 "peerDependencies": {
771 "bare-buffer": "*"
772 },
773 "peerDependenciesMeta": {
774 "bare-buffer": {
775 "optional": true
776 }
777 }
778 },
779 "node_modules/bare-assert": {
780 "version": "1.2.0",
781 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
782 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
783 "license": "Apache-2.0",
784 "dependencies": {
785 "bare-inspect": "^3.1.2"
786 }
787 },
788 "node_modules/bare-buffer": {
789 "version": "3.6.0",
790 "resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.0.tgz",
791 "integrity": "sha512-/maRWEQ2eBkVNMbNFVsq1pHXJYVj4Y3AixwruB24eKZDs5Gtu0fixzvjYmBIuTsBMtVH5Yb27pQO9BhFa+IlIQ==",
792 "devOptional": true,
793 "license": "Apache-2.0",
794 "peer": true,
795 "engines": {
796 "bare": ">=1.20.0"
797 }
798 },
799 "node_modules/bare-bundle": {
800 "version": "1.10.0",
801 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
802 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
803 "license": "Apache-2.0",
804 "peerDependencies": {
805 "bare-buffer": "*",
806 "bare-url": "*"
807 },
808 "peerDependenciesMeta": {
809 "bare-buffer": {
810 "optional": true
811 },
812 "bare-url": {
813 "optional": true
814 }
815 }
816 },
817 "node_modules/bare-bundle-id": {
818 "version": "1.0.2",
819 "resolved": "https://registry.npmjs.org/bare-bundle-id/-/bare-bundle-id-1.0.2.tgz",
820 "integrity": "sha512-RG/y1J/s6zWmsqUIDtclXh+xxMRTh1jo/10vFL58FKhe9UESchMNkwn0Cz10o5AdA/35WR/KUGoHhIGdyCjQrg==",
821 "dev": true,
822 "license": "Apache-2.0",
823 "dependencies": {
824 "sodium-native": "^5.0.9"
825 },
826 "peerDependencies": {
827 "bare-buffer": "*",
828 "bare-bundle": "*"
829 }
830 },
831 "node_modules/bare-crypto": {
832 "version": "1.13.4",
833 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz",
834 "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==",
835 "license": "Apache-2.0",
836 "dependencies": {
837 "bare-assert": "^1.2.0",
838 "bare-stream": "^2.6.3"
839 },
840 "peerDependencies": {
841 "bare-buffer": "*"
842 },
843 "peerDependenciesMeta": {
844 "bare-buffer": {
845 "optional": true
846 }
847 }
848 },
849 "node_modules/bare-daemon": {
850 "version": "1.2.4",
851 "resolved": "https://registry.npmjs.org/bare-daemon/-/bare-daemon-1.2.4.tgz",
852 "integrity": "sha512-pyWmqqsN9x2RNzXFpHObyU8+9vgFL7q63Nhtk+xRTXcWL/LkZrYpLmIpYlyPzwZrT2vL+lrjBwVMyCljThfC+A==",
853 "license": "Apache-2.0",
854 "dependencies": {
855 "bare-env": "^3.0.0",
856 "bare-os": "^3.6.2",
857 "require-addon": "^1.1.0"
858 }
859 },
860 "node_modules/bare-debug-log": {
861 "version": "2.0.0",
862 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
863 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
864 "license": "Apache-2.0",
865 "dependencies": {
866 "bare-os": "^3.0.1"
867 }
868 },
869 "node_modules/bare-dgram": {
870 "version": "1.0.1",
871 "resolved": "https://registry.npmjs.org/bare-dgram/-/bare-dgram-1.0.1.tgz",
872 "integrity": "sha512-EdsyRErrkWgN8fENdrDdXFEE9HAuJ/m6ehXz13fVj9JhdCaLWIA+L8o5aYNRLt66x08RlyG2vbrRAZoxGfcdlg==",
873 "license": "Apache-2.0",
874 "dependencies": {
875 "bare-events": "^2.5.0",
876 "udx-native": "^1.11.2"
877 }
878 },
879 "node_modules/bare-dns": {
880 "version": "2.1.4",
881 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
882 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
883 "license": "Apache-2.0",
884 "engines": {
885 "bare": ">=1.7.0"
886 }
887 },
888 "node_modules/bare-encoding": {
889 "version": "1.0.3",
890 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
891 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
892 "license": "Apache-2.0",
893 "peerDependencies": {
894 "bare-buffer": "*"
895 },
896 "peerDependenciesMeta": {
897 "bare-buffer": {
898 "optional": true
899 }
900 }
901 },
902 "node_modules/bare-env": {
903 "version": "3.0.0",
904 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
905 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
906 "license": "Apache-2.0",
907 "dependencies": {
908 "bare-os": "^3.0.1"
909 }
910 },
911 "node_modules/bare-events": {
912 "version": "2.8.2",
913 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
914 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
915 "license": "Apache-2.0",
916 "peerDependencies": {
917 "bare-abort-controller": "*"
918 },
919 "peerDependenciesMeta": {
920 "bare-abort-controller": {
921 "optional": true
922 }
923 }
924 },
925 "node_modules/bare-format": {
926 "version": "1.0.2",
927 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz",
928 "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==",
929 "license": "Apache-2.0",
930 "dependencies": {
931 "bare-inspect": "^3.0.0"
932 }
933 },
934 "node_modules/bare-fs": {
935 "version": "4.7.1",
936 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz",
937 "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==",
938 "license": "Apache-2.0",
939 "dependencies": {
940 "bare-events": "^2.5.4",
941 "bare-path": "^3.0.0",
942 "bare-stream": "^2.6.4",
943 "bare-url": "^2.2.2",
944 "fast-fifo": "^1.3.2"
945 },
946 "engines": {
947 "bare": ">=1.16.0"
948 },
949 "peerDependencies": {
950 "bare-buffer": "*"
951 },
952 "peerDependenciesMeta": {
953 "bare-buffer": {
954 "optional": true
955 }
956 }
957 },
958 "node_modules/bare-hrtime": {
959 "version": "2.1.1",
960 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
961 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
962 "license": "Apache-2.0"
963 },
964 "node_modules/bare-http-parser": {
965 "version": "1.1.3",
966 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz",
967 "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==",
968 "license": "Apache-2.0"
969 },
970 "node_modules/bare-inspect": {
971 "version": "3.1.4",
972 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
973 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
974 "license": "Apache-2.0",
975 "dependencies": {
976 "bare-ansi-escapes": "^2.1.0",
977 "bare-type": "^1.0.0"
978 },
979 "engines": {
980 "bare": ">=1.18.0"
981 }
982 },
983 "node_modules/bare-ipc": {
984 "version": "1.1.1",
985 "resolved": "https://registry.npmjs.org/bare-ipc/-/bare-ipc-1.1.1.tgz",
986 "integrity": "sha512-9Zi80FN8hJKAvmXQXzaxa7dEOMO+WwEhkSnihPW9P8cT2IJON8kv/Oj0OdDrJtvQil5EGafuCd5lZQwVxdbMNw==",
987 "license": "Apache-2.0",
988 "dependencies": {
989 "bare-pipe": "^4.0.0",
990 "bare-stream": "^2.1.3",
991 "bare-structured-clone": "^1.4.0"
992 }
993 },
994 "node_modules/bare-module": {
995 "version": "6.2.0",
996 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.2.0.tgz",
997 "integrity": "sha512-CoCTG8y8HKPnNW317OW1hynl28DasVPtbX033ZEAXk0Q3SYCUXi3OOnNAr6wTrDgMgpOsW0kl7Nifcom5GGH8Q==",
998 "license": "Apache-2.0",
999 "dependencies": {
1000 "bare-bundle": "^1.3.0",
1001 "bare-module-lexer": "^1.0.0",
1002 "bare-module-resolve": "^1.8.0",
1003 "bare-path": "^3.0.0",
1004 "bare-url": "^2.0.1"
1005 },
1006 "engines": {
1007 "bare": ">=1.23.0"
1008 },
1009 "peerDependencies": {
1010 "bare-buffer": "*"
1011 },
1012 "peerDependenciesMeta": {
1013 "bare-buffer": {
1014 "optional": true
1015 }
1016 }
1017 },
1018 "node_modules/bare-module-lexer": {
1019 "version": "1.4.7",
1020 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
1021 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
1022 "license": "Apache-2.0",
1023 "dependencies": {
1024 "require-addon": "^1.0.2"
1025 },
1026 "peerDependencies": {
1027 "bare-buffer": "*"
1028 },
1029 "peerDependenciesMeta": {
1030 "bare-buffer": {
1031 "optional": true
1032 }
1033 }
1034 },
1035 "node_modules/bare-module-resolve": {
1036 "version": "1.12.1",
1037 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
1038 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
1039 "license": "Apache-2.0",
1040 "dependencies": {
1041 "bare-semver": "^1.0.0"
1042 },
1043 "peerDependencies": {
1044 "bare-url": "*"
1045 },
1046 "peerDependenciesMeta": {
1047 "bare-url": {
1048 "optional": true
1049 }
1050 }
1051 },
1052 "node_modules/bare-module-traverse": {
1053 "version": "2.0.1",
1054 "resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.0.1.tgz",
1055 "integrity": "sha512-1au+Og5p97T9b6Y7xmHZ7KtpW8vEYtz2jC2whmm+YJp46EaHfk26j91MmQhufdkR/8sdK1Q5p+P9A/Y5GrJg7Q==",
1056 "dev": true,
1057 "license": "Apache-2.0",
1058 "dependencies": {
1059 "bare-addon-resolve": "^1.5.0",
1060 "bare-module-lexer": "^1.4.0",
1061 "bare-module-resolve": "^1.7.0"
1062 },
1063 "peerDependencies": {
1064 "bare-buffer": "*",
1065 "bare-url": "*"
1066 },
1067 "peerDependenciesMeta": {
1068 "bare-buffer": {
1069 "optional": true
1070 },
1071 "bare-url": {
1072 "optional": true
1073 }
1074 }
1075 },
1076 "node_modules/bare-net": {
1077 "version": "2.3.1",
1078 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz",
1079 "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==",
1080 "license": "Apache-2.0",
1081 "dependencies": {
1082 "bare-events": "^2.2.2",
1083 "bare-pipe": "^4.0.0",
1084 "bare-stream": "^2.0.0",
1085 "bare-tcp": "^2.0.0"
1086 }
1087 },
1088 "node_modules/bare-os": {
1089 "version": "3.8.7",
1090 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz",
1091 "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==",
1092 "license": "Apache-2.0",
1093 "engines": {
1094 "bare": ">=1.14.0"
1095 }
1096 },
1097 "node_modules/bare-pack": {
1098 "version": "2.0.1",
1099 "resolved": "https://registry.npmjs.org/bare-pack/-/bare-pack-2.0.1.tgz",
1100 "integrity": "sha512-zpItExb4Kue1vq97/ZiHNwSBwUGyv3uqhgi1GpT2S7qezkXmc3K/Wx6qkth/7ayS3i+suIn6EGE96EhJxKkCzA==",
1101 "dev": true,
1102 "license": "Apache-2.0",
1103 "dependencies": {
1104 "bare-bundle": "^1.8.3",
1105 "bare-bundle-id": "^1.0.0",
1106 "bare-fs": "^4.2.1",
1107 "bare-module-traverse": "~2.0.0",
1108 "bare-path": "^3.0.0",
1109 "bare-url": "*",
1110 "paparam": "^1.5.0",
1111 "promaphore": "^1.0.0"
1112 },
1113 "bin": {
1114 "bare-pack": "bin.js"
1115 },
1116 "peerDependencies": {
1117 "bare-buffer": "*",
1118 "bare-url": "*"
1119 },
1120 "peerDependenciesMeta": {
1121 "bare-buffer": {
1122 "optional": true
1123 },
1124 "bare-url": {
1125 "optional": true
1126 }
1127 }
1128 },
1129 "node_modules/bare-path": {
1130 "version": "3.0.0",
1131 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
1132 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
1133 "license": "Apache-2.0",
1134 "dependencies": {
1135 "bare-os": "^3.0.1"
1136 }
1137 },
1138 "node_modules/bare-pipe": {
1139 "version": "4.1.5",
1140 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz",
1141 "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==",
1142 "license": "Apache-2.0",
1143 "dependencies": {
1144 "bare-events": "^2.0.0",
1145 "bare-stream": "^2.0.0"
1146 },
1147 "engines": {
1148 "bare": ">=1.16.0"
1149 }
1150 },
1151 "node_modules/bare-process": {
1152 "version": "4.4.1",
1153 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.4.1.tgz",
1154 "integrity": "sha512-JfcTtymq6akqM2bdyTsP4A4GYJceBzb91k/ECmFps75uFf6uO33SM1bOZsRAqyRXExabsQJLn5S41NBl8HTM4A==",
1155 "license": "Apache-2.0",
1156 "dependencies": {
1157 "bare-abort": "^2.0.13",
1158 "bare-env": "^3.0.0",
1159 "bare-events": "^2.3.1",
1160 "bare-hrtime": "^2.0.0",
1161 "bare-os": "^3.7.1",
1162 "bare-signals": "^4.0.0",
1163 "bare-stdio": "^1.0.1"
1164 }
1165 },
1166 "node_modules/bare-querystring": {
1167 "version": "1.1.0",
1168 "resolved": "https://registry.npmjs.org/bare-querystring/-/bare-querystring-1.1.0.tgz",
1169 "integrity": "sha512-pUtEM6JrX53MbEJFwO92F0Ch7BwZ67KD7LyglcB8/tvkkVdwTgN1f7oIklRe+NTT/WCYZgwjDFYS9efBxDSq8g==",
1170 "license": "Apache-2.0"
1171 },
1172 "node_modules/bare-readline": {
1173 "version": "1.3.1",
1174 "resolved": "https://registry.npmjs.org/bare-readline/-/bare-readline-1.3.1.tgz",
1175 "integrity": "sha512-QtSU4ZfgQcDI6AQssjDFqTiRe4rCiciMn+Yqx6siMJZBIftAIFrNSOK0sa5SBrmNv/a7CD9Dm0onUDCRyn5Fdg==",
1176 "license": "Apache-2.0",
1177 "dependencies": {
1178 "bare-ansi-escapes": "^2.0.0",
1179 "bare-stream": "^2.0.0"
1180 }
1181 },
1182 "node_modules/bare-runtime": {
1183 "version": "1.28.4",
1184 "resolved": "https://registry.npmjs.org/bare-runtime/-/bare-runtime-1.28.4.tgz",
1185 "integrity": "sha512-o0zmJgu5wvTn+oNkzOceSNuKx+pKwYYyD6ZT4gRazqr+dZE976IkZv7CPkNkp4wf4b4NChntdiAsdk/W30toUw==",
1186 "dev": true,
1187 "license": "Apache-2.0",
1188 "peer": true,
1189 "workspaces": [
1190 "npm/*"
1191 ],
1192 "dependencies": {
1193 "bare-fs": "^4.4.4",
1194 "bare-os": "^3.0.1",
1195 "bare-path": "^3.0.0",
1196 "bare-process": "^4.2.1",
1197 "bare-subprocess": "^5.0.0"
1198 },
1199 "bin": {
1200 "bare": "bin/bare"
1201 },
1202 "optionalDependencies": {
1203 "bare-runtime-android-arm": "1.28.4",
1204 "bare-runtime-android-arm64": "1.28.4",
1205 "bare-runtime-android-ia32": "1.28.4",
1206 "bare-runtime-android-x64": "1.28.4",
1207 "bare-runtime-darwin-arm64": "1.28.4",
1208 "bare-runtime-darwin-x64": "1.28.4",
1209 "bare-runtime-ios-arm64": "1.28.4",
1210 "bare-runtime-ios-arm64-simulator": "1.28.4",
1211 "bare-runtime-ios-x64-simulator": "1.28.4",
1212 "bare-runtime-linux-arm64": "1.28.4",
1213 "bare-runtime-linux-x64": "1.28.4",
1214 "bare-runtime-win32-arm64": "1.28.4",
1215 "bare-runtime-win32-x64": "1.28.4"
1216 }
1217 },
1218 "node_modules/bare-runtime-android-arm": {
1219 "version": "1.28.4",
1220 "resolved": "https://registry.npmjs.org/bare-runtime-android-arm/-/bare-runtime-android-arm-1.28.4.tgz",
1221 "integrity": "sha512-63miGmvSSWZM5EnMUpzDFVHlIpmlPTZl6KdneDP9uEW1MaTDX7od/Qm3XZj07VHMasPOu4XBiln+gOzJy7R3Tg==",
1222 "cpu": [
1223 "arm"
1224 ],
1225 "dev": true,
1226 "license": "Apache-2.0",
1227 "optional": true,
1228 "os": [
1229 "android"
1230 ],
1231 "peer": true,
1232 "dependencies": {
1233 "require-asset": "^1.0.2"
1234 },
1235 "bin": {
1236 "bare": "bin/bare"
1237 }
1238 },
1239 "node_modules/bare-runtime-android-arm64": {
1240 "version": "1.28.4",
1241 "resolved": "https://registry.npmjs.org/bare-runtime-android-arm64/-/bare-runtime-android-arm64-1.28.4.tgz",
1242 "integrity": "sha512-bJKdRmgCpd0/Vn6O92tJq9ZGbnyOy3z/7kc9YqVZeuzrDPAS/He8r+RZVMoTGyuUFtkjgDiNXKZXfZYMJtLH+w==",
1243 "cpu": [
1244 "arm64"
1245 ],
1246 "dev": true,
1247 "license": "Apache-2.0",
1248 "optional": true,
1249 "os": [
1250 "android"
1251 ],
1252 "peer": true,
1253 "dependencies": {
1254 "require-asset": "^1.0.2"
1255 },
1256 "bin": {
1257 "bare": "bin/bare"
1258 }
1259 },
1260 "node_modules/bare-runtime-android-ia32": {
1261 "version": "1.28.4",
1262 "resolved": "https://registry.npmjs.org/bare-runtime-android-ia32/-/bare-runtime-android-ia32-1.28.4.tgz",
1263 "integrity": "sha512-dyg33MmoDyyBHEBj55oEYUCkFQzYokx1Va+JmBXWu8ag4N4tNZhkp79YcuM7ehH9M0U8R4nyt1Gl6zYL7ob0rQ==",
1264 "cpu": [
1265 "ia32"
1266 ],
1267 "dev": true,
1268 "license": "Apache-2.0",
1269 "optional": true,
1270 "os": [
1271 "android"
1272 ],
1273 "peer": true,
1274 "dependencies": {
1275 "require-asset": "^1.0.2"
1276 },
1277 "bin": {
1278 "bare": "bin/bare"
1279 }
1280 },
1281 "node_modules/bare-runtime-android-x64": {
1282 "version": "1.28.4",
1283 "resolved": "https://registry.npmjs.org/bare-runtime-android-x64/-/bare-runtime-android-x64-1.28.4.tgz",
1284 "integrity": "sha512-NsuchwCDLlNC3BlABUla2eKbSd/2YvuqHywpJFyHL9IxaPPni0o4D3oVEOeU5ytkeC0HsatzSkZ4Quyi3HD9MA==",
1285 "cpu": [
1286 "x64"
1287 ],
1288 "dev": true,
1289 "license": "Apache-2.0",
1290 "optional": true,
1291 "os": [
1292 "android"
1293 ],
1294 "peer": true,
1295 "dependencies": {
1296 "require-asset": "^1.0.2"
1297 },
1298 "bin": {
1299 "bare": "bin/bare"
1300 }
1301 },
1302 "node_modules/bare-runtime-darwin-arm64": {
1303 "version": "1.28.4",
1304 "resolved": "https://registry.npmjs.org/bare-runtime-darwin-arm64/-/bare-runtime-darwin-arm64-1.28.4.tgz",
1305 "integrity": "sha512-WHR8/OrvN+Wh3t+yY+ISF3UMhIS4aywbxySB+2ktMfVuOw+y6m+d4vEfgWIgRjsA2snaqi2Zgk7FvABdS+NT8w==",
1306 "cpu": [
1307 "arm64"
1308 ],
1309 "dev": true,
1310 "license": "Apache-2.0",
1311 "optional": true,
1312 "os": [
1313 "darwin"
1314 ],
1315 "peer": true,
1316 "dependencies": {
1317 "require-asset": "^1.0.2"
1318 },
1319 "bin": {
1320 "bare": "bin/bare"
1321 }
1322 },
1323 "node_modules/bare-runtime-darwin-x64": {
1324 "version": "1.28.4",
1325 "resolved": "https://registry.npmjs.org/bare-runtime-darwin-x64/-/bare-runtime-darwin-x64-1.28.4.tgz",
1326 "integrity": "sha512-zjQ8ej4ZQcY1/BFdVopo+m+Y11cM36LUjUWMrR5oS8v4GqFJEDfn/2PcMHzCRl6dptItrnWQjGqtGvbfOghEbQ==",
1327 "cpu": [
1328 "x64"
1329 ],
1330 "dev": true,
1331 "license": "Apache-2.0",
1332 "optional": true,
1333 "os": [
1334 "darwin"
1335 ],
1336 "peer": true,
1337 "dependencies": {
1338 "require-asset": "^1.0.2"
1339 },
1340 "bin": {
1341 "bare": "bin/bare"
1342 }
1343 },
1344 "node_modules/bare-runtime-ios-arm64": {
1345 "version": "1.28.4",
1346 "resolved": "https://registry.npmjs.org/bare-runtime-ios-arm64/-/bare-runtime-ios-arm64-1.28.4.tgz",
1347 "integrity": "sha512-GGgXLIt4wKo2yZ45im/yWf6FRYlclJmT3LkFgv/RYd1YjNE+Om0RfRHjQBaHKO0Jinuo7/u64ueNU1DbrBxrAA==",
1348 "cpu": [
1349 "arm64"
1350 ],
1351 "dev": true,
1352 "license": "Apache-2.0",
1353 "optional": true,
1354 "os": [
1355 "ios"
1356 ],
1357 "peer": true,
1358 "dependencies": {
1359 "require-asset": "^1.0.2"
1360 },
1361 "bin": {
1362 "bare": "bin/bare"
1363 }
1364 },
1365 "node_modules/bare-runtime-ios-arm64-simulator": {
1366 "version": "1.28.4",
1367 "resolved": "https://registry.npmjs.org/bare-runtime-ios-arm64-simulator/-/bare-runtime-ios-arm64-simulator-1.28.4.tgz",
1368 "integrity": "sha512-f8kLmE1QznvRY2TX6XDGEZFGsdSuQfMRmjGenSMinhh/ZvYbbeXc1vw4KDi6Bq8oXD1MMhLP+mUESLvWH3zD/A==",
1369 "cpu": [
1370 "arm64"
1371 ],
1372 "dev": true,
1373 "license": "Apache-2.0",
1374 "optional": true,
1375 "os": [
1376 "ios"
1377 ],
1378 "peer": true,
1379 "dependencies": {
1380 "require-asset": "^1.0.2"
1381 },
1382 "bin": {
1383 "bare": "bin/bare"
1384 }
1385 },
1386 "node_modules/bare-runtime-ios-x64-simulator": {
1387 "version": "1.28.4",
1388 "resolved": "https://registry.npmjs.org/bare-runtime-ios-x64-simulator/-/bare-runtime-ios-x64-simulator-1.28.4.tgz",
1389 "integrity": "sha512-ZWSSu0TuDiF/SbaRASPCyHnvwmG48nk04ICf4Fo+slB8h3DYe7je8Hqk7ktzvyGLaj781HFvMk9nlaQK2M/nDA==",
1390 "cpu": [
1391 "x64"
1392 ],
1393 "dev": true,
1394 "license": "Apache-2.0",
1395 "optional": true,
1396 "os": [
1397 "ios"
1398 ],
1399 "peer": true,
1400 "dependencies": {
1401 "require-asset": "^1.0.2"
1402 },
1403 "bin": {
1404 "bare": "bin/bare"
1405 }
1406 },
1407 "node_modules/bare-runtime-linux-arm64": {
1408 "version": "1.28.4",
1409 "resolved": "https://registry.npmjs.org/bare-runtime-linux-arm64/-/bare-runtime-linux-arm64-1.28.4.tgz",
1410 "integrity": "sha512-zHPFfuJzmQw75n8EHwVpzu1IK3Baq5peCin0Hms40Nti+C02NIjJdQLNDNS1nI9nyMluDKDLOGIKBJO78cze8Q==",
1411 "cpu": [
1412 "arm64"
1413 ],
1414 "dev": true,
1415 "license": "Apache-2.0",
1416 "optional": true,
1417 "os": [
1418 "linux"
1419 ],
1420 "peer": true,
1421 "dependencies": {
1422 "require-asset": "^1.0.2"
1423 },
1424 "bin": {
1425 "bare": "bin/bare"
1426 }
1427 },
1428 "node_modules/bare-runtime-linux-x64": {
1429 "version": "1.28.4",
1430 "resolved": "https://registry.npmjs.org/bare-runtime-linux-x64/-/bare-runtime-linux-x64-1.28.4.tgz",
1431 "integrity": "sha512-qPRAxSdAwmCabTXfvQeiWhLqAYOeh1bnQhfwNShrL2MkuY49Sar4i4ggk3UatjTnYMj1HrWu51U5BXIQWdudmQ==",
1432 "cpu": [
1433 "x64"
1434 ],
1435 "dev": true,
1436 "license": "Apache-2.0",
1437 "optional": true,
1438 "os": [
1439 "linux"
1440 ],
1441 "peer": true,
1442 "dependencies": {
1443 "require-asset": "^1.0.2"
1444 },
1445 "bin": {
1446 "bare": "bin/bare"
1447 }
1448 },
1449 "node_modules/bare-runtime-win32-arm64": {
1450 "version": "1.28.4",
1451 "resolved": "https://registry.npmjs.org/bare-runtime-win32-arm64/-/bare-runtime-win32-arm64-1.28.4.tgz",
1452 "integrity": "sha512-TamtpCuR6DpWuxXEaj6kST1bh413KtEBjzxsfSf5T0Mcs+EFccOxx7leGyY+B1KRgXXapaAd19fPD+QSFQ/bQA==",
1453 "cpu": [
1454 "arm64"
1455 ],
1456 "dev": true,
1457 "license": "Apache-2.0",
1458 "optional": true,
1459 "os": [
1460 "win32"
1461 ],
1462 "peer": true,
1463 "dependencies": {
1464 "require-asset": "^1.0.2"
1465 },
1466 "bin": {
1467 "bare": "bin/bare.exe"
1468 }
1469 },
1470 "node_modules/bare-runtime-win32-x64": {
1471 "version": "1.28.4",
1472 "resolved": "https://registry.npmjs.org/bare-runtime-win32-x64/-/bare-runtime-win32-x64-1.28.4.tgz",
1473 "integrity": "sha512-+pawq6iPoWgejSVl2r5CIwOvYqKsK9fcHBuYPr4BSoKddj1p0dGvCG6GxYShXwb65lg4pQQJqIP+SPrwfAJU/g==",
1474 "cpu": [
1475 "x64"
1476 ],
1477 "dev": true,
1478 "license": "Apache-2.0",
1479 "optional": true,
1480 "os": [
1481 "win32"
1482 ],
1483 "peer": true,
1484 "dependencies": {
1485 "require-asset": "^1.0.2"
1486 },
1487 "bin": {
1488 "bare": "bin/bare.exe"
1489 }
1490 },
1491 "node_modules/bare-semver": {
1492 "version": "1.0.3",
1493 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.3.tgz",
1494 "integrity": "sha512-HS/A30bi2+PiRJfU6R4+Kp+6KeLSCSByjYM2iiobOKzLAvtu1CT+S8xWfiU7wz0erknjkUoC+yXy108tzIuP5Q==",
1495 "license": "Apache-2.0"
1496 },
1497 "node_modules/bare-signals": {
1498 "version": "4.2.0",
1499 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
1500 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
1501 "license": "Apache-2.0",
1502 "dependencies": {
1503 "bare-events": "^2.5.3",
1504 "bare-os": "^3.3.1"
1505 },
1506 "engines": {
1507 "bare": ">=1.7.0"
1508 }
1509 },
1510 "node_modules/bare-stdio": {
1511 "version": "1.0.2",
1512 "resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.2.tgz",
1513 "integrity": "sha512-3WJDqtvVGP4f+j68kyEC05umOYNwKJ1xG+YAXL8yZ605WgNqiRhVaFq+mVIhBt2eKNp7pa5vCQdhOt1pNh79SA==",
1514 "license": "Apache-2.0",
1515 "dependencies": {
1516 "bare-fs": "^4.5.2",
1517 "bare-pipe": "^4.1.5",
1518 "bare-tty": "^5.0.3"
1519 }
1520 },
1521 "node_modules/bare-stream": {
1522 "version": "2.13.0",
1523 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz",
1524 "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==",
1525 "license": "Apache-2.0",
1526 "dependencies": {
1527 "streamx": "^2.25.0",
1528 "teex": "^1.0.1"
1529 },
1530 "peerDependencies": {
1531 "bare-abort-controller": "*",
1532 "bare-buffer": "*",
1533 "bare-events": "*"
1534 },
1535 "peerDependenciesMeta": {
1536 "bare-abort-controller": {
1537 "optional": true
1538 },
1539 "bare-buffer": {
1540 "optional": true
1541 },
1542 "bare-events": {
1543 "optional": true
1544 }
1545 }
1546 },
1547 "node_modules/bare-structured-clone": {
1548 "version": "1.5.3",
1549 "resolved": "https://registry.npmjs.org/bare-structured-clone/-/bare-structured-clone-1.5.3.tgz",
1550 "integrity": "sha512-vC/YqGsp67ZeFnpyAskwaEIXtNscnCwFVKlSk0Oh2X3AqWT6H+7DC9vVI080FohUJeuUuB9UNx4UFdDcyQgZaw==",
1551 "license": "Apache-2.0",
1552 "dependencies": {
1553 "bare-type": "^1.1.0",
1554 "compact-encoding": "^2.15.0"
1555 },
1556 "engines": {
1557 "bare": ">=1.2.0"
1558 },
1559 "peerDependencies": {
1560 "bare-buffer": "*",
1561 "bare-url": "*"
1562 },
1563 "peerDependenciesMeta": {
1564 "bare-buffer": {
1565 "optional": true
1566 },
1567 "bare-url": {
1568 "optional": true
1569 }
1570 }
1571 },
1572 "node_modules/bare-stylize": {
1573 "version": "0.0.1",
1574 "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz",
1575 "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==",
1576 "license": "Apache-2.0",
1577 "dependencies": {
1578 "bare-ansi-escapes": "^2.2.3",
1579 "bare-process": "^4.2.1"
1580 }
1581 },
1582 "node_modules/bare-subprocess": {
1583 "version": "5.2.3",
1584 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
1585 "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
1586 "license": "Apache-2.0",
1587 "dependencies": {
1588 "bare-env": "^3.0.0",
1589 "bare-events": "^2.5.4",
1590 "bare-os": "^3.0.1",
1591 "bare-pipe": "^4.0.0",
1592 "bare-url": "^2.2.2"
1593 },
1594 "engines": {
1595 "bare": ">=1.7.0"
1596 },
1597 "peerDependencies": {
1598 "bare-buffer": "*"
1599 },
1600 "peerDependenciesMeta": {
1601 "bare-buffer": {
1602 "optional": true
1603 }
1604 }
1605 },
1606 "node_modules/bare-tcp": {
1607 "version": "2.2.7",
1608 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz",
1609 "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==",
1610 "license": "Apache-2.0",
1611 "dependencies": {
1612 "bare-dns": "^2.0.4",
1613 "bare-events": "^2.5.4",
1614 "bare-stream": "^2.6.4"
1615 },
1616 "engines": {
1617 "bare": ">=1.16.0"
1618 }
1619 },
1620 "node_modules/bare-tty": {
1621 "version": "5.1.0",
1622 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.0.tgz",
1623 "integrity": "sha512-EZLvW4A+XiJgI3TW+e1pMME9PIJsfEXe/DA/WSKzIkq/v7Yarpv/rvG6Z5pGnpo4V/Bd+qopwnCLSR71hMMBYA==",
1624 "license": "Apache-2.0",
1625 "dependencies": {
1626 "bare-events": "^2.2.0",
1627 "bare-signals": "^4.0.0",
1628 "bare-stream": "^2.0.0"
1629 },
1630 "engines": {
1631 "bare": ">=1.16.0"
1632 }
1633 },
1634 "node_modules/bare-type": {
1635 "version": "1.1.0",
1636 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
1637 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
1638 "license": "Apache-2.0",
1639 "engines": {
1640 "bare": ">=1.2.0"
1641 }
1642 },
1643 "node_modules/bare-url": {
1644 "version": "2.4.0",
1645 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
1646 "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
1647 "license": "Apache-2.0",
1648 "dependencies": {
1649 "bare-path": "^3.0.0"
1650 }
1651 },
1652 "node_modules/bare-utils": {
1653 "version": "1.6.0",
1654 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz",
1655 "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==",
1656 "license": "Apache-2.0",
1657 "dependencies": {
1658 "bare-debug-log": "^2.0.0",
1659 "bare-encoding": "^1.0.0",
1660 "bare-format": "^1.0.0",
1661 "bare-inspect": "^3.0.0",
1662 "bare-stylize": "^0.0.1",
1663 "bare-type": "^1.0.6"
1664 }
1665 },
1666 "node_modules/barely-colours": {
1667 "version": "1.0.0",
1668 "resolved": "https://registry.npmjs.org/barely-colours/-/barely-colours-1.0.0.tgz",
1669 "integrity": "sha512-kzOuBDHD3O4BmVwq7yuD62bqc7WnF8YG648W3nQ2lRjRr7psv5MWrpWkUTTUoCbv6V3WjsWJMuACDz4eSNa6jQ==",
1670 "license": "GPL v3",
1671 "dependencies": {
1672 "bare-ansi-escapes": "^2.2.3"
1673 }
1674 },
1675 "node_modules/big-sparse-array": {
1676 "version": "1.0.3",
1677 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
1678 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
1679 "license": "MIT"
1680 },
1681 "node_modules/binary-stream-equals": {
1682 "version": "1.0.0",
1683 "resolved": "https://registry.npmjs.org/binary-stream-equals/-/binary-stream-equals-1.0.0.tgz",
1684 "integrity": "sha512-xiUT5LGfD8JiLhbXiG+ByOnbgb9f2ssRLfZDQMl3nZdf89EotQZGZuMkDN8J3n46emabE7RnJ1q0r7Hv3INExw==",
1685 "license": "MIT",
1686 "dependencies": {
1687 "b4a": "^1.3.1"
1688 }
1689 },
1690 "node_modules/bits-to-bytes": {
1691 "version": "1.3.0",
1692 "resolved": "https://registry.npmjs.org/bits-to-bytes/-/bits-to-bytes-1.3.0.tgz",
1693 "integrity": "sha512-OJoHTpFXS9bXHBCekGTByf3MqM8CGblBDIduKQeeVVeiU9dDWywSSirXIBYGgg3d1zbVuvnMa1vD4r6PA0kOKg==",
1694 "license": "ISC",
1695 "dependencies": {
1696 "b4a": "^1.5.0"
1697 }
1698 },
1699 "node_modules/blake2b": {
1700 "version": "2.1.4",
1701 "resolved": "https://registry.npmjs.org/blake2b/-/blake2b-2.1.4.tgz",
1702 "integrity": "sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==",
1703 "license": "ISC",
1704 "dependencies": {
1705 "blake2b-wasm": "^2.4.0",
1706 "nanoassert": "^2.0.0"
1707 }
1708 },
1709 "node_modules/blake2b-wasm": {
1710 "version": "2.4.0",
1711 "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz",
1712 "integrity": "sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==",
1713 "license": "MIT",
1714 "dependencies": {
1715 "b4a": "^1.0.1",
1716 "nanoassert": "^2.0.0"
1717 }
1718 },
1719 "node_modules/blind-relay": {
1720 "version": "1.4.0",
1721 "resolved": "https://registry.npmjs.org/blind-relay/-/blind-relay-1.4.0.tgz",
1722 "integrity": "sha512-6xt7fDfCs6eGmNNym6I9N42jmjcMQn2qwwOVnkP9ZnrkXFk6c4/tdO1xqRmDEzKzV8gigd+DVdCUG/RUYnen7Q==",
1723 "license": "Apache-2.0",
1724 "dependencies": {
1725 "b4a": "^1.6.4",
1726 "bare-events": "^2.2.0",
1727 "bits-to-bytes": "^1.3.0",
1728 "compact-encoding": "^2.12.0",
1729 "compact-encoding-bitfield": "^1.0.0",
1730 "protomux": "^3.5.1",
1731 "sodium-universal": "^5.0.0",
1732 "streamx": "^2.15.1"
1733 }
1734 },
1735 "node_modules/bogon": {
1736 "version": "1.2.0",
1737 "resolved": "https://registry.npmjs.org/bogon/-/bogon-1.2.0.tgz",
1738 "integrity": "sha512-FqOBr/1VMzCOsoJd+fzNUMarUYki2+TKt07A2+xaulsNx4r53iJ7MV5k0jbqg7W2U0CsLqxCZOrFibdG6h6HSg==",
1739 "license": "MIT",
1740 "dependencies": {
1741 "compact-encoding": "^2.11.0",
1742 "compact-encoding-net": "^1.2.0"
1743 }
1744 },
1745 "node_modules/chardet": {
1746 "version": "2.1.1",
1747 "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
1748 "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
1749 "license": "MIT"
1750 },
1751 "node_modules/child_process": {
1752 "name": "bare-subprocess",
1753 "version": "5.2.3",
1754 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
1755 "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
1756 "license": "Apache-2.0",
1757 "dependencies": {
1758 "bare-env": "^3.0.0",
1759 "bare-events": "^2.5.4",
1760 "bare-os": "^3.0.1",
1761 "bare-pipe": "^4.0.0",
1762 "bare-url": "^2.2.2"
1763 },
1764 "engines": {
1765 "bare": ">=1.7.0"
1766 },
1767 "peerDependencies": {
1768 "bare-buffer": "*"
1769 },
1770 "peerDependenciesMeta": {
1771 "bare-buffer": {
1772 "optional": true
1773 }
1774 }
1775 },
1776 "node_modules/cjs-module-lexer": {
1777 "version": "1.4.3",
1778 "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
1779 "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
1780 "license": "MIT"
1781 },
1782 "node_modules/cli-box": {
1783 "version": "6.0.10",
1784 "resolved": "https://registry.npmjs.org/cli-box/-/cli-box-6.0.10.tgz",
1785 "integrity": "sha512-6jjSF6G1gOXaCyBQJKo3L3lZKxP8jzdECG7FBiA4m2w7K8jBxXXwsMb75fBLwoVwOtKwvgKjLJurw8HU5XNciw==",
1786 "license": "MIT",
1787 "dependencies": {
1788 "ansi-parser": "^3.2.1",
1789 "deffy": "^2.2.1",
1790 "is-undefined": "^1.0.0",
1791 "is-win": "^1.0.0",
1792 "ul": "^5.2.1"
1793 }
1794 },
1795 "node_modules/cli-width": {
1796 "version": "4.1.0",
1797 "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
1798 "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
1799 "license": "ISC",
1800 "engines": {
1801 "node": ">= 12"
1802 }
1803 },
1804 "node_modules/cliui": {
1805 "version": "8.0.1",
1806 "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
1807 "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
1808 "license": "ISC",
1809 "dependencies": {
1810 "string-width": "^4.2.0",
1811 "strip-ansi": "^6.0.1",
1812 "wrap-ansi": "^7.0.0"
1813 },
1814 "engines": {
1815 "node": ">=12"
1816 }
1817 },
1818 "node_modules/codecs": {
1819 "version": "3.1.0",
1820 "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz",
1821 "integrity": "sha512-Dqx8NwvBvnMeuPQdVKy/XEF71igjR5apxBvCGeV0pP1tXadOiaLvDTXt7xh+/5wI1ASB195mXQGJbw3Ml4YDWQ==",
1822 "license": "MIT",
1823 "dependencies": {
1824 "b4a": "^1.6.3"
1825 }
1826 },
1827 "node_modules/color-convert": {
1828 "version": "2.0.1",
1829 "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1830 "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1831 "license": "MIT",
1832 "dependencies": {
1833 "color-name": "~1.1.4"
1834 },
1835 "engines": {
1836 "node": ">=7.0.0"
1837 }
1838 },
1839 "node_modules/color-name": {
1840 "version": "1.1.4",
1841 "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1842 "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1843 "license": "MIT"
1844 },
1845 "node_modules/colors": {
1846 "version": "1.4.0",
1847 "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
1848 "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
1849 "license": "MIT",
1850 "engines": {
1851 "node": ">=0.1.90"
1852 }
1853 },
1854 "node_modules/compact-encoding": {
1855 "version": "2.19.2",
1856 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.2.tgz",
1857 "integrity": "sha512-/YjhHQE/5L4F7l5Bht69dRbP9RV6zoJPeowi8bMKQxNKe3Nh6hOY8pBGoVE9fz5GaWfEd8fWJ2aU9sB4KZuMYg==",
1858 "license": "Apache-2.0",
1859 "dependencies": {
1860 "b4a": "^1.3.0"
1861 }
1862 },
1863 "node_modules/compact-encoding-bitfield": {
1864 "version": "1.0.0",
1865 "resolved": "https://registry.npmjs.org/compact-encoding-bitfield/-/compact-encoding-bitfield-1.0.0.tgz",
1866 "integrity": "sha512-3nMVKUg+PF72UHfainmCL8uKvyWfxsjqOtUY+HiMPGLPCTjnwzoKfFAMo1Ad7nwTPdjBqtGK5b3BOFTFW4EBTg==",
1867 "license": "ISC",
1868 "dependencies": {
1869 "compact-encoding": "^2.4.1"
1870 }
1871 },
1872 "node_modules/compact-encoding-net": {
1873 "version": "1.2.0",
1874 "resolved": "https://registry.npmjs.org/compact-encoding-net/-/compact-encoding-net-1.2.0.tgz",
1875 "integrity": "sha512-LVXpNpF7PGQeHRVVLGgYWzuVoYAaDZvKUsUxRioGfkotzvOh4AzoQF1HBH3zMNaSnx7gJXuUr3hkjnijaH/Eng==",
1876 "license": "ISC",
1877 "dependencies": {
1878 "compact-encoding": "^2.4.1"
1879 }
1880 },
1881 "node_modules/corestore": {
1882 "version": "7.9.2",
1883 "resolved": "https://registry.npmjs.org/corestore/-/corestore-7.9.2.tgz",
1884 "integrity": "sha512-dyIktVSVLu0skZ1UM4yODbNbZ46dBpTHypzwlwET1btVuS3YIf66Zz8Tx8h3Xa8nbLO2USGqrYtvPYzSRj2wCA==",
1885 "license": "MIT",
1886 "dependencies": {
1887 "b4a": "^1.6.7",
1888 "hypercore": "^11.19.0",
1889 "hypercore-crypto": "^3.4.2",
1890 "hypercore-errors": "^1.4.0",
1891 "hypercore-id-encoding": "^1.3.0",
1892 "ready-resource": "^1.1.1",
1893 "sodium-universal": "^5.0.1",
1894 "which-runtime": "^1.2.1"
1895 }
1896 },
1897 "node_modules/crc-native": {
1898 "version": "1.1.8",
1899 "resolved": "https://registry.npmjs.org/crc-native/-/crc-native-1.1.8.tgz",
1900 "integrity": "sha512-R/RYbD8lhG5M1k0iLX49tJdwHM0ipevJPpZw2Wk7b4EShyvx2P0uaq79xyRpcknjiVGiUfjFA8Gyy0mOlhABvw==",
1901 "license": "Apache-2.0",
1902 "optional": true,
1903 "dependencies": {
1904 "require-addon": "^1.1.0"
1905 }
1906 },
1907 "node_modules/crc-universal": {
1908 "version": "1.0.4",
1909 "resolved": "https://registry.npmjs.org/crc-universal/-/crc-universal-1.0.4.tgz",
1910 "integrity": "sha512-CE9xWEI6Gd5V0Bdmj5NoWH3d7+EIe4zUpy1sv6uvKYznzsDP1vhiWsTBmL6q9IrH2P6RRshp+8AkhR4CMtY5Hg==",
1911 "license": "ISC",
1912 "optionalDependencies": {
1913 "crc-native": "^1.0.3"
1914 }
1915 },
1916 "node_modules/debounceify": {
1917 "version": "1.1.0",
1918 "resolved": "https://registry.npmjs.org/debounceify/-/debounceify-1.1.0.tgz",
1919 "integrity": "sha512-eKuHDVfJVg+u/0nPy8P+fhnLgbyuTgVxuCRrS/R7EpDSMMkBDgSes41MJtSAY1F1hcqfHz3Zy/qpqHHIp/EhdA==",
1920 "license": "MIT"
1921 },
1922 "node_modules/deffy": {
1923 "version": "2.2.5",
1924 "resolved": "https://registry.npmjs.org/deffy/-/deffy-2.2.5.tgz",
1925 "integrity": "sha512-6TX2cfIo97eKqWmqgMDAUulCwnveAe3K+4VGsTGPJsL3NtSEnSBFZ3sUXdS4EBhZ8GbdaZBzXQ04ton18dJrug==",
1926 "license": "MIT",
1927 "dependencies": {
1928 "typpy": "^2.0.0"
1929 }
1930 },
1931 "node_modules/device-file": {
1932 "version": "2.3.1",
1933 "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.3.1.tgz",
1934 "integrity": "sha512-bmON44lwxJPle9N2OcH4tqM44pMGZKT8G6OkzXkz0urvqQ9LKkoQdTS6w0ztYfmLdUE27R4UUmx0+y2gEz4Jug==",
1935 "license": "Apache-2.0",
1936 "dependencies": {
1937 "b4a": "^1.6.7",
1938 "bare-fs": "^4.0.1",
1939 "bare-path": "^3.0.0",
1940 "fd-lock": "^2.1.0",
1941 "fs-native-extensions": "^1.4.0",
1942 "ready-resource": "^1.2.0"
1943 }
1944 },
1945 "node_modules/dht-rpc": {
1946 "version": "6.26.4",
1947 "resolved": "https://registry.npmjs.org/dht-rpc/-/dht-rpc-6.26.4.tgz",
1948 "integrity": "sha512-bMI625c13DXaiYN8vHEFmcM4CIauwK1SJgOMWAAqsXhsKXO8WCdp0fgYwTHH40cBB6J9MkDXPa2f79Vj5Ah1iA==",
1949 "license": "MIT",
1950 "dependencies": {
1951 "adaptive-timeout": "^1.0.1",
1952 "b4a": "^1.6.1",
1953 "bare-events": "^2.2.0",
1954 "compact-encoding": "^2.11.0",
1955 "compact-encoding-net": "^1.2.0",
1956 "fast-fifo": "^1.1.0",
1957 "kademlia-routing-table": "^1.0.1",
1958 "nat-sampler": "^1.0.1",
1959 "sodium-universal": "^5.0.0",
1960 "streamx": "^2.13.2",
1961 "time-ordered-set": "^2.0.0",
1962 "udx-native": "^1.5.3"
1963 }
1964 },
1965 "node_modules/distributed-drive": {
1966 "version": "0.1.1",
1967 "resolved": "https://registry.npmjs.org/distributed-drive/-/distributed-drive-0.1.1.tgz",
1968 "integrity": "sha512-sFQP1ojKlpZ8StLnJQXnzepAKTHwkxlf63AKRfDMYff/E1ePEkeNxeyg3mrulpElwzudYjRI41hFLDJEcI8V5g==",
1969 "license": "Apache-2.0",
1970 "dependencies": {
1971 "compact-encoding": "^2.19.2",
1972 "hyperschema": "^1.20.1",
1973 "mirror-drive": "^1.14.1",
1974 "protomux-rpc": "^1.9.0",
1975 "ready-resource": "^1.2.0",
1976 "record-cache": "^2.0.0",
1977 "safety-catch": "^1.0.3",
1978 "streamx": "^2.25.0"
1979 }
1980 },
1981 "node_modules/distributed-drive/node_modules/record-cache": {
1982 "version": "2.0.0",
1983 "resolved": "https://registry.npmjs.org/record-cache/-/record-cache-2.0.0.tgz",
1984 "integrity": "sha512-xOQG4BL/sqDhGAaM7J7TQ8r68aetLElKyAeeWrOQc1GSbzFwW8nRbNuL264tT4V3UnOIroPecJsqtjisutat/g==",
1985 "license": "MIT",
1986 "dependencies": {
1987 "b4a": "^1.3.1"
1988 }
1989 },
1990 "node_modules/dns-packet": {
1991 "version": "5.6.1",
1992 "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
1993 "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
1994 "license": "MIT",
1995 "dependencies": {
1996 "@leichtgewicht/ip-codec": "^2.0.1"
1997 },
1998 "engines": {
1999 "node": ">=6"
2000 }
2001 },
2002 "node_modules/drive-resolve": {
2003 "version": "1.3.1",
2004 "resolved": "https://registry.npmjs.org/drive-resolve/-/drive-resolve-1.3.1.tgz",
2005 "integrity": "sha512-Fn88wa5vry4JKtE1g4md1SPzU9/fO2h8l73Fr9kmQtaatFIvquR/V6UGjTOcBsWbNgrRMQjY8N6SNoY40o57JQ==",
2006 "license": "Apache-2.0",
2007 "dependencies": {
2008 "b4a": "^1.6.4",
2009 "bare-module-resolve": "^1.2.5",
2010 "unix-path-resolve": "^1.0.2"
2011 }
2012 },
2013 "node_modules/emoji-regex": {
2014 "version": "8.0.0",
2015 "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
2016 "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
2017 "license": "MIT"
2018 },
2019 "node_modules/es-module-lexer": {
2020 "version": "1.7.0",
2021 "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
2022 "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
2023 "license": "MIT"
2024 },
2025 "node_modules/escalade": {
2026 "version": "3.2.0",
2027 "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
2028 "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
2029 "license": "MIT",
2030 "engines": {
2031 "node": ">=6"
2032 }
2033 },
2034 "node_modules/events": {
2035 "version": "3.3.0",
2036 "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
2037 "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
2038 "license": "MIT",
2039 "engines": {
2040 "node": ">=0.8.x"
2041 }
2042 },
2043 "node_modules/events-universal": {
2044 "version": "1.0.1",
2045 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
2046 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
2047 "license": "Apache-2.0",
2048 "dependencies": {
2049 "bare-events": "^2.7.0"
2050 }
2051 },
2052 "node_modules/fast-fifo": {
2053 "version": "1.3.2",
2054 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
2055 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
2056 "license": "MIT"
2057 },
2058 "node_modules/fast-string-truncated-width": {
2059 "version": "3.0.3",
2060 "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
2061 "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
2062 "license": "MIT"
2063 },
2064 "node_modules/fast-string-width": {
2065 "version": "3.0.2",
2066 "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz",
2067 "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
2068 "license": "MIT",
2069 "dependencies": {
2070 "fast-string-truncated-width": "^3.0.2"
2071 }
2072 },
2073 "node_modules/fast-wrap-ansi": {
2074 "version": "0.2.0",
2075 "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz",
2076 "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==",
2077 "license": "MIT",
2078 "dependencies": {
2079 "fast-string-width": "^3.0.2"
2080 }
2081 },
2082 "node_modules/fd-lock": {
2083 "version": "2.1.1",
2084 "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-2.1.1.tgz",
2085 "integrity": "sha512-H3VkcWFl39Rk0xBokDcUyBcVs6VrYTUo/DhDWMzJOF99wXWDAvmvq/GlLTS2NTehsznZhp3fXVyrtB2ldDXhwg==",
2086 "license": "Apache-2.0",
2087 "dependencies": {
2088 "bare-fs": "^4.5.0",
2089 "fs-native-extensions": "^1.4.4",
2090 "ready-resource": "^1.2.0",
2091 "resource-on-exit": "^1.0.0"
2092 }
2093 },
2094 "node_modules/flat-tree": {
2095 "version": "1.13.0",
2096 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
2097 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
2098 "license": "MIT"
2099 },
2100 "node_modules/framed-stream": {
2101 "version": "1.0.1",
2102 "resolved": "https://registry.npmjs.org/framed-stream/-/framed-stream-1.0.1.tgz",
2103 "integrity": "sha512-x0At1ETY20yXARxM6Qr5BGasgPpD0/HVYDyA7Uiaf9LfSqrb8p/F9i0s2/8YoeHkZcJeKsnwWBrnJCfZZoxHjA==",
2104 "license": "MIT",
2105 "dependencies": {
2106 "b4a": "^1.6.1",
2107 "streamx": "^2.13.0"
2108 }
2109 },
2110 "node_modules/fs": {
2111 "name": "bare-fs",
2112 "version": "4.7.1",
2113 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz",
2114 "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==",
2115 "license": "Apache-2.0",
2116 "dependencies": {
2117 "bare-events": "^2.5.4",
2118 "bare-path": "^3.0.0",
2119 "bare-stream": "^2.6.4",
2120 "bare-url": "^2.2.2",
2121 "fast-fifo": "^1.3.2"
2122 },
2123 "engines": {
2124 "bare": ">=1.16.0"
2125 },
2126 "peerDependencies": {
2127 "bare-buffer": "*"
2128 },
2129 "peerDependenciesMeta": {
2130 "bare-buffer": {
2131 "optional": true
2132 }
2133 }
2134 },
2135 "node_modules/fs-native-extensions": {
2136 "version": "1.5.0",
2137 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.5.0.tgz",
2138 "integrity": "sha512-nuZLFm9mGCxvyi7Llww/J4OyifKCS21nEUTAmnlTZp3FObPOvA32aCedCmt4Z+8yk+caqfClNaSCfn/P7T7FLQ==",
2139 "license": "Apache-2.0",
2140 "dependencies": {
2141 "require-addon": "^1.1.0",
2142 "which-runtime": "^1.2.0"
2143 }
2144 },
2145 "node_modules/fs-native-lock": {
2146 "version": "1.2.0",
2147 "resolved": "https://registry.npmjs.org/fs-native-lock/-/fs-native-lock-1.2.0.tgz",
2148 "integrity": "sha512-x2KlVzvJbNhI2vc+z9+5naq04+6XMyoeLv816sGmP4DWFonu+/N+pigIoDzFV+bdnC5BD3GLeI8eqV/dwCY57Q==",
2149 "license": "Apache-2.0",
2150 "dependencies": {
2151 "bare-fs": "^4.0.1",
2152 "bare-path": "^3.0.0",
2153 "fs-native-extensions": "^1.3.4",
2154 "resource-on-exit": "^1.0.0"
2155 }
2156 },
2157 "node_modules/function.name": {
2158 "version": "1.0.14",
2159 "resolved": "https://registry.npmjs.org/function.name/-/function.name-1.0.14.tgz",
2160 "integrity": "sha512-s99L814NRuLxwF2sJMIcLhkQhueGXb3oKyvorzrUKKwlVB0SBbWrgZt4+EwKAo3ujCXnT7vshmCvXgZA09kCMw==",
2161 "license": "MIT",
2162 "dependencies": {
2163 "noop6": "^1.0.1"
2164 }
2165 },
2166 "node_modules/generate-object-property": {
2167 "version": "2.0.0",
2168 "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-2.0.0.tgz",
2169 "integrity": "sha512-KwuURPyqn2Mz8DdV29pJwQu0Y7tcsbkULr82eeOcY/ZllFK6I9Wm8dsRByIu7CKWlFi9BdW1b3mcXMp/kQBQsw==",
2170 "license": "MIT",
2171 "dependencies": {
2172 "is-property": "^1.0.0"
2173 }
2174 },
2175 "node_modules/generate-string": {
2176 "version": "1.0.1",
2177 "resolved": "https://registry.npmjs.org/generate-string/-/generate-string-1.0.1.tgz",
2178 "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==",
2179 "license": "MIT"
2180 },
2181 "node_modules/get-caller-file": {
2182 "version": "2.0.5",
2183 "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
2184 "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
2185 "license": "ISC",
2186 "engines": {
2187 "node": "6.* || 8.* || >= 10.*"
2188 }
2189 },
2190 "node_modules/graceful-goodbye": {
2191 "version": "1.3.3",
2192 "resolved": "https://registry.npmjs.org/graceful-goodbye/-/graceful-goodbye-1.3.3.tgz",
2193 "integrity": "sha512-ABj07mmHrK3FC8fJ3PP5QCrbA31zkB4Qg34NFmYgwJnBxzfbRmtMzOg54PL55OG5sFjvmBzD0q85JAtRlA3dSA==",
2194 "license": "MIT",
2195 "dependencies": {
2196 "bare-process": "^4.0.0",
2197 "safety-catch": "^1.0.2"
2198 }
2199 },
2200 "node_modules/holesail": {
2201 "version": "2.4.1",
2202 "resolved": "https://registry.npmjs.org/holesail/-/holesail-2.4.1.tgz",
2203 "integrity": "sha512-u6Ta1LaEYsgvT447JG18OLeEpjWERx7Iy/icpCeR1BYT43wkpisWoZR9ydz3j9yk6PlAJDNCssEKZXm/Q9BQ7Q==",
2204 "license": "AGPL-3.0",
2205 "dependencies": {
2206 "bare-crypto": "^1.12.0",
2207 "bare-module": "^6.1.2",
2208 "bare-path": "^3.0.0",
2209 "bare-process": "^4.2.2",
2210 "bare-querystring": "^1.0.0",
2211 "barely-colours": "^1.0.0",
2212 "child_process": "npm:bare-subprocess@^5.1.5",
2213 "cli-box": "6.0.10",
2214 "colors": "^1.4.0",
2215 "graceful-goodbye": "^1.3.3",
2216 "holesail-client": "^2.3.0",
2217 "holesail-logger": "^1.1.0",
2218 "holesail-server": "^2.4.0",
2219 "hyper-cmd-lib-keys": "^0.1.1",
2220 "livefiles": "^1.1.0",
2221 "minimist": "^1.2.8",
2222 "module": "npm:bare-node-module@^1.0.0",
2223 "path": "npm:bare-node-path@^1.0.1",
2224 "prettier": "^3.6.2",
2225 "prettier-config-holepunch": "^2.0.0",
2226 "process": "npm:bare-process@^4.2.1",
2227 "qrcode-terminal": "^0.12.0",
2228 "querystring": "npm:bare-node-querystring@^1.0.0",
2229 "ready-resource": "^1.2.0",
2230 "url": "npm:bare-url@^2.3.2",
2231 "which-runtime": "^1.3.2",
2232 "z32": "^1.1.0"
2233 },
2234 "bin": {
2235 "holesail": "src/bin/holesail.mjs"
2236 }
2237 },
2238 "node_modules/holesail-client": {
2239 "version": "2.3.0",
2240 "resolved": "https://registry.npmjs.org/holesail-client/-/holesail-client-2.3.0.tgz",
2241 "integrity": "sha512-tI0YBr6//eAcfir2+Idikukl9YdTFBt/VrWXNsLSFomQgQi/XonsXF2V+FJ9H6cWFvkH5SJivVsPohaP6bQD8w==",
2242 "license": "GPL-3.0",
2243 "dependencies": {
2244 "@holesail/hyper-cmd-lib-net": "^1.1.1",
2245 "b4a": "^1.7.3",
2246 "hyperdht": "^6.27.0",
2247 "z32": "^1.1.0"
2248 }
2249 },
2250 "node_modules/holesail-logger": {
2251 "version": "1.1.0",
2252 "resolved": "https://registry.npmjs.org/holesail-logger/-/holesail-logger-1.1.0.tgz",
2253 "integrity": "sha512-iYxRLlApyGn94moP48vx8+Fg8q4rniK8LoqsHJWcUkQNgW2HOa7e/xdWWxl0BmNMPV8iIJHsTuIYNj4Yq5D8Cg==",
2254 "license": "GPL 3.0",
2255 "dependencies": {
2256 "barely-colours": "^1.0.0"
2257 }
2258 },
2259 "node_modules/holesail-server": {
2260 "version": "2.4.0",
2261 "resolved": "https://registry.npmjs.org/holesail-server/-/holesail-server-2.4.0.tgz",
2262 "integrity": "sha512-4iTSK0EVGKGWFoS5aA6g6ErgI8zgPhjnso1swXaPT8GCEbAZmecgetRhfb5nwoF7uHXS6ZJJWVfaFLF3xngZbw==",
2263 "license": "GNU GPL v3",
2264 "dependencies": {
2265 "@holesail/hyper-cmd-lib-net": "^1.1.2",
2266 "b4a": "^1.7.3",
2267 "holesail-logger": "^1.1.0",
2268 "hyper-cmd-lib-keys": "^0.2.0",
2269 "hyperdht": "^6.27.0",
2270 "z32": "^1.1.0"
2271 }
2272 },
2273 "node_modules/holesail-server/node_modules/hyper-cmd-lib-keys": {
2274 "version": "0.2.0",
2275 "resolved": "https://registry.npmjs.org/hyper-cmd-lib-keys/-/hyper-cmd-lib-keys-0.2.0.tgz",
2276 "integrity": "sha512-uCo4Mlbq/Proez5c5BlpmRqn9OnDlnniIuM5KO/DlJq5diGFZHBNeNxon8OiUft7xSk4Gh+VKUsRLVKJVPW8NQ==",
2277 "license": "Apache-2.0",
2278 "dependencies": {
2279 "sodium-universal": "^5.0.1"
2280 }
2281 },
2282 "node_modules/http": {
2283 "name": "bare-http1",
2284 "version": "4.5.6",
2285 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.6.tgz",
2286 "integrity": "sha512-31OAwMkSU+z1VuUOCk65hx3aWQgzCfH/zQ6LGxbJtmiy2Czsw0+uvOBM9YkqaL6zUSTSYG2pLbL0v/TjME3Buw==",
2287 "license": "Apache-2.0",
2288 "dependencies": {
2289 "bare-events": "^2.6.0",
2290 "bare-http-parser": "^1.1.1",
2291 "bare-stream": "^2.10.0",
2292 "bare-tcp": "^2.2.0"
2293 },
2294 "peerDependencies": {
2295 "bare-buffer": "*",
2296 "bare-url": "*"
2297 },
2298 "peerDependenciesMeta": {
2299 "bare-buffer": {
2300 "optional": true
2301 },
2302 "bare-url": {
2303 "optional": true
2304 }
2305 }
2306 },
2307 "node_modules/hyper-cmd-lib-keys": {
2308 "version": "0.1.1",
2309 "resolved": "https://registry.npmjs.org/hyper-cmd-lib-keys/-/hyper-cmd-lib-keys-0.1.1.tgz",
2310 "integrity": "sha512-K1RfDEGD2WaXlSaXJTjR0dAUJ0KLts6e+IMWStkBfW7+uRjz4mCfwdgk6hysenrZerBqXlsy3ArDmnj41e0lNg==",
2311 "license": "Apache-2.0",
2312 "dependencies": {
2313 "sodium-universal": "^4.0.1"
2314 }
2315 },
2316 "node_modules/hyper-cmd-lib-keys/node_modules/sodium-native": {
2317 "version": "4.3.3",
2318 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
2319 "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==",
2320 "license": "MIT",
2321 "dependencies": {
2322 "require-addon": "^1.1.0"
2323 }
2324 },
2325 "node_modules/hyper-cmd-lib-keys/node_modules/sodium-universal": {
2326 "version": "4.0.1",
2327 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-4.0.1.tgz",
2328 "integrity": "sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==",
2329 "license": "MIT",
2330 "dependencies": {
2331 "sodium-native": "^4.0.0"
2332 },
2333 "peerDependencies": {
2334 "sodium-javascript": "~0.8.0"
2335 },
2336 "peerDependenciesMeta": {
2337 "sodium-javascript": {
2338 "optional": true
2339 }
2340 }
2341 },
2342 "node_modules/hyperbee": {
2343 "version": "2.27.3",
2344 "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz",
2345 "integrity": "sha512-PXURH2U4juUZyJRKHTrY5z1zX851pmI1Q0jfv5F/hCIErDt/ND8jOZuxc3hfOLM9f0W3qJEDTMlV5AJBkVPy8w==",
2346 "license": "MIT",
2347 "dependencies": {
2348 "b4a": "^1.6.0",
2349 "codecs": "^3.0.0",
2350 "debounceify": "^1.0.0",
2351 "hypercore-errors": "^1.0.0",
2352 "mutexify": "^1.4.0",
2353 "protocol-buffers-encodings": "^1.2.0",
2354 "rache": "^1.0.0",
2355 "ready-resource": "^1.0.0",
2356 "resolve-reject-promise": "^1.1.0",
2357 "safety-catch": "^1.0.2",
2358 "streamx": "^2.12.4",
2359 "unslab": "^1.2.0"
2360 }
2361 },
2362 "node_modules/hyperblobs": {
2363 "version": "2.11.1",
2364 "resolved": "https://registry.npmjs.org/hyperblobs/-/hyperblobs-2.11.1.tgz",
2365 "integrity": "sha512-sHseEZDA1JI7NI+sqT+KLcs3fyhCnKKCMbIOt1MCOF4CafrBB1mwtFYq/89RJSKhd16C+8uGPT6iH35kjv1nBw==",
2366 "license": "Apache-2.0",
2367 "dependencies": {
2368 "b4a": "^1.6.1",
2369 "bare-events": "^2.5.0",
2370 "compact-encoding": "^2.18.0",
2371 "hypercore-crypto": "^3.6.1",
2372 "hypercore-errors": "^1.1.1",
2373 "mutexify": "^1.4.0",
2374 "speedometer": "^1.1.0",
2375 "streamx": "^2.13.2"
2376 }
2377 },
2378 "node_modules/hypercore": {
2379 "version": "11.28.1",
2380 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.28.1.tgz",
2381 "integrity": "sha512-d8jqUilG7Gri8wT+sRdkFTYRPMHGyvBz/NCYHf5rX7+LoivRNRxQm5nWChT7caiUKbcsDak6h7IiEQ5wbD1wCg==",
2382 "license": "MIT",
2383 "dependencies": {
2384 "@hyperswarm/secret-stream": "^6.0.0",
2385 "b4a": "^1.1.0",
2386 "bare-events": "^2.2.0",
2387 "big-sparse-array": "^1.0.3",
2388 "compact-encoding": "^2.11.0",
2389 "fast-fifo": "^1.3.0",
2390 "flat-tree": "^1.9.0",
2391 "hypercore-crypto": "^3.2.1",
2392 "hypercore-errors": "^1.5.0",
2393 "hypercore-id-encoding": "^1.2.0",
2394 "hypercore-storage": "^2.8.0",
2395 "is-options": "^1.0.1",
2396 "nanoassert": "^2.0.0",
2397 "protomux": "^3.5.0",
2398 "quickbit-universal": "^2.2.0",
2399 "random-array-iterator": "^1.0.0",
2400 "safety-catch": "^1.0.1",
2401 "sodium-universal": "^5.0.1",
2402 "streamx": "^2.12.4",
2403 "unslab": "^1.3.0",
2404 "z32": "^1.0.0"
2405 }
2406 },
2407 "node_modules/hypercore-crypto": {
2408 "version": "3.6.1",
2409 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
2410 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
2411 "license": "MIT",
2412 "dependencies": {
2413 "b4a": "^1.6.6",
2414 "compact-encoding": "^2.15.0",
2415 "sodium-universal": "^5.0.0"
2416 }
2417 },
2418 "node_modules/hypercore-errors": {
2419 "version": "1.5.0",
2420 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
2421 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
2422 "license": "Apache-2.0",
2423 "dependencies": {
2424 "hypercore-id-encoding": "^1.3.0"
2425 }
2426 },
2427 "node_modules/hypercore-id-encoding": {
2428 "version": "1.3.0",
2429 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
2430 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
2431 "license": "Apache-2.0",
2432 "dependencies": {
2433 "b4a": "^1.5.3",
2434 "z32": "^1.0.0"
2435 }
2436 },
2437 "node_modules/hypercore-storage": {
2438 "version": "2.8.0",
2439 "resolved": "https://registry.npmjs.org/hypercore-storage/-/hypercore-storage-2.8.0.tgz",
2440 "integrity": "sha512-s35HllFMYvrkSHvkIIklVFmCI/bRVNO7H3pi4mKvWl0XLyhGGGM+NtJ7fpsh82oPRCmL9XtMRO+pT6LiMAO8MA==",
2441 "license": "Apache-2.0",
2442 "dependencies": {
2443 "b4a": "^1.6.7",
2444 "bare-fs": "^4.0.1",
2445 "bare-path": "^3.0.0",
2446 "compact-encoding": "^2.16.0",
2447 "device-file": "^2.1.2",
2448 "flat-tree": "^1.12.1",
2449 "hypercore-crypto": "^3.4.2",
2450 "hyperschema": "^1.7.0",
2451 "index-encoder": "^3.3.2",
2452 "resolve-reject-promise": "^1.0.0",
2453 "rocksdb-native": "^3.11.0",
2454 "scope-lock": "^1.2.4",
2455 "streamx": "^2.21.1"
2456 }
2457 },
2458 "node_modules/hyperdht": {
2459 "version": "6.30.0",
2460 "resolved": "https://registry.npmjs.org/hyperdht/-/hyperdht-6.30.0.tgz",
2461 "integrity": "sha512-LkfeAFVnOIvOpr2ILtJ38CzPgXmye7jXDS12xndVKfNoxzIrM0GonY+Bz5lHutoc08ZbT3Olr/w2NSeq5Pj0Ng==",
2462 "license": "MIT",
2463 "dependencies": {
2464 "@hyperswarm/secret-stream": "^6.6.2",
2465 "b4a": "^1.3.1",
2466 "bare-events": "^2.2.0",
2467 "blind-relay": "^1.3.0",
2468 "bogon": "^1.0.0",
2469 "compact-encoding": "^2.4.1",
2470 "dht-rpc": "^6.15.1",
2471 "hypercore-crypto": "^3.3.0",
2472 "hypercore-id-encoding": "^1.2.0",
2473 "hyperdht-address": "^1.0.1",
2474 "noise-curve-ed": "^2.0.0",
2475 "noise-handshake": "^4.0.0",
2476 "record-cache": "^1.1.1",
2477 "safety-catch": "^1.0.1",
2478 "signal-promise": "^1.0.3",
2479 "sodium-universal": "^5.0.1",
2480 "streamx": "^2.16.1",
2481 "unslab": "^1.3.0",
2482 "xache": "^1.1.0"
2483 },
2484 "bin": {
2485 "hyperdht": "bin.js"
2486 }
2487 },
2488 "node_modules/hyperdht-address": {
2489 "version": "1.0.1",
2490 "resolved": "https://registry.npmjs.org/hyperdht-address/-/hyperdht-address-1.0.1.tgz",
2491 "integrity": "sha512-v817eJkhryWNtMGQHLxVo6jtfEeIV9k429fRUWUKkp1bm+3/rzH8r5BFef9VT7Jq4Tvgt0Gw11FV32CacsqMZA==",
2492 "license": "Apache-2.0",
2493 "dependencies": {
2494 "compact-encoding": "^2.19.0",
2495 "hyperschema": "^1.20.1"
2496 }
2497 },
2498 "node_modules/hyperdrive": {
2499 "version": "11.13.4",
2500 "resolved": "https://registry.npmjs.org/hyperdrive/-/hyperdrive-11.13.4.tgz",
2501 "integrity": "sha512-PN0gClSRx+QBbsLGnGqo2ei9TavJN8d6D3jv2yNMlu7zgXSnqR6QcGo8mgyKnrzgR336oNfvQ2C2Y6cY5klScg==",
2502 "license": "Apache-2.0",
2503 "dependencies": {
2504 "hyperbee": "^2.11.1",
2505 "hyperblobs": "^2.3.0",
2506 "hypercore": "^10.33.0",
2507 "hypercore-errors": "^1.0.0",
2508 "is-options": "^1.0.2",
2509 "mirror-drive": "^1.2.0",
2510 "ready-resource": "^1.0.0",
2511 "safety-catch": "^1.0.2",
2512 "speedometer": "^1.1.0",
2513 "streamx": "^2.12.4",
2514 "sub-encoder": "^2.1.1",
2515 "unix-path-resolve": "^1.0.2"
2516 }
2517 },
2518 "node_modules/hyperdrive/node_modules/hypercore": {
2519 "version": "10.38.2",
2520 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-10.38.2.tgz",
2521 "integrity": "sha512-2vx42g2RKdP/o2XzlxKfvgEaZ3+h/B855gCR8iscUDnxUoOZej7vXZ2buex0sweDUEW5aEfMxFR6o5hncio3Nw==",
2522 "license": "MIT",
2523 "dependencies": {
2524 "@hyperswarm/secret-stream": "^6.0.0",
2525 "b4a": "^1.1.0",
2526 "bare-events": "^2.2.0",
2527 "big-sparse-array": "^1.0.3",
2528 "compact-encoding": "^2.11.0",
2529 "crc-universal": "^1.0.2",
2530 "fast-fifo": "^1.3.0",
2531 "flat-tree": "^1.9.0",
2532 "hypercore-crypto": "^3.2.1",
2533 "hypercore-errors": "^1.2.0",
2534 "hypercore-id-encoding": "^1.2.0",
2535 "is-options": "^1.0.1",
2536 "protomux": "^3.5.0",
2537 "quickbit-universal": "^2.2.0",
2538 "random-access-file": "^4.0.0",
2539 "random-array-iterator": "^1.0.0",
2540 "safety-catch": "^1.0.1",
2541 "sodium-universal": "^4.0.0",
2542 "streamx": "^2.12.4",
2543 "unslab": "^1.3.0",
2544 "xache": "^1.1.0",
2545 "z32": "^1.0.0"
2546 }
2547 },
2548 "node_modules/hyperdrive/node_modules/sodium-native": {
2549 "version": "4.3.3",
2550 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
2551 "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==",
2552 "license": "MIT",
2553 "dependencies": {
2554 "require-addon": "^1.1.0"
2555 }
2556 },
2557 "node_modules/hyperdrive/node_modules/sodium-universal": {
2558 "version": "4.0.1",
2559 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-4.0.1.tgz",
2560 "integrity": "sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==",
2561 "license": "MIT",
2562 "dependencies": {
2563 "sodium-native": "^4.0.0"
2564 },
2565 "peerDependencies": {
2566 "sodium-javascript": "~0.8.0"
2567 },
2568 "peerDependenciesMeta": {
2569 "sodium-javascript": {
2570 "optional": true
2571 }
2572 }
2573 },
2574 "node_modules/hyperschema": {
2575 "version": "1.20.1",
2576 "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.20.1.tgz",
2577 "integrity": "sha512-7gnvaTNUs8FqdrU3NNE6nzhOneGyDlUlpROu9YFlHA61fE8ppKKf2gLZjZjsx/enNt+VFN4ITxy8ijypfyWBLw==",
2578 "license": "Apache-2.0",
2579 "dependencies": {
2580 "bare-fs": "^4.0.1",
2581 "compact-encoding": "^2.19.0",
2582 "generate-object-property": "^2.0.0",
2583 "generate-string": "^1.0.1"
2584 }
2585 },
2586 "node_modules/hyperswarm": {
2587 "version": "4.17.0",
2588 "resolved": "https://registry.npmjs.org/hyperswarm/-/hyperswarm-4.17.0.tgz",
2589 "integrity": "sha512-oe86sK961Ueg7rvDN/veFwG8xH+Iv6vObPhGDkPJcDVxk/NduW41ZhAcVDnHzRbm7S0eLU7WaDUvehOYoKSpRQ==",
2590 "license": "MIT",
2591 "dependencies": {
2592 "b4a": "^1.3.1",
2593 "bare-events": "^2.2.0",
2594 "hyperdht": "^6.21.0",
2595 "safety-catch": "^1.0.2",
2596 "shuffled-priority-queue": "^2.1.0",
2597 "streamx": "^2.22.1",
2598 "unslab": "^1.3.0"
2599 }
2600 },
2601 "node_modules/iconv-lite": {
2602 "version": "0.7.2",
2603 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
2604 "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
2605 "license": "MIT",
2606 "dependencies": {
2607 "safer-buffer": ">= 2.1.2 < 3.0.0"
2608 },
2609 "engines": {
2610 "node": ">=0.10.0"
2611 },
2612 "funding": {
2613 "type": "opencollective",
2614 "url": "https://opencollective.com/express"
2615 }
2616 },
2617 "node_modules/index-encoder": {
2618 "version": "3.5.0",
2619 "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz",
2620 "integrity": "sha512-idZ1cxtZz2dRV6rUiaP9Xo99UjXbSzjcMacoQmxUMu/A7fEQcNPngvwDJYeWelQUS5XFlY71/or70lKn6XnwbQ==",
2621 "license": "Apache-2.0",
2622 "dependencies": {
2623 "b4a": "^1.6.4"
2624 }
2625 },
2626 "node_modules/ip-address": {
2627 "version": "10.1.0",
2628 "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
2629 "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
2630 "license": "MIT",
2631 "engines": {
2632 "node": ">= 12"
2633 }
2634 },
2635 "node_modules/is-fullwidth-code-point": {
2636 "version": "3.0.0",
2637 "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
2638 "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
2639 "license": "MIT",
2640 "engines": {
2641 "node": ">=8"
2642 }
2643 },
2644 "node_modules/is-options": {
2645 "version": "1.0.2",
2646 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
2647 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
2648 "license": "MIT",
2649 "dependencies": {
2650 "b4a": "^1.1.1"
2651 }
2652 },
2653 "node_modules/is-property": {
2654 "version": "1.0.2",
2655 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
2656 "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
2657 "license": "MIT"
2658 },
2659 "node_modules/is-undefined": {
2660 "version": "1.0.12",
2661 "resolved": "https://registry.npmjs.org/is-undefined/-/is-undefined-1.0.12.tgz",
2662 "integrity": "sha512-qaX2mymwUhMq+NQPnx5iR/u2PgqhL6jLzDunMmonOgVofqoFhxzd6kOmiL0DLYZUkN/RvNWYPenoANVn5phlaA==",
2663 "license": "MIT"
2664 },
2665 "node_modules/is-valid-variable": {
2666 "version": "1.0.1",
2667 "resolved": "https://registry.npmjs.org/is-valid-variable/-/is-valid-variable-1.0.1.tgz",
2668 "integrity": "sha512-ucsrYzt8kcgu2CAOFFWSfOTwa+vPNzQHK1RL6RooSkwozJ92e9wl4cyG/GLDeuNEVj56kvuNZGCR7eSkzrhudg==",
2669 "license": "MIT"
2670 },
2671 "node_modules/is-win": {
2672 "version": "1.0.11",
2673 "resolved": "https://registry.npmjs.org/is-win/-/is-win-1.0.11.tgz",
2674 "integrity": "sha512-+XpgpizPqNzohXiqme7pfhAhpoG0Eo+CtuSx/XYW4enarERuheDbNbFrm4+XYylpV1w/eI+si5itFA0RfCWjog==",
2675 "license": "MIT"
2676 },
2677 "node_modules/kademlia-routing-table": {
2678 "version": "1.0.6",
2679 "resolved": "https://registry.npmjs.org/kademlia-routing-table/-/kademlia-routing-table-1.0.6.tgz",
2680 "integrity": "sha512-Ve6jwIlUCYvUzBnXnzVRHDZCFgXURW9gmF3r7n05kZs/2rNbLHXwGdcq0qIaSwdmJCvtosgR4JensnVU65hzNQ==",
2681 "license": "MIT",
2682 "dependencies": {
2683 "bare-events": "^2.2.0"
2684 }
2685 },
2686 "node_modules/livefiles": {
2687 "version": "1.1.0",
2688 "resolved": "https://registry.npmjs.org/livefiles/-/livefiles-1.1.0.tgz",
2689 "integrity": "sha512-2yVGglq9w7GWPYFy9yUVp1iK2dcod6n/BKLt9t+N2AhYTS+YWXjlh8TSC6ACYeLE7V+PAIbDjCtrVsQfFRk6Qg==",
2690 "license": "GPLv3",
2691 "dependencies": {
2692 "bare-utils": "^1.5.1",
2693 "fs": "npm:bare-fs@^4.2.1",
2694 "http": "npm:bare-http1@^4.0.2",
2695 "ready-resource": "^1.1.2"
2696 }
2697 },
2698 "node_modules/localdrive": {
2699 "version": "1.12.2",
2700 "resolved": "https://registry.npmjs.org/localdrive/-/localdrive-1.12.2.tgz",
2701 "integrity": "sha512-kISUZK6naLZrvcK/+IqXk5IsAu8S6owlVM0MUT6MZrQSIzLplZ4FXljkEBHwPDe3InVue4FDG9d4p8lKXGgRCQ==",
2702 "license": "Apache-2.0",
2703 "dependencies": {
2704 "b4a": "^1.6.1",
2705 "bare-fs": "^4.0.1",
2706 "bare-path": "^3.0.0",
2707 "mirror-drive": "^1.2.0",
2708 "mutexify": "^1.4.0",
2709 "streamx": "^2.12.5",
2710 "unix-path-resolve": "^1.0.2"
2711 }
2712 },
2713 "node_modules/lodash.camelcase": {
2714 "version": "4.3.0",
2715 "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
2716 "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
2717 "license": "MIT"
2718 },
2719 "node_modules/lodash.sortby": {
2720 "version": "4.7.0",
2721 "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
2722 "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
2723 "license": "MIT"
2724 },
2725 "node_modules/long": {
2726 "version": "5.3.2",
2727 "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
2728 "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
2729 "license": "Apache-2.0"
2730 },
2731 "node_modules/minimist": {
2732 "version": "1.2.8",
2733 "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
2734 "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
2735 "license": "MIT",
2736 "funding": {
2737 "url": "https://github.com/sponsors/ljharb"
2738 }
2739 },
2740 "node_modules/mirror-drive": {
2741 "version": "1.14.1",
2742 "resolved": "https://registry.npmjs.org/mirror-drive/-/mirror-drive-1.14.1.tgz",
2743 "integrity": "sha512-RRFT+rRQybO5dwUQcetErfZb3IZGll529DZWkzHGlSlfzxzkqRl27Tz46QO1sfK/2VYQxmtosDieAWbInHEoSA==",
2744 "license": "Apache-2.0",
2745 "dependencies": {
2746 "bare-events": "^2.8.2",
2747 "binary-stream-equals": "^1.0.0",
2748 "rabin-stream": "^2.0.0",
2749 "same-data": "^1.0.0",
2750 "speedometer": "^1.1.0",
2751 "streamx": "^2.22.1",
2752 "unix-path-resolve": "^1.0.2"
2753 }
2754 },
2755 "node_modules/module": {
2756 "name": "bare-node-module",
2757 "version": "1.0.0",
2758 "resolved": "https://registry.npmjs.org/bare-node-module/-/bare-node-module-1.0.0.tgz",
2759 "integrity": "sha512-7ZTCfExhk24aNqEGKgLOb3HsApNFdlcxTxeR8POsHyoQQeOy1pA3i8JxS97djNxunCnsfHAMJ5HeKo+55YsY5g==",
2760 "license": "Apache-2.0",
2761 "dependencies": {
2762 "bare-module": "*"
2763 }
2764 },
2765 "node_modules/multicast-dns": {
2766 "version": "7.2.5",
2767 "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
2768 "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
2769 "license": "MIT",
2770 "dependencies": {
2771 "dns-packet": "^5.2.2",
2772 "thunky": "^1.0.2"
2773 },
2774 "bin": {
2775 "multicast-dns": "cli.js"
2776 }
2777 },
2778 "node_modules/mute-stream": {
2779 "version": "3.0.0",
2780 "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
2781 "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
2782 "license": "ISC",
2783 "engines": {
2784 "node": "^20.17.0 || >=22.9.0"
2785 }
2786 },
2787 "node_modules/mutexify": {
2788 "version": "1.4.0",
2789 "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
2790 "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==",
2791 "license": "MIT",
2792 "dependencies": {
2793 "queue-tick": "^1.0.0"
2794 }
2795 },
2796 "node_modules/nanoassert": {
2797 "version": "2.0.0",
2798 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
2799 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
2800 "license": "ISC"
2801 },
2802 "node_modules/nat-sampler": {
2803 "version": "1.0.1",
2804 "resolved": "https://registry.npmjs.org/nat-sampler/-/nat-sampler-1.0.1.tgz",
2805 "integrity": "sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==",
2806 "license": "MIT"
2807 },
2808 "node_modules/net": {
2809 "name": "bare-net",
2810 "version": "2.3.1",
2811 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz",
2812 "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==",
2813 "license": "Apache-2.0",
2814 "dependencies": {
2815 "bare-events": "^2.2.2",
2816 "bare-pipe": "^4.0.0",
2817 "bare-stream": "^2.0.0",
2818 "bare-tcp": "^2.0.0"
2819 }
2820 },
2821 "node_modules/noise-curve-ed": {
2822 "version": "2.1.0",
2823 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
2824 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
2825 "license": "ISC",
2826 "dependencies": {
2827 "b4a": "^1.1.0",
2828 "nanoassert": "^2.0.0",
2829 "sodium-universal": "^5.0.0"
2830 }
2831 },
2832 "node_modules/noise-handshake": {
2833 "version": "4.2.0",
2834 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
2835 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
2836 "license": "Apache-2.0",
2837 "dependencies": {
2838 "b4a": "^1.1.0",
2839 "nanoassert": "^2.0.0",
2840 "sodium-universal": "^5.0.0"
2841 }
2842 },
2843 "node_modules/noop6": {
2844 "version": "1.0.10",
2845 "resolved": "https://registry.npmjs.org/noop6/-/noop6-1.0.10.tgz",
2846 "integrity": "sha512-WZvuCILZFZHK+WuqCQwxLBGllkBK1ct8s8Mu9FMDbEsBE6/bqNxyFGbX7Xky+6bYFL8X2Ou4Cis4CJyrwXLvQA==",
2847 "license": "MIT"
2848 },
2849 "node_modules/on-exit-leak-free": {
2850 "version": "2.1.2",
2851 "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
2852 "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
2853 "license": "MIT",
2854 "engines": {
2855 "node": ">=14.0.0"
2856 }
2857 },
2858 "node_modules/p2p-hiverelay": {
2859 "version": "0.4.2",
2860 "resolved": "https://registry.npmjs.org/p2p-hiverelay/-/p2p-hiverelay-0.4.2.tgz",
2861 "integrity": "sha512-L7/x+AgCcQfxWdhWxW/VWHJNIjJle+yeqcGjeuCPePsv6QbI630G5FuYCwF1N8WnKvKzXVSIKNMhSodcA//c+g==",
2862 "license": "Apache-2.0",
2863 "dependencies": {
2864 "@grpc/grpc-js": "^1.14.3",
2865 "@grpc/proto-loader": "^0.7.15",
2866 "@inquirer/prompts": "^8.4.1",
2867 "@noble/hashes": "^2.2.0",
2868 "@noble/secp256k1": "^3.1.0",
2869 "b4a": "^1.6.6",
2870 "bare-events": "^2.2.0",
2871 "bare-fs": "^2.1.0",
2872 "bare-path": "^2.1.0",
2873 "compact-encoding": "^2.15.0",
2874 "corestore": "^6.18.0",
2875 "distributed-drive": "^0.1.1",
2876 "graceful-goodbye": "^1.3.0",
2877 "holesail": "^2.4.1",
2878 "hypercore": "^10.36.0",
2879 "hyperdht": "^6.15.0",
2880 "hyperdrive": "^11.8.0",
2881 "hyperswarm": "^4.7.0",
2882 "minimist": "^1.2.8",
2883 "multicast-dns": "^7.2.5",
2884 "pino": "^9.0.0",
2885 "protomux": "^3.6.0",
2886 "socks": "^2.8.7",
2887 "sodium-universal": "^4.0.0",
2888 "ws": "^8.20.0"
2889 },
2890 "bin": {
2891 "hiverelay": "cli/index.js",
2892 "p2p-hiverelay": "cli/index.js"
2893 },
2894 "engines": {
2895 "node": ">=20.0.0"
2896 }
2897 },
2898 "node_modules/p2p-hiverelay/node_modules/bare-fs": {
2899 "version": "2.3.5",
2900 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
2901 "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
2902 "license": "Apache-2.0",
2903 "dependencies": {
2904 "bare-events": "^2.0.0",
2905 "bare-path": "^2.0.0",
2906 "bare-stream": "^2.0.0"
2907 }
2908 },
2909 "node_modules/p2p-hiverelay/node_modules/bare-os": {
2910 "version": "2.4.4",
2911 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
2912 "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
2913 "license": "Apache-2.0"
2914 },
2915 "node_modules/p2p-hiverelay/node_modules/bare-path": {
2916 "version": "2.1.3",
2917 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
2918 "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
2919 "license": "Apache-2.0",
2920 "dependencies": {
2921 "bare-os": "^2.1.0"
2922 }
2923 },
2924 "node_modules/p2p-hiverelay/node_modules/corestore": {
2925 "version": "6.18.4",
2926 "resolved": "https://registry.npmjs.org/corestore/-/corestore-6.18.4.tgz",
2927 "integrity": "sha512-KL1cOUyj3DFRNzeVRekGPRiGN4sIfGo7BsncSJQQBT3Rh2y1nQ32MOR9X5DlOMlWIT4txwqBzhzam6+JXtwFVw==",
2928 "license": "MIT",
2929 "dependencies": {
2930 "b4a": "^1.6.4",
2931 "hypercore": "^10.37.10",
2932 "hypercore-crypto": "^3.4.0",
2933 "hypercore-id-encoding": "^1.2.0",
2934 "read-write-mutexify": "^2.1.0",
2935 "ready-resource": "^1.0.0",
2936 "safety-catch": "^1.0.1",
2937 "sodium-universal": "^4.0.0",
2938 "xache": "^1.1.0"
2939 }
2940 },
2941 "node_modules/p2p-hiverelay/node_modules/hypercore": {
2942 "version": "10.38.2",
2943 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-10.38.2.tgz",
2944 "integrity": "sha512-2vx42g2RKdP/o2XzlxKfvgEaZ3+h/B855gCR8iscUDnxUoOZej7vXZ2buex0sweDUEW5aEfMxFR6o5hncio3Nw==",
2945 "license": "MIT",
2946 "dependencies": {
2947 "@hyperswarm/secret-stream": "^6.0.0",
2948 "b4a": "^1.1.0",
2949 "bare-events": "^2.2.0",
2950 "big-sparse-array": "^1.0.3",
2951 "compact-encoding": "^2.11.0",
2952 "crc-universal": "^1.0.2",
2953 "fast-fifo": "^1.3.0",
2954 "flat-tree": "^1.9.0",
2955 "hypercore-crypto": "^3.2.1",
2956 "hypercore-errors": "^1.2.0",
2957 "hypercore-id-encoding": "^1.2.0",
2958 "is-options": "^1.0.1",
2959 "protomux": "^3.5.0",
2960 "quickbit-universal": "^2.2.0",
2961 "random-access-file": "^4.0.0",
2962 "random-array-iterator": "^1.0.0",
2963 "safety-catch": "^1.0.1",
2964 "sodium-universal": "^4.0.0",
2965 "streamx": "^2.12.4",
2966 "unslab": "^1.3.0",
2967 "xache": "^1.1.0",
2968 "z32": "^1.0.0"
2969 }
2970 },
2971 "node_modules/p2p-hiverelay/node_modules/sodium-native": {
2972 "version": "4.3.3",
2973 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
2974 "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==",
2975 "license": "MIT",
2976 "dependencies": {
2977 "require-addon": "^1.1.0"
2978 }
2979 },
2980 "node_modules/p2p-hiverelay/node_modules/sodium-universal": {
2981 "version": "4.0.1",
2982 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-4.0.1.tgz",
2983 "integrity": "sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==",
2984 "license": "MIT",
2985 "dependencies": {
2986 "sodium-native": "^4.0.0"
2987 },
2988 "peerDependencies": {
2989 "sodium-javascript": "~0.8.0"
2990 },
2991 "peerDependenciesMeta": {
2992 "sodium-javascript": {
2993 "optional": true
2994 }
2995 }
2996 },
2997 "node_modules/paparam": {
2998 "version": "1.10.1",
2999 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.1.tgz",
3000 "integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==",
3001 "license": "Apache-2.0"
3002 },
3003 "node_modules/path": {
3004 "name": "bare-node-path",
3005 "version": "1.0.1",
3006 "resolved": "https://registry.npmjs.org/bare-node-path/-/bare-node-path-1.0.1.tgz",
3007 "integrity": "sha512-44YiNN/ofG1cEOWwdRVbG9ZFyskZKXPJnSUOyUpdeRm+MeSXiY+pSjSa7SZ56C/XpPVxE0OEl/AV8u+3FKsclw==",
3008 "license": "Apache-2.0",
3009 "dependencies": {
3010 "bare-path": "*"
3011 }
3012 },
3013 "node_modules/pear-aliases": {
3014 "version": "1.0.7",
3015 "resolved": "https://registry.npmjs.org/pear-aliases/-/pear-aliases-1.0.7.tgz",
3016 "integrity": "sha512-Z85C0naSKh3XMBKm1P1L2kYbIGE+Ph0HRSjTaQz5O99JvQq/wHylu9M2df1Ysfx7LG3UJrCCwZenrud18bZytw==",
3017 "license": "Apache-2.0",
3018 "dependencies": {
3019 "hypercore-id-encoding": "^1.3.0"
3020 }
3021 },
3022 "node_modules/pear-api": {
3023 "version": "1.30.0",
3024 "resolved": "https://registry.npmjs.org/pear-api/-/pear-api-1.30.0.tgz",
3025 "integrity": "sha512-b9y4hCBTCmP7m3riC4O+mc5g8KNKJsLjbqi7TOFSZyvuIjJeP/d/caBYXdhKab0rcOz/kkSdNWvedox4zBtERQ==",
3026 "license": "Apache-2.0",
3027 "dependencies": {
3028 "pear-constants": "^1.0.0",
3029 "pear-errors": "^1.0.0",
3030 "pear-gracedown": "^1.0.0",
3031 "pear-gunk": "^1.0.0",
3032 "pear-message": "^1.0.0",
3033 "pear-messages": "^1.0.0",
3034 "pear-pipe": "^1.0.1",
3035 "pear-ref": "^1.0.2",
3036 "pear-run": "^1.0.0",
3037 "pear-updates": "^1.0.1",
3038 "pear-wakeups": "^1.0.0"
3039 }
3040 },
3041 "node_modules/pear-cmd": {
3042 "version": "1.0.2",
3043 "resolved": "https://registry.npmjs.org/pear-cmd/-/pear-cmd-1.0.2.tgz",
3044 "integrity": "sha512-vecXkSAFUS0ODh9I2zgsDcC4cW6sTQ1ZZTq5Fj/6G20HprvPNARbvraGJrkv5dRZ8Epezdgjws0vqB4W4VVd6w==",
3045 "license": "Apache-2.0",
3046 "dependencies": {
3047 "paparam": "^1.8.5"
3048 }
3049 },
3050 "node_modules/pear-constants": {
3051 "version": "1.1.3",
3052 "resolved": "https://registry.npmjs.org/pear-constants/-/pear-constants-1.1.3.tgz",
3053 "integrity": "sha512-dW8EkiLXjmzr8C8gUqZEIZ4dEKKljRNBznlRM53JPzkDhd053alCbq+T18sE/D2KvzMBW69/Pp1ExFN07xi1EA==",
3054 "license": "Apache-2.0",
3055 "dependencies": {
3056 "b4a": "^1.6.7",
3057 "blake2b": "^2.1.4",
3058 "pear-aliases": "^1.0.2",
3059 "pear-rti": "^1.0.1",
3060 "sodium-native": "^5.0.6",
3061 "url-file-url": "^1.0.5",
3062 "which-runtime": "^1.3.0"
3063 }
3064 },
3065 "node_modules/pear-crasher": {
3066 "version": "1.0.1",
3067 "resolved": "https://registry.npmjs.org/pear-crasher/-/pear-crasher-1.0.1.tgz",
3068 "integrity": "sha512-Ott6ANdu01t5zUXDQ50g0QRQ57SZu35KLuRjuKRsrOLLwdKeBTNYscX4VCgLnfKINlpGtRz/vzUGFPgBXGf6lg==",
3069 "license": "Apache-2.0",
3070 "dependencies": {
3071 "pear-rti": "^1.0.1"
3072 }
3073 },
3074 "node_modules/pear-electron": {
3075 "version": "1.7.28",
3076 "resolved": "https://registry.npmjs.org/pear-electron/-/pear-electron-1.7.28.tgz",
3077 "integrity": "sha512-ZCOFvVmUZgPxl8bCG0EllweW+eO2svGM5BLFNI8nppXdiIeE7ns8fLsQnGS2JdVALpG7ilaNja5pq1XRHbvhYw==",
3078 "license": "Apache-2.0",
3079 "dependencies": {
3080 "bare-env": "^3.0.0",
3081 "bare-fs": "^4.0.1",
3082 "bare-os": "^3.4.0",
3083 "bare-path": "^3.0.0",
3084 "bare-subprocess": "^5.0.2",
3085 "compact-encoding": "^2.16.1",
3086 "localdrive": "^1.12.1",
3087 "paparam": "^1.6.1",
3088 "pear-api": "^1.29.1",
3089 "pear-cmd": "^1.0.0",
3090 "pear-constants": "^1.0.0",
3091 "pear-crasher": "^1.0.1",
3092 "pear-errors": "^1.0.0",
3093 "pear-gunk": "^1.0.0",
3094 "pear-ipc": "^6.4.0",
3095 "pear-link": "^4.1.0",
3096 "pear-logger": "^1.0.0",
3097 "pear-pipe": "^1.0.2",
3098 "pear-run": "^1.0.0",
3099 "pear-state": "^1.0.2",
3100 "pear-terminal": "^1.0.0",
3101 "pear-tryboot": "^1.0.0",
3102 "script-linker": "^2.5.3",
3103 "streamx": "^2.20.2",
3104 "unix-path-resolve": "^1.0.2",
3105 "url-file-url": "^1.0.4",
3106 "which-runtime": "^1.2.1"
3107 }
3108 },
3109 "node_modules/pear-errors": {
3110 "version": "1.0.1",
3111 "resolved": "https://registry.npmjs.org/pear-errors/-/pear-errors-1.0.1.tgz",
3112 "integrity": "sha512-KQyhym09QpLT4H/z3uLW0bqixgIZic2Eip2Q8Z72QmIpFzPtp6QmnzOUwB3kuHB4dqKNa+FQWHWH0NSK5lJjlw==",
3113 "license": "Apache-2.0"
3114 },
3115 "node_modules/pear-gracedown": {
3116 "version": "1.0.3",
3117 "resolved": "https://registry.npmjs.org/pear-gracedown/-/pear-gracedown-1.0.3.tgz",
3118 "integrity": "sha512-PVh4AmpMDFxd+kMiCkuTyJ7NzRPgb0AEE7TR5GCl91x90uzWmFWon4TZMrCVhGtADG26OrQpOI+kL6S/o2w00w==",
3119 "license": "Apache-2.0",
3120 "dependencies": {
3121 "bare-os": "^3.6.1",
3122 "bare-signals": "^4.0.3",
3123 "safety-catch": "^1.0.2",
3124 "url-file-url": "^1.0.5"
3125 }
3126 },
3127 "node_modules/pear-gunk": {
3128 "version": "1.0.1",
3129 "resolved": "https://registry.npmjs.org/pear-gunk/-/pear-gunk-1.0.1.tgz",
3130 "integrity": "sha512-xN2aGZxGXY/rpSlq24UHsc/RMX12DLJzCM5XPJriDBn7MHUrHoKmD/SZTLT6RlFeL2z9MheRnrge6oR6eIGUMA==",
3131 "license": "Apache-2.0",
3132 "dependencies": {
3133 "crc-universal": "^1.0.4",
3134 "fs-native-extensions": "^1.4.3",
3135 "quickbit-universal": "^2.2.0",
3136 "rocksdb-native": "^3.7.5",
3137 "script-linker": "^2.5.4",
3138 "sodium-native": "^5.0.8",
3139 "udx-native": "^1.18.3"
3140 }
3141 },
3142 "node_modules/pear-ipc": {
3143 "version": "6.11.0",
3144 "resolved": "https://registry.npmjs.org/pear-ipc/-/pear-ipc-6.11.0.tgz",
3145 "integrity": "sha512-7peANATe7VlhbLfTHx3CpIQO9AiHqPkfHK0Wgw5T0EY9KllO/XTPVNAV49VNJ5o0QASrPdrUy6FOvB46HWJflA==",
3146 "license": "Apache-2.0",
3147 "dependencies": {
3148 "bare-fs": "^4.0.1",
3149 "bare-os": "^3.2.0",
3150 "bare-path": "^3.0.0",
3151 "bare-pipe": "^4.0.5",
3152 "framed-stream": "^1.0.1",
3153 "fs-native-extensions": "^1.2.7",
3154 "fs-native-lock": "^1.1.0",
3155 "ready-resource": "^1.0.3",
3156 "streamx": "^2.16.1",
3157 "tiny-buffer-rpc": "^2.2.0",
3158 "which-runtime": "^1.2.0"
3159 }
3160 },
3161 "node_modules/pear-link": {
3162 "version": "4.2.1",
3163 "resolved": "https://registry.npmjs.org/pear-link/-/pear-link-4.2.1.tgz",
3164 "integrity": "sha512-Y6xSOY36EkWmaeTYqNVT2iV0xWfzCDCBtPCF898793ADk9Lbzfq7RLJ7PhqHRS+dLI7yTlLtCCfjGR3erQ0FKQ==",
3165 "license": "Apache-2.0",
3166 "dependencies": {
3167 "bare-path": "^3.0.0",
3168 "hypercore-id-encoding": "^1.2.0",
3169 "pear-aliases": "^1.0.0",
3170 "pear-errors": "^1.0.0"
3171 }
3172 },
3173 "node_modules/pear-logger": {
3174 "version": "1.0.1",
3175 "resolved": "https://registry.npmjs.org/pear-logger/-/pear-logger-1.0.1.tgz",
3176 "integrity": "sha512-DMFcqReoaAoxliOSAgw09RwVmWdwXaEPXG/rZ/aITPAVqZetY06P17ipuywEKECH1N1kt5JZ2hVvJPRE/nUllg==",
3177 "license": "Apache-2.0",
3178 "dependencies": {
3179 "bare-format": "^1.0.1",
3180 "bare-hrtime": "^2.0.11",
3181 "pear-cmd": "^1.0.0",
3182 "which-runtime": "^1.3.0"
3183 }
3184 },
3185 "node_modules/pear-message": {
3186 "version": "1.0.0",
3187 "resolved": "https://registry.npmjs.org/pear-message/-/pear-message-1.0.0.tgz",
3188 "integrity": "sha512-ZBZsaIv4PVS38psP/x32D2xaRKf1qI9FnB0gTsRPY0NChV38qhWsnQDbXOhAYGxkTqjREC0OkDoQAha7z6wo/Q==",
3189 "license": "Apache-2.0",
3190 "dependencies": {
3191 "pear-ref": "^1.0.1",
3192 "sodium-native": "^5.0.6"
3193 }
3194 },
3195 "node_modules/pear-messages": {
3196 "version": "1.0.5",
3197 "resolved": "https://registry.npmjs.org/pear-messages/-/pear-messages-1.0.5.tgz",
3198 "integrity": "sha512-2kCw5lnnFxBZ+d4wz8L+AmKi25M0rwdgPWsMTGLBbmkyKmaPVBQBpG8ZktsFwVPGBSLu9dFtGC79VYYDSaA3Vw==",
3199 "license": "Apache-2.0",
3200 "dependencies": {
3201 "b4a": "^1.6.7",
3202 "pear-ipc": "^6.5.0",
3203 "pear-ref": "^1.0.1",
3204 "sodium-native": "^5.0.6"
3205 }
3206 },
3207 "node_modules/pear-opwait": {
3208 "version": "1.0.0",
3209 "resolved": "https://registry.npmjs.org/pear-opwait/-/pear-opwait-1.0.0.tgz",
3210 "integrity": "sha512-V8h7GWihtc5Cam4e0EDJebuwpTxndSRfVsIPAll1YZgf+QG5XrpwQeF8umvxiDBYJXwtJ9P3pVnbsM4jk7vDuA==",
3211 "license": "Apache-2.0",
3212 "dependencies": {
3213 "pear-errors": "^1.0.0"
3214 }
3215 },
3216 "node_modules/pear-pipe": {
3217 "version": "1.0.6",
3218 "resolved": "https://registry.npmjs.org/pear-pipe/-/pear-pipe-1.0.6.tgz",
3219 "integrity": "sha512-7XApSERIgctSlSjsZ8ATkFXwbkIyCEHG90GQdsc1DG2GkDYxYuGrq4c4H/YEE2jt1oq6VywwE/eNtvrlOEUeHg==",
3220 "license": "Apache-2.0",
3221 "dependencies": {
3222 "bare-fs": "^4.1.6",
3223 "bare-pipe": "^4.0.6",
3224 "streamx": "^2.22.1",
3225 "which-runtime": "^1.3.0"
3226 }
3227 },
3228 "node_modules/pear-ref": {
3229 "version": "1.0.2",
3230 "resolved": "https://registry.npmjs.org/pear-ref/-/pear-ref-1.0.2.tgz",
3231 "integrity": "sha512-IUGGSULYVF6jfdtsHY0XOokMp1v1KI89q8yPhsnqrLFf+Wq8EEOD0capny8gqXyu0eGkIh753X1cFUVL36n7FA==",
3232 "license": "Apache-2.0",
3233 "dependencies": {
3234 "bare-events": "^2.6.1"
3235 }
3236 },
3237 "node_modules/pear-rti": {
3238 "version": "1.0.1",
3239 "resolved": "https://registry.npmjs.org/pear-rti/-/pear-rti-1.0.1.tgz",
3240 "integrity": "sha512-rpVNVY9Zwu8ng5sTywaMV8exCkzRWATVElmCsgFH2zfdvVqvsYMxVN1oW9xKSAP2eKJ8cInoL1UmMJLcdbFIXA==",
3241 "license": "Apache-2.0",
3242 "dependencies": {
3243 "bare-path": "^3.0.0"
3244 }
3245 },
3246 "node_modules/pear-run": {
3247 "version": "1.0.8",
3248 "resolved": "https://registry.npmjs.org/pear-run/-/pear-run-1.0.8.tgz",
3249 "integrity": "sha512-X56S6WJEvE5UKxH+XI1WhsgN2oCzPMg2jO/FH/bNFWX9a/LQmQobcIuxLPDBH+iuiHPM1inFScrYWVNZGWqWDw==",
3250 "license": "Apache-2.0",
3251 "dependencies": {
3252 "b4a": "^1.6.7",
3253 "bare-path": "^3.0.0",
3254 "bare-subprocess": "^5.1.0",
3255 "paparam": "^1.8.5",
3256 "pear-cmd": "^1.0.0",
3257 "pear-link": "^4.1.0",
3258 "pear-ref": "^1.0.0",
3259 "unix-path-resolve": "^1.0.2",
3260 "url-file-url": "^1.0.5",
3261 "which-runtime": "^1.3.2"
3262 }
3263 },
3264 "node_modules/pear-state": {
3265 "version": "1.1.0",
3266 "resolved": "https://registry.npmjs.org/pear-state/-/pear-state-1.1.0.tgz",
3267 "integrity": "sha512-zs09OU6hwvx/25OuMVaCRgLQWKGXVIcd0yFWjCqa1yo9SY9u5GMSxix7nWjtGusTYTtBKAJhFo+i53GcXsosfg==",
3268 "license": "Apache-2.0",
3269 "dependencies": {
3270 "bare-env": "^3.0.0",
3271 "bare-fs": "^4.2.0",
3272 "bare-os": "^3.6.1",
3273 "bare-path": "^3.0.0",
3274 "hypercore-crypto": "^3.6.1",
3275 "hypercore-id-encoding": "^1.3.0",
3276 "pear-constants": "^1.0.0",
3277 "pear-link": "^4.1.0",
3278 "which-runtime": "^1.3.0"
3279 }
3280 },
3281 "node_modules/pear-terminal": {
3282 "version": "1.2.1",
3283 "resolved": "https://registry.npmjs.org/pear-terminal/-/pear-terminal-1.2.1.tgz",
3284 "integrity": "sha512-NPdghB3DAHb8F/BQS1YyZiIAzjtwYxwfzhlk6pkZpFGtT6Lizk0oyM97eq0Np+h/gIyny+l7/Z8G6uZkJyYfSw==",
3285 "license": "Apache-2.0",
3286 "dependencies": {
3287 "bare-events": "^2.6.1",
3288 "bare-fs": "^4.2.0",
3289 "bare-readline": "^1.0.9",
3290 "bare-stream": "^2.7.0",
3291 "bare-tty": "^5.0.2",
3292 "hypercore-id-encoding": "^1.3.0",
3293 "pear-constants": "^1.0.0",
3294 "pear-errors": "^1.0.1",
3295 "pear-gracedown": "^1.0.0",
3296 "pear-opwait": "^1.0.0",
3297 "streamx": "^2.22.1",
3298 "tiny-byte-size": "^1.1.0",
3299 "which-runtime": "^1.3.0"
3300 }
3301 },
3302 "node_modules/pear-tryboot": {
3303 "version": "1.0.0",
3304 "resolved": "https://registry.npmjs.org/pear-tryboot/-/pear-tryboot-1.0.0.tgz",
3305 "integrity": "sha512-0zTaj9vAXsXoKjtKhJfDBRDcfNY+1+zXu9Ttks9UgnCH0yNYTffghABjqNEOvwX5xPwLqzvHlxBamV9MckBkRA==",
3306 "license": "Apache-2.0",
3307 "dependencies": {
3308 "bare-daemon": "^1.2.1",
3309 "pear-constants": "^1.0.0"
3310 }
3311 },
3312 "node_modules/pear-updates": {
3313 "version": "1.1.0",
3314 "resolved": "https://registry.npmjs.org/pear-updates/-/pear-updates-1.1.0.tgz",
3315 "integrity": "sha512-cD759YRSDFLqlr7sM44imVtdQTc18hflvV2v4ENtoBoKfrLnn5HWklsbgTYfE82az7G+3qaE1JBCtBih/W5dvg==",
3316 "license": "Apache-2.0",
3317 "dependencies": {
3318 "pear-messages": "^1.0.0"
3319 }
3320 },
3321 "node_modules/pear-wakeups": {
3322 "version": "1.0.0",
3323 "resolved": "https://registry.npmjs.org/pear-wakeups/-/pear-wakeups-1.0.0.tgz",
3324 "integrity": "sha512-by8XW24RjQcgETxwGJYnOAnnyFtMOC6++G/3zdwDDBuxouKZnib3JVWBdGvNdTiI8IpedApCOLqNZaHrkBxyvA==",
3325 "license": "Apache-2.0",
3326 "dependencies": {
3327 "pear-messages": "^1.0.0"
3328 }
3329 },
3330 "node_modules/pino": {
3331 "version": "9.14.0",
3332 "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
3333 "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==",
3334 "license": "MIT",
3335 "dependencies": {
3336 "@pinojs/redact": "^0.4.0",
3337 "atomic-sleep": "^1.0.0",
3338 "on-exit-leak-free": "^2.1.0",
3339 "pino-abstract-transport": "^2.0.0",
3340 "pino-std-serializers": "^7.0.0",
3341 "process-warning": "^5.0.0",
3342 "quick-format-unescaped": "^4.0.3",
3343 "real-require": "^0.2.0",
3344 "safe-stable-stringify": "^2.3.1",
3345 "sonic-boom": "^4.0.1",
3346 "thread-stream": "^3.0.0"
3347 },
3348 "bin": {
3349 "pino": "bin.js"
3350 }
3351 },
3352 "node_modules/pino-abstract-transport": {
3353 "version": "2.0.0",
3354 "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
3355 "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
3356 "license": "MIT",
3357 "dependencies": {
3358 "split2": "^4.0.0"
3359 }
3360 },
3361 "node_modules/pino-std-serializers": {
3362 "version": "7.1.0",
3363 "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
3364 "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
3365 "license": "MIT"
3366 },
3367 "node_modules/prettier": {
3368 "version": "3.8.3",
3369 "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
3370 "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
3371 "license": "MIT",
3372 "bin": {
3373 "prettier": "bin/prettier.cjs"
3374 },
3375 "engines": {
3376 "node": ">=14"
3377 },
3378 "funding": {
3379 "url": "https://github.com/prettier/prettier?sponsor=1"
3380 }
3381 },
3382 "node_modules/prettier-config-holepunch": {
3383 "version": "2.0.0",
3384 "resolved": "https://registry.npmjs.org/prettier-config-holepunch/-/prettier-config-holepunch-2.0.0.tgz",
3385 "integrity": "sha512-yuskcdPRfQYLFPkOGvxS4TFmCb7QvAowjO+YaNLPLBWRev+qu6HXoKkPWH9lfNsL9gUThV7IeZ6t/B1QSu/jng==",
3386 "license": "Apache-2.0",
3387 "peerDependencies": {
3388 "prettier": "^3.6.2"
3389 }
3390 },
3391 "node_modules/process": {
3392 "name": "bare-process",
3393 "version": "4.4.1",
3394 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.4.1.tgz",
3395 "integrity": "sha512-JfcTtymq6akqM2bdyTsP4A4GYJceBzb91k/ECmFps75uFf6uO33SM1bOZsRAqyRXExabsQJLn5S41NBl8HTM4A==",
3396 "license": "Apache-2.0",
3397 "dependencies": {
3398 "bare-abort": "^2.0.13",
3399 "bare-env": "^3.0.0",
3400 "bare-events": "^2.3.1",
3401 "bare-hrtime": "^2.0.0",
3402 "bare-os": "^3.7.1",
3403 "bare-signals": "^4.0.0",
3404 "bare-stdio": "^1.0.1"
3405 }
3406 },
3407 "node_modules/process-warning": {
3408 "version": "5.0.0",
3409 "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
3410 "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
3411 "funding": [
3412 {
3413 "type": "github",
3414 "url": "https://github.com/sponsors/fastify"
3415 },
3416 {
3417 "type": "opencollective",
3418 "url": "https://opencollective.com/fastify"
3419 }
3420 ],
3421 "license": "MIT"
3422 },
3423 "node_modules/promaphore": {
3424 "version": "1.0.0",
3425 "resolved": "https://registry.npmjs.org/promaphore/-/promaphore-1.0.0.tgz",
3426 "integrity": "sha512-Eg8401+KJddVvDULkpy8bR964GMX8xMPegL6NdxTeBH2Wa3L86cZlEHizbkFJikr5u+E3wFoR5dLWJ+1OPyEfw==",
3427 "dev": true,
3428 "license": "MIT"
3429 },
3430 "node_modules/protobufjs": {
3431 "version": "7.5.5",
3432 "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
3433 "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
3434 "hasInstallScript": true,
3435 "license": "BSD-3-Clause",
3436 "dependencies": {
3437 "@protobufjs/aspromise": "^1.1.2",
3438 "@protobufjs/base64": "^1.1.2",
3439 "@protobufjs/codegen": "^2.0.4",
3440 "@protobufjs/eventemitter": "^1.1.0",
3441 "@protobufjs/fetch": "^1.1.0",
3442 "@protobufjs/float": "^1.0.2",
3443 "@protobufjs/inquire": "^1.1.0",
3444 "@protobufjs/path": "^1.1.2",
3445 "@protobufjs/pool": "^1.1.0",
3446 "@protobufjs/utf8": "^1.1.0",
3447 "@types/node": ">=13.7.0",
3448 "long": "^5.0.0"
3449 },
3450 "engines": {
3451 "node": ">=12.0.0"
3452 }
3453 },
3454 "node_modules/protocol-buffers-encodings": {
3455 "version": "1.2.0",
3456 "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz",
3457 "integrity": "sha512-daeNPuKh1NlLD1uDfbLpD+xyUTc07nEtfHwmBZmt/vH0B7VOM+JOCOpDcx9ZRpqHjAiIkGqyTDi+wfGSl17R9w==",
3458 "license": "MIT",
3459 "dependencies": {
3460 "b4a": "^1.6.0",
3461 "signed-varint": "^2.0.1",
3462 "varint": "5.0.0"
3463 }
3464 },
3465 "node_modules/protomux": {
3466 "version": "3.10.1",
3467 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
3468 "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==",
3469 "license": "MIT",
3470 "dependencies": {
3471 "b4a": "^1.3.1",
3472 "compact-encoding": "^2.5.1",
3473 "queue-tick": "^1.0.0",
3474 "safety-catch": "^1.0.1",
3475 "unslab": "^1.3.0"
3476 }
3477 },
3478 "node_modules/protomux-rpc": {
3479 "version": "1.9.0",
3480 "resolved": "https://registry.npmjs.org/protomux-rpc/-/protomux-rpc-1.9.0.tgz",
3481 "integrity": "sha512-+nOXXIDKZL849F6adj3R1SEi7PfPmvS6Y9HAArVC/RpONepRFq/Ot1LHVA+vyDKQMxBd8xTCHfy2fc3tchE1gA==",
3482 "license": "Apache-2.0",
3483 "dependencies": {
3484 "bits-to-bytes": "^1.0.0",
3485 "compact-encoding": "^2.6.1",
3486 "compact-encoding-bitfield": "^1.0.0",
3487 "protomux": "^3.7.0",
3488 "safety-catch": "^1.0.2"
3489 },
3490 "optionalDependencies": {
3491 "bare-events": "^2.2.0"
3492 }
3493 },
3494 "node_modules/punycode": {
3495 "version": "2.3.1",
3496 "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
3497 "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
3498 "license": "MIT",
3499 "engines": {
3500 "node": ">=6"
3501 }
3502 },
3503 "node_modules/qrcode-terminal": {
3504 "version": "0.12.0",
3505 "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz",
3506 "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==",
3507 "bin": {
3508 "qrcode-terminal": "bin/qrcode-terminal.js"
3509 }
3510 },
3511 "node_modules/querystring": {
3512 "name": "bare-node-querystring",
3513 "version": "1.0.0",
3514 "resolved": "https://registry.npmjs.org/bare-node-querystring/-/bare-node-querystring-1.0.0.tgz",
3515 "integrity": "sha512-IjSuYNNxWRzUTHb0bLXJkMaUahMVX74BfCy/Rva/V+3J07zLMpXAWXYPMiCCZHUAxCeIhqfkcIenUgg7UXCV4A==",
3516 "license": "Apache-2.0",
3517 "dependencies": {
3518 "bare-querystring": "*"
3519 }
3520 },
3521 "node_modules/queue-tick": {
3522 "version": "1.0.1",
3523 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
3524 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
3525 "license": "MIT"
3526 },
3527 "node_modules/quick-format-unescaped": {
3528 "version": "4.0.4",
3529 "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
3530 "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
3531 "license": "MIT"
3532 },
3533 "node_modules/quickbit-native": {
3534 "version": "2.4.8",
3535 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
3536 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
3537 "license": "Apache-2.0",
3538 "optional": true,
3539 "dependencies": {
3540 "require-addon": "^1.1.0"
3541 }
3542 },
3543 "node_modules/quickbit-universal": {
3544 "version": "2.2.0",
3545 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
3546 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
3547 "license": "ISC",
3548 "dependencies": {
3549 "b4a": "^1.6.0",
3550 "simdle-universal": "^1.1.0"
3551 },
3552 "optionalDependencies": {
3553 "quickbit-native": "^2.2.0"
3554 }
3555 },
3556 "node_modules/rabin-native": {
3557 "version": "2.0.0",
3558 "resolved": "https://registry.npmjs.org/rabin-native/-/rabin-native-2.0.0.tgz",
3559 "integrity": "sha512-x1BlYdIWh+nk9G0scvxyscrYiDPJ7vQZepqqPlVUSInIko1Zxooi8cm6lzBghEGI9aN3DXiunC8FXLgy6kke3Q==",
3560 "license": "Apache-2.0",
3561 "dependencies": {
3562 "require-addon": "^1.1.0"
3563 }
3564 },
3565 "node_modules/rabin-stream": {
3566 "version": "2.0.0",
3567 "resolved": "https://registry.npmjs.org/rabin-stream/-/rabin-stream-2.0.0.tgz",
3568 "integrity": "sha512-EzS2Ig/qsMBSk1PahC0O0IPdRZe3bspHHLWChhqW1feKz+J46tlVL3g4wWr4lrQdXGzABIMsOhvdT6bjXYMuUw==",
3569 "license": "Apache-2.0",
3570 "dependencies": {
3571 "rabin-native": "^2.0.0",
3572 "streamx": "^2.23.0"
3573 }
3574 },
3575 "node_modules/rache": {
3576 "version": "1.0.0",
3577 "resolved": "https://registry.npmjs.org/rache/-/rache-1.0.0.tgz",
3578 "integrity": "sha512-e0k0g0w/8jOCB+7YqCIlOa+OJ38k0wrYS4x18pMSmqOvLKoyhmMhmQyCcvfY6VaP8D75cqkEnlakXs+RYYLqNg==",
3579 "license": "Apache-2.0"
3580 },
3581 "node_modules/random-access-file": {
3582 "version": "4.1.2",
3583 "resolved": "https://registry.npmjs.org/random-access-file/-/random-access-file-4.1.2.tgz",
3584 "integrity": "sha512-GQM6R78DceZDcQod8KxlDFwXIiUvlvuy1EOzxTDsjuDjW5NlnlZi0MOk6iI4itAj/2vcvdqcEExYbVpC/dJcEw==",
3585 "license": "MIT",
3586 "dependencies": {
3587 "bare-fs": "^4.0.1",
3588 "bare-path": "^3.0.0",
3589 "random-access-storage": "^3.0.0"
3590 },
3591 "optionalDependencies": {
3592 "fs-native-extensions": "^1.3.1"
3593 }
3594 },
3595 "node_modules/random-access-memory": {
3596 "version": "6.2.1",
3597 "resolved": "https://registry.npmjs.org/random-access-memory/-/random-access-memory-6.2.1.tgz",
3598 "integrity": "sha512-hUeu1PbGLmWeyze9LwwSNaqloivNYjFsARIYxRdgUgn0wrdvMG+RszrfTG8814zfcXOgy4pFO2TpX/Cl3hRO4w==",
3599 "license": "MIT",
3600 "dependencies": {
3601 "b4a": "^1.6.0",
3602 "is-options": "^1.0.2",
3603 "random-access-storage": "^3.0.0"
3604 }
3605 },
3606 "node_modules/random-access-storage": {
3607 "version": "3.0.2",
3608 "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-3.0.2.tgz",
3609 "integrity": "sha512-Es9maUyWdJXWKckKy9s1+vT+DEgAt+PBb9lxPaake/0EDUsHehloKGv9v1zimS2V3gpFAcQXubvc1Rgci2sDPQ==",
3610 "license": "MIT",
3611 "dependencies": {
3612 "bare-events": "^2.2.0",
3613 "queue-tick": "^1.0.0"
3614 }
3615 },
3616 "node_modules/random-array-iterator": {
3617 "version": "1.0.0",
3618 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
3619 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
3620 "license": "MIT"
3621 },
3622 "node_modules/read-write-mutexify": {
3623 "version": "2.1.0",
3624 "resolved": "https://registry.npmjs.org/read-write-mutexify/-/read-write-mutexify-2.1.0.tgz",
3625 "integrity": "sha512-fDw/p5/acI1ytVY1UbxEDma/ej1yJH/n9NcjS9YNzcE6sPBPWdlru3ydRa/UBowUg4zqOvNMD5SOGYJrlQ6MzQ==",
3626 "license": "MIT"
3627 },
3628 "node_modules/ready-resource": {
3629 "version": "1.2.0",
3630 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
3631 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
3632 "license": "MIT",
3633 "dependencies": {
3634 "bare-events": "^2.2.0"
3635 }
3636 },
3637 "node_modules/real-require": {
3638 "version": "0.2.0",
3639 "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
3640 "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
3641 "license": "MIT",
3642 "engines": {
3643 "node": ">= 12.13.0"
3644 }
3645 },
3646 "node_modules/record-cache": {
3647 "version": "1.2.0",
3648 "resolved": "https://registry.npmjs.org/record-cache/-/record-cache-1.2.0.tgz",
3649 "integrity": "sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==",
3650 "license": "MIT",
3651 "dependencies": {
3652 "b4a": "^1.3.1"
3653 }
3654 },
3655 "node_modules/refcounter": {
3656 "version": "1.0.0",
3657 "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz",
3658 "integrity": "sha512-1WosVzUy0kPUaPMEtlNDwm99UsteALIhXXR8rerELoa63WkYIXAl0hxgwPFrIYBRWZPGUyekQ04FRtPJ7dHk9w==",
3659 "license": "Apache-2.0"
3660 },
3661 "node_modules/require-addon": {
3662 "version": "1.2.0",
3663 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
3664 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
3665 "license": "Apache-2.0",
3666 "dependencies": {
3667 "bare-addon-resolve": "^1.3.0"
3668 },
3669 "engines": {
3670 "bare": ">=1.10.0"
3671 }
3672 },
3673 "node_modules/require-asset": {
3674 "version": "1.2.1",
3675 "resolved": "https://registry.npmjs.org/require-asset/-/require-asset-1.2.1.tgz",
3676 "integrity": "sha512-wFFxOxJHL/agpiXNDMzDBzC8hnmjJZYW0wDgolojZZkydS76talLlIH+DU3DWjBxQcu0vair30H7V3C4FcLnSg==",
3677 "dev": true,
3678 "license": "Apache-2.0",
3679 "optional": true,
3680 "peer": true,
3681 "dependencies": {
3682 "bare-module-resolve": "^1.6.2"
3683 },
3684 "engines": {
3685 "bare": ">=1.10.0"
3686 }
3687 },
3688 "node_modules/require-directory": {
3689 "version": "2.1.1",
3690 "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
3691 "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
3692 "license": "MIT",
3693 "engines": {
3694 "node": ">=0.10.0"
3695 }
3696 },
3697 "node_modules/resolve-reject-promise": {
3698 "version": "1.1.0",
3699 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
3700 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
3701 "license": "MIT"
3702 },
3703 "node_modules/resource-on-exit": {
3704 "version": "1.0.0",
3705 "resolved": "https://registry.npmjs.org/resource-on-exit/-/resource-on-exit-1.0.0.tgz",
3706 "integrity": "sha512-ViJwJAknCkLRJRPR+9SISQQ7R5eRgtdIHLJsM2hHx1MweAJbJxJ5XnMjjq0Lc7ZGv44ufzAqds1nKxiVkdy4ag==",
3707 "license": "Apache-2.0"
3708 },
3709 "node_modules/rocksdb-native": {
3710 "version": "3.15.0",
3711 "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.15.0.tgz",
3712 "integrity": "sha512-czYx0hOzKAy+HYmJROZm7thvJ1tHFWHNkGI3nomumXWzrJghJdrWo4MIhehGX4OTff0lnOCoB2zu6ekzIKY0rQ==",
3713 "license": "Apache-2.0",
3714 "dependencies": {
3715 "compact-encoding": "^2.15.0",
3716 "ready-resource": "^1.0.0",
3717 "refcounter": "^1.0.0",
3718 "require-addon": "^1.0.2",
3719 "resolve-reject-promise": "^1.1.0",
3720 "signal-promise": "^1.0.3",
3721 "streamx": "^2.16.1"
3722 },
3723 "engines": {
3724 "bare": ">=1.16.0"
3725 }
3726 },
3727 "node_modules/safe-stable-stringify": {
3728 "version": "2.5.0",
3729 "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
3730 "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
3731 "license": "MIT",
3732 "engines": {
3733 "node": ">=10"
3734 }
3735 },
3736 "node_modules/safer-buffer": {
3737 "version": "2.1.2",
3738 "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
3739 "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
3740 "license": "MIT"
3741 },
3742 "node_modules/safety-catch": {
3743 "version": "1.0.3",
3744 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.3.tgz",
3745 "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==",
3746 "license": "MIT"
3747 },
3748 "node_modules/same-data": {
3749 "version": "1.0.0",
3750 "resolved": "https://registry.npmjs.org/same-data/-/same-data-1.0.0.tgz",
3751 "integrity": "sha512-Eqn7N2yV+aKMlUHTRqUwYG1Iv0cJqjlvLKj/GoP5PozJn361QaOYX14+v87r7NqQUZC22noP/LfLrSQiPwAygw==",
3752 "license": "MIT"
3753 },
3754 "node_modules/scope-lock": {
3755 "version": "1.2.4",
3756 "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz",
3757 "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==",
3758 "license": "Apache-2.0"
3759 },
3760 "node_modules/script-linker": {
3761 "version": "2.5.4",
3762 "resolved": "https://registry.npmjs.org/script-linker/-/script-linker-2.5.4.tgz",
3763 "integrity": "sha512-hzVNCEyu/1g9egVPk7s+jTgus+ZGd12d/Rjkm2QjOmPW5l07EANXd20cjAjA3aLPRY/n5z1UUESKHUf4tli5Tg==",
3764 "license": "Apache-2.0",
3765 "dependencies": {
3766 "acorn": "^8.6.0",
3767 "b4a": "^1.3.1",
3768 "cjs-module-lexer": "^1.2.2",
3769 "drive-resolve": "^1.0.1",
3770 "es-module-lexer": "^1.6.0",
3771 "is-valid-variable": "^1.0.0",
3772 "read-write-mutexify": "^2.0.0",
3773 "source-map-bare": "^1.0.0",
3774 "unix-path-resolve": "^1.0.2"
3775 }
3776 },
3777 "node_modules/shuffled-priority-queue": {
3778 "version": "2.1.0",
3779 "resolved": "https://registry.npmjs.org/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz",
3780 "integrity": "sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA==",
3781 "license": "MIT",
3782 "dependencies": {
3783 "unordered-set": "^2.0.1"
3784 }
3785 },
3786 "node_modules/signal-exit": {
3787 "version": "4.1.0",
3788 "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
3789 "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
3790 "license": "ISC",
3791 "engines": {
3792 "node": ">=14"
3793 },
3794 "funding": {
3795 "url": "https://github.com/sponsors/isaacs"
3796 }
3797 },
3798 "node_modules/signal-promise": {
3799 "version": "1.0.3",
3800 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
3801 "integrity": "sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==",
3802 "license": "MIT"
3803 },
3804 "node_modules/signed-varint": {
3805 "version": "2.0.1",
3806 "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz",
3807 "integrity": "sha512-abgDPg1106vuZZOvw7cFwdCABddfJRz5akcCcchzTbhyhYnsG31y4AlZEgp315T7W3nQq5P4xeOm186ZiPVFzw==",
3808 "license": "MIT",
3809 "dependencies": {
3810 "varint": "~5.0.0"
3811 }
3812 },
3813 "node_modules/simdle-native": {
3814 "version": "1.3.9",
3815 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
3816 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
3817 "license": "Apache-2.0",
3818 "optional": true,
3819 "dependencies": {
3820 "b4a": "^1.6.0",
3821 "require-addon": "^1.1.0"
3822 }
3823 },
3824 "node_modules/simdle-universal": {
3825 "version": "1.1.2",
3826 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
3827 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
3828 "license": "ISC",
3829 "dependencies": {
3830 "b4a": "^1.6.0"
3831 },
3832 "optionalDependencies": {
3833 "simdle-native": "^1.1.1"
3834 }
3835 },
3836 "node_modules/smart-buffer": {
3837 "version": "4.2.0",
3838 "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
3839 "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
3840 "license": "MIT",
3841 "engines": {
3842 "node": ">= 6.0.0",
3843 "npm": ">= 3.0.0"
3844 }
3845 },
3846 "node_modules/socks": {
3847 "version": "2.8.7",
3848 "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
3849 "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
3850 "license": "MIT",
3851 "dependencies": {
3852 "ip-address": "^10.0.1",
3853 "smart-buffer": "^4.2.0"
3854 },
3855 "engines": {
3856 "node": ">= 10.0.0",
3857 "npm": ">= 3.0.0"
3858 }
3859 },
3860 "node_modules/sodium-native": {
3861 "version": "5.1.0",
3862 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
3863 "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==",
3864 "license": "MIT",
3865 "dependencies": {
3866 "bare-assert": "^1.2.0",
3867 "require-addon": "^1.1.0",
3868 "which-runtime": "^1.2.1"
3869 },
3870 "engines": {
3871 "bare": ">=1.16.0"
3872 }
3873 },
3874 "node_modules/sodium-secretstream": {
3875 "version": "1.2.0",
3876 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
3877 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
3878 "license": "MIT",
3879 "dependencies": {
3880 "b4a": "^1.1.1",
3881 "sodium-universal": "^5.0.0"
3882 }
3883 },
3884 "node_modules/sodium-universal": {
3885 "version": "5.0.1",
3886 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
3887 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
3888 "license": "MIT",
3889 "dependencies": {
3890 "sodium-native": "^5.0.1"
3891 },
3892 "peerDependencies": {
3893 "sodium-javascript": "~0.8.0"
3894 },
3895 "peerDependenciesMeta": {
3896 "sodium-javascript": {
3897 "optional": true
3898 }
3899 }
3900 },
3901 "node_modules/sonic-boom": {
3902 "version": "4.2.1",
3903 "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
3904 "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
3905 "license": "MIT",
3906 "dependencies": {
3907 "atomic-sleep": "^1.0.0"
3908 }
3909 },
3910 "node_modules/source-map-bare": {
3911 "version": "1.0.0",
3912 "resolved": "https://registry.npmjs.org/source-map-bare/-/source-map-bare-1.0.0.tgz",
3913 "integrity": "sha512-e1AGLUcnbp8l4URDmHXuCRlts4CRwjPVd/2XDD6a3zUdcF5lr7hp2pTYMoklJXaN4CArYBb53jXGVRsSsVsDPA==",
3914 "license": "BSD-3-Clause",
3915 "dependencies": {
3916 "whatwg-url": "^7.0.0"
3917 },
3918 "engines": {
3919 "node": ">= 12"
3920 }
3921 },
3922 "node_modules/speedometer": {
3923 "version": "1.1.0",
3924 "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.1.0.tgz",
3925 "integrity": "sha512-z/wAiTESw2XVPssY2XRcme4niTc4S5FkkJ4gknudtVoc33Zil8TdTxHy5torRcgqMqksJV2Yz8HQcvtbsnw0mQ==",
3926 "license": "MIT"
3927 },
3928 "node_modules/split2": {
3929 "version": "4.2.0",
3930 "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
3931 "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
3932 "license": "ISC",
3933 "engines": {
3934 "node": ">= 10.x"
3935 }
3936 },
3937 "node_modules/streamx": {
3938 "version": "2.25.0",
3939 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
3940 "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
3941 "license": "MIT",
3942 "dependencies": {
3943 "events-universal": "^1.0.0",
3944 "fast-fifo": "^1.3.2",
3945 "text-decoder": "^1.1.0"
3946 }
3947 },
3948 "node_modules/string-width": {
3949 "version": "4.2.3",
3950 "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
3951 "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
3952 "license": "MIT",
3953 "dependencies": {
3954 "emoji-regex": "^8.0.0",
3955 "is-fullwidth-code-point": "^3.0.0",
3956 "strip-ansi": "^6.0.1"
3957 },
3958 "engines": {
3959 "node": ">=8"
3960 }
3961 },
3962 "node_modules/strip-ansi": {
3963 "version": "6.0.1",
3964 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
3965 "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
3966 "license": "MIT",
3967 "dependencies": {
3968 "ansi-regex": "^5.0.1"
3969 },
3970 "engines": {
3971 "node": ">=8"
3972 }
3973 },
3974 "node_modules/sub-encoder": {
3975 "version": "2.1.3",
3976 "resolved": "https://registry.npmjs.org/sub-encoder/-/sub-encoder-2.1.3.tgz",
3977 "integrity": "sha512-Xxx04ygZo/1J3yHvaSA6VhDmiSaBQkw/PmO3YnnYFXle+tfOGToC6FcDpIfMztWZXJzuKG14b/57HMkiL58C6A==",
3978 "license": "Apache-2.0",
3979 "dependencies": {
3980 "b4a": "^1.6.0",
3981 "codecs": "^3.1.0"
3982 }
3983 },
3984 "node_modules/teex": {
3985 "version": "1.0.1",
3986 "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
3987 "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
3988 "license": "MIT",
3989 "dependencies": {
3990 "streamx": "^2.12.5"
3991 }
3992 },
3993 "node_modules/text-decoder": {
3994 "version": "1.2.7",
3995 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
3996 "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
3997 "license": "Apache-2.0",
3998 "dependencies": {
3999 "b4a": "^1.6.4"
4000 }
4001 },
4002 "node_modules/thread-stream": {
4003 "version": "3.1.0",
4004 "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
4005 "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
4006 "license": "MIT",
4007 "dependencies": {
4008 "real-require": "^0.2.0"
4009 }
4010 },
4011 "node_modules/thunky": {
4012 "version": "1.1.0",
4013 "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
4014 "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
4015 "license": "MIT"
4016 },
4017 "node_modules/time-ordered-set": {
4018 "version": "2.0.1",
4019 "resolved": "https://registry.npmjs.org/time-ordered-set/-/time-ordered-set-2.0.1.tgz",
4020 "integrity": "sha512-VJEKmgSN2UiOLB8BpN8Sh2b9LGMHTP5OPrQRpnKjvOheOyzk0mufbjzjKTIG2gO4A+Y+vDJ+0TcLbpUmMLsg8A==",
4021 "license": "MIT"
4022 },
4023 "node_modules/timeout-refresh": {
4024 "version": "2.0.1",
4025 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
4026 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
4027 "license": "MIT"
4028 },
4029 "node_modules/tiny-buffer-rpc": {
4030 "version": "2.3.1",
4031 "resolved": "https://registry.npmjs.org/tiny-buffer-rpc/-/tiny-buffer-rpc-2.3.1.tgz",
4032 "integrity": "sha512-oJvKOwEKYAt3IwqX9TOI76DaLKebbWyf3/A723mSw1Vm2qKswV21w0Qc34sSJ1bbBQTmo9IT7ToA7diwz4sgCg==",
4033 "license": "Apache-2.0",
4034 "dependencies": {
4035 "b4a": "^1.6.3",
4036 "compact-encoding": "^2.11.0",
4037 "safety-catch": "^1.0.2",
4038 "streamx": "^2.13.2"
4039 }
4040 },
4041 "node_modules/tiny-byte-size": {
4042 "version": "1.1.0",
4043 "resolved": "https://registry.npmjs.org/tiny-byte-size/-/tiny-byte-size-1.1.0.tgz",
4044 "integrity": "sha512-OQ+i4RZzIQ0CR22DEr3G+ckRwN93R9UQMNqeym41Ntfj/dchPyWu1cuu8mvdCv61Ng3uOmNgX+8WesK1f37x+w==",
4045 "license": "MIT"
4046 },
4047 "node_modules/tr46": {
4048 "version": "1.0.1",
4049 "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
4050 "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==",
4051 "license": "MIT",
4052 "dependencies": {
4053 "punycode": "^2.1.0"
4054 }
4055 },
4056 "node_modules/typpy": {
4057 "version": "2.4.0",
4058 "resolved": "https://registry.npmjs.org/typpy/-/typpy-2.4.0.tgz",
4059 "integrity": "sha512-a16Uv5doNtvHzaG4wZCHmXN+l9xxmTMpyODtPz7B3DSTsDVNXilTSJGuNw68sUh0Un4bf+ghRMbEcJCI6r06mQ==",
4060 "license": "MIT",
4061 "dependencies": {
4062 "function.name": "^1.0.3"
4063 }
4064 },
4065 "node_modules/udx-native": {
4066 "version": "1.19.2",
4067 "resolved": "https://registry.npmjs.org/udx-native/-/udx-native-1.19.2.tgz",
4068 "integrity": "sha512-RNYh+UhfryCsF5hE2ZOuIqcZ+qdipXK3UsarwxWJwsUQZFE3ybwz0mPjwb5ev1PMBcjFahWiepS/q0wwL51c2g==",
4069 "license": "Apache-2.0",
4070 "dependencies": {
4071 "b4a": "^1.5.0",
4072 "bare-events": "^2.2.0",
4073 "require-addon": "^1.1.0",
4074 "streamx": "^2.22.0"
4075 },
4076 "engines": {
4077 "bare": ">=1.17.4"
4078 }
4079 },
4080 "node_modules/ul": {
4081 "version": "5.2.16",
4082 "resolved": "https://registry.npmjs.org/ul/-/ul-5.2.16.tgz",
4083 "integrity": "sha512-v1YrSEsJZpJsywzF/MKgsQwMdOwBlwwmNiUOJh/yX6FHrq7dYjeua1YOhLV0q0KioqEFZC4P7MsKmpEsGdZz3w==",
4084 "license": "MIT",
4085 "dependencies": {
4086 "deffy": "^2.2.2",
4087 "typpy": "^2.3.4"
4088 }
4089 },
4090 "node_modules/undici-types": {
4091 "version": "7.19.2",
4092 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
4093 "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
4094 "license": "MIT"
4095 },
4096 "node_modules/unix-path-resolve": {
4097 "version": "1.0.2",
4098 "resolved": "https://registry.npmjs.org/unix-path-resolve/-/unix-path-resolve-1.0.2.tgz",
4099 "integrity": "sha512-kG4g5nobBBaMnH2XbrS4sLUXEpx4nY2J3C6KAlAUcnahG2HChxSPVKWYrqEq76iTo+cyMkLUjqxGaQR2tz097Q==",
4100 "license": "MIT"
4101 },
4102 "node_modules/unordered-set": {
4103 "version": "2.0.1",
4104 "resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz",
4105 "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==",
4106 "license": "MIT"
4107 },
4108 "node_modules/unslab": {
4109 "version": "1.3.0",
4110 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
4111 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
4112 "license": "Apache-2.0",
4113 "dependencies": {
4114 "b4a": "^1.6.6"
4115 }
4116 },
4117 "node_modules/url": {
4118 "name": "bare-url",
4119 "version": "2.4.1",
4120 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.1.tgz",
4121 "integrity": "sha512-fZapLWNB25gS+etK27NV9KgBNXgo2yeYHuj+OyPblQd6GYAE3JVy6aKxszMV5jhGGFwraXQKA5fldvf3lMyEqw==",
4122 "license": "Apache-2.0",
4123 "dependencies": {
4124 "bare-path": "^3.0.0"
4125 }
4126 },
4127 "node_modules/url-file-url": {
4128 "version": "1.0.5",
4129 "resolved": "https://registry.npmjs.org/url-file-url/-/url-file-url-1.0.5.tgz",
4130 "integrity": "sha512-RkUWAFWSmCDpyFn2qf2QO8Gnwf0y5RadgJuDpBeFD6gEzDuh6hTNpd/jox4znFFqZ79ghAS0gGM7F3ZU4nrMWw==",
4131 "license": "Apache-2.0",
4132 "dependencies": {
4133 "bare-path": "^3.0.0",
4134 "which-runtime": "^1.2.0"
4135 }
4136 },
4137 "node_modules/varint": {
4138 "version": "5.0.0",
4139 "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
4140 "integrity": "sha512-gC13b/bWrqQoKY2EmROCZ+AR0jitc6DnDGaQ6Ls9QpKmuSgJB1eQ7H3KETtQm7qSdMWMKCmsshyCmUwMLh3OAA==",
4141 "license": "MIT"
4142 },
4143 "node_modules/webidl-conversions": {
4144 "version": "4.0.2",
4145 "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
4146 "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
4147 "license": "BSD-2-Clause"
4148 },
4149 "node_modules/whatwg-url": {
4150 "version": "7.1.0",
4151 "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
4152 "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
4153 "license": "MIT",
4154 "dependencies": {
4155 "lodash.sortby": "^4.7.0",
4156 "tr46": "^1.0.1",
4157 "webidl-conversions": "^4.0.2"
4158 }
4159 },
4160 "node_modules/which-runtime": {
4161 "version": "1.3.2",
4162 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
4163 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
4164 "license": "Apache-2.0"
4165 },
4166 "node_modules/wrap-ansi": {
4167 "version": "7.0.0",
4168 "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
4169 "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
4170 "license": "MIT",
4171 "dependencies": {
4172 "ansi-styles": "^4.0.0",
4173 "string-width": "^4.1.0",
4174 "strip-ansi": "^6.0.0"
4175 },
4176 "engines": {
4177 "node": ">=10"
4178 },
4179 "funding": {
4180 "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
4181 }
4182 },
4183 "node_modules/ws": {
4184 "version": "8.20.0",
4185 "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
4186 "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
4187 "license": "MIT",
4188 "engines": {
4189 "node": ">=10.0.0"
4190 },
4191 "peerDependencies": {
4192 "bufferutil": "^4.0.1",
4193 "utf-8-validate": ">=5.0.2"
4194 },
4195 "peerDependenciesMeta": {
4196 "bufferutil": {
4197 "optional": true
4198 },
4199 "utf-8-validate": {
4200 "optional": true
4201 }
4202 }
4203 },
4204 "node_modules/xache": {
4205 "version": "1.2.1",
4206 "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz",
4207 "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==",
4208 "license": "MIT"
4209 },
4210 "node_modules/y18n": {
4211 "version": "5.0.8",
4212 "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
4213 "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
4214 "license": "ISC",
4215 "engines": {
4216 "node": ">=10"
4217 }
4218 },
4219 "node_modules/yargs": {
4220 "version": "17.7.2",
4221 "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
4222 "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
4223 "license": "MIT",
4224 "dependencies": {
4225 "cliui": "^8.0.1",
4226 "escalade": "^3.1.1",
4227 "get-caller-file": "^2.0.5",
4228 "require-directory": "^2.1.1",
4229 "string-width": "^4.2.3",
4230 "y18n": "^5.0.5",
4231 "yargs-parser": "^21.1.1"
4232 },
4233 "engines": {
4234 "node": ">=12"
4235 }
4236 },
4237 "node_modules/yargs-parser": {
4238 "version": "21.1.1",
4239 "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
4240 "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
4241 "license": "ISC",
4242 "engines": {
4243 "node": ">=12"
4244 }
4245 },
4246 "node_modules/z32": {
4247 "version": "1.1.0",
4248 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
4249 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
4250 "license": "MIT",
4251 "dependencies": {
4252 "b4a": "^1.5.3"
4253 }
4254 }
4255 }
4256}
package.json +74 -0
……@@ -0,0 +1,74 @@
1{
2 "name": "p2pbuilders",
3 "version": "0.0.1",
4 "description": "Permissionless P2P messageboard on the Pear/Bare stack",
5 "main": "src/terminal/main.js",
6 "type": "commonjs",
7 "imports": {
8 "#fs": {
9 "bare": "bare-fs",
10 "default": "./src/backend/_node-fs.js"
11 },
12 "#path": {
13 "bare": "bare-path",
14 "default": "./src/backend/_node-path.js"
15 },
16 "#os": {
17 "bare": "bare-os",
18 "default": "./src/backend/_node-os.js"
19 },
20 "#env": {
21 "bare": "bare-env",
22 "default": "./src/backend/_node-env.js"
23 },
24 "#events": {
25 "bare": "bare-events",
26 "default": "./src/backend/_node-events.js"
27 },
28 "#process": {
29 "bare": "bare-process",
30 "default": "./src/backend/_node-process.js"
31 }
32 },
33 "scripts": {
34 "start": "node src/backend/server.js",
35 "dev": "P2PBUILDERS_NO_SWARM=1 P2PBUILDERS_DIR=./.data P2PBUILDERS_PORT=8787 node src/backend/server.js",
36 "pear": "pear run -d . --dev",
37 "pear:stage": "pear stage dev .",
38 "relay": "node src/relay/server.js",
39 "publish": "node scripts/publish.js",
40 "test": "node test/m1.js && node test/m2.js && node test/m3.js && node test/m4.js && node test/m5.js && node test/m6.js && node test/m7.js && node test/m8.js && node test/m9.js && node test/m10-bare.js && node test/m11.js"
41 },
42 "pear": {
43 "name": "p2pbuilders",
44 "type": "terminal",
45 "main": "src/terminal/main.js"
46 },
47 "dependencies": {
48 "@hyperswarm/dht-relay": "^0.4.3",
49 "b4a": "^1.6.6",
50 "bare-env": "^3.0.0",
51 "bare-fs": "^4.7.1",
52 "bare-ipc": "^1.1.1",
53 "bare-os": "^3.8.7",
54 "bare-path": "^3.0.0",
55 "bare-process": "^4.4.1",
56 "bare-readline": "^1.3.1",
57 "bare-tty": "^5.1.0",
58 "compact-encoding": "^2.15.0",
59 "corestore": "^7.0.0",
60 "hyperbee": "^2.27.3",
61 "hypercore": "^11.0.0",
62 "hyperdht": "^6.30.0",
63 "hyperswarm": "^4.17.0",
64 "p2p-hiverelay": "^0.4.2",
65 "pear-electron": "^1.7.28",
66 "random-access-memory": "^6.2.1",
67 "sodium-universal": "^5.0.0",
68 "ws": "^8.20.0"
69 },
70 "devDependencies": {
71 "bare": "^1.28.4",
72 "bare-pack": "^2.0.1"
73 }
74}
public/app.js +576 -0
……@@ -0,0 +1,576 @@
1// p2pbuilders frontend — HN-shaped single feed. Vanilla JS, no build step.
2
3import { createTransport } from './transport.js'
4
5const DEFAULT_BOARD = 'front'
6const ONBOARD_FLAG = 'p2pbuilders.onboarded'
7
8// Auto-detects environment: Pear IPC pipe if available, else localhost WebSocket.
9const rpc = createTransport()
10
11// Cached identity, populated on boot. Used for ownership checks.
12let me = null
13
14// ----- helpers -----
15
16const h = (tag, attrs = {}, ...kids) => {
17 const el = document.createElement(tag)
18 for (const [k, v] of Object.entries(attrs || {})) {
19 if (k === 'class') el.className = v
20 else if (k === 'html') el.innerHTML = v
21 else if (k.startsWith('on')) el.addEventListener(k.slice(2), v)
22 else if (v !== undefined && v !== null) el.setAttribute(k, v)
23 }
24 for (const kid of kids.flat()) {
25 if (kid == null || kid === false) continue
26 el.appendChild(kid.nodeType ? kid : document.createTextNode(String(kid)))
27 }
28 return el
29}
30const $view = () => document.getElementById('view')
31const shortHex = (s, n = 8) => (s ? s.slice(0, n) : '')
32const displayName = (obj) => obj.authorNick || shortHex(obj.author || obj.pubkey)
33const isMine = (obj) => me && obj.author && obj.author === me.pubkey
34
35function hostOf (url) {
36 try { return new URL(url).host.replace(/^www\./, '') } catch { return null }
37}
38
39function ago (ts) {
40 const s = Math.floor((Date.now() - ts) / 1000)
41 if (s < 45) return `${s} seconds ago`
42 if (s < 60 * 45) return `${Math.floor(s / 60)} minutes ago`
43 if (s < 60 * 60 * 22) return `${Math.floor(s / 3600)} hours ago`
44 const d = Math.floor(s / 86400)
45 return d === 1 ? 'yesterday' : `${d} days ago`
46}
47
48function render (node) {
49 const v = $view(); v.replaceChildren(); v.appendChild(node)
50}
51function loading (msg = 'loading…') { return h('div', { class: 'empty' }, msg) }
52function errorBox (msg) { return h('div', { class: 'err' }, msg) }
53
54function toast (msg, { kind = 'ok', ms = 2400 } = {}) {
55 const t = document.createElement('div')
56 t.className = `toast toast-${kind}`
57 t.textContent = msg
58 document.body.appendChild(t)
59 setTimeout(() => t.classList.add('show'), 10)
60 setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 400) }, ms)
61}
62const toastError = (msg) => toast(msg, { kind: 'err', ms: 4000 })
63
64// A small "working on it" pill near an action button. Shows a pulsing progress
65// track and the elapsed ms, swapping itself out for a result toast when done.
66function powPill (expectedMs = 100, label = 'minting pow') {
67 const pill = h('span', { class: 'pow-pill' },
68 h('span', { class: 'pow-bar' }, h('span', { class: 'pow-fill' })),
69 h('span', { class: 'pow-label' }, `${label}…`)
70 )
71 const t0 = performance.now()
72 const fill = pill.querySelector('.pow-fill')
73 const labelEl = pill.querySelector('.pow-label')
74 let raf = 0
75 const tick = () => {
76 const elapsed = performance.now() - t0
77 // Ease toward 90% as elapsed approaches 3×expected, never quite reaching 100.
78 const pct = Math.min(90, 90 * (elapsed / (expectedMs * 1.5 + elapsed)))
79 fill.style.width = pct.toFixed(1) + '%'
80 labelEl.textContent = `${label}… ${Math.round(elapsed)}ms`
81 raf = requestAnimationFrame(tick)
82 }
83 tick()
84 return {
85 node: pill,
86 finish () { cancelAnimationFrame(raf); fill.style.width = '100%'; pill.classList.add('done') },
87 detach () { cancelAnimationFrame(raf); pill.remove() }
88 }
89}
90
91async function copyToClipboard (str) {
92 try {
93 if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(str)
94 else {
95 const ta = document.createElement('textarea')
96 ta.value = str; document.body.appendChild(ta); ta.select()
97 document.execCommand('copy'); ta.remove()
98 }
99 toast('copied')
100 } catch { toast('copy failed') }
101}
102
103// ----- router -----
104
105const routes = []
106function route (pattern, handler) {
107 const parts = pattern.split('/').filter(Boolean)
108 routes.push({ parts, handler })
109}
110
111async function dispatch () {
112 const hash = location.hash.replace(/^#/, '') || '/'
113 const [pathRaw] = hash.split('?')
114 const parts = pathRaw.split('/').filter(Boolean)
115 for (const r of routes) {
116 if (r.parts.length !== parts.length) continue
117 const params = {}
118 let ok = true
119 for (let i = 0; i < parts.length; i++) {
120 if (r.parts[i].startsWith(':')) params[r.parts[i].slice(1)] = decodeURIComponent(parts[i])
121 else if (r.parts[i] !== parts[i]) { ok = false; break }
122 }
123 if (ok) {
124 try { await r.handler(params) } catch (err) {
125 console.error(err); render(errorBox(err.message || String(err)))
126 }
127 return
128 }
129 }
130 render(errorBox('not found'))
131}
132window.addEventListener('hashchange', dispatch)
133
134// ----- feed views -----
135
136async function renderFeed (sort) {
137 render(loading('loading feed…'))
138 const posts = await rpc.call('listPosts', { board: DEFAULT_BOARD, sort, limit: 50 })
139 const frag = document.createDocumentFragment()
140 if (!posts.length) {
141 frag.appendChild(h('div', { class: 'empty' },
142 'nothing on the front page yet. ',
143 h('a', { href: '#/submit' }, 'submit something'),
144 ' to get things rolling.'
145 ))
146 } else {
147 const feed = h('div', { class: 'feed' })
148 posts.forEach((p, i) => feed.appendChild(feedRow(p, i + 1)))
149 frag.appendChild(feed)
150 }
151 render(frag)
152}
153
154route('/', () => renderFeed('hot'))
155route('/new', () => renderFeed('new'))
156
157function feedRow (p, rank) {
158 const host = p.link ? hostOf(p.link) : null
159 const titleHref = p.link || `#/t/${p.opId}`
160 const titleTarget = p.link ? '_blank' : null
161 const up = h('button', { class: 'vote-btn', title: 'upvote' })
162 up.addEventListener('click', async (e) => {
163 e.preventDefault(); e.stopPropagation()
164 try {
165 await rpc.call('vote', { targetOpId: p.opId, dir: 1 })
166 up.classList.add('active')
167 } catch (err) { toastError(err.message) }
168 })
169 const sublineKids = [
170 `${p.score || 0} points by ${displayName(p)} · `,
171 h('a', { href: `#/t/${p.opId}` }, ago(p.ts)),
172 ' · ',
173 h('a', { href: `#/t/${p.opId}` }, `${p.commentCount || 0} comments`)
174 ]
175 if (isMine(p)) {
176 sublineKids.push(' · ')
177 sublineKids.push(h('a', { href: '#', onclick: (e) => { e.preventDefault(); openEditor(p, 'post') } }, 'edit'))
178 sublineKids.push(' · ')
179 sublineKids.push(h('a', { href: '#', onclick: (e) => { e.preventDefault(); confirmDelete(p) } }, 'delete'))
180 }
181 return h('div', { class: 'feed-row' },
182 h('span', { class: 'rank' }, `${rank}.`),
183 h('span', { class: 'vote-cell' }, up),
184 h('div', { class: 'main' },
185 h('div', { class: 'title-line' },
186 h('a', {
187 class: 'title',
188 href: titleHref,
189 target: titleTarget,
190 rel: titleTarget ? 'noopener' : null
191 }, p.title || '(no title)'),
192 host ? h('span', { class: 'host' }, `(${host})`) : null
193 ),
194 h('div', { class: 'subline' }, ...sublineKids)
195 )
196 )
197}
198
199// ----- thread view + nested comments -----
200
201route('/t/:opId', async ({ opId }) => {
202 render(loading('loading thread…'))
203 const post = await rpc.call('getPost', { opId })
204 if (!post) return render(errorBox('post not found — author may not be replicated yet.'))
205 const flat = await rpc.call('listCommentTree', { rootOpId: opId })
206
207 const frag = document.createDocumentFragment()
208 frag.appendChild(renderPost(post))
209 frag.appendChild(replyForm(opId, { placeholder: 'add a top-level comment…' }))
210
211 const tree = h('div', { class: 'comment-tree' })
212 renderCommentTree(tree, flat, opId)
213 frag.appendChild(tree)
214
215 render(frag)
216})
217
218function renderPost (post) {
219 const host = post.link ? hostOf(post.link) : null
220 const sublineKids = [
221 `${post.score || 0} points by ${displayName(post)} · `,
222 ago(post.ts),
223 post.edited ? h('span', {}, ' · edited') : null
224 ]
225 if (isMine(post)) {
226 sublineKids.push(' · ')
227 sublineKids.push(h('a', { href: '#', onclick: (e) => { e.preventDefault(); openEditor(post, 'post') } }, 'edit'))
228 sublineKids.push(' · ')
229 sublineKids.push(h('a', { href: '#', onclick: (e) => { e.preventDefault(); confirmDelete(post) } }, 'delete'))
230 }
231 return h('article', { class: 'thread-post' },
232 h('div', { class: 'title-line' },
233 h('a', {
234 class: 'title',
235 href: post.link || `#/t/${post.opId}`,
236 target: post.link ? '_blank' : null,
237 rel: post.link ? 'noopener' : null
238 }, post.title),
239 host ? h('span', { class: 'host' }, `(${host})`) : null
240 ),
241 h('div', { class: 'subline' }, ...sublineKids),
242 post.body ? h('div', { class: 'body-text' }, post.body) : null
243 )
244}
245
246// Build a nested tree from the flat array (each comment has `parent` = opId hex).
247function renderCommentTree (container, flat, rootOpId) {
248 if (!flat.length) {
249 container.appendChild(h('div', { class: 'empty' }, 'no comments yet.'))
250 return
251 }
252 // parent → children map
253 const children = new Map()
254 for (const c of flat) {
255 const key = c.parent
256 if (!children.has(key)) children.set(key, [])
257 children.get(key).push(c)
258 }
259 // sort siblings by ts asc
260 for (const arr of children.values()) arr.sort((a, b) => a.ts - b.ts)
261
262 const roots = children.get(rootOpId) || []
263 for (const root of roots) container.appendChild(commentSubtree(root, children, 0))
264}
265
266function commentSubtree (c, children, depth) {
267 const kids = children.get(c.opId) || []
268 const node = h('div', { class: 'comment', style: depth > 0 ? `margin-left:${Math.min(depth, 6) * 18}px` : null },
269 commentTileBody(c),
270 ...kids.map((k) => commentSubtree(k, children, depth + 1))
271 )
272 return node
273}
274
275function commentTileBody (c) {
276 const actionKids = [
277 h('a', { href: '#', onclick: (e) => { e.preventDefault(); showInlineReply(c) } }, 'reply')
278 ]
279 if (isMine(c)) {
280 actionKids.push(' · ')
281 actionKids.push(h('a', { href: '#', onclick: (e) => { e.preventDefault(); openEditor(c, 'comment') } }, 'edit'))
282 actionKids.push(' · ')
283 actionKids.push(h('a', { href: '#', onclick: (e) => { e.preventDefault(); confirmDelete(c) } }, 'delete'))
284 }
285 return h('div', {},
286 h('div', { class: 'head' },
287 h('span', { class: c.authorNick ? '' : 'mono' }, displayName(c)),
288 h('span', {}, '·'),
289 h('span', {}, ago(c.ts)),
290 c.edited ? h('span', {}, '· edited') : null,
291 h('span', {}, `(${c.score >= 0 ? '+' : ''}${c.score})`)
292 ),
293 h('div', { class: 'body', id: `body-${c.opId}` }, c.body),
294 h('div', { class: 'actions', id: `actions-${c.opId}` }, ...actionKids)
295 )
296}
297
298function showInlineReply (parentComment) {
299 const existing = document.getElementById(`reply-${parentComment.opId}`)
300 if (existing) { existing.remove(); return }
301 const form = replyForm(parentComment.opId, { placeholder: `reply to ${displayName(parentComment)}…` })
302 form.id = `reply-${parentComment.opId}`
303 document.getElementById(`actions-${parentComment.opId}`).after(form)
304}
305
306function replyForm (parentOpId, { placeholder = 'reply…' } = {}) {
307 const ta = h('textarea', { placeholder })
308 const btn = h('button', { class: 'primary' }, 'reply')
309 const actions = h('div', { class: 'actions' }, btn)
310 btn.addEventListener('click', async () => {
311 const body = ta.value.trim()
312 if (!body) return toastError('write something first')
313 btn.disabled = true
314 const pill = powPill(30, 'minting reply pow')
315 actions.appendChild(pill.node)
316 try {
317 await rpc.call('createComment', { parentOpId, body })
318 pill.finish()
319 await dispatch()
320 } catch (err) {
321 pill.detach(); btn.disabled = false
322 toastError(err.message)
323 }
324 })
325 return h('div', { class: 'reply-form' }, ta, actions)
326}
327
328// ----- edit / delete -----
329
330function openEditor (item, kind) {
331 const dialog = h('div', { class: 'modal' },
332 h('div', { class: 'modal-inner' },
333 h('h3', {}, `edit ${kind}`),
334 kind === 'post' ? h('input', { id: 'edit-title', value: item.title || '', maxlength: '200' }) : null,
335 h('textarea', { id: 'edit-body' }, item.body || ''),
336 h('div', { class: 'actions' },
337 h('button', {
338 class: 'primary',
339 onclick: async (e) => {
340 const btn = e.currentTarget
341 btn.disabled = true; btn.textContent = 'saving…'
342 try {
343 await rpc.call('editOp', {
344 opId: item.opId,
345 body: document.getElementById('edit-body').value,
346 title: kind === 'post' ? document.getElementById('edit-title').value : null
347 })
348 dialog.remove()
349 await dispatch()
350 } catch (err) { toastError(err.message); btn.disabled = false; btn.textContent = 'save' }
351 }
352 }, 'save'),
353 h('button', { class: 'btn-ghost', onclick: () => dialog.remove() }, 'cancel')
354 )
355 )
356 )
357 document.body.appendChild(dialog)
358}
359
360async function confirmDelete (item) {
361 if (!confirm(`delete this ${item.title ? 'post' : 'comment'}? tombstones hide it but cannot remove signed history.`)) return
362 try {
363 await rpc.call('deleteOp', { opId: item.opId })
364 await dispatch()
365 } catch (err) { toastError(err.message) }
366}
367
368// ----- submit -----
369
370route('/submit', async () => {
371 const form = h('div', { class: 'submit-form' },
372 h('h2', { style: 'margin-top: 8px;' }, 'submit'),
373 h('table', {},
374 h('tbody', {},
375 row('title', h('input', { id: 'title', maxlength: '200' })),
376 row('url', h('input', { id: 'link', placeholder: 'https://…' })),
377 row('text', h('textarea', { id: 'body', placeholder: 'optional' }))
378 )
379 ),
380 h('div', { class: 'actions' },
381 h('button', {
382 class: 'primary',
383 onclick: async (e) => {
384 const btn = e.currentTarget
385 const title = document.getElementById('title').value.trim()
386 const body = document.getElementById('body').value.trim()
387 const link = document.getElementById('link').value.trim() || null
388 if (!title) return toastError('title is required')
389 if (!body && !link) return toastError('either text or url is required')
390 btn.disabled = true
391 const pill = powPill(100, 'minting post pow')
392 btn.after(pill.node)
393 try {
394 const { opId } = await rpc.call('createPost', { title, body, link })
395 pill.finish()
396 location.hash = `#/t/${opId}`
397 } catch (err) {
398 pill.detach(); btn.disabled = false
399 toastError(err.message)
400 }
401 }
402 }, 'submit')
403 ),
404 h('div', { class: 'subline', style: 'margin-top: 12px' },
405 'leave url blank to post text. leave text blank to post a link.'
406 )
407 )
408 render(form)
409})
410
411function row (label, ctl) {
412 return h('tr', {},
413 h('td', { class: 'label' }, label),
414 h('td', {}, ctl)
415 )
416}
417
418// ----- /me: profile + identity backup -----
419
420route('/me', async () => {
421 render(loading('loading…'))
422 const profile = me ? await rpc.call('getProfile', { pubkey: me.pubkey }).catch(() => null) : null
423 const keyRes = await rpc.call('exportPrimaryKey').catch(() => null)
424
425 const nickInput = h('input', { id: 'nick', value: profile?.nick || '', maxlength: '40', placeholder: 'pick a nickname' })
426 const bioInput = h('textarea', { id: 'bio', placeholder: 'optional bio' }, profile?.bio || '')
427 const keyField = h('span', { class: 'mono key-field' }, keyRes?.hex || '(unavailable)')
428 const keyReveal = h('button', { class: 'btn-ghost' }, 'reveal key')
429 let revealed = false
430 keyField.classList.add('blurred')
431 keyReveal.addEventListener('click', () => {
432 revealed = !revealed
433 keyField.classList.toggle('blurred', !revealed)
434 keyReveal.textContent = revealed ? 'hide key' : 'reveal key'
435 })
436
437 const frag = document.createDocumentFragment()
438 frag.appendChild(h('div', { class: 'submit-form' },
439 h('h2', { style: 'margin-top: 8px' }, 'identity'),
440 h('div', { class: 'subline', style: 'margin-bottom:10px' },
441 'pubkey: ', h('span', { class: 'me' }, me.pubkey)
442 ),
443 h('table', {}, h('tbody', {}, row('nick', nickInput), row('bio', bioInput))),
444 h('div', { class: 'actions' },
445 h('button', {
446 class: 'primary',
447 onclick: async (e) => {
448 const btn = e.currentTarget
449 btn.disabled = true; btn.textContent = 'saving…'
450 try {
451 await rpc.call('setProfile', { nick: nickInput.value.trim(), bio: bioInput.value.trim() })
452 btn.textContent = 'saved'
453 toast('profile saved')
454 setTimeout(() => { btn.disabled = false; btn.textContent = 'save' }, 700)
455 } catch (err) { toastError(err.message); btn.disabled = false; btn.textContent = 'save' }
456 }
457 }, 'save')
458 ),
459 h('hr', { style: 'margin: 20px 0; border: 0; border-top: 1px solid var(--border);' }),
460 h('h3', {}, 'backup key'),
461 h('div', { class: 'subline' },
462 'this 64-character hex string is the secret that controls your identity. ',
463 h('b', {}, 'save it somewhere safe'),
464 '. anyone with this key can post as you. if you lose it you cannot recover the account.'
465 ),
466 h('div', { style: 'margin-top: 8px; display: flex; gap: 6px; align-items: center; flex-wrap: wrap;' },
467 keyField,
468 keyReveal,
469 h('button', {
470 class: 'btn-ghost',
471 onclick: () => keyRes?.hex && copyToClipboard(keyRes.hex)
472 }, 'copy')
473 )
474 ))
475 render(frag)
476})
477
478// ----- /settings (diagnostics; read-only for now) -----
479
480route('/settings', async () => {
481 render(loading('gathering stats…'))
482 const stats = await rpc.call('getStats').catch((e) => ({ error: e.message }))
483 const frag = h('div', { class: 'submit-form' },
484 h('h2', { style: 'margin-top: 8px' }, 'settings'),
485 stats.error
486 ? errorBox(stats.error)
487 : h('table', { class: 'kv' }, h('tbody', {},
488 kv('transport', rpc.kind),
489 kv('storage', stats.storage || '(disk)'),
490 kv('swarm', stats.swarm),
491 kv('peers', stats.peerCount),
492 kv('tracked users', stats.trackedUsers),
493 kv('known boards', stats.boards),
494 kv('your core length', stats.length),
495 kv('pow (post / comment / board)', `${stats.policy.minPowBits.post} / ${stats.policy.minPowBits.comment} / ${stats.policy.minPowBits.board_create} bits`),
496 kv('rate limits / hour', `posts ${stats.policy.posts.perHour}, comments ${stats.policy.comments.perHour}, votes ${stats.policy.votes.perHour}`),
497 kv('enforce pow', String(stats.policy.enforcePow)),
498 kv('enforce rate limit', String(stats.policy.enforceRateLimit))
499 )),
500 h('div', { class: 'subline', style: 'margin-top: 16px' },
501 'these values are baked into the indexer policy. ',
502 'write-through settings (overrides, relay endpoints) land in v0.2.'
503 )
504 )
505 render(frag)
506})
507
508function kv (k, v) {
509 return h('tr', {},
510 h('td', { class: 'label' }, k),
511 h('td', { class: 'mono' }, String(v))
512 )
513}
514
515// ----- onboarding (first-launch) -----
516
517route('/welcome', async () => {
518 const nickInput = h('input', { id: 'nick', maxlength: '40', placeholder: 'pick a nickname (optional)' })
519 const frag = h('div', { class: 'submit-form' },
520 h('h1', { style: 'margin-top: 8px' }, 'welcome to p2pbuilders'),
521 h('p', { class: 'subline' },
522 'this is a ', h('b', {}, 'permissionless, peer-to-peer'),
523 ' hacker news. no servers. no accounts to register. your identity is a keypair on this device.'
524 ),
525 h('p', { class: 'subline' },
526 'your pubkey: ', h('span', { class: 'mono' }, me.pubkey)
527 ),
528 h('h3', {}, 'pick a nickname'),
529 h('div', {}, nickInput),
530 h('h3', { style: 'margin-top: 20px' }, 'one thing before you start'),
531 h('p', { class: 'subline' },
532 'your identity is stored as a file on this device only. if you lose it you lose the account. ',
533 h('a', { href: '#/me' }, 'back it up from /me'),
534 ' when you have a moment.'
535 ),
536 h('div', { class: 'actions' },
537 h('button', {
538 class: 'primary',
539 onclick: async (e) => {
540 const btn = e.currentTarget
541 btn.disabled = true; btn.textContent = 'starting…'
542 try {
543 const nick = nickInput.value.trim()
544 if (nick) await rpc.call('setProfile', { nick })
545 localStorage.setItem(ONBOARD_FLAG, '1')
546 location.hash = '#/'
547 } catch (err) { toastError(err.message); btn.disabled = false; btn.textContent = 'get started' }
548 }
549 }, 'get started')
550 )
551 )
552 render(frag)
553})
554
555// ----- boot -----
556
557rpc.onStatus((s) => { document.getElementById('status').textContent = `${rpc.kind}: ${s}` })
558
559;(async () => {
560 try {
561 me = await rpc.call('me')
562 document.getElementById('me').textContent = me ? `id: ${shortHex(me.pubkey, 12)}…` : ''
563 // First-launch redirect
564 if (!localStorage.getItem(ONBOARD_FLAG) && location.hash.replace(/^#/, '') !== '/welcome') {
565 const profile = await rpc.call('getProfile', { pubkey: me.pubkey }).catch(() => null)
566 if (!profile?.nick) {
567 location.hash = '#/welcome'
568 return // dispatch is triggered by hashchange
569 }
570 localStorage.setItem(ONBOARD_FLAG, '1')
571 }
572 dispatch()
573 } catch (err) {
574 render(errorBox(err.message || String(err)))
575 }
576})()
public/index.html +30 -0
……@@ -0,0 +1,30 @@
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, viewport-fit=cover" />
6 <meta name="theme-color" content="#f6f6ef" />
7 <title>p2pbuilders</title>
8 <link rel="stylesheet" href="./styles.css" />
9</head>
10<body>
11 <header class="topbar">
12 <div class="topbar-inner">
13 <a href="#/" class="brand">p2pbuilders</a>
14 <nav class="top-nav">
15 <a href="#/" class="nav-link">top</a>
16 <a href="#/new" class="nav-link">new</a>
17 <a href="#/submit" class="nav-link submit">submit</a>
18 <a href="#/me" class="nav-link">me</a>
19 <a href="#/settings" class="nav-link">settings</a>
20 </nav>
21 </div>
22 </header>
23 <main id="view" class="view"></main>
24 <footer class="statusbar">
25 <span id="status">connecting…</span>
26 <span id="me" class="me"></span>
27 </footer>
28 <script src="./app.js" type="module"></script>
29</body>
30</html>
public/styles.css +397 -0
……@@ -0,0 +1,397 @@
1:root {
2 --bg: #f6f6ef; /* HN's signature beige */
3 --bg-elev: #ffffff;
4 --topbar: #ff6600; /* HN orange */
5 --border: #e0e0d2;
6 --text: #0b0f14;
7 --text-dim: #555;
8 --text-faint: #828282;
9 --accent: #ff6600;
10 --link: #000;
11 --link-visited: #5a5a5a;
12 --up: #5a5a5a;
13 --radius: 4px;
14}
15
16@media (prefers-color-scheme: dark) {
17 :root {
18 --bg: #14130e;
19 --bg-elev: #1b1a15;
20 --topbar: #b34700;
21 --border: #2c2a22;
22 --text: #ebe7d7;
23 --text-dim: #aaa69a;
24 --text-faint: #7a7568;
25 --link: #f4efdd;
26 --link-visited: #9a9486;
27 }
28}
29
30* { box-sizing: border-box; }
31
32html, body {
33 margin: 0;
34 padding: 0;
35 background: var(--bg);
36 color: var(--text);
37 font-family: Verdana, Geneva, sans-serif;
38 font-size: 14px;
39 line-height: 1.35;
40 -webkit-tap-highlight-color: transparent;
41 min-height: 100vh;
42}
43body { display: flex; flex-direction: column; min-height: 100vh; }
44
45a { color: var(--link); text-decoration: none; }
46a:hover { text-decoration: underline; }
47a:visited { color: var(--link-visited); }
48
49.topbar {
50 background: var(--topbar);
51 padding-top: env(safe-area-inset-top);
52 position: sticky;
53 top: 0;
54 z-index: 10;
55}
56.topbar-inner {
57 display: flex;
58 align-items: center;
59 gap: 10px;
60 padding: 6px 12px;
61 max-width: 900px;
62 margin: 0 auto;
63}
64.brand {
65 color: #fff;
66 font-weight: 700;
67 font-size: 14px;
68 margin-right: 8px;
69 letter-spacing: -0.01em;
70}
71.brand:hover, .brand:visited { color: #fff; text-decoration: none; }
72.top-nav { display: flex; gap: 10px; flex-wrap: wrap; }
73.nav-link {
74 color: #000;
75 font-size: 13px;
76 padding: 2px 4px;
77}
78.nav-link:visited { color: #000; }
79.nav-link.submit { font-weight: 600; }
80
81.view {
82 flex: 1;
83 max-width: 900px;
84 width: 100%;
85 margin: 0 auto;
86 padding: 10px 12px 80px;
87}
88
89/* ---------- feed ---------- */
90
91.feed { display: table; width: 100%; border-collapse: collapse; }
92.feed-row { display: table-row; }
93.feed-row .rank, .feed-row .vote-cell, .feed-row .main {
94 display: table-cell;
95 vertical-align: top;
96 padding: 4px 2px;
97}
98.feed-row .rank {
99 color: var(--text-faint);
100 font-size: 12px;
101 text-align: right;
102 width: 28px;
103 padding-right: 2px;
104 padding-top: 5px;
105}
106.feed-row .vote-cell {
107 width: 18px;
108 text-align: center;
109 padding-top: 5px;
110}
111.vote-btn {
112 background: transparent;
113 border: 0;
114 cursor: pointer;
115 color: var(--text-faint);
116 padding: 0;
117 font: inherit;
118 line-height: 1;
119}
120.vote-btn::before { content: '▲'; font-size: 11px; }
121.vote-btn.active { color: var(--accent); }
122.vote-btn:hover { color: var(--accent); }
123.vote-btn.down::before { content: '▼'; }
124
125.feed-row .main { padding-bottom: 4px; }
126.title-line {
127 font-size: 14px;
128 line-height: 1.35;
129}
130.title-line .title {
131 color: var(--text);
132 font-weight: 500;
133}
134.title-line .title:visited { color: var(--link-visited); }
135.title-line .host {
136 color: var(--text-faint);
137 font-size: 12px;
138 margin-left: 4px;
139}
140.subline {
141 font-size: 11px;
142 color: var(--text-faint);
143 margin-top: 2px;
144}
145.subline a { color: var(--text-faint); }
146.subline a:hover { text-decoration: underline; }
147
148/* ---------- thread view ---------- */
149
150.thread-post {
151 margin-bottom: 12px;
152}
153.thread-post .title-line { font-size: 15px; }
154.thread-post .body-text {
155 margin-top: 8px;
156 padding: 8px 0 0;
157 white-space: pre-wrap;
158 font-size: 13px;
159 line-height: 1.5;
160 color: var(--text);
161}
162
163.reply-form { margin: 16px 0; }
164.reply-form textarea {
165 width: 100%;
166 min-height: 100px;
167 padding: 6px;
168 font: inherit;
169 font-family: Verdana, Geneva, sans-serif;
170 font-size: 13px;
171 background: var(--bg-elev);
172 color: var(--text);
173 border: 1px solid var(--border);
174 border-radius: var(--radius);
175}
176.reply-form .actions { margin-top: 6px; }
177
178.comment-tree { margin-top: 12px; }
179.comment {
180 margin: 8px 0;
181 font-size: 13px;
182}
183.comment .head {
184 font-size: 11px;
185 color: var(--text-faint);
186 margin-bottom: 4px;
187 display: flex;
188 gap: 4px;
189 align-items: center;
190}
191.comment .head .mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
192.comment .body { white-space: pre-wrap; line-height: 1.5; }
193.comment .actions a {
194 font-size: 11px;
195 color: var(--text-faint);
196 margin-right: 8px;
197}
198.comment .actions { margin-top: 4px; }
199
200/* ---------- submit form ---------- */
201
202.submit-form { max-width: 600px; }
203.submit-form table { width: 100%; border-collapse: collapse; }
204.submit-form td { padding: 4px 0; }
205.submit-form td.label { width: 80px; font-size: 12px; color: var(--text-dim); padding-right: 8px; }
206.submit-form input, .submit-form textarea {
207 width: 100%;
208 padding: 4px 6px;
209 font: inherit;
210 font-family: Verdana, Geneva, sans-serif;
211 font-size: 13px;
212 background: var(--bg-elev);
213 color: var(--text);
214 border: 1px solid var(--border);
215 border-radius: var(--radius);
216}
217.submit-form textarea { min-height: 100px; resize: vertical; }
218.submit-form .actions { margin-top: 10px; padding-left: 88px; }
219
220button.primary {
221 background: var(--topbar);
222 color: #fff;
223 font: inherit;
224 font-size: 13px;
225 padding: 4px 10px;
226 border: 1px solid var(--topbar);
227 border-radius: var(--radius);
228 cursor: pointer;
229}
230button.primary:hover { filter: brightness(1.05); }
231button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
232
233.pow-status {
234 display: inline-flex;
235 align-items: center;
236 gap: 6px;
237 margin-left: 8px;
238 font-size: 11px;
239 color: var(--text-faint);
240}
241.pow-status .dot {
242 width: 6px; height: 6px; border-radius: 50%;
243 background: var(--accent); opacity: 0.4;
244 animation: pulse 900ms infinite ease-in-out;
245}
246@keyframes pulse { 0%,100% { opacity: 0.35 } 50% { opacity: 1 } }
247
248/* ---------- status / misc ---------- */
249
250.statusbar {
251 position: fixed;
252 bottom: 0; left: 0; right: 0;
253 padding: 4px 12px;
254 padding-bottom: max(4px, env(safe-area-inset-bottom));
255 font-size: 11px;
256 color: var(--text-faint);
257 background: var(--bg-elev);
258 border-top: 1px solid var(--border);
259 display: flex;
260 justify-content: space-between;
261 gap: 12px;
262}
263.me { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
264
265.empty { color: var(--text-faint); padding: 20px 0; }
266.err {
267 padding: 8px;
268 background: rgba(239,83,80,0.1);
269 border: 1px solid rgba(239,83,80,0.4);
270 color: #c62828;
271 border-radius: var(--radius);
272 margin: 8px 0;
273 font-size: 12px;
274}
275
276/* ---------- modal + toast ---------- */
277
278.modal {
279 position: fixed;
280 inset: 0;
281 background: rgba(0, 0, 0, 0.45);
282 z-index: 100;
283 display: flex;
284 align-items: flex-start;
285 justify-content: center;
286 padding: 60px 16px 16px;
287 overflow-y: auto;
288}
289.modal-inner {
290 background: var(--bg-elev);
291 border: 1px solid var(--border);
292 border-radius: var(--radius);
293 padding: 16px;
294 max-width: 600px;
295 width: 100%;
296}
297.modal-inner h3 { margin-top: 0; }
298.modal-inner input,
299.modal-inner textarea {
300 width: 100%;
301 padding: 6px;
302 font: inherit;
303 font-family: Verdana, Geneva, sans-serif;
304 font-size: 13px;
305 background: var(--bg);
306 color: var(--text);
307 border: 1px solid var(--border);
308 border-radius: var(--radius);
309 margin-bottom: 8px;
310}
311.modal-inner textarea { min-height: 120px; resize: vertical; }
312.modal-inner .actions { display: flex; gap: 8px; margin-top: 8px; }
313.btn-ghost {
314 background: transparent;
315 border: 1px solid var(--border);
316 color: var(--text-dim);
317 font: inherit;
318 font-size: 13px;
319 padding: 4px 10px;
320 border-radius: var(--radius);
321 cursor: pointer;
322}
323.btn-ghost:hover { color: var(--text); }
324
325.toast {
326 position: fixed;
327 bottom: 40px;
328 left: 50%;
329 transform: translateX(-50%) translateY(8px);
330 background: var(--text);
331 color: var(--bg);
332 padding: 6px 12px;
333 font-size: 12px;
334 border-radius: 999px;
335 opacity: 0;
336 transition: opacity 250ms, transform 250ms;
337 z-index: 200;
338 pointer-events: none;
339}
340.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
341.toast-err { background: #c62828; color: #fff; }
342
343/* ---------- PoW progress pill ---------- */
344
345.pow-pill {
346 display: inline-flex;
347 align-items: center;
348 gap: 8px;
349 font-size: 11px;
350 color: var(--text-dim);
351 margin-left: 8px;
352}
353.pow-pill .pow-bar {
354 display: inline-block;
355 width: 80px;
356 height: 6px;
357 background: var(--border);
358 border-radius: 999px;
359 overflow: hidden;
360 vertical-align: middle;
361}
362.pow-pill .pow-fill {
363 display: block;
364 width: 0%;
365 height: 100%;
366 background: var(--accent);
367 transition: width 120ms linear;
368}
369.pow-pill.done .pow-fill { background: var(--up, #5a5a5a); }
370
371/* ---------- key/value table on /settings ---------- */
372
373table.kv { width: 100%; border-collapse: collapse; font-size: 13px; }
374table.kv td { padding: 4px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
375table.kv td.label { color: var(--text-dim); width: 40%; }
376
377.key-field {
378 word-break: break-all;
379 padding: 6px 8px;
380 background: var(--bg-elev-2, var(--bg));
381 border: 1px solid var(--border);
382 border-radius: var(--radius);
383 display: inline-block;
384 max-width: 100%;
385}
386.key-field.blurred { filter: blur(5px); cursor: pointer; user-select: none; }
387
388/* ---------- mobile ---------- */
389
390@media (max-width: 500px) {
391 .feed-row .rank { width: 22px; font-size: 11px; }
392 .feed-row .vote-cell { width: 16px; }
393 .view { padding: 8px 10px 80px; }
394 .submit-form td.label { display: block; width: 100%; padding-bottom: 0; }
395 .submit-form td { display: block; padding: 2px 0; }
396 .submit-form .actions { padding-left: 0; }
397}
public/transport.js +180 -0
……@@ -0,0 +1,180 @@
1// Transport auto-detection for p2pbuilders.
2//
3// - In a Pear desktop app, global `Pear` + a worker pipe is available.
4// - In a regular browser (dev mode, iOS Safari, etc.), we fall back to the
5// local WebSocket RPC endpoint.
6//
7// Both transports expose the same surface: { call(method, params), onStatus(fn) }.
8// The JSON-RPC framing is identical on both sides.
9
10export function createTransport () {
11 // iOS WKWebView with our native bridge (BareBridge.swift posts messages
12 // under the "bare" handler name and calls window.__bareRx for responses).
13 if (typeof globalThis.webkit !== 'undefined' &&
14 globalThis.webkit?.messageHandlers?.bare) {
15 return createWebKitBridgeTransport()
16 }
17 // In Pear desktop GUI: spawn our Bare worker and talk over its pipe.
18 if (typeof globalThis.Pear !== 'undefined' && globalThis.Pear?.Worker?.run) {
19 const pipe = globalThis.Pear.Worker.run('./src/pear/worker.js')
20 return createPearTransport(pipe)
21 }
22 // Fallback: browser dev mode — localhost WebSocket.
23 return createWebSocketTransport()
24}
25
26// ----- iOS WebKit bridge transport (WKWebView <-> native BareKit.IPC) -----
27//
28// Native side (Swift):
29// - WKUserContentController registers a script-message handler named "bare"
30// - On receiving a message, writes the bytes to BareKit.IPC
31// - On BareKit.IPC data, evaluates `window.__bareRx(<json-string>)` in the webview
32//
33// The wire format is the same line-delimited JSON-RPC the rest of the
34// stack uses, minus the trailing newline (each postMessage = one frame).
35
36function createWebKitBridgeTransport () {
37 let nextId = 1
38 const pending = new Map()
39 let onStatus = () => {}
40 const bridge = globalThis.webkit.messageHandlers.bare
41
42 // Native drops each frame (or concatenated frames) into window.__bareRx.
43 let pendingChunk = ''
44 globalThis.__bareRx = (chunk) => {
45 if (!chunk) return
46 pendingChunk += String(chunk)
47 // Accept either single-JSON-per-call (no newline) or newline-delimited.
48 const parts = pendingChunk.split('\n')
49 pendingChunk = parts.pop() // leave the trailing partial
50 for (const raw of parts) {
51 if (!raw) continue
52 handleFrame(raw)
53 }
54 // If the last part is a complete JSON on its own (no newline), try it.
55 if (pendingChunk) {
56 try {
57 JSON.parse(pendingChunk)
58 handleFrame(pendingChunk)
59 pendingChunk = ''
60 } catch { /* keep buffering */ }
61 }
62 }
63
64 function handleFrame (raw) {
65 let msg
66 try { msg = JSON.parse(raw) } catch { return }
67 if (msg.event) return // informational events (e.g. "ready") — no id
68 const p = pending.get(msg.id)
69 if (!p) return
70 pending.delete(msg.id)
71 if (msg.error) p.reject(new Error(msg.error))
72 else p.resolve(msg.result)
73 }
74
75 queueMicrotask(() => onStatus('connected'))
76
77 function call (method, params) {
78 const id = nextId++
79 bridge.postMessage(JSON.stringify({ id, method, params }))
80 return new Promise((resolve, reject) => pending.set(id, { resolve, reject }))
81 }
82
83 return {
84 kind: 'ios-bare',
85 call,
86 onStatus: (fn) => { onStatus = fn }
87 }
88}
89
90// ----- WebSocket transport (browser dev mode) -----
91
92function createWebSocketTransport () {
93 let ws = null
94 let nextId = 1
95 const pending = new Map()
96 const queue = []
97 let onStatus = () => {}
98
99 function connect () {
100 const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
101 ws = new WebSocket(`${proto}//${location.host}/rpc`)
102 ws.onopen = () => {
103 onStatus('connected')
104 while (queue.length) ws.send(queue.shift())
105 }
106 ws.onmessage = (ev) => handleLine(ev.data)
107 ws.onclose = () => { onStatus('disconnected'); setTimeout(connect, 1000) }
108 ws.onerror = () => onStatus('error')
109 }
110
111 function handleLine (data) {
112 let msg
113 try { msg = JSON.parse(data) } catch { return }
114 const p = pending.get(msg.id)
115 if (!p) return
116 pending.delete(msg.id)
117 if (msg.error) p.reject(new Error(msg.error))
118 else p.resolve(msg.result)
119 }
120
121 function call (method, params) {
122 const id = nextId++
123 const payload = JSON.stringify({ id, method, params })
124 return new Promise((resolve, reject) => {
125 pending.set(id, { resolve, reject })
126 if (ws && ws.readyState === WebSocket.OPEN) ws.send(payload)
127 else queue.push(payload)
128 })
129 }
130
131 connect()
132 return {
133 kind: 'websocket',
134 call,
135 onStatus: (fn) => { onStatus = fn }
136 }
137}
138
139// ----- Pear transport (desktop app, worker pipe) -----
140//
141// Pear.pipe is a duplex stream between the webview and the Bare worker.
142// We use line-delimited JSON frames to match the WebSocket protocol.
143
144function createPearTransport (pipe) {
145 let nextId = 1
146 const pending = new Map()
147 let onStatus = () => {}
148 let buf = ''
149
150 pipe.on('data', (chunk) => {
151 buf += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
152 let i
153 while ((i = buf.indexOf('\n')) >= 0) {
154 const line = buf.slice(0, i); buf = buf.slice(i + 1)
155 if (!line) continue
156 let msg
157 try { msg = JSON.parse(line) } catch { continue }
158 const p = pending.get(msg.id)
159 if (!p) continue
160 pending.delete(msg.id)
161 if (msg.error) p.reject(new Error(msg.error))
162 else p.resolve(msg.result)
163 }
164 })
165 pipe.on('close', () => onStatus('disconnected'))
166 onStatus = (fn) => { onStatus = fn }
167 queueMicrotask(() => onStatus('connected'))
168
169 function call (method, params) {
170 const id = nextId++
171 pipe.write(JSON.stringify({ id, method, params }) + '\n')
172 return new Promise((resolve, reject) => pending.set(id, { resolve, reject }))
173 }
174
175 return {
176 kind: 'pear',
177 call,
178 onStatus: (fn) => { onStatus = fn }
179 }
180}
scripts/probe-relays.mjs +18 -0
……@@ -0,0 +1,18 @@
1import { HiveRelayClient } from 'p2p-hiverelay/client'
2import b4a from 'b4a'
3import hypercoreId from 'hypercore-id-encoding'
4
5const client = new HiveRelayClient('./.hiverelay-probe')
6await client.start()
7await new Promise(r => setTimeout(r, 4000))
8
9const k = hypercoreId.decode('gopfpwat99tcuaakasfnftrds3j6t7srdmi3qidbhm9xeizt1a5y')
10const acceptances = await client.seed(k, { replicas: 8, timeout: 25000 })
11console.log(`\nseeded on ${acceptances.length} relay(s):\n`)
12for (const a of acceptances) {
13 const pk = b4a.toString(a.relayPubkey, 'hex')
14 const gb = (a.availableStorageBytes / 1024 / 1024 / 1024).toFixed(1)
15 console.log(` ${a.region.padEnd(5)} ${pk.slice(0, 16)}… (${gb} GB free)`)
16}
17await client.close?.()
18process.exit(0)
scripts/publish.js +121 -0
……@@ -0,0 +1,121 @@
1'use strict'
2
3// scripts/publish.js — ship the p2pbuilders frontend to the HiveRelay network.
4//
5// Usage:
6// node scripts/publish.js # publish ./public with auto-seeding
7// node scripts/publish.js --dir ./public # explicit dir
8// node scripts/publish.js --encryption-key <hex> # blind mode (relay stores ciphertext only)
9//
10// Prints the drive key on success. Share that key with:
11// - PearBrowser: pear://<key> (or hyper://<key>/ if that's the URL scheme)
12// - HTTP gateway: http://<relay>:9100/v1/hyper/<key>/index.html
13//
14// The script keeps the process alive for ~30s to give DHT announce time to
15// propagate, then exits. The drive remains seeded by the relay network; you
16// can close this process.
17
18const path = require('path')
19const fs = require('fs')
20
21async function main () {
22 const args = parseArgs(process.argv.slice(2))
23
24 const dir = path.resolve(args.dir || 'public')
25 const encryptionKey = args['encryption-key'] || null
26 const storageDir = path.resolve(args.storage || '.hiverelay-publish')
27
28 if (!fs.existsSync(dir)) {
29 console.error(`[publish] directory not found: ${dir}`)
30 process.exit(1)
31 }
32
33 console.log(`[publish] publishing ${dir}`)
34 console.log(`[publish] local storage: ${storageDir}`)
35
36 // ESM import — p2p-hiverelay is type=module.
37 const { HiveRelayClient } = await import('p2p-hiverelay/client')
38
39 const client = new HiveRelayClient(storageDir, {
40 // Auto-discover relays on the public HiveRelay DHT topic.
41 // You can override with { bootstrap: […] } if you're on a testnet.
42 })
43 await client.start()
44
45 console.log(`[publish] client started. discovering relays…`)
46 // Brief wait for relay discovery
47 await new Promise(r => setTimeout(r, 2500))
48 if (client._relays) {
49 console.log(`[publish] connected to ${client._relays.size || 0} relay(s)`)
50 }
51
52 const publishOpts = {
53 appId: 'p2pbuilders-frontend',
54 seed: true,
55 replicas: 3,
56 timeout: 30000
57 }
58 if (encryptionKey) {
59 const b4a = require('b4a')
60 publishOpts.encryptionKey = b4a.from(encryptionKey, 'hex')
61 console.log(`[publish] blind mode: relay will store ciphertext only`)
62 }
63
64 const drive = await client.publish(dir, publishOpts)
65
66 const keyHex = require('b4a').toString(drive.key, 'hex')
67 console.log('')
68 console.log('===========================================================')
69 console.log(` drive key: ${keyHex}`)
70 console.log(` pear link: pear://${keyHex}`)
71 console.log(` hyper url: hyper://${keyHex}/index.html`)
72 console.log('===========================================================')
73 console.log('')
74 console.log('[publish] waiting 30s for seed acceptances to propagate…')
75
76 // Listen for seed events
77 client.on('seeded', ({ key, acceptances }) => {
78 if (key === keyHex) {
79 console.log(`[publish] seeded on ${acceptances} relay(s)`)
80 }
81 })
82
83 // Stay running so the drive is reachable. Relays may take time to accept seed
84 // requests, and even after seeding the drive benefits from our local replica
85 // while the network warms up. Ctrl+C to stop.
86 console.log('[publish] staying online. press Ctrl+C to stop.')
87 process.on('SIGINT', async () => {
88 console.log('\n[publish] shutting down…')
89 await client.close?.().catch(() => {})
90 process.exit(0)
91 })
92 // Heartbeat every 10s: report current peer/relay status
93 setInterval(() => {
94 const peers = client.swarm?.connections?.size ?? 0
95 const relays = client._relays?.size ?? 0
96 console.log(`[publish] peers=${peers} relays=${relays}`)
97 }, 10000).unref()
98}
99
100// Tiny argv parser so we don't need a dep.
101function parseArgs (argv) {
102 const out = {}
103 for (let i = 0; i < argv.length; i++) {
104 const a = argv[i]
105 if (a.startsWith('--')) {
106 const key = a.slice(2)
107 const next = argv[i + 1]
108 if (next !== undefined && !next.startsWith('--')) {
109 out[key] = next; i++
110 } else {
111 out[key] = true
112 }
113 }
114 }
115 return out
116}
117
118main().catch((err) => {
119 console.error('[publish] fatal:', err.stack || err.message)
120 process.exit(1)
121})
scripts/seed-pear.js +86 -0
……@@ -0,0 +1,86 @@
1'use strict'
2
3// scripts/seed-pear.js — ask public HiveRelay relays to pin a Pear drive key.
4//
5// Usage:
6// node scripts/seed-pear.js <pearKey | pear://... URL>
7//
8// Example:
9// node scripts/seed-pear.js pear://fa91f7z8x6qp3fcfbi7zaggnw6w7ncab96qiaswemmoufjjeod6y
10//
11// Stays running until Ctrl+C so the local replica is also available while
12// relays are catching up.
13
14const path = require('path')
15
16async function main () {
17 const input = process.argv[2]
18 if (!input) {
19 console.error('usage: node scripts/seed-pear.js <pearKey | pear://...>')
20 process.exit(1)
21 }
22
23 // Normalize: "pear://<possibly-versioned-prefix>.<key>" → bare z32 key string.
24 // Pear links look like: pear://<key> or pear://<channel>.<key> or pear://0.<length>.<key>
25 let key = input.replace(/^pear:\/\//, '')
26 const lastDot = key.lastIndexOf('.')
27 if (lastDot >= 0) key = key.slice(lastDot + 1)
28
29 console.log(`[seed] target key: ${key}`)
30
31 const storageDir = path.resolve('.hiverelay-seed')
32 const b4a = require('b4a')
33 const { HiveRelayClient } = await import('p2p-hiverelay/client')
34
35 const client = new HiveRelayClient(storageDir)
36 await client.start()
37 console.log(`[seed] client started, discovering relays…`)
38
39 // Small wait for relay discovery to populate
40 await new Promise(r => setTimeout(r, 3000))
41
42 // HiveRelayClient.seed accepts either a hex string or a buffer. Our key is
43 // z-base-32 (Pear's format); convert via hypercore-id-encoding if needed.
44 let keyBuf
45 try {
46 // Try z32 decode first — Pear keys are z-base-32.
47 const { default: hypercoreId } = await import('hypercore-id-encoding')
48 keyBuf = hypercoreId.decode(key)
49 } catch {
50 // Fallback: treat as hex
51 try { keyBuf = b4a.from(key, 'hex') } catch {}
52 }
53 if (!keyBuf || keyBuf.length !== 32) {
54 console.error(`[seed] could not decode key "${key}" to 32 bytes`)
55 process.exit(1)
56 }
57
58 console.log(`[seed] requesting seed on ${keyBuf.length} relays…`)
59
60 try {
61 const acceptances = await client.seed(keyBuf, { replicas: 3, timeout: 30000 })
62 console.log(`[seed] seeded on ${acceptances.length} relay(s)`)
63 for (const a of acceptances) {
64 const id = a.pubkey ? b4a.toString(a.pubkey, 'hex').slice(0, 12) : 'unknown'
65 console.log(` - relay ${id}`)
66 }
67 } catch (err) {
68 console.error(`[seed] seed request failed: ${err.message}`)
69 }
70
71 console.log('[seed] staying online. press Ctrl+C to stop.')
72 process.on('SIGINT', async () => {
73 console.log('\n[seed] shutting down…')
74 await client.close?.().catch(() => {})
75 process.exit(0)
76 })
77 setInterval(() => {
78 const peers = client.swarm?.connections?.size ?? 0
79 console.log(`[seed] peers=${peers}`)
80 }, 30000).unref()
81}
82
83main().catch((err) => {
84 console.error('[seed] fatal:', err.stack || err.message)
85 process.exit(1)
86})
src/backend/_node-env.js +4 -0
……@@ -0,0 +1,4 @@
1'use strict'
2// Node-only shim to match the shape that `bare-env` returns under Bare.
3// Referenced by the `#env` subpath import defined in package.json.
4module.exports = process.env
src/backend/_node-events.js +2 -0
……@@ -0,0 +1,2 @@
1'use strict'
2module.exports = require('events')
src/backend/_node-fs.js +4 -0
……@@ -0,0 +1,4 @@
1'use strict'
2// Node-only shim so package.json subpath imports can target a real file path
3// instead of a bare specifier (which Node's CJS loader rejects in imports).
4module.exports = require('fs')
src/backend/_node-os.js +2 -0
……@@ -0,0 +1,2 @@
1'use strict'
2module.exports = require('os')
src/backend/_node-path.js +2 -0
……@@ -0,0 +1,2 @@
1'use strict'
2module.exports = require('path')
src/backend/_node-process.js +3 -0
……@@ -0,0 +1,3 @@
1'use strict'
2// Node-only shim exposing `process` for the #process subpath import.
3module.exports = process
src/backend/_rt.js +18 -0
……@@ -0,0 +1,18 @@
1'use strict'
2
3// Runtime shim: swap fs/path/os/env between Bare and Node via the package.json
4// subpath imports map. bare-pack can statically resolve a single branch per
5// target this way, unlike a typeof Bare ternary.
6//
7// If you add a new Node built-in here, also add it to "imports" in package.json.
8
9const inBare = typeof Bare !== 'undefined'
10
11module.exports = {
12 inBare,
13 fs: require('#fs'),
14 path: require('#path'),
15 os: require('#os'),
16 env: require('#env'),
17 exit: inBare ? (code = 0) => Bare.exit(code) : (code = 0) => process.exit(code)
18}
src/backend/announce.js +41 -0
……@@ -0,0 +1,41 @@
1'use strict'
2
3const cenc = require('compact-encoding')
4const b4a = require('b4a')
5
6const PROTOCOL = 'p2pbuilders/announce/v1'
7const PUBKEY_BYTES = 32
8const MAX_KEYS_PER_ANNOUNCE = 256
9
10const pubkeyArray = cenc.array(cenc.fixed(PUBKEY_BYTES))
11
12// Wire up the announce channel on an existing protomux muxer.
13// Options:
14// selfPubkey — our own user pubkey (so we don't echo it back)
15// getKnownKeys() — returns Buffer[] of pubkeys we'd like to gossip
16// onKey(pubkey) — callback for each new pubkey the peer announces
17function setupAnnounce (muxer, { selfPubkey, getKnownKeys, onKey }) {
18 let keysMsg
19 const channel = muxer.createChannel({
20 protocol: PROTOCOL,
21 onopen () {
22 const keys = getKnownKeys().slice(0, MAX_KEYS_PER_ANNOUNCE)
23 keysMsg.send(keys)
24 }
25 })
26 if (!channel) return null // peer doesn't support the protocol
27 keysMsg = channel.addMessage({
28 encoding: pubkeyArray,
29 onmessage (keys) {
30 for (const k of keys) {
31 if (k.length !== PUBKEY_BYTES) continue
32 if (b4a.equals(k, selfPubkey)) continue
33 try { onKey(k) } catch { /* ignore per-key errors */ }
34 }
35 }
36 })
37 channel.open()
38 return channel
39}
40
41module.exports = { setupAnnounce, PROTOCOL, MAX_KEYS_PER_ANNOUNCE }
src/backend/board.js +38 -0
……@@ -0,0 +1,38 @@
1'use strict'
2
3const sodium = require('sodium-universal')
4const b4a = require('b4a')
5
6const BOARD_NAME_RE = /^[a-z0-9_-]{2,32}$/
7const TOPIC_BYTES = 32
8const BOARD_TOPIC_PREFIX = 'p2pbuilders:board:v1:'
9const ROSTER_TOPIC_STR = 'p2pbuilders:roster:v1'
10
11function validateBoardName (name) {
12 if (typeof name !== 'string') throw new Error('board name must be a string')
13 if (!BOARD_NAME_RE.test(name)) {
14 throw new Error('board name must match /^[a-z0-9_-]{2,32}$/')
15 }
16}
17
18function hashTopic (str) {
19 const out = b4a.alloc(TOPIC_BYTES)
20 sodium.crypto_generichash(out, b4a.from(str))
21 return out
22}
23
24function boardTopic (name) {
25 validateBoardName(name)
26 return hashTopic(BOARD_TOPIC_PREFIX + name)
27}
28
29const rosterTopic = hashTopic(ROSTER_TOPIC_STR)
30
31module.exports = {
32 BOARD_NAME_RE,
33 TOPIC_BYTES,
34 validateBoardName,
35 boardTopic,
36 rosterTopic,
37 hashTopic
38}
src/backend/identity.js +54 -0
……@@ -0,0 +1,54 @@
1'use strict'
2
3const { fs, path } = require('./_rt')
4const sodium = require('sodium-universal')
5const b4a = require('b4a')
6
7const PRIMARY_KEY_BYTES = 32
8const USER_CORE_NAME = 'p2pbuilders/user'
9
10function generatePrimaryKey () {
11 const key = b4a.alloc(PRIMARY_KEY_BYTES)
12 sodium.randombytes_buf(key)
13 return key
14}
15
16function loadOrCreatePrimaryKey (dir) {
17 // Store the identity key in a sibling directory — NOT the corestore dir —
18 // so corestore's own filesystem layout can't interfere.
19 const keyDir = path.join(dir, '..', 'p2pbuilders-identity')
20 const file = path.join(keyDir, 'key.bin')
21 const legacy = path.join(dir, '.p2pbuilders.key')
22 const legacyOlder = path.join(dir, 'primary.key')
23 try {
24 if (fs.existsSync(file)) {
25 const buf = fs.readFileSync(file)
26 if (buf.length === PRIMARY_KEY_BYTES) return buf
27 }
28 } catch {}
29 // Migrate from earlier filenames if present.
30 for (const leg of [legacy, legacyOlder]) {
31 try {
32 if (fs.existsSync(leg)) {
33 const buf = fs.readFileSync(leg)
34 if (buf.length === PRIMARY_KEY_BYTES) {
35 fs.mkdirSync(keyDir, { recursive: true })
36 fs.writeFileSync(file, buf, { mode: 0o600 })
37 try { fs.unlinkSync(leg) } catch {}
38 return buf
39 }
40 }
41 } catch {}
42 }
43 fs.mkdirSync(keyDir, { recursive: true })
44 const key = generatePrimaryKey()
45 fs.writeFileSync(file, key, { mode: 0o600 })
46 return key
47}
48
49module.exports = {
50 PRIMARY_KEY_BYTES,
51 USER_CORE_NAME,
52 generatePrimaryKey,
53 loadOrCreatePrimaryKey
54}
src/backend/indexer.js +498 -0
……@@ -0,0 +1,498 @@
1'use strict'
2
3const b4a = require('b4a')
4const Hyperbee = require('hyperbee')
5const cenc = require('compact-encoding')
6const { TYPE, TYPE_NAME, REQUIRES_POW, decodeOp, makeOpId, parseOpId, OP_ID_BYTES } = require('./ops')
7const pow = require('./pow')
8const reputation = require('./reputation')
9
10const INDEX_CORE_NAME = 'p2pbuilders/index'
11
12// Default indexer policy (SPEC §6.1)
13const DEFAULT_POLICY = {
14 posts: { perHour: 10 },
15 comments: { perHour: 60 },
16 votes: { perHour: 600 },
17 minPowBits: { ...pow.DEFAULT_BITS }, // floor per op type
18 enforcePow: true,
19 enforceRateLimit: true
20}
21
22const MS_PER_HOUR = 3600 * 1000
23
24const TS_PAD = 15 // enough for ms timestamps through year 33658
25const tsKey = (ts) => String(ts).padStart(TS_PAD, '0')
26const hex = (buf) => b4a.toString(buf, 'hex')
27const fromHex = (s) => b4a.from(s, 'hex')
28
29const voteEnc = cenc.int8
30const opMetaEnc = {
31 preencode (state, m) {
32 cenc.uint8.preencode(state, m.type)
33 cenc.fixed(32).preencode(state, m.author)
34 cenc.uint64.preencode(state, m.ts)
35 cenc.string.preencode(state, m.board || '')
36 cenc.fixed(40).preencode(state, m.parent || b4a.alloc(40))
37 cenc.fixed(40).preencode(state, m.target || b4a.alloc(40))
38 },
39 encode (state, m) {
40 cenc.uint8.encode(state, m.type)
41 cenc.fixed(32).encode(state, m.author)
42 cenc.uint64.encode(state, m.ts)
43 cenc.string.encode(state, m.board || '')
44 cenc.fixed(40).encode(state, m.parent || b4a.alloc(40))
45 cenc.fixed(40).encode(state, m.target || b4a.alloc(40))
46 },
47 decode (state) {
48 return {
49 type: cenc.uint8.decode(state),
50 author: cenc.fixed(32).decode(state),
51 ts: cenc.uint64.decode(state),
52 board: cenc.string.decode(state),
53 parent: cenc.fixed(40).decode(state),
54 target: cenc.fixed(40).decode(state)
55 }
56 }
57}
58
59// Index entry helpers
60function postNewKey (boardName, ts, opId) { return `post/${boardName}/new/${tsKey(ts)}/${hex(opId)}` }
61function commentKey (parentOpId, ts, opId) { return `comment/${hex(parentOpId)}/${tsKey(ts)}/${hex(opId)}` }
62function voteKey (targetOpId, voter) { return `vote/${hex(targetOpId)}/${hex(voter)}` }
63function userPostKey (author, ts, opId) { return `user/${hex(author)}/posts/${tsKey(ts)}/${hex(opId)}` }
64function boardKey (name) { return `board/${name}` }
65function seenKey (pubkey) { return `_seen/${hex(pubkey)}` }
66function opMetaKey (opId) { return `_opmeta/${hex(opId)}` }
67function overlayKey (opId) { return `overlay/${hex(opId)}` }
68function tombKey (opId) { return `tomb/${hex(opId)}` }
69function profileKey (pubkey) { return `profile/${hex(pubkey)}` }
70
71const overlayEnc = {
72 preencode (state, o) {
73 cenc.string.preencode(state, o.body || '')
74 cenc.string.preencode(state, o.title || '')
75 cenc.uint64.preencode(state, o.ts)
76 },
77 encode (state, o) {
78 cenc.string.encode(state, o.body || '')
79 cenc.string.encode(state, o.title || '')
80 cenc.uint64.encode(state, o.ts)
81 },
82 decode (state) {
83 return {
84 body: cenc.string.decode(state),
85 title: cenc.string.decode(state),
86 ts: Number(cenc.uint64.decode(state))
87 }
88 }
89}
90
91const profileEnc = {
92 preencode (state, p) {
93 cenc.string.preencode(state, p.nick || '')
94 cenc.string.preencode(state, p.bio || '')
95 cenc.string.preencode(state, p.avatar || '')
96 cenc.uint64.preencode(state, p.ts)
97 },
98 encode (state, p) {
99 cenc.string.encode(state, p.nick || '')
100 cenc.string.encode(state, p.bio || '')
101 cenc.string.encode(state, p.avatar || '')
102 cenc.uint64.encode(state, p.ts)
103 },
104 decode (state) {
105 return {
106 nick: cenc.string.decode(state),
107 bio: cenc.string.decode(state),
108 avatar: cenc.string.decode(state),
109 ts: Number(cenc.uint64.decode(state))
110 }
111 }
112}
113
114class Indexer {
115 constructor (node, { name = INDEX_CORE_NAME, policy, admin = null } = {}) {
116 this.node = node
117 this.name = name
118 this.bee = null
119 this.policy = { ...DEFAULT_POLICY, ...(policy || {}) }
120 // admin: optional 32-byte pubkey buffer. If set, this identity can
121 // tombstone anyone's posts/comments (not just their own).
122 this.admin = admin ? (b4a.isBuffer(admin) ? admin : b4a.from(admin, 'hex')) : null
123 this._cores = new Map() // hex(pubkey) -> { core, unsubscribe }
124 this._pending = Promise.resolve()
125 this._closing = false
126 this._blocked = new Set() // local blocklist: hex(pubkey)
127 // Per-author cached stats for rate limits and reputation.
128 this._stats = new Map() // hex(pubkey) -> { firstTs, posts:[], comments:[], votes:[], received: number }
129 }
130
131 block (pubkey) { this._blocked.add(hex(pubkey)) }
132 unblock (pubkey) { this._blocked.delete(hex(pubkey)) }
133 isBlocked (pubkey) { return this._blocked.has(hex(pubkey)) }
134
135 async ready () {
136 const core = this.node.store.get({ name: this.name })
137 await core.ready()
138 this.bee = new Hyperbee(core, { keyEncoding: 'utf-8', valueEncoding: 'binary' })
139 await this.bee.ready()
140 // Start indexing own core + any already-tracked cores.
141 await this.addCore(this.node.core)
142 for (const remote of this.node._tracked.values()) await this.addCore(remote)
143 // Keep up with newly-tracked cores.
144 this.node.on('track', async (pubkey) => {
145 const remote = this.node.userCore(pubkey)
146 if (remote) await this.addCore(remote)
147 })
148 }
149
150 async close () {
151 this._closing = true
152 for (const { unsubscribe } of this._cores.values()) unsubscribe()
153 await this._pending
154 if (this.bee) await this.bee.close()
155 }
156
157 async addCore (core) {
158 const key = hex(core.key)
159 if (this._cores.has(key)) return
160 const onChange = () => { this._queueIndex(core) }
161 core.on('append', onChange)
162 core.on('truncate', onChange)
163 core.on('download', onChange)
164 this._cores.set(key, {
165 core,
166 unsubscribe: () => {
167 core.off('append', onChange)
168 core.off('truncate', onChange)
169 core.off('download', onChange)
170 }
171 })
172 await this._queueIndex(core)
173 }
174
175 _queueIndex (core) {
176 this._pending = this._pending.then(() => this._indexCore(core)).catch(() => {})
177 return this._pending
178 }
179
180 async _indexCore (core) {
181 if (this._closing) return
182 const seenBuf = await this.bee.get(seenKey(core.key))
183 const start = seenBuf ? readUint32(seenBuf.value) : 0
184 const end = core.length
185 if (start >= end) return
186
187 // Make sure every block from start..end is locally available before indexing.
188 // For our own core these are always local. For remote cores, download first.
189 if (core.writable === false || core.peers?.length) {
190 try { await core.download({ start, end }).done() } catch { /* proceed best-effort */ }
191 }
192
193 const batch = this.bee.batch()
194 let indexed = start
195 for (let seq = start; seq < end; seq++) {
196 let raw
197 try { raw = await core.get(seq) } catch { break }
198 if (!raw) break
199 let op
200 try { op = decodeOp(raw) } catch { indexed = seq + 1; continue }
201 const opId = makeOpId(core.key, seq)
202 if (this._gate(op, core.key)) {
203 await this._applyOp(batch, op, opId, core.key)
204 }
205 indexed = seq + 1
206 }
207 if (indexed > start) await batch.put(seenKey(core.key), writeUint32(indexed))
208 await batch.flush()
209 }
210
211 // Returns true if op should be indexed, false if it should be dropped.
212 // Mutates _stats as a side-effect to drive rate-limit accounting.
213 _gate (op, author) {
214 // 1. Local blocklist
215 if (this.isBlocked(author)) return false
216
217 // 2. PoW gate
218 if (this.policy.enforcePow && REQUIRES_POW.has(op.type)) {
219 const minBits = this.policy.minPowBits[TYPE_NAME[op.type]] || 0
220 if (!op.pow || op.pow.bits < minBits) return false
221 if (!pow.verify(op, minBits)) return false
222 }
223
224 // 3. Rate limits — tracked via _stats; use the op's ts as the event time
225 // (SPEC allows ts to be author-claimed; we still enforce per-hour window.)
226 const stats = this._getStats(author)
227 const nowTs = Number(op.ts)
228 if (stats.firstTs === 0 || nowTs < stats.firstTs) stats.firstTs = nowTs
229
230 const buckets = {
231 [TYPE.POST]: { arr: stats.posts, limit: this.policy.posts.perHour },
232 [TYPE.COMMENT]: { arr: stats.comments, limit: this.policy.comments.perHour },
233 [TYPE.VOTE]: { arr: stats.votes, limit: this.policy.votes.perHour }
234 }
235 const bucket = buckets[op.type]
236 if (bucket && this.policy.enforceRateLimit) {
237 // Evict timestamps older than 1h from nowTs
238 const cutoff = nowTs - MS_PER_HOUR
239 while (bucket.arr.length && bucket.arr[0] < cutoff) bucket.arr.shift()
240 if (bucket.arr.length >= bucket.limit) return false
241 bucket.arr.push(nowTs)
242 }
243
244 return true
245 }
246
247 _getStats (author) {
248 const key = hex(author)
249 let s = this._stats.get(key)
250 if (!s) {
251 s = { firstTs: 0, posts: [], comments: [], votes: [], received: 0 }
252 this._stats.set(key, s)
253 }
254 return s
255 }
256
257 // Reputation + weighted vote helpers
258 repOf (pubkey, { now = Date.now() } = {}) {
259 const s = this._getStats(pubkey)
260 const ageDays = s.firstTs > 0 ? (now - s.firstTs) / 86400000 : 0
261 return reputation.computeRep(ageDays, s.received)
262 }
263
264 weightOf (pubkey, { now = Date.now() } = {}) {
265 const s = this._getStats(pubkey)
266 const ageDays = s.firstTs > 0 ? (now - s.firstTs) / 86400000 : 0
267 return reputation.computeWeight(ageDays, s.received)
268 }
269
270 // Returns { up, down, score, weightedScore } — weighted uses reputation.
271 async getWeightedVoteTotals (targetOpId, { now = Date.now() } = {}) {
272 let up = 0; let down = 0
273 let weighted = 0
274 const prefix = `vote/${hex(targetOpId)}/`
275 const range = { gte: prefix, lt: prefix + '\uFFFF' }
276 for await (const { key, value } of this.bee.createReadStream(range)) {
277 const voterHex = key.slice(prefix.length)
278 const voter = fromHex(voterHex)
279 if (this.isBlocked(voter)) continue
280 const dir = cenc.decode(voteEnc, value)
281 if (dir > 0) up++
282 else if (dir < 0) down++
283 const w = this.weightOf(voter, { now })
284 weighted += dir * w
285 }
286 return { up, down, score: up - down, weightedScore: weighted }
287 }
288
289 async _applyOp (batch, op, opId, author) {
290 const value = opId
291 switch (op.type) {
292 case TYPE.POST: {
293 const { board } = op.payload
294 await batch.put(postNewKey(board, op.ts, opId), value)
295 await batch.put(userPostKey(author, op.ts, opId), value)
296 await batch.put(opMetaKey(opId), encodeMeta({ type: op.type, author, ts: op.ts, board }))
297 break
298 }
299 case TYPE.COMMENT: {
300 const { parent } = op.payload
301 await batch.put(commentKey(parent, op.ts, opId), value)
302 await batch.put(opMetaKey(opId), encodeMeta({ type: op.type, author, ts: op.ts, parent }))
303 break
304 }
305 case TYPE.VOTE: {
306 const { target, dir } = op.payload
307 // replace previous vote by same voter on same target
308 const prev = await this.bee.get(voteKey(target, author))
309 const prevDir = prev ? cenc.decode(voteEnc, prev.value) : 0
310 await batch.put(voteKey(target, author), cenc.encode(voteEnc, dir))
311 // update target-author's received-upvote stats
312 try {
313 const { pubkey: targetAuthor } = parseOpId(target)
314 const s = this._getStats(targetAuthor)
315 s.received += (dir > 0 ? 1 : 0) - (prevDir > 0 ? 1 : 0)
316 if (s.received < 0) s.received = 0
317 } catch { /* target not a valid opId */ }
318 break
319 }
320 case TYPE.EDIT: {
321 const { target, body, title } = op.payload
322 // Authorization: only the original author may edit.
323 const { pubkey: targetAuthor } = parseOpId(target)
324 if (!b4a.equals(targetAuthor, author)) break
325 // Keep latest edit only (ts-compared, newer wins).
326 const existing = await this.bee.get(overlayKey(target))
327 const prevTs = existing ? cenc.decode(overlayEnc, existing.value).ts : 0
328 if (Number(op.ts) >= prevTs) {
329 await batch.put(overlayKey(target),
330 cenc.encode(overlayEnc, { body, title: title || '', ts: Number(op.ts) }))
331 }
332 break
333 }
334 case TYPE.TOMBSTONE: {
335 const { target } = op.payload
336 const { pubkey: targetAuthor } = parseOpId(target)
337 const isAuthor = b4a.equals(targetAuthor, author)
338 const isAdmin = this.admin && b4a.equals(this.admin, author)
339 if (!isAuthor && !isAdmin) break
340 await batch.put(tombKey(target), b4a.from([1]))
341 break
342 }
343 case TYPE.PROFILE: {
344 const existing = await this.bee.get(profileKey(author))
345 const prevTs = existing ? cenc.decode(profileEnc, existing.value).ts : 0
346 if (Number(op.ts) >= prevTs) {
347 await batch.put(profileKey(author),
348 cenc.encode(profileEnc, {
349 nick: op.payload.nick,
350 bio: op.payload.bio || '',
351 avatar: op.payload.avatar || '',
352 ts: Number(op.ts)
353 }))
354 }
355 break
356 }
357 case TYPE.BOARD_CREATE: {
358 const { name } = op.payload
359 const exists = await this.bee.get(boardKey(name))
360 if (!exists) await batch.put(boardKey(name), cenc.encode({
361 preencode (s, o) {
362 cenc.uint8.preencode(s, o.minPowBits)
363 cenc.string.preencode(s, o.description)
364 cenc.fixed(32).preencode(s, o.creator)
365 cenc.uint64.preencode(s, o.ts)
366 },
367 encode (s, o) {
368 cenc.uint8.encode(s, o.minPowBits)
369 cenc.string.encode(s, o.description)
370 cenc.fixed(32).encode(s, o.creator)
371 cenc.uint64.encode(s, o.ts)
372 }
373 }, { minPowBits: op.payload.minPowBits, description: op.payload.description, creator: author, ts: op.ts }))
374 break
375 }
376 default:
377 break // follow/block/profile/blocklist_publish: not indexed at this layer in v0.1
378 }
379 }
380
381 // -------- queries --------
382
383 // Yields { opId, author, seq, ts } newest-first (reverse=true default) or oldest-first.
384 async * listPosts (board, { sort = 'new', limit = 50, reverse = true, includeTombstoned = false } = {}) {
385 const prefix = `post/${board}/new/`
386 const range = { gte: prefix, lt: prefix + '\uFFFF', reverse, limit }
387 for await (const { key, value } of this.bee.createReadStream(range)) {
388 const opId = value
389 const { pubkey, seq } = parseOpId(opId)
390 if (this.isBlocked(pubkey)) continue
391 if (!includeTombstoned && await this.isTombstoned(opId)) continue
392 yield {
393 opId,
394 author: pubkey,
395 seq,
396 ts: Number(key.slice(prefix.length, prefix.length + TS_PAD))
397 }
398 }
399 }
400
401 async isTombstoned (opId) {
402 return (await this.bee.get(tombKey(opId))) != null
403 }
404
405 async getOverlay (opId) {
406 const node = await this.bee.get(overlayKey(opId))
407 return node ? cenc.decode(overlayEnc, node.value) : null
408 }
409
410 async getProfile (pubkey) {
411 const node = await this.bee.get(profileKey(pubkey))
412 return node ? cenc.decode(profileEnc, node.value) : null
413 }
414
415 // Ranks posts by weighted vote sum; returns { opId, author, seq, ts, score, weightedScore, up, down, hot }.
416 async scoreBoard (board, { limit = 50, sort = 'hot', weighted = true } = {}) {
417 const posts = []
418 for await (const entry of this.listPosts(board, { limit: 10000, reverse: false })) {
419 if (this.isBlocked(entry.author)) continue
420 posts.push(entry)
421 }
422 const scored = []
423 const now = Date.now()
424 for (const p of posts) {
425 const t = await this.getWeightedVoteTotals(p.opId, { now })
426 const hoursOld = Math.max(0, (now - p.ts) / 3600000)
427 // HN-style hot formula (SPEC §9.1). weightedScore may be negative.
428 const sPart = Math.max(0, t.weightedScore) + 1
429 const hot = Math.pow(sPart, 0.8) / Math.pow(hoursOld + 2, 1.8)
430 scored.push({ ...p, ...t, hot })
431 }
432 scored.sort((a, b) => {
433 if (sort === 'hot') return b.hot - a.hot || b.ts - a.ts
434 if (weighted) return b.weightedScore - a.weightedScore || b.ts - a.ts
435 return b.score - a.score || b.ts - a.ts
436 })
437 return scored.slice(0, limit)
438 }
439
440 async * listComments (parentOpId, { limit = 500, reverse = false, includeTombstoned = false } = {}) {
441 const prefix = `comment/${hex(parentOpId)}/`
442 const range = { gte: prefix, lt: prefix + '\uFFFF', reverse, limit }
443 for await (const { key, value } of this.bee.createReadStream(range)) {
444 const opId = value
445 const { pubkey, seq } = parseOpId(opId)
446 if (this.isBlocked(pubkey)) continue
447 if (!includeTombstoned && await this.isTombstoned(opId)) continue
448 yield {
449 opId,
450 author: pubkey,
451 seq,
452 parent: parentOpId,
453 ts: Number(key.slice(prefix.length, prefix.length + TS_PAD))
454 }
455 }
456 }
457
458 async getVoteTotals (targetOpId) {
459 let up = 0
460 let down = 0
461 const prefix = `vote/${hex(targetOpId)}/`
462 const range = { gte: prefix, lt: prefix + '\uFFFF' }
463 for await (const { value } of this.bee.createReadStream(range)) {
464 const dir = cenc.decode(voteEnc, value)
465 if (dir > 0) up++
466 else if (dir < 0) down++
467 }
468 return { up, down, score: up - down }
469 }
470
471 async getBoard (name) {
472 const node = await this.bee.get(boardKey(name))
473 if (!node) return null
474 const dec = cenc.decode({
475 decode (s) {
476 return {
477 minPowBits: cenc.uint8.decode(s),
478 description: cenc.string.decode(s),
479 creator: cenc.fixed(32).decode(s),
480 ts: Number(cenc.uint64.decode(s))
481 }
482 }
483 }, node.value)
484 return dec
485 }
486
487 async * listBoards () {
488 for await (const { key } of this.bee.createReadStream({ gte: 'board/', lt: 'board/\uFFFF' })) {
489 yield key.slice('board/'.length)
490 }
491 }
492}
493
494function encodeMeta (m) { return cenc.encode(opMetaEnc, m) }
495function readUint32 (buf) { return buf.readUInt32BE ? buf.readUInt32BE(0) : new DataView(buf.buffer, buf.byteOffset, 4).getUint32(0, false) }
496function writeUint32 (n) { const b = b4a.alloc(4); new DataView(b.buffer, b.byteOffset, 4).setUint32(0, n, false); return b }
497
498module.exports = { Indexer, INDEX_CORE_NAME }
src/backend/node.js +194 -0
……@@ -0,0 +1,194 @@
1'use strict'
2
3const b4a = require('b4a')
4const { EventEmitter } = require('#events')
5const { openDiskNode, openTempNode } = require('./store')
6const { SwarmHub } = require('./swarm')
7const { setupAnnounce } = require('./announce')
8const board = require('./board')
9const ops = require('./ops')
10const pow = require('./pow')
11
12const { TYPE, REQUIRES_POW, SCHEMA_VERSION, makeOpId, encodeOp, decodeOp } = ops
13
14const DEFAULT_POW_BITS_BY_TYPE = {
15 [TYPE.POST]: pow.DEFAULT_BITS.post,
16 [TYPE.COMMENT]: pow.DEFAULT_BITS.comment,
17 [TYPE.BOARD_CREATE]: pow.DEFAULT_BITS.board_create
18}
19
20class Node extends EventEmitter {
21 constructor ({ store, core, primaryKey, cleanup }) {
22 super()
23 this.store = store
24 this.core = core
25 this.primaryKey = primaryKey
26 this._cleanup = cleanup
27 this.swarm = null
28 this._tracked = new Map() // hex(pubkey) -> remote Hypercore
29 }
30
31 static async openDisk (dir, opts) {
32 const node = new Node(await openDiskNode(dir))
33 if (opts?.swarm) await node.attachSwarm(opts.swarm)
34 return node
35 }
36
37 static async openTemp (opts = {}) {
38 const node = new Node(await openTempNode(opts))
39 if (opts.swarm) await node.attachSwarm(opts.swarm)
40 return node
41 }
42
43 get pubkey () { return this.core.key }
44 get length () { return this.core.length }
45
46 async attachSwarm ({ bootstrap, keyPair, autoTrack = true, relays = [] } = {}) {
47 if (this.swarm) throw new Error('swarm already attached')
48 const onConnection = ({ muxer }) => {
49 setupAnnounce(muxer, {
50 selfPubkey: this.pubkey,
51 getKnownKeys: () => this._knownKeys(),
52 onKey: (pubkey) => {
53 if (autoTrack) this.trackUser(pubkey).catch(err => this.emit('error', err))
54 this.emit('announce', pubkey)
55 }
56 })
57 }
58 this.swarm = new SwarmHub(this.store, { bootstrap, keyPair, onConnection })
59 for (const pk of relays) this.swarm.joinPeer(pk)
60 return this.swarm
61 }
62
63 _knownKeys () {
64 const keys = [b4a.from(this.pubkey)]
65 for (const core of this._tracked.values()) keys.push(b4a.from(core.key))
66 return keys
67 }
68
69 async joinBoard (name) {
70 if (!this.swarm) throw new Error('no swarm attached')
71 return this.swarm.joinBoard(board.boardTopic(name))
72 }
73
74 async leaveBoard (name) {
75 if (!this.swarm) throw new Error('no swarm attached')
76 return this.swarm.leaveBoard(board.boardTopic(name))
77 }
78
79 // Runtime-level "start replicating this pubkey's core into my store."
80 // Distinct from the `follow` op which is the on-chain intent.
81 async trackUser (pubkey) {
82 const key = b4a.toString(pubkey, 'hex')
83 const existing = this._tracked.get(key)
84 if (existing) return existing
85 const remote = this.store.get({ key: pubkey })
86 await remote.ready()
87 this._tracked.set(key, remote)
88 // Start an open-ended download — this marks the replicator as "downloading",
89 // which triggers Corestore's ondownloading → attach to all existing muxers,
90 // and continues to pull future data.
91 remote.download({ start: 0, end: -1 })
92 // findingPeers keeps update()/get() from giving up before a peer attaches.
93 // We leave the token hanging open (session lifetime) — Hypercore allows it;
94 // it costs a small counter. close() takes care of teardown.
95 const done = remote.findingPeers()
96 remote.on('close', done)
97 this.emit('track', pubkey)
98 return remote
99 }
100
101 userCore (pubkey) {
102 if (b4a.equals(pubkey, this.pubkey)) return this.core
103 return this._tracked.get(b4a.toString(pubkey, 'hex')) || null
104 }
105
106 async close () {
107 if (this.swarm) await this.swarm.destroy()
108 for (const core of this._tracked.values()) await core.close()
109 await this.core.close()
110 await this.store.close()
111 if (this._cleanup) this._cleanup()
112 }
113
114 // --- low-level: append a raw op object, minting PoW if required ---
115 async append (type, payload, { bits, ts } = {}) {
116 const op = {
117 v: SCHEMA_VERSION,
118 type,
119 ts: ts ?? Date.now(),
120 payload,
121 pow: null
122 }
123 if (REQUIRES_POW.has(type)) {
124 const powBits = bits ?? DEFAULT_POW_BITS_BY_TYPE[type]
125 op.pow = pow.mint(op, powBits)
126 }
127 const seq = this.core.length
128 const buf = encodeOp(op)
129 await this.core.append(buf)
130 return { op, opId: makeOpId(this.pubkey, seq), seq }
131 }
132
133 // --- high-level ops ---
134 async post (board, title, body, { link, bits, ts } = {}) {
135 return this.append(TYPE.POST, { board, title, body, link: link || null }, { bits, ts })
136 }
137
138 async comment (parent, body, { bits, ts } = {}) {
139 return this.append(TYPE.COMMENT, { parent, body }, { bits, ts })
140 }
141
142 async vote (target, dir, { ts } = {}) {
143 if (dir !== 1 && dir !== -1 && dir !== 0) throw new Error('dir must be -1, 0, or +1')
144 return this.append(TYPE.VOTE, { target, dir }, { ts })
145 }
146
147 async edit (target, body, { title, ts } = {}) {
148 return this.append(TYPE.EDIT, { target, body, title: title || null }, { ts })
149 }
150
151 async tombstone (target, { ts } = {}) {
152 return this.append(TYPE.TOMBSTONE, { target }, { ts })
153 }
154
155 async follow (target, { ts } = {}) { return this.append(TYPE.FOLLOW, { target }, { ts }) }
156 async block (target, { public: pub = false, ts } = {}) {
157 return this.append(TYPE.BLOCK, { target, public: pub }, { ts })
158 }
159 async setProfile ({ nick, bio, avatar, ts } = {}) {
160 return this.append(TYPE.PROFILE, { nick, bio: bio || null, avatar: avatar || null }, { ts })
161 }
162 async createBoard (name, description, { minPowBits = pow.DEFAULT_BITS.post, ts } = {}) {
163 return this.append(TYPE.BOARD_CREATE, { name, description, minPowBits }, { ts })
164 }
165 async publishBlocklist (list, version, { ts } = {}) {
166 return this.append(TYPE.BLOCKLIST_PUBLISH, { list, version }, { ts })
167 }
168
169 // --- reading ---
170 async getOp (seq) {
171 const buf = await this.core.get(seq)
172 return decodeOp(buf)
173 }
174
175 async * ops ({ start = 0, end } = {}) {
176 const last = end ?? this.core.length
177 for (let i = start; i < last; i++) {
178 yield { seq: i, opId: makeOpId(this.pubkey, i), op: await this.getOp(i) }
179 }
180 }
181
182 // Read ops from a tracked remote user's core. Caller should await updates first.
183 async * userOps (pubkey, { start = 0, end } = {}) {
184 const core = this.userCore(pubkey)
185 if (!core) throw new Error('user not tracked; call trackUser() first')
186 const last = end ?? core.length
187 for (let i = start; i < last; i++) {
188 const buf = await core.get(i)
189 yield { seq: i, opId: makeOpId(pubkey, i), op: decodeOp(buf) }
190 }
191 }
192}
193
194module.exports = { Node, DEFAULT_POW_BITS_BY_TYPE }
src/backend/ops.js +270 -0
……@@ -0,0 +1,270 @@
1'use strict'
2
3const cenc = require('compact-encoding')
4const b4a = require('b4a')
5
6const SCHEMA_VERSION = 1
7
8const TYPE = {
9 POST: 1,
10 COMMENT: 2,
11 VOTE: 3,
12 EDIT: 4,
13 TOMBSTONE: 5,
14 FOLLOW: 6,
15 BLOCK: 7,
16 PROFILE: 8,
17 BOARD_CREATE: 9,
18 BLOCKLIST_PUBLISH: 10
19}
20
21const TYPE_NAME = Object.fromEntries(Object.entries(TYPE).map(([k, v]) => [v, k.toLowerCase()]))
22
23const REQUIRES_POW = new Set([TYPE.POST, TYPE.COMMENT, TYPE.BOARD_CREATE])
24
25const OP_ID_BYTES = 40 // 32-byte pubkey + 8-byte seq
26
27function makeOpId (pubkey, seq) {
28 if (pubkey.length !== 32) throw new Error('pubkey must be 32 bytes')
29 const out = b4a.alloc(OP_ID_BYTES)
30 out.set(pubkey, 0)
31 const view = new DataView(out.buffer, out.byteOffset + 32, 8)
32 view.setBigUint64(0, BigInt(seq), false) // big-endian
33 return out
34}
35
36function parseOpId (opId) {
37 if (opId.length !== OP_ID_BYTES) throw new Error('opId must be 40 bytes')
38 const view = new DataView(opId.buffer, opId.byteOffset + 32, 8)
39 return { pubkey: opId.subarray(0, 32), seq: Number(view.getBigUint64(0, false)) }
40}
41
42// --- payload encoders per type ---
43
44const optionalString = {
45 preencode (state, s) {
46 cenc.bool.preencode(state, !!s)
47 if (s) cenc.string.preencode(state, s)
48 },
49 encode (state, s) {
50 cenc.bool.encode(state, !!s)
51 if (s) cenc.string.encode(state, s)
52 },
53 decode (state) {
54 return cenc.bool.decode(state) ? cenc.string.decode(state) : null
55 }
56}
57
58const opIdEnc = cenc.fixed(OP_ID_BYTES)
59const pubkeyEnc = cenc.fixed(32)
60
61const payloadEncoders = {
62 [TYPE.POST]: {
63 preencode (state, p) {
64 cenc.string.preencode(state, p.board)
65 cenc.string.preencode(state, p.title)
66 cenc.string.preencode(state, p.body)
67 optionalString.preencode(state, p.link)
68 },
69 encode (state, p) {
70 cenc.string.encode(state, p.board)
71 cenc.string.encode(state, p.title)
72 cenc.string.encode(state, p.body)
73 optionalString.encode(state, p.link)
74 },
75 decode (state) {
76 return {
77 board: cenc.string.decode(state),
78 title: cenc.string.decode(state),
79 body: cenc.string.decode(state),
80 link: optionalString.decode(state)
81 }
82 }
83 },
84 [TYPE.COMMENT]: {
85 preencode (state, p) { opIdEnc.preencode(state, p.parent); cenc.string.preencode(state, p.body) },
86 encode (state, p) { opIdEnc.encode(state, p.parent); cenc.string.encode(state, p.body) },
87 decode (state) { return { parent: opIdEnc.decode(state), body: cenc.string.decode(state) } }
88 },
89 [TYPE.VOTE]: {
90 preencode (state, p) { opIdEnc.preencode(state, p.target); cenc.int8.preencode(state, p.dir) },
91 encode (state, p) { opIdEnc.encode(state, p.target); cenc.int8.encode(state, p.dir) },
92 decode (state) { return { target: opIdEnc.decode(state), dir: cenc.int8.decode(state) } }
93 },
94 [TYPE.EDIT]: {
95 preencode (state, p) {
96 opIdEnc.preencode(state, p.target)
97 cenc.string.preencode(state, p.body)
98 optionalString.preencode(state, p.title)
99 },
100 encode (state, p) {
101 opIdEnc.encode(state, p.target)
102 cenc.string.encode(state, p.body)
103 optionalString.encode(state, p.title)
104 },
105 decode (state) {
106 return {
107 target: opIdEnc.decode(state),
108 body: cenc.string.decode(state),
109 title: optionalString.decode(state)
110 }
111 }
112 },
113 [TYPE.TOMBSTONE]: {
114 preencode (state, p) { opIdEnc.preencode(state, p.target) },
115 encode (state, p) { opIdEnc.encode(state, p.target) },
116 decode (state) { return { target: opIdEnc.decode(state) } }
117 },
118 [TYPE.FOLLOW]: {
119 preencode (state, p) { pubkeyEnc.preencode(state, p.target) },
120 encode (state, p) { pubkeyEnc.encode(state, p.target) },
121 decode (state) { return { target: pubkeyEnc.decode(state) } }
122 },
123 [TYPE.BLOCK]: {
124 preencode (state, p) { pubkeyEnc.preencode(state, p.target); cenc.bool.preencode(state, !!p.public) },
125 encode (state, p) { pubkeyEnc.encode(state, p.target); cenc.bool.encode(state, !!p.public) },
126 decode (state) { return { target: pubkeyEnc.decode(state), public: cenc.bool.decode(state) } }
127 },
128 [TYPE.PROFILE]: {
129 preencode (state, p) {
130 cenc.string.preencode(state, p.nick)
131 optionalString.preencode(state, p.bio)
132 optionalString.preencode(state, p.avatar)
133 },
134 encode (state, p) {
135 cenc.string.encode(state, p.nick)
136 optionalString.encode(state, p.bio)
137 optionalString.encode(state, p.avatar)
138 },
139 decode (state) {
140 return {
141 nick: cenc.string.decode(state),
142 bio: optionalString.decode(state),
143 avatar: optionalString.decode(state)
144 }
145 }
146 },
147 [TYPE.BOARD_CREATE]: {
148 preencode (state, p) {
149 cenc.string.preencode(state, p.name)
150 cenc.string.preencode(state, p.description)
151 cenc.uint8.preencode(state, p.minPowBits)
152 },
153 encode (state, p) {
154 cenc.string.encode(state, p.name)
155 cenc.string.encode(state, p.description)
156 cenc.uint8.encode(state, p.minPowBits)
157 },
158 decode (state) {
159 return {
160 name: cenc.string.decode(state),
161 description: cenc.string.decode(state),
162 minPowBits: cenc.uint8.decode(state)
163 }
164 }
165 },
166 [TYPE.BLOCKLIST_PUBLISH]: {
167 preencode (state, p) {
168 cenc.uint32.preencode(state, p.version)
169 cenc.uint32.preencode(state, p.list.length)
170 for (const k of p.list) pubkeyEnc.preencode(state, k)
171 },
172 encode (state, p) {
173 cenc.uint32.encode(state, p.version)
174 cenc.uint32.encode(state, p.list.length)
175 for (const k of p.list) pubkeyEnc.encode(state, k)
176 },
177 decode (state) {
178 const version = cenc.uint32.decode(state)
179 const n = cenc.uint32.decode(state)
180 const list = []
181 for (let i = 0; i < n; i++) list.push(pubkeyEnc.decode(state))
182 return { version, list }
183 }
184 }
185}
186
187// --- op encoder ---
188
189// Wire format:
190// uint8 v
191// uint8 type
192// uint64 ts
193// <payload>
194// uint8 powFlag (0 or 1)
195// [uint8 bits, fixed8 nonce] if powFlag=1
196
197const NONCE_BYTES = 8
198const nonceEnc = cenc.fixed(NONCE_BYTES)
199
200const op = {
201 preencode (state, o) {
202 cenc.uint8.preencode(state, o.v)
203 cenc.uint8.preencode(state, o.type)
204 cenc.uint64.preencode(state, o.ts)
205 payloadEncoders[o.type].preencode(state, o.payload)
206 cenc.uint8.preencode(state, o.pow ? 1 : 0)
207 if (o.pow) {
208 cenc.uint8.preencode(state, o.pow.bits)
209 nonceEnc.preencode(state, o.pow.nonce)
210 }
211 },
212 encode (state, o) {
213 cenc.uint8.encode(state, o.v)
214 cenc.uint8.encode(state, o.type)
215 cenc.uint64.encode(state, o.ts)
216 payloadEncoders[o.type].encode(state, o.payload)
217 cenc.uint8.encode(state, o.pow ? 1 : 0)
218 if (o.pow) {
219 cenc.uint8.encode(state, o.pow.bits)
220 nonceEnc.encode(state, o.pow.nonce)
221 }
222 },
223 decode (state) {
224 const v = cenc.uint8.decode(state)
225 const type = cenc.uint8.decode(state)
226 const ts = cenc.uint64.decode(state)
227 const payload = payloadEncoders[type].decode(state)
228 const flag = cenc.uint8.decode(state)
229 let pow = null
230 if (flag === 1) {
231 pow = { bits: cenc.uint8.decode(state), nonce: nonceEnc.decode(state) }
232 }
233 return { v, type, ts, payload, pow }
234 }
235}
236
237// Preimage = everything EXCEPT the pow field.
238// Used to compute the PoW hash. Nonce is appended to this buffer to hash.
239const preimage = {
240 preencode (state, o) {
241 cenc.uint8.preencode(state, o.v)
242 cenc.uint8.preencode(state, o.type)
243 cenc.uint64.preencode(state, o.ts)
244 payloadEncoders[o.type].preencode(state, o.payload)
245 },
246 encode (state, o) {
247 cenc.uint8.encode(state, o.v)
248 cenc.uint8.encode(state, o.type)
249 cenc.uint64.encode(state, o.ts)
250 payloadEncoders[o.type].encode(state, o.payload)
251 }
252}
253
254function encodeOp (o) { return cenc.encode(op, o) }
255function decodeOp (buf) { return cenc.decode(op, buf) }
256function encodePreimage (o) { return cenc.encode(preimage, o) }
257
258module.exports = {
259 SCHEMA_VERSION,
260 TYPE,
261 TYPE_NAME,
262 REQUIRES_POW,
263 OP_ID_BYTES,
264 NONCE_BYTES,
265 makeOpId,
266 parseOpId,
267 encodeOp,
268 decodeOp,
269 encodePreimage
270}
src/backend/pow.js +82 -0
……@@ -0,0 +1,82 @@
1'use strict'
2
3const sodium = require('sodium-universal')
4const b4a = require('b4a')
5const { encodePreimage, NONCE_BYTES } = require('./ops')
6
7const HASH_BYTES = 32
8
9const DEFAULT_BITS = {
10 comment: 16,
11 post: 18,
12 board_create: 22
13}
14
15function blake2b (input, out) {
16 out = out || b4a.alloc(HASH_BYTES)
17 sodium.crypto_generichash(out, input)
18 return out
19}
20
21function leadingZeroBits (buf) {
22 let bits = 0
23 for (let i = 0; i < buf.length; i++) {
24 const b = buf[i]
25 if (b === 0) { bits += 8; continue }
26 // count leading zeros in this byte
27 for (let m = 0x80; m; m >>>= 1) {
28 if (b & m) return bits
29 bits++
30 }
31 return bits
32 }
33 return bits
34}
35
36function digestPreimage (op) {
37 return blake2b(encodePreimage(op))
38}
39
40function hashWithNonce (digest, nonce, out) {
41 const buf = b4a.alloc(digest.length + nonce.length)
42 buf.set(digest, 0)
43 buf.set(nonce, digest.length)
44 return blake2b(buf, out)
45}
46
47function writeNonce (nonce, counter) {
48 // 8-byte big-endian counter, room to grow
49 const view = new DataView(nonce.buffer, nonce.byteOffset, nonce.length)
50 view.setBigUint64(0, BigInt(counter), false)
51}
52
53function mint (op, bits, { maxAttempts = 1e9 } = {}) {
54 const digest = digestPreimage(op)
55 const nonce = b4a.alloc(NONCE_BYTES)
56 const hashOut = b4a.alloc(HASH_BYTES)
57 for (let counter = 0; counter < maxAttempts; counter++) {
58 writeNonce(nonce, counter)
59 hashWithNonce(digest, nonce, hashOut)
60 if (leadingZeroBits(hashOut) >= bits) {
61 return { bits, nonce: b4a.from(nonce) }
62 }
63 }
64 throw new Error(`pow mint exceeded ${maxAttempts} attempts`)
65}
66
67function verify (op, bits) {
68 if (!op.pow) return false
69 if (op.pow.bits < bits) return false
70 const digest = digestPreimage(op)
71 const hash = hashWithNonce(digest, op.pow.nonce)
72 return leadingZeroBits(hash) >= op.pow.bits
73}
74
75module.exports = {
76 DEFAULT_BITS,
77 HASH_BYTES,
78 leadingZeroBits,
79 digestPreimage,
80 mint,
81 verify
82}
src/backend/reputation.js +21 -0
……@@ -0,0 +1,21 @@
1'use strict'
2
3// Reputation formula per SPEC §6.2:
4// rep(key) = log2(1 + max(ageDays, 1/24)) * sqrt(1 + receivedUpvotes)
5// voteWeight(key) = clamp(rep(key) / 50, 0.02, 1.0)
6
7const FLOOR = 0.02
8const CAP = 1.0
9const CAP_DENOM = 50
10
11function computeRep (ageDays, receivedUpvotes) {
12 const safeAge = Math.max(ageDays, 1 / 24)
13 return Math.log2(1 + safeAge) * Math.sqrt(1 + Math.max(0, receivedUpvotes))
14}
15
16function computeWeight (ageDays, receivedUpvotes) {
17 const r = computeRep(ageDays, receivedUpvotes)
18 return Math.max(FLOOR, Math.min(CAP, r / CAP_DENOM))
19}
20
21module.exports = { computeRep, computeWeight, FLOOR, CAP, CAP_DENOM }
src/backend/rpc.js +281 -0
……@@ -0,0 +1,281 @@
1'use strict'
2
3const b4a = require('b4a')
4const { decodeOp, makeOpId, parseOpId } = require('./ops')
5const pow = require('./pow')
6
7const DEFAULT_BOARD = 'front'
8
9// Converts a Buffer/Uint8Array into hex string. null → null.
10const hex = (b) => (b ? b4a.toString(b, 'hex') : null)
11const fromHex = (s) => (s ? b4a.from(s, 'hex') : null)
12
13// Create an RPC dispatcher bound to a Node + Indexer.
14function createRPC ({ node, indexer }) {
15 async function enrichPost ({ opId, author, seq, ts }) {
16 const core = node.userCore(author)
17 if (!core) return null
18 let op
19 try { op = decodeOp(await core.get(seq)) } catch { return null }
20 const { up, down, score } = await indexer.getVoteTotals(opId)
21 const overlay = await indexer.getOverlay(opId)
22 const profile = await indexer.getProfile(author)
23 const commentCount = await countComments(opId)
24 return {
25 opId: hex(opId),
26 author: hex(author),
27 authorNick: profile?.nick || null,
28 seq,
29 ts,
30 title: (overlay?.title) || op.payload.title,
31 body: (overlay?.body) ?? op.payload.body,
32 edited: !!overlay,
33 link: op.payload.link,
34 board: op.payload.board,
35 up,
36 down,
37 score,
38 commentCount
39 }
40 }
41
42 async function countComments (parentOpId) {
43 let n = 0
44 for await (const _ of indexer.listComments(parentOpId, { limit: 10000 })) n++
45 return n
46 }
47
48 async function enrichComment ({ opId, author, seq, ts, parent }) {
49 const core = node.userCore(author)
50 if (!core) return null
51 let op
52 try { op = decodeOp(await core.get(seq)) } catch { return null }
53 const { up, down, score } = await indexer.getVoteTotals(opId)
54 const overlay = await indexer.getOverlay(opId)
55 const profile = await indexer.getProfile(author)
56 return {
57 opId: hex(opId),
58 parent: hex(parent),
59 author: hex(author),
60 authorNick: profile?.nick || null,
61 seq,
62 ts,
63 body: overlay?.body ?? op.payload.body,
64 edited: !!overlay,
65 up, down, score
66 }
67 }
68
69 const methods = {
70 async me () {
71 return { pubkey: hex(node.pubkey), length: node.length }
72 },
73
74 async listBoards () {
75 const out = []
76 for await (const name of indexer.listBoards()) out.push(name)
77 return out
78 },
79
80 async getBoard ({ name }) {
81 const b = await indexer.getBoard(name)
82 if (!b) return null
83 return {
84 name,
85 description: b.description,
86 minPowBits: b.minPowBits,
87 creator: hex(b.creator),
88 ts: b.ts
89 }
90 },
91
92 async createBoard ({ name, description, minPowBits }) {
93 const { opId } = await node.createBoard(name, description, { minPowBits })
94 return { opId: hex(opId) }
95 },
96
97 async listPosts ({ board = DEFAULT_BOARD, sort = 'hot', limit = 50 }) {
98 const posts = []
99 if (sort === 'hot' || sort === 'top') {
100 const ranked = await indexer.scoreBoard(board, { limit, sort })
101 for (const r of ranked) {
102 const p = await enrichPost(r)
103 if (p) { p.hot = r.hot; posts.push(p) }
104 }
105 } else {
106 for await (const entry of indexer.listPosts(board, { limit })) {
107 const p = await enrichPost(entry)
108 if (p) posts.push(p)
109 }
110 }
111 return posts
112 },
113
114 async getPost ({ opId }) {
115 const raw = fromHex(opId)
116 const { pubkey, seq } = parseOpId(raw)
117 const core = node.userCore(pubkey)
118 if (!core) return null
119 let op
120 try { op = decodeOp(await core.get(seq)) } catch { return null }
121 const { up, down, score } = await indexer.getVoteTotals(raw)
122 const overlay = await indexer.getOverlay(raw)
123 const profile = await indexer.getProfile(pubkey)
124 const tombstoned = await indexer.isTombstoned(raw)
125 return {
126 opId,
127 author: hex(pubkey),
128 authorNick: profile?.nick || null,
129 seq,
130 ts: Number(op.ts),
131 title: overlay?.title || op.payload.title,
132 body: overlay?.body ?? op.payload.body,
133 edited: !!overlay,
134 tombstoned,
135 link: op.payload.link,
136 board: op.payload.board,
137 up, down, score
138 }
139 },
140
141 async listComments ({ parentOpId }) {
142 const parent = fromHex(parentOpId)
143 const out = []
144 for await (const entry of indexer.listComments(parent)) {
145 const c = await enrichComment(entry)
146 if (c) out.push(c)
147 }
148 return out
149 },
150
151 // Returns all comments in a thread (direct + nested descendants), flat,
152 // each with a `parent` pointer. Client rebuilds the tree from parents.
153 async listCommentTree ({ rootOpId, maxDepth = 20, maxNodes = 1000 }) {
154 const out = []
155 const seen = new Set()
156 const queue = [{ id: fromHex(rootOpId), depth: 0 }]
157 while (queue.length && out.length < maxNodes) {
158 const { id, depth } = queue.shift()
159 if (depth >= maxDepth) continue
160 for await (const entry of indexer.listComments(id)) {
161 const key = hex(entry.opId)
162 if (seen.has(key)) continue
163 seen.add(key)
164 const c = await enrichComment(entry)
165 if (!c) continue
166 out.push(c)
167 queue.push({ id: entry.opId, depth: depth + 1 })
168 if (out.length >= maxNodes) break
169 }
170 }
171 return out
172 },
173
174 async createPost ({ board = DEFAULT_BOARD, title, body, link }) {
175 // Auto-register the default board on first post, so the UI doesn't need
176 // to expose a board-management flow.
177 if (board === DEFAULT_BOARD) {
178 const existing = await indexer.getBoard(board)
179 if (!existing) {
180 try {
181 await node.createBoard(board, 'p2pbuilders front page', { minPowBits: pow.DEFAULT_BITS.post })
182 } catch { /* ignore if someone raced us */ }
183 }
184 }
185 const t0 = Date.now()
186 const { opId, op } = await node.post(board, title, body, { link })
187 return { opId: hex(opId), powMs: Date.now() - t0, powBits: op.pow?.bits || 0 }
188 },
189
190 async createComment ({ parentOpId, body }) {
191 const parent = fromHex(parentOpId)
192 const t0 = Date.now()
193 const { opId, op } = await node.comment(parent, body)
194 return { opId: hex(opId), powMs: Date.now() - t0, powBits: op.pow?.bits || 0 }
195 },
196
197 async vote ({ targetOpId, dir }) {
198 const target = fromHex(targetOpId)
199 await node.vote(target, dir)
200 return { ok: true }
201 },
202
203 async getVoteTotals ({ opId }) {
204 return indexer.getVoteTotals(fromHex(opId))
205 },
206
207 async trackUser ({ pubkey }) {
208 await node.trackUser(fromHex(pubkey))
209 return { ok: true }
210 },
211
212 async editOp ({ opId, body, title }) {
213 await node.edit(fromHex(opId), body, { title: title || null })
214 return { ok: true }
215 },
216
217 async deleteOp ({ opId }) {
218 await node.tombstone(fromHex(opId))
219 return { ok: true }
220 },
221
222 async setProfile ({ nick, bio, avatar }) {
223 await node.setProfile({ nick, bio, avatar })
224 return { ok: true }
225 },
226
227 async getProfile ({ pubkey }) {
228 const p = await indexer.getProfile(fromHex(pubkey))
229 if (!p) return null
230 return {
231 pubkey,
232 nick: p.nick || null,
233 bio: p.bio || null,
234 avatar: p.avatar || null,
235 ts: p.ts
236 }
237 },
238
239 // Reveal the 32-byte identity primary key as hex. This is the secret that
240 // controls the user's core — losing it means losing the identity, sharing
241 // it means giving someone else the ability to write under your pubkey.
242 async exportPrimaryKey () {
243 return { hex: hex(node.primaryKey) }
244 },
245
246 // Read-only diagnostic snapshot for the settings page.
247 async getStats () {
248 let boardCount = 0
249 for await (const _ of indexer.listBoards()) boardCount++
250 const peerCount = node.swarm ? node.swarm.peerCount : 0
251 const trackedCount = node._tracked ? node._tracked.size : 0
252 return {
253 pubkey: hex(node.pubkey),
254 length: node.length,
255 storage: node._cleanup ? null : 'disk', // best-effort
256 swarm: node.swarm ? 'attached' : 'offline',
257 peerCount,
258 trackedUsers: trackedCount,
259 boards: boardCount,
260 policy: {
261 minPowBits: indexer.policy.minPowBits,
262 posts: indexer.policy.posts,
263 comments: indexer.policy.comments,
264 votes: indexer.policy.votes,
265 enforcePow: indexer.policy.enforcePow,
266 enforceRateLimit: indexer.policy.enforceRateLimit
267 }
268 }
269 }
270 }
271
272 async function dispatch (method, params) {
273 const fn = methods[method]
274 if (!fn) throw new Error(`unknown method: ${method}`)
275 return fn(params || {})
276 }
277
278 return { dispatch, methods }
279}
280
281module.exports = { createRPC }
src/backend/server.js +93 -0
……@@ -0,0 +1,93 @@
1'use strict'
2
3const http = require('http')
4const fs = require('fs')
5const path = require('path')
6const { WebSocketServer } = require('ws')
7
8const { Node } = require('./node')
9const { Indexer } = require('./indexer')
10const { createRPC } = require('./rpc')
11
12const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public')
13const MIME = {
14 '.html': 'text/html; charset=utf-8',
15 '.js': 'application/javascript; charset=utf-8',
16 '.css': 'text/css; charset=utf-8',
17 '.json': 'application/json; charset=utf-8',
18 '.png': 'image/png',
19 '.svg': 'image/svg+xml',
20 '.ico': 'image/x-icon'
21}
22
23function serveStatic (req, res) {
24 let urlPath = req.url.split('?')[0]
25 if (urlPath === '/' || urlPath === '') urlPath = '/index.html'
26 // SPA fallback: any route that's not a static file → index.html
27 const filePath = path.join(PUBLIC_DIR, urlPath)
28 if (!filePath.startsWith(PUBLIC_DIR)) { res.writeHead(403); return res.end() }
29 fs.stat(filePath, (err, stat) => {
30 if (err || !stat.isFile()) {
31 if (urlPath.includes('.')) { res.writeHead(404); return res.end('not found') }
32 return fs.createReadStream(path.join(PUBLIC_DIR, 'index.html'))
33 .on('open', () => res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }))
34 .pipe(res)
35 }
36 const ext = path.extname(filePath).toLowerCase()
37 res.writeHead(200, { 'content-type': MIME[ext] || 'application/octet-stream' })
38 fs.createReadStream(filePath).pipe(res)
39 })
40}
41
42async function start ({ dir, port = 8787, swarm } = {}) {
43 if (!dir) throw new Error('dir is required')
44 const node = await Node.openDisk(dir, { swarm })
45 const indexer = new Indexer(node)
46 await indexer.ready()
47 const rpc = createRPC({ node, indexer })
48
49 const server = http.createServer(serveStatic)
50 const wss = new WebSocketServer({ server, path: '/rpc' })
51
52 wss.on('connection', (ws) => {
53 ws.on('message', async (data) => {
54 let msg
55 try { msg = JSON.parse(data.toString()) } catch { return }
56 const { id, method, params } = msg
57 try {
58 const result = await rpc.dispatch(method, params)
59 ws.send(JSON.stringify({ id, result }))
60 } catch (err) {
61 ws.send(JSON.stringify({ id, error: err.message || String(err) }))
62 }
63 })
64 })
65
66 await new Promise((resolve) => server.listen(port, resolve))
67 const url = `http://localhost:${port}/`
68 console.log(`p2pbuilders backend listening`)
69 console.log(` url: ${url}`)
70 console.log(` pubkey: ${require('b4a').toString(node.pubkey, 'hex')}`)
71 return { server, wss, node, indexer, rpc, close: async () => {
72 wss.close()
73 server.close()
74 await indexer.close()
75 await node.close()
76 } }
77}
78
79module.exports = { start }
80
81// CLI entry
82if (require.main === module) {
83 const dir = process.env.P2PBUILDERS_DIR || path.join(require('os').homedir(), '.p2pbuilders')
84 const port = Number(process.env.P2PBUILDERS_PORT || 8787)
85 const bootstrap = process.env.P2PBUILDERS_BOOTSTRAP
86 ? JSON.parse(process.env.P2PBUILDERS_BOOTSTRAP)
87 : undefined
88 const swarm = process.env.P2PBUILDERS_NO_SWARM
89 ? null
90 : (bootstrap ? { bootstrap } : {})
91 start({ dir, port, swarm })
92 .catch(err => { console.error(err); process.exit(1) })
93}
src/backend/store.js +48 -0
……@@ -0,0 +1,48 @@
1'use strict'
2
3const { fs, os, path } = require('./_rt')
4const Corestore = require('corestore')
5const { USER_CORE_NAME, loadOrCreatePrimaryKey, generatePrimaryKey } = require('./identity')
6
7function openStore ({ storage, primaryKey } = {}) {
8 if (!storage) throw new Error('storage required')
9 // unsafe:true acknowledges we intentionally provide the primary key — it is our identity root.
10 return new Corestore(storage, primaryKey ? { primaryKey, unsafe: true } : {})
11}
12
13async function openUserCore (store) {
14 const core = store.get({ name: USER_CORE_NAME })
15 await core.ready()
16 return core
17}
18
19// disk-backed node. We let Corestore manage its own seed file at `dir/` —
20// trying to pass our own primaryKey on top of an existing corestore causes
21// "Another corestore is stored here" errors in v7. `store.primaryKey` is
22// readable after `.ready()` if anything needs it.
23async function openDiskNode (dir) {
24 fs.mkdirSync(dir, { recursive: true })
25 const store = new Corestore(dir)
26 await store.ready()
27 const core = await openUserCore(store)
28 return { store, core, primaryKey: store.primaryKey, dir }
29}
30
31// ephemeral node: fresh tmp dir, cleaned up on close. good for tests.
32async function openTempNode ({ primaryKey } = {}) {
33 const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p2pbuilders-'))
34 const pk = primaryKey || generatePrimaryKey()
35 // persist the key so the tmp dir is a valid node on its own
36 fs.writeFileSync(path.join(dir, '.p2pbuilders.key'), pk, { mode: 0o600 })
37 const store = openStore({ storage: dir, primaryKey: pk })
38 const core = await openUserCore(store)
39 return {
40 store,
41 core,
42 primaryKey: pk,
43 dir,
44 cleanup () { fs.rmSync(dir, { recursive: true, force: true }) }
45 }
46}
47
48module.exports = { openStore, openUserCore, openDiskNode, openTempNode }
src/backend/swarm.js +86 -0
……@@ -0,0 +1,86 @@
1'use strict'
2
3const Hyperswarm = require('hyperswarm')
4
5// Wraps Hyperswarm, pipes every connection into the corestore replicator.
6// If onConnection is provided, it's invoked per connection with { conn, stream, muxer }
7// so callers can attach additional protomux channels (e.g. announce).
8class SwarmHub {
9 constructor (store, { bootstrap, keyPair, onConnection } = {}) {
10 this.store = store
11 this.swarm = new Hyperswarm({ bootstrap, keyPair })
12 this.swarm.on('connection', (conn) => {
13 const stream = this.store.replicate(conn)
14 const muxer = stream.noiseStream.userData
15 if (onConnection) {
16 try { onConnection({ conn, stream, muxer }) } catch (err) {
17 this.swarm.emit('error', err)
18 }
19 }
20 })
21 this._discoveries = new Map() // hex(topic) -> discovery handle
22 }
23
24 async joinBoard (topic, { server = true, client = true } = {}) {
25 const key = topic.toString('hex')
26 let discovery = this._discoveries.get(key)
27 if (!discovery) {
28 discovery = this.swarm.join(topic, { server, client })
29 this._discoveries.set(key, discovery)
30 }
31 await discovery.flushed()
32 return discovery
33 }
34
35 async leaveBoard (topic) {
36 const key = topic.toString('hex')
37 const discovery = this._discoveries.get(key)
38 if (!discovery) return
39 this._discoveries.delete(key)
40 await this.swarm.leave(topic)
41 }
42
43 // Explicitly connect to a known relay peer by DHT pubkey.
44 joinPeer (pubkey) { this.swarm.joinPeer(pubkey) }
45
46 // Wait until we have at least n connected peers.
47 async waitForPeers (n = 1, { timeoutMs = 10000 } = {}) {
48 if (this.swarm.connections.size >= n) return
49 return new Promise((resolve, reject) => {
50 const timer = setTimeout(() => {
51 this.swarm.off('connection', onConn)
52 reject(new Error(`timeout waiting for ${n} peer(s)`))
53 }, timeoutMs)
54 const onConn = () => {
55 if (this.swarm.connections.size >= n) {
56 clearTimeout(timer)
57 this.swarm.off('connection', onConn)
58 resolve()
59 }
60 }
61 this.swarm.on('connection', onConn)
62 })
63 }
64
65 get peerCount () { return this.swarm.connections.size }
66
67 async destroy () {
68 await this.swarm.destroy()
69 }
70}
71
72// Test helper: wire two corestores together with an in-memory duplex pair.
73// Returns a { close } handle.
74function connectDirect (storeA, storeB) {
75 const streamA = storeA.replicate(true) // A is initiator
76 const streamB = storeB.replicate(false)
77 streamA.pipe(streamB).pipe(streamA)
78 return {
79 close () {
80 streamA.destroy()
81 streamB.destroy()
82 }
83 }
84}
85
86module.exports = { SwarmHub, connectDirect }
src/bare/IOS_INTEGRATION.md +137 -0
……@@ -0,0 +1,137 @@
1# iOS integration — bare-kit-pear ↔ p2pbuilders
2
3This document describes how to wire the p2pbuilders Bare backend into an iOS
4app using [bare-kit](https://github.com/holepunchto/bare-kit) (or your
5`bare-kit-pear` fork). Everything below is derived from holepunchto/bare-kit's
6public API + `node_modules/bare-ipc`.
7
8## 1. Bundle the JS
9
10The app loads `src/bare/ios-entry.js` which in turn requires the full backend.
11You need to bundle:
12```
13src/bare/ios-entry.js
14src/bare/harness.js
15src/backend/**/*.js (everything)
16node_modules/** (hypercore, corestore, hyperswarm, b4a, compact-encoding,
17 sodium-universal, hyperbee, @hyperswarm/dht-relay,
18 bare-fs, bare-path, bare-os, bare-env, bare-ipc, bare-pipe,
19 bare-stream, …)
20```
21
22Two ways to bundle for the iOS build:
23
24**(a) ship the whole tree as-is** and let bare-kit's module resolver walk it —
25simplest, biggest binary.
26
27**(b) pre-bundle with `bare-pack`** (the Bare-native bundler analogous to esbuild):
28```
29npx bare-pack src/bare/ios-entry.js > bundle.js
30```
31then ship the single `bundle.js`. Faster startup, smaller install.
32
33## 2. Start the worklet
34
35```swift
36import BareKit
37
38let worklet = BareWorklet()
39try worklet.start(
40 name: "p2pbuilders",
41 source: bundleSourceString, // contents of ios-entry.js or bundle.js
42 arguments: []
43)
44
45// Attach the IPC channel. bare-kit auto-wires this to the global BareKit.IPC
46// on the JS side when the worklet runs.
47let ipc = BareIPC(worklet: worklet)
48```
49
50## 3. Speak JSON-RPC over the pipe
51
52The JS backend treats the IPC stream as line-delimited JSON-RPC. Frame protocol:
53
54```
55request: {"id": <int>, "method": "<name>", "params": { ... }}\n
56response: {"id": <int>, "result": ...}\n
57 {"id": <int>, "error": "<message>"}\n
58event: {"event": "ready", "pubkey": "<hex64>", "dir": "<path>"}\n
59```
60
61The first frame you'll see (unsolicited) is the `ready` event — our backend
62emits it when the node + indexer are booted.
63
64### Swift call example
65
66```swift
67// Send:
68let request = #"{"id":1,"method":"listPosts","params":{"sort":"hot"}}"# + "\n"
69try ipc.write(data: request.data(using: .utf8)!)
70
71// Read (async):
72ipc.read { data, _ in
73 guard let data = data, let line = String(data: data, encoding: .utf8) else { return }
74 // Parse JSON, dispatch on "id" or "event"
75}
76```
77
78## 4. Methods the backend exposes
79
80All of these are available via the pipe:
81
82| Method | Params | Returns |
83| --------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------ |
84| `me` | — | `{ pubkey, length }` |
85| `listBoards` | — | `[name, …]` |
86| `getBoard` | `{ name }` | `{ name, description, minPowBits, creator, ts }` |
87| `listPosts` | `{ board?: string, sort?: 'hot'|'new'|'top', limit?: number }` | `[post, …]` |
88| `getPost` | `{ opId: hex }` | `post` |
89| `listComments` | `{ parentOpId: hex }` | `[comment, …]` (direct replies only) |
90| `listCommentTree` | `{ rootOpId: hex, maxDepth?: number, maxNodes?: number }` | `[comment, …]` (all descendants, flat, with `parent`) |
91| `createPost` | `{ title, body?, link?, board? }` | `{ opId, powMs, powBits }` |
92| `createComment` | `{ parentOpId, body }` | `{ opId, powMs, powBits }` |
93| `vote` | `{ targetOpId, dir: -1 | 0 | 1 }` | `{ ok: true }` |
94| `editOp` | `{ opId, body, title? }` | `{ ok: true }` |
95| `deleteOp` | `{ opId }` | `{ ok: true }` |
96| `setProfile` | `{ nick, bio?, avatar? }` | `{ ok: true }` |
97| `getProfile` | `{ pubkey }` | `{ pubkey, nick, bio, avatar, ts } \| null` |
98| `exportPrimaryKey` | — | `{ hex: string }` — for identity backup |
99| `trackUser` | `{ pubkey }` | `{ ok: true }` |
100| `getStats` | — | `{ pubkey, length, swarm, peerCount, … }` |
101
102`post` shape: `{ opId, author, authorNick, ts, title, body, link, board, up, down, score, commentCount, edited }`
103`comment` shape: `{ opId, parent, author, authorNick, ts, body, up, down, score, edited }`
104
105## 5. Suspend / resume
106
107When the iOS app backgrounds:
108```swift
109worklet.suspend() // JS side: Bare.on('suspend')
110// ... later ...
111worklet.resume() // JS side: Bare.on('resume')
112```
113Our `ios-entry.js` already calls `pipe.unref()` on suspend and `pipe.ref()` on
114resume so the Bare event loop doesn't keep the pipe hot while backgrounded.
115
116## 6. Minimal round-trip test
117
1181. Swift: start worklet, attach IPC.
1192. Swift: read one line, parse it → expect `{"event":"ready","pubkey":"…"}`.
1203. Swift: `ipc.write("{\"id\":1,\"method\":\"me\",\"params\":{}}\n")`.
1214. Swift: read one line → `{"id":1,"result":{"pubkey":"…","length":0}}`.
122
123If that round-trip works you can drive the whole UI from Swift.
124
125## 7. Known gotchas
126
127- **Storage path.** By default the backend writes to `$TMPDIR/p2pbuilders-ios/`.
128 On iOS you almost certainly want a persistent path. Pass it via
129 environment variable `P2PBUILDERS_DIR` before starting the worklet, OR
130 append it to `arguments:` (it's read from `Bare.argv[2]`).
131- **Networking.** Hyperswarm uses UDP; bare-kit-pear must provide raw UDP on
132 iOS (which is why you built the SDK in the first place). If UDP is blocked
133 on carrier networks, the app falls back to no peers; the DHT relay
134 endpoint on a hiverelay would be the mitigation.
135- **Sodium.** `sodium-universal` needs the Bare-compatible `sodium-native`
136 addon. This should already be handled by the bare-kit build, but double-check
137 that native addons are included in the `.framework`.
src/bare/harness.js +97 -0
……@@ -0,0 +1,97 @@
1'use strict'
2
3// Bare entry point for p2pbuilders — runs the full backend under the Bare runtime.
4// Used by:
5// 1. `bare src/bare/harness.js` for a quick smoke test under standalone Bare.
6// 2. bare-kit-pear on iOS — the SDK loads this file, hands us a pipe, and we
7// answer the same JSON-RPC protocol that the Pear worker and the
8// WebSocket transport speak.
9//
10// Protocol: newline-delimited JSON frames.
11// request: {"id": <n>, "method": "<name>", "params": {...}}
12// response: {"id": <n>, "result": ...} | {"id": <n>, "error": "<msg>"}
13//
14// The caller provides a Duplex-shaped object (or uses the built-in smoke-test
15// pipe). Everything else is identical to the Node backend.
16
17const { path } = require('../backend/_rt')
18const { Node } = require('../backend/node')
19const { Indexer } = require('../backend/indexer')
20const { createRPC } = require('../backend/rpc')
21
22async function start ({ dir, swarm = {}, pipe, onReady } = {}) {
23 if (!dir) throw new Error('dir is required')
24 const node = await Node.openDisk(dir, { swarm })
25 const indexer = new Indexer(node)
26 await indexer.ready()
27 const rpc = createRPC({ node, indexer })
28
29 if (pipe) attachPipe(pipe, rpc)
30
31 if (onReady) {
32 onReady({
33 pubkey: require('b4a').toString(node.pubkey, 'hex'),
34 dir,
35 indexer,
36 node,
37 rpc
38 })
39 }
40
41 return {
42 node,
43 indexer,
44 rpc,
45 async close () {
46 try { await indexer.close() } catch {}
47 try { await node.close() } catch {}
48 }
49 }
50}
51
52function attachPipe (pipe, rpc) {
53 let buf = ''
54 pipe.on('data', async (chunk) => {
55 buf += typeof chunk === 'string' ? chunk : chunk.toString('utf8')
56 let i
57 while ((i = buf.indexOf('\n')) >= 0) {
58 const line = buf.slice(0, i); buf = buf.slice(i + 1)
59 if (!line) continue
60 let msg
61 try { msg = JSON.parse(line) } catch { continue }
62 const { id, method, params } = msg
63 try {
64 const result = await rpc.dispatch(method, params)
65 pipe.write(JSON.stringify({ id, result }) + '\n')
66 } catch (err) {
67 pipe.write(JSON.stringify({ id, error: err.message || String(err) }) + '\n')
68 }
69 }
70 })
71}
72
73module.exports = { start, attachPipe }
74
75// When run directly under Bare (for local smoke-testing), spin up a node,
76// post "hello world", and print the result. No pipe — just proves the whole
77// backend boots in Bare.
78if (require.main === module) {
79 (async () => {
80 const { os } = require('../backend/_rt')
81 const dir = path.join(os.tmpdir(), `p2pbuilders-bare-${Date.now()}`)
82 console.log(`[bare] starting in ${dir}`)
83 const app = await start({ dir, swarm: null })
84 console.log(`[bare] pubkey: ${require('b4a').toString(app.node.pubkey, 'hex')}`)
85 const { opId } = await app.node.post('front', 'hello from bare', 'this is a smoke-test post')
86 console.log(`[bare] posted opId: ${require('b4a').toString(opId, 'hex').slice(0, 16)}…`)
87 await new Promise(r => setTimeout(r, 200)) // let indexer catch up
88 const posts = await app.rpc.dispatch('listPosts', { board: 'front', sort: 'new' })
89 console.log(`[bare] listPosts returned ${posts.length} post(s)`)
90 console.log(`[bare] first title: ${posts[0]?.title}`)
91 await app.close()
92 console.log('[bare] ok')
93 })().catch((err) => {
94 console.error('[bare] fatal:', err)
95 process.exit(1)
96 })
97}
src/bare/ios-entry.js +85 -0
……@@ -0,0 +1,85 @@
1'use strict'
2
3// Entry point for bare-kit-pear (and holepunchto/bare-kit) on iOS.
4//
5// bare-kit exposes a GLOBAL `BareKit.IPC` object inside every worklet. It's a
6// Duplex-shaped pipe auto-wired to the native host via `bare_ipc_init()` —
7// no require, no argv, no manual setup needed.
8//
9// Native pattern on the Swift side (bare-kit's holepunchto/bare-ios example):
10// 1. [BareWorklet start:source:arguments:] loads this file.
11// 2. bare_ipc_init(&ipc, &worklet) wires up the native ↔ JS pipe.
12// 3. Swift uses [ipc read] / [ipc write:data] to speak to us.
13//
14// JS-side usage:
15// BareKit.IPC.on('data', chunk => ...)
16// BareKit.IPC.write(bytes)
17//
18// We plug that straight into harness.start({pipe}) so the exact same
19// line-delimited JSON-RPC protocol as the Pear worker and the WebSocket
20// transport works here too.
21
22/* global BareKit, Bare */
23
24const { path, os, env, exit } = require('../backend/_rt')
25const { start } = require('./harness')
26
27function resolvePipe () {
28 // Primary: bare-kit's global IPC (iOS, Android, desktop bare-kit hosts)
29 if (typeof BareKit !== 'undefined' && BareKit.IPC) return BareKit.IPC
30
31 // Secondary: some SDK builds expose it under Bare.IPC instead
32 if (typeof Bare !== 'undefined' && Bare.IPC && typeof Bare.IPC.on === 'function') {
33 return Bare.IPC
34 }
35
36 // Fallback: running under plain `bare` with no host — no pipe available.
37 return null
38}
39
40function resolveStorageDir () {
41 // bare-kit hosts typically pass a per-app storage path via env or argv.
42 // Fallbacks let this run under standalone `bare` for smoke-testing.
43 return (
44 env.P2PBUILDERS_DIR ||
45 (typeof Bare !== 'undefined' && Bare.argv && Bare.argv[2]) ||
46 path.join(os.tmpdir(), 'p2pbuilders-ios')
47 )
48}
49
50;(async () => {
51 const pipe = resolvePipe()
52 const dir = resolveStorageDir()
53
54 const app = await start({
55 dir,
56 swarm: {}, // default hyperswarm; bare-kit provides UDP on iOS
57 pipe,
58 onReady ({ pubkey }) {
59 const greeting = JSON.stringify({ event: 'ready', pubkey, dir }) + '\n'
60 if (pipe) {
61 pipe.write(greeting)
62 } else {
63 console.log(`[p2pbuilders-ios] ready pubkey=${pubkey} dir=${dir}`)
64 console.log('[p2pbuilders-ios] no BareKit.IPC found; running headless.')
65 }
66 }
67 })
68
69 // Graceful shutdown when the host detaches the pipe.
70 if (pipe && typeof pipe.on === 'function') {
71 pipe.on('end', () => app.close())
72 pipe.on('close', () => app.close())
73 }
74
75 // Bare-kit suspend/resume lifecycle hints (keeps the IPC refcount sensible).
76 if (typeof Bare !== 'undefined' && Bare.on) {
77 if (pipe && typeof pipe.ref === 'function') {
78 Bare.on('suspend', () => { try { pipe.unref() } catch {} })
79 Bare.on('resume', () => { try { pipe.ref() } catch {} })
80 }
81 }
82})().catch((err) => {
83 console.error('[p2pbuilders-ios] fatal:', err && err.stack ? err.stack : err.message)
84 exit(1)
85})
src/pear/worker.js +58 -0
……@@ -0,0 +1,58 @@
1'use strict'
2
3// Pear Bare worker: runs the p2pbuilders backend and speaks line-delimited JSON-RPC
4// to the GUI webview over Pear's Pipe.
5//
6// Launched from src/pear/gui.js via Pear.Worker.run('./src/pear/worker.js').
7// The GUI does: pipe.write(JSON.stringify({id, method, params}) + '\n')
8// We respond: pipe.write(JSON.stringify({id, result}) + '\n')
9
10const { path } = require('../backend/_rt')
11const { Node } = require('../backend/node')
12const { Indexer } = require('../backend/indexer')
13const { createRPC } = require('../backend/rpc')
14
15// Pear exposes global `Pear` when this module runs as a worker.
16/* global Pear */
17
18async function main () {
19 // Pear.config.storage is the app's per-install storage directory.
20 const dir = path.join(Pear.config.storage, 'p2pbuilders')
21 const node = await Node.openDisk(dir, { swarm: {} })
22 const indexer = new Indexer(node)
23 await indexer.ready()
24 const rpc = createRPC({ node, indexer })
25
26 // Grab the worker's side of the pipe to the GUI.
27 const pipe = Pear.worker.pipe()
28
29 let buf = ''
30 pipe.on('data', async (chunk) => {
31 buf += typeof chunk === 'string' ? chunk : chunk.toString('utf8')
32 let i
33 while ((i = buf.indexOf('\n')) >= 0) {
34 const line = buf.slice(0, i); buf = buf.slice(i + 1)
35 if (!line) continue
36 let msg
37 try { msg = JSON.parse(line) } catch { continue }
38 const { id, method, params } = msg
39 try {
40 const result = await rpc.dispatch(method, params)
41 pipe.write(JSON.stringify({ id, result }) + '\n')
42 } catch (err) {
43 pipe.write(JSON.stringify({ id, error: err.message || String(err) }) + '\n')
44 }
45 }
46 })
47
48 pipe.on('close', async () => {
49 try { await indexer.close() } catch {}
50 try { await node.close() } catch {}
51 })
52}
53
54main().catch((err) => {
55 // eslint-disable-next-line no-console
56 console.error('p2pbuilders worker fatal:', err)
57 process.exit(1)
58})
src/relay/server.js +106 -0
……@@ -0,0 +1,106 @@
1'use strict'
2
3// P2PBuilders hiverelay — a long-running peer that seeds user cores AND (optionally)
4// exposes a DHT-relay WebSocket endpoint so browser clients can tunnel through.
5//
6// Usage:
7// P2PBUILDERS_DIR=./.relay \
8// P2PBUILDERS_BOARDS=front,meta \
9// P2PBUILDERS_DHT_RELAY_PORT=8443 \
10// node src/relay/server.js
11//
12// Two cooperating services run in one process:
13// 1. Pinning node — joins board topics, auto-tracks announced pubkeys,
14// keeps cores replicated 24/7 (blind seeding).
15// 2. DHT relay (WS) — wraps the shared HyperDHT instance with
16// @hyperswarm/dht-relay so WebSocket clients (browsers,
17// iOS apps) can make DHT queries they can't do directly
18// (browsers have no UDP). The relay only sees framed
19// DHT messages; hypercore replication between peers is
20// end-to-end encrypted through it.
21
22const { path, os } = require('../backend/_rt')
23const b4a = require('b4a')
24const { Node } = require('../backend/node')
25
26let dhtRelayModule = null
27try {
28 dhtRelayModule = require('@hyperswarm/dht-relay')
29} catch { /* optional: relay still works without the WS endpoint */ }
30
31async function startRelay ({
32 dir,
33 boards = [],
34 bootstrap,
35 dhtRelayPort,
36 announcePubkey = true
37} = {}) {
38 if (!dir) throw new Error('dir required')
39 const node = await Node.openDisk(dir, {
40 swarm: bootstrap ? { bootstrap } : {}
41 })
42 for (const name of boards) await node.joinBoard(name)
43
44 let dhtRelayServer = null
45 if (dhtRelayPort != null && dhtRelayModule) {
46 dhtRelayServer = await startDhtRelayEndpoint({
47 dht: node.swarm.swarm.dht,
48 port: dhtRelayPort
49 })
50 }
51
52 const summary = {
53 pubkey: b4a.toString(node.pubkey, 'hex'),
54 dir,
55 boards,
56 dhtRelayPort: dhtRelayServer ? dhtRelayPort : null
57 }
58 if (announcePubkey) {
59 console.log('p2pbuilders-hiverelay online')
60 console.log(` pubkey: ${summary.pubkey}`)
61 console.log(` dir: ${summary.dir}`)
62 console.log(` boards: ${boards.join(', ') || '(none configured)'}`)
63 console.log(` dht-relay: ${summary.dhtRelayPort ? `ws://0.0.0.0:${summary.dhtRelayPort}` : '(disabled)'}`)
64 }
65
66 return {
67 node,
68 summary,
69 dhtRelayServer,
70 async close () {
71 if (dhtRelayServer) await new Promise((r) => dhtRelayServer.close(r))
72 await node.close()
73 }
74 }
75}
76
77async function startDhtRelayEndpoint ({ dht, port }) {
78 if (!dhtRelayModule) throw new Error('@hyperswarm/dht-relay not installed')
79 const { WebSocketServer } = require('ws')
80 const { relay } = dhtRelayModule
81 const Stream = require('@hyperswarm/dht-relay/ws')
82 const wss = new WebSocketServer({ port })
83 await new Promise((resolve) => wss.once('listening', resolve))
84 wss.on('connection', (socket) => {
85 try {
86 relay(dht, new Stream(false, socket))
87 } catch (err) {
88 try { socket.close() } catch {}
89 console.error('[dht-relay] connection failed:', err.message)
90 }
91 })
92 return wss
93}
94
95module.exports = { startRelay, startDhtRelayEndpoint }
96
97if (require.main === module) {
98 const dir = process.env.P2PBUILDERS_DIR || path.join(os.homedir(), '.p2pbuilders-relay')
99 const boards = (process.env.P2PBUILDERS_BOARDS || 'front').split(',').map(s => s.trim()).filter(Boolean)
100 const bootstrap = process.env.P2PBUILDERS_BOOTSTRAP ? JSON.parse(process.env.P2PBUILDERS_BOOTSTRAP) : undefined
101 const dhtRelayPort = process.env.P2PBUILDERS_DHT_RELAY_PORT
102 ? Number(process.env.P2PBUILDERS_DHT_RELAY_PORT)
103 : null
104 startRelay({ dir, boards, bootstrap, dhtRelayPort })
105 .catch(err => { console.error(err); process.exit(1) })
106}
src/terminal/main.js +494 -0
……@@ -0,0 +1,494 @@
1/** @typedef {import('pear-interface')} */ /* global Pear, Bare */
2'use strict'
3
4// p2pbuilders terminal app — HN-shaped feed driven by our Bare backend.
5// Runs as a Pear terminal application:
6//
7// pear run pear://<key>
8//
9// No GUI, no webview. Same backend (ops, PoW, indexer, swarm, antispam,
10// edit/delete, profiles) as the desktop/iOS paths.
11
12// Resolve process via direct conditional (not via subpath imports — Pear's
13// bundled Bare may not honor package.json "imports" yet).
14const process = (typeof Bare !== 'undefined') ? require('bare-process') : global.process
15
16console.log('p2pbuilders terminal starting…')
17
18// ---- ADMIN -----------------------------------------------------------------
19// The single identity allowed to pin posts and delete other users' posts.
20// To set this up:
21// 1. run the app once (`pear run pear://<key>`)
22// 2. at the prompt type `me` — copy the 64-char hex pubkey
23// 3. paste it below, re-stage (`pear stage --no-ask dev .`), redistribute
24const ADMIN_PUBKEY = '16752aeb998f33904cfa0165526d7f217590e85096a964a07bd29c347edbe86d'
25
26// ---- PINNED POSTS ----------------------------------------------------------
27// opIds listed here render at the top of the feed with a 📌 label, above
28// the sorted hot/new/top results. Edit this list, re-stage, restart.
29// To get an opId: submit the post, the PoW-mint line prints the opId.
30const PINNED_OPIDS = [
31 '16752aeb998f33904cfa0165526d7f217590e85096a964a07bd29c347edbe86d0000000000000001'
32]
33
34const { path } = require('../backend/_rt')
35const { Node } = require('../backend/node')
36const { Indexer } = require('../backend/indexer')
37const { createRPC } = require('../backend/rpc')
38const b4a = require('b4a')
39
40// ---- ANSI helpers -----------------------------------------------------------
41
42const ANSI = {
43 reset: '\x1b[0m',
44 bold: '\x1b[1m',
45 dim: '\x1b[2m',
46 orange: '\x1b[38;5;208m',
47 cyan: '\x1b[36m',
48 green: '\x1b[32m',
49 red: '\x1b[31m',
50 gray: '\x1b[90m',
51 clear: '\x1b[2J\x1b[H'
52}
53
54const short = (s, n = 8) => (s ? s.slice(0, n) : '')
55const fmtAgo = (ts) => {
56 const s = Math.floor((Date.now() - ts) / 1000)
57 if (s < 45) return `${s}s`
58 if (s < 60 * 45) return `${Math.floor(s / 60)}m`
59 if (s < 60 * 60 * 22) return `${Math.floor(s / 3600)}h`
60 return `${Math.floor(s / 86400)}d`
61}
62
63// ---- Relay seeding ---------------------------------------------------------
64// Asks the public HiveRelay network to pin our own user core and every
65// peer we track. Fire-and-forget; errors only go to the background log
66// so the foreground UI stays clean.
67
68async function startRelaySeeder ({ node, storage }) {
69 // p2p-hiverelay is an ESM module; we dynamic-import so the CommonJS
70 // terminal entry can still load it cleanly.
71 const { HiveRelayClient } = await import('p2p-hiverelay/client')
72 const seederStorage = path.join(storage, '..', 'p2pbuilders-relay-client')
73 const client = new HiveRelayClient(seederStorage)
74 await client.start()
75
76 const seeded = new Set()
77 async function tryseed (pubkey) {
78 const hex = b4a.toString(pubkey, 'hex')
79 if (seeded.has(hex)) return
80 seeded.add(hex)
81 try {
82 const acc = await client.seed(pubkey, { replicas: 3, timeout: 20000 })
83 if (acc.length) {
84 // quiet by default — visible through `relays` command below
85 }
86 } catch {
87 seeded.delete(hex) // allow retry
88 }
89 }
90
91 // Seed our own core now, and every pubkey we track as they come in.
92 await tryseed(node.pubkey)
93 node.on('track', (pubkey) => { tryseed(pubkey).catch(() => {}) })
94 // Also re-scan periodically in case we missed a track event
95 setInterval(() => {
96 tryseed(node.pubkey).catch(() => {})
97 for (const core of node._tracked.values()) {
98 tryseed(core.key).catch(() => {})
99 }
100 }, 5 * 60 * 1000).unref?.()
101
102 // Expose the client so a `relays` command can print status.
103 return client
104}
105
106// ---- Boot -------------------------------------------------------------------
107
108async function main () {
109 const storage = (typeof Pear !== 'undefined' && Pear.config?.storage)
110 ? path.join(Pear.config.storage, 'p2pbuilders')
111 : path.join(require('../backend/_rt').os.tmpdir(), `p2pbuilders-term-${Date.now()}`)
112
113 process.stdout.write(`${ANSI.orange}p2pbuilders${ANSI.reset} ${ANSI.dim}booting at ${storage}${ANSI.reset}\n`)
114
115 console.log('[boot] opening node…')
116 const node = await Node.openDisk(storage, { swarm: {} })
117 console.log('[boot] node ready, pubkey prefix:', b4a.toString(node.pubkey, 'hex').slice(0, 16))
118 console.log('[boot] opening indexer…')
119 const indexer = new Indexer(node, {
120 admin: ADMIN_PUBKEY ? b4a.from(ADMIN_PUBKEY, 'hex') : null
121 })
122 await indexer.ready()
123 console.log('[boot] indexer ready')
124 const rpc = createRPC({ node, indexer })
125 console.log('[boot] rpc wired')
126
127 // Hiverelay persistence — seed our own core + every peer we track onto
128 // the public relay network so posts survive when their author goes offline.
129 // Runs in the background; any failure is logged but non-fatal.
130 let relayClient = null
131 startRelaySeeder({ node, storage }).then(c => { relayClient = c }).catch(err => {
132 console.log(`${ANSI.dim}[relay] seeding disabled: ${err.message}${ANSI.reset}`)
133 })
134
135 // Auto-join the front board so we discover peers
136 try { await node.joinBoard('front') } catch {}
137
138 const myPubkey = b4a.toString(node.pubkey, 'hex')
139 const amAdmin = !!ADMIN_PUBKEY && myPubkey.toLowerCase() === ADMIN_PUBKEY.toLowerCase()
140
141 // First-launch: if no profile nickname, prompt once
142 const profile = await indexer.getProfile(node.pubkey)
143 let nick = profile?.nick || null
144
145 process.stdout.write(`${ANSI.dim}your id: ${myPubkey}${amAdmin ? ' ' + ANSI.orange + '(admin)' + ANSI.reset : ''}${ANSI.reset}\n\n`)
146
147 // Current view state
148 const state = {
149 sort: 'hot', // 'hot' | 'new' | 'top'
150 posts: [], // raw enriched posts from the RPC
151 _display: [], // what's actually on screen: pinned + rest
152 threadId: null, // opId hex when viewing a thread
153 threadPost: null,
154 threadComments: []
155 }
156
157 // ---- rendering ----
158 function header () {
159 const peers = node.swarm ? node.swarm.peerCount : 0
160 const tracked = node._tracked ? node._tracked.size : 0
161 process.stdout.write(
162 `${ANSI.orange}${ANSI.bold}p2pbuilders${ANSI.reset} ` +
163 `${ANSI.dim}· ${state.sort} · ${peers} peers · tracking ${tracked} · ${nick || short(myPubkey, 8)}${ANSI.reset}\n` +
164 `${ANSI.gray}${'─'.repeat(64)}${ANSI.reset}\n`
165 )
166 }
167
168 function renderPostLine (p, idx, pinned = false) {
169 const num = String(idx).padStart(3)
170 const score = (p.score >= 0 ? '+' : '') + p.score
171 const author = p.authorNick || short(p.author, 8)
172 const pin = pinned ? ` ${ANSI.orange}📌${ANSI.reset}` : ''
173 const meta = `${ANSI.dim}${score} pts · ${p.commentCount || 0} cm · ${fmtAgo(p.ts)} · ${author}${ANSI.reset}`
174 const link = p.link ? ` ${ANSI.cyan}${p.link}${ANSI.reset}` : ''
175 process.stdout.write(`${ANSI.gray}${num}.${ANSI.reset}${pin} ${ANSI.bold}${p.title || '(no title)'}${ANSI.reset}${link}\n ${meta}\n`)
176 }
177
178 function renderFeed () {
179 process.stdout.write(ANSI.clear)
180 header()
181
182 // Split pinned from the rest. Keep declared order within pinned; keep sort
183 // order (hot/new/top) within non-pinned.
184 const pinnedSet = new Set(PINNED_OPIDS.map((s) => s.toLowerCase()))
185 const pinned = []
186 const rest = []
187 for (const p of state.posts) {
188 if (pinnedSet.has(p.opId.toLowerCase())) pinned.push(p)
189 else rest.push(p)
190 }
191 // Also preserve the order declared in PINNED_OPIDS.
192 pinned.sort((a, b) => PINNED_OPIDS.indexOf(a.opId) - PINNED_OPIDS.indexOf(b.opId))
193
194 // Display indices are continuous across sections so `open <n>` still works.
195 state._display = [...pinned, ...rest]
196
197 if (!state._display.length) {
198 process.stdout.write(`${ANSI.dim} no posts yet. try: submit "hello"${ANSI.reset}\n`)
199 } else {
200 if (pinned.length) {
201 process.stdout.write(`${ANSI.orange}${ANSI.dim}─── pinned ───${ANSI.reset}\n`)
202 pinned.forEach((p, i) => renderPostLine(p, i + 1, true))
203 if (rest.length) process.stdout.write(`${ANSI.gray}${ANSI.dim}─── ${state.sort} ───${ANSI.reset}\n`)
204 }
205 rest.forEach((p, i) => renderPostLine(p, pinned.length + i + 1, false))
206 }
207
208 process.stdout.write(`\n${ANSI.dim}commands: submit | open <n> | up/down <n> | delete <n> | sort <hot|new|top> | refresh | help | quit${ANSI.reset}\n`)
209 }
210
211 function renderThread () {
212 process.stdout.write(ANSI.clear)
213 header()
214 const p = state.threadPost
215 if (!p) { process.stdout.write(`${ANSI.red}thread gone${ANSI.reset}\n`); return }
216 const score = (p.score >= 0 ? '+' : '') + p.score
217 const author = p.authorNick || short(p.author, 8)
218 process.stdout.write(`${ANSI.bold}${p.title || '(no title)'}${ANSI.reset}\n`)
219 if (p.link) process.stdout.write(`${ANSI.cyan}${p.link}${ANSI.reset}\n`)
220 process.stdout.write(`${ANSI.dim}${score} pts · ${fmtAgo(p.ts)} · ${author}${ANSI.reset}\n\n`)
221 if (p.body) process.stdout.write(p.body + '\n\n')
222 process.stdout.write(`${ANSI.gray}── ${state.threadComments.length} comment(s) ──${ANSI.reset}\n`)
223 state.threadComments.forEach((c, i) => {
224 const a = c.authorNick || short(c.author, 8)
225 process.stdout.write(`\n${ANSI.gray}[${i + 1}]${ANSI.reset} ${a} ${ANSI.dim}· ${fmtAgo(c.ts)} · ${c.score >= 0 ? '+' : ''}${c.score}${ANSI.reset}\n`)
226 process.stdout.write(c.body + '\n')
227 })
228 process.stdout.write(`\n${ANSI.dim}commands: reply <body> | up | down | back | refresh | help | quit${ANSI.reset}\n`)
229 }
230
231 async function refreshFeed () {
232 state.posts = await rpc.dispatch('listPosts', { board: 'front', sort: state.sort, limit: 50 })
233 renderFeed()
234 }
235
236 async function refreshThread () {
237 const p = await rpc.dispatch('getPost', { opId: state.threadId })
238 if (!p) { state.threadId = null; await refreshFeed(); return }
239 state.threadPost = p
240 state.threadComments = await rpc.dispatch('listCommentTree', { rootOpId: state.threadId })
241 // flatten and sort by ts asc for simple rendering
242 state.threadComments.sort((a, b) => a.ts - b.ts)
243 renderThread()
244 }
245
246 // Auto-redraw when new data arrives from peers
247 let redrawTimer = null
248 const scheduleRedraw = () => {
249 if (redrawTimer) return
250 redrawTimer = setTimeout(async () => {
251 redrawTimer = null
252 if (state.threadId) await refreshThread()
253 else await refreshFeed()
254 }, 500)
255 }
256 node.on('track', scheduleRedraw)
257
258 // ---- REPL (stdin line buffer, portable between Bare and Node) ----
259 const prompt = () => { process.stdout.write(`${ANSI.orange}›${ANSI.reset} `) }
260 function printLine (s) { process.stdout.write(s + '\n'); prompt() }
261
262 let stdinBuf = ''
263 if (process.stdin.setEncoding) process.stdin.setEncoding('utf8')
264 if (process.stdin.ref) process.stdin.ref() // keep event loop alive
265 if (process.stdin.resume) process.stdin.resume() // start reading
266 process.stdin.on('data', (chunk) => {
267 stdinBuf += typeof chunk === 'string' ? chunk : chunk.toString('utf8')
268 let nl
269 while ((nl = stdinBuf.indexOf('\n')) >= 0) {
270 const line = stdinBuf.slice(0, nl).replace(/\r$/, '')
271 stdinBuf = stdinBuf.slice(nl + 1)
272 handle(line)
273 }
274 })
275 const shutdown = async () => {
276 process.stdout.write('\n' + ANSI.dim + 'goodbye.' + ANSI.reset + '\n')
277 try { await indexer.close() } catch {}
278 try { await node.close() } catch {}
279 if (typeof Pear !== 'undefined' && Pear.exit) Pear.exit(0)
280 else process.exit(0)
281 }
282 process.stdin.on('end', shutdown)
283 if (typeof Pear !== 'undefined' && Pear.teardown) Pear.teardown(shutdown)
284
285 function parseCmd (line) {
286 const s = line.trim()
287 if (!s) return null
288 // Primitive parse: first word + rest
289 const i = s.indexOf(' ')
290 if (i < 0) return { cmd: s.toLowerCase(), rest: '' }
291 return { cmd: s.slice(0, i).toLowerCase(), rest: s.slice(i + 1).trim() }
292 }
293
294 function parseQuoted (s) {
295 // "title" rest-as-body
296 const m = s.match(/^"([^"]*)"\s*(.*)$/)
297 if (m) return { title: m[1], rest: m[2] }
298 // fallback: split on first space
299 const i = s.indexOf(' ')
300 if (i < 0) return { title: s, rest: '' }
301 return { title: s.slice(0, i), rest: s.slice(i + 1) }
302 }
303
304 const isUrl = (s) => /^https?:\/\//.test(s)
305
306 async function handle (line) {
307 const parsed = parseCmd(line)
308 if (!parsed) { prompt(); return }
309 const { cmd, rest } = parsed
310 try {
311 switch (cmd) {
312 case 'help':
313 case '?':
314 printLine(`
315 ${ANSI.bold}feed commands${ANSI.reset}
316 submit "title" <body-or-url> submit a post
317 open <n> open thread for post #n
318 up <n> down <n> vote on post #n
319 sort <hot|new|top> change feed sort
320 refresh r reload feed
321
322 ${ANSI.bold}thread commands${ANSI.reset}
323 reply <body> comment on current thread
324 up down vote on the post (in thread)
325 back b back to feed
326
327 ${ANSI.bold}moderation${ANSI.reset}
328 delete <n> delete a post (author or admin)
329 delete delete current thread (in thread)
330
331 ${ANSI.bold}identity + infra${ANSI.reset}
332 nick <name> set your nickname
333 me show your pubkey + stats
334 relays where am i pinned? (or current thread author)
335 opid <n> copy a post's opId
336 quit q exit
337${amAdmin ? '\n ' + ANSI.orange + '(admin) ' + ANSI.reset + 'you can delete anyone\'s posts + comments.' : ''}
338`)
339 break
340 case 'q':
341 case 'quit':
342 case 'exit':
343 await shutdown()
344 return
345 case 'me': {
346 const stats = await rpc.dispatch('getStats')
347 printLine(` pubkey: ${stats.pubkey}\n peers: ${stats.peerCount} · tracked: ${stats.trackedUsers} · boards: ${stats.boards}`)
348 break
349 }
350 case 'nick': {
351 const name = rest.trim()
352 if (!name) { printLine('usage: nick <name>'); break }
353 await rpc.dispatch('setProfile', { nick: name })
354 nick = name
355 printLine(`${ANSI.green}nickname set: ${name}${ANSI.reset}`)
356 break
357 }
358 case 'sort': {
359 const s = rest.trim()
360 if (!['hot', 'new', 'top'].includes(s)) { printLine('usage: sort hot|new|top'); break }
361 state.sort = s
362 await refreshFeed()
363 prompt()
364 return
365 }
366 case 'r':
367 case 'refresh':
368 if (state.threadId) await refreshThread()
369 else await refreshFeed()
370 prompt()
371 return
372 case 'submit': {
373 const { title, rest: body } = parseQuoted(rest)
374 if (!title) { printLine('usage: submit "title" <body-or-url>'); break }
375 const link = isUrl(body.trim()) ? body.trim() : null
376 const text = link ? '' : body
377 process.stdout.write(`${ANSI.dim}minting pow…${ANSI.reset}`)
378 const t0 = Date.now()
379 const { opId } = await rpc.dispatch('createPost', { title, body: text, link })
380 await refreshFeed()
381 // Print the opId AFTER the feed clear so it stays visible.
382 process.stdout.write(`${ANSI.green}posted in ${Date.now() - t0}ms${ANSI.reset}\n`)
383 process.stdout.write(`${ANSI.dim}opId (copy for pinning): ${ANSI.reset}${opId}\n`)
384 prompt()
385 return
386 }
387 case 'open': {
388 const n = parseInt(rest, 10)
389 if (!n || !state._display[n - 1]) { printLine(`no post #${n}`); break }
390 state.threadId = state._display[n - 1].opId
391 await refreshThread()
392 prompt()
393 return
394 }
395 case 'back':
396 case 'b':
397 state.threadId = null
398 state.threadPost = null
399 state.threadComments = []
400 await refreshFeed()
401 prompt()
402 return
403 case 'up':
404 case 'down': {
405 const dir = cmd === 'up' ? 1 : -1
406 let targetOpId
407 if (state.threadId) targetOpId = state.threadId
408 else {
409 const n = parseInt(rest, 10)
410 if (!n || !state._display[n - 1]) { printLine(`no post #${n}`); break }
411 targetOpId = state._display[n - 1].opId
412 }
413 await rpc.dispatch('vote', { targetOpId, dir })
414 printLine(`${ANSI.green}voted ${cmd}${ANSI.reset}`)
415 if (state.threadId) await refreshThread()
416 else await refreshFeed()
417 return
418 }
419 case 'relays': {
420 if (!relayClient) { printLine(`${ANSI.dim}relay client not connected yet…${ANSI.reset}`); break }
421 const targetHex = state.threadPost?.author || b4a.toString(node.pubkey, 'hex')
422 const keyBuf = b4a.from(targetHex, 'hex')
423 process.stdout.write(`${ANSI.dim}querying…${ANSI.reset}`)
424 try {
425 const acc = await relayClient.seed(keyBuf, { replicas: 5, timeout: 15000 })
426 process.stdout.write('\r \r')
427 printLine(`${ANSI.dim}pinned on ${acc.length} relay(s) for ${b4a.toString(keyBuf, 'hex').slice(0, 12)}…:${ANSI.reset}`)
428 for (const a of acc) {
429 const pk = b4a.toString(a.relayPubkey, 'hex').slice(0, 12)
430 printLine(` ${a.region.padEnd(5)} ${pk}…`)
431 }
432 } catch (err) {
433 printLine(`${ANSI.red}relay query failed: ${err.message}${ANSI.reset}`)
434 }
435 return
436 }
437 case 'opid': {
438 const n = parseInt(rest, 10)
439 const post = state._display[n - 1]
440 if (!post) { printLine(`no post #${n}`); break }
441 printLine(`${ANSI.dim}opId #${n}:${ANSI.reset} ${post.opId}`)
442 break
443 }
444 case 'delete':
445 case 'rm': {
446 let target, author
447 if (state.threadId) {
448 target = state.threadId
449 author = state.threadPost?.author
450 } else {
451 const n = parseInt(rest, 10)
452 const post = state._display[n - 1]
453 if (!post) { printLine(`no post #${n}`); break }
454 target = post.opId
455 author = post.author
456 }
457 const isAuthor = author && author.toLowerCase() === myPubkey.toLowerCase()
458 if (!isAuthor && !amAdmin) { printLine(`${ANSI.red}only the author (or admin) can delete${ANSI.reset}`); break }
459 await rpc.dispatch('deleteOp', { opId: target })
460 printLine(`${ANSI.green}tombstoned (hidden — signed history remains)${ANSI.reset}`)
461 if (state.threadId) { state.threadId = null; state.threadPost = null }
462 await refreshFeed()
463 prompt()
464 return
465 }
466 case 'reply': {
467 if (!state.threadId) { printLine('no thread open. use: open <n>'); break }
468 if (!rest.trim()) { printLine('usage: reply <body>'); break }
469 process.stdout.write(`${ANSI.dim}minting pow…${ANSI.reset}`)
470 const t0 = Date.now()
471 await rpc.dispatch('createComment', { parentOpId: state.threadId, body: rest })
472 process.stdout.write(`\r${ANSI.green}replied in ${Date.now() - t0}ms${ANSI.reset} \n`)
473 await refreshThread()
474 prompt()
475 return
476 }
477 default:
478 printLine(`unknown: ${cmd}. try 'help'`)
479 }
480 } catch (err) {
481 printLine(`${ANSI.red}error: ${err.message}${ANSI.reset}`)
482 }
483 prompt()
484 }
485
486 await refreshFeed()
487 prompt()
488}
489
490main().catch((err) => {
491 console.error('[boot] fatal error:', (err && err.stack) || String(err))
492 if (typeof Bare !== 'undefined' && Bare.exit) Bare.exit(1)
493 else process.exit(1)
494})
test/m1.js +165 -0
……@@ -0,0 +1,165 @@
1'use strict'
2
3// M1 integration test — runs with plain `node test/m1.js`.
4// No networking, no Pear/Bare runtime. Just identity + ops + PoW + local core.
5
6const assert = require('assert/strict')
7const b4a = require('b4a')
8const { Node } = require('../src/backend/node')
9const { TYPE, parseOpId, encodeOp, decodeOp, makeOpId } = require('../src/backend/ops')
10const pow = require('../src/backend/pow')
11
12const tests = []
13function test (name, fn) { tests.push({ name, fn }) }
14
15test('identity is stable: same primary key → same pubkey', async () => {
16 const a = await Node.openTemp()
17 const pk = a.primaryKey
18 const pubkey1 = b4a.from(a.pubkey)
19 await a.close()
20
21 const b = await Node.openTemp({ primaryKey: pk })
22 const pubkey2 = b4a.from(b.pubkey)
23 await b.close()
24
25 assert.equal(b4a.toString(pubkey1, 'hex'), b4a.toString(pubkey2, 'hex'))
26})
27
28test('post appends, mints PoW, round-trips through encode/decode', async () => {
29 const node = await Node.openTemp()
30 const t0 = Date.now()
31 const { op, opId, seq } = await node.post('general', 'hello p2pbuilders', 'first post ever')
32 const t1 = Date.now()
33
34 assert.equal(op.type, TYPE.POST)
35 assert.equal(op.payload.board, 'general')
36 assert.equal(op.payload.title, 'hello p2pbuilders')
37 assert.equal(op.payload.body, 'first post ever')
38 assert.equal(op.payload.link, null)
39 assert.ok(op.pow, 'post must carry PoW')
40 assert.ok(op.pow.bits >= pow.DEFAULT_BITS.post)
41 assert.equal(op.pow.nonce.length, 8)
42
43 assert.equal(seq, 0)
44 const parsed = parseOpId(opId)
45 assert.equal(b4a.toString(parsed.pubkey, 'hex'), b4a.toString(node.pubkey, 'hex'))
46 assert.equal(parsed.seq, 0)
47
48 // round-trip via core read
49 const readBack = await node.getOp(0)
50 assert.equal(readBack.payload.title, 'hello p2pbuilders')
51 assert.ok(b4a.equals(readBack.pow.nonce, op.pow.nonce))
52
53 console.log(` post minted in ${t1 - t0}ms at ${op.pow.bits} bits`)
54 await node.close()
55})
56
57test('comment threads by parent opId', async () => {
58 const node = await Node.openTemp()
59 const postRes = await node.post('general', 't', 'b')
60 const commentRes = await node.comment(postRes.opId, 'nice post')
61
62 const c = await node.getOp(commentRes.seq)
63 assert.equal(c.type, TYPE.COMMENT)
64 assert.ok(b4a.equals(c.payload.parent, postRes.opId))
65 assert.equal(c.payload.body, 'nice post')
66 assert.ok(c.pow, 'comment must carry PoW')
67 await node.close()
68})
69
70test('vote has no PoW, dir constrained to -1/0/+1', async () => {
71 const node = await Node.openTemp()
72 const { opId } = await node.post('general', 't', 'b')
73
74 const up = await node.vote(opId, 1)
75 const upOp = await node.getOp(up.seq)
76 assert.equal(upOp.type, TYPE.VOTE)
77 assert.equal(upOp.payload.dir, 1)
78 assert.equal(upOp.pow, null, 'vote must not carry PoW')
79
80 await assert.rejects(() => node.vote(opId, 2), /dir must be/)
81 await node.close()
82})
83
84test('PoW verify accepts valid, rejects tampered op', async () => {
85 const node = await Node.openTemp()
86 const { op } = await node.post('general', 'x', 'y')
87
88 assert.equal(pow.verify(op, op.pow.bits), true, 'fresh PoW verifies')
89
90 // tamper body — hash changes, PoW fails
91 const tampered = { ...op, payload: { ...op.payload, body: 'different' } }
92 assert.equal(pow.verify(tampered, op.pow.bits), false, 'tampered op fails verify')
93
94 // tamper nonce
95 const badNonce = b4a.from(op.pow.nonce)
96 badNonce[0] ^= 0xff
97 const tampered2 = { ...op, pow: { ...op.pow, nonce: badNonce } }
98 assert.equal(pow.verify(tampered2, op.pow.bits), false, 'bad nonce fails verify')
99 await node.close()
100})
101
102test('encodeOp / decodeOp round-trip exact', async () => {
103 const node = await Node.openTemp()
104 const { op } = await node.post('general', 'title', 'body', { link: 'https://example.com' })
105 const buf = encodeOp(op)
106 const back = decodeOp(buf)
107 assert.equal(back.v, op.v)
108 assert.equal(back.type, op.type)
109 assert.equal(back.ts, op.ts)
110 assert.deepEqual(back.payload, op.payload)
111 assert.ok(b4a.equals(back.pow.nonce, op.pow.nonce))
112 assert.equal(back.pow.bits, op.pow.bits)
113 await node.close()
114})
115
116test('opId parse <-> makeOpId is consistent', () => {
117 const pk = b4a.alloc(32)
118 for (let i = 0; i < 32; i++) pk[i] = i
119 const id = makeOpId(pk, 42)
120 const parsed = parseOpId(id)
121 assert.equal(b4a.toString(parsed.pubkey, 'hex'), b4a.toString(pk, 'hex'))
122 assert.equal(parsed.seq, 42)
123})
124
125test('leadingZeroBits counts correctly', () => {
126 assert.equal(pow.leadingZeroBits(b4a.from([0xff])), 0)
127 assert.equal(pow.leadingZeroBits(b4a.from([0x7f])), 1)
128 assert.equal(pow.leadingZeroBits(b4a.from([0x01])), 7)
129 assert.equal(pow.leadingZeroBits(b4a.from([0x00, 0xff])), 8)
130 assert.equal(pow.leadingZeroBits(b4a.from([0x00, 0x00, 0x40])), 17)
131 assert.equal(pow.leadingZeroBits(b4a.from([0x00, 0x00, 0x00])), 24)
132})
133
134test('iterate ops', async () => {
135 const node = await Node.openTemp()
136 await node.post('general', 'a', '1')
137 await node.post('general', 'b', '2')
138 await node.post('general', 'c', '3')
139
140 const collected = []
141 for await (const entry of node.ops()) collected.push(entry)
142 assert.equal(collected.length, 3)
143 assert.equal(collected[0].op.payload.title, 'a')
144 assert.equal(collected[2].op.payload.title, 'c')
145 assert.equal(collected[2].seq, 2)
146 await node.close()
147})
148
149// runner
150;(async () => {
151 let pass = 0; let fail = 0
152 for (const { name, fn } of tests) {
153 try {
154 await fn()
155 console.log(` ✓ ${name}`)
156 pass++
157 } catch (err) {
158 console.log(` ✗ ${name}`)
159 console.log(` ${err.stack || err.message}`)
160 fail++
161 }
162 }
163 console.log(`\n${pass}/${pass + fail} passed`)
164 process.exit(fail ? 1 : 0)
165})()
test/m10-bare.js +54 -0
……@@ -0,0 +1,54 @@
1'use strict'
2
3// M10: end-to-end smoke test for the Bare harness (shared by Pear desktop
4// worker and bare-kit-pear iOS). Verifies our backend boots and speaks
5// RPC under the real Bare runtime.
6//
7// Spawns `bare src/bare/harness.js` as a subprocess, captures its stdout,
8// and asserts the expected milestones appear.
9
10const assert = require('assert/strict')
11const { spawn } = require('child_process')
12const path = require('path')
13
14const BARE = path.join(__dirname, '..', 'node_modules', 'bare', 'bin', 'bare')
15const HARNESS = path.join(__dirname, '..', 'src', 'bare', 'harness.js')
16
17function runBareHarness ({ timeoutMs = 30000 } = {}) {
18 return new Promise((resolve, reject) => {
19 const proc = spawn(BARE, [HARNESS], { stdio: ['ignore', 'pipe', 'pipe'] })
20 let stdout = ''
21 let stderr = ''
22 proc.stdout.on('data', (b) => { stdout += b.toString() })
23 proc.stderr.on('data', (b) => { stderr += b.toString() })
24 const timer = setTimeout(() => {
25 proc.kill('SIGKILL')
26 reject(new Error(`bare harness timed out after ${timeoutMs}ms\nstdout:\n${stdout}\nstderr:\n${stderr}`))
27 }, timeoutMs)
28 proc.on('exit', (code) => {
29 clearTimeout(timer)
30 if (code !== 0) return reject(new Error(`bare exited ${code}\nstdout:\n${stdout}\nstderr:\n${stderr}`))
31 resolve({ stdout, stderr })
32 })
33 })
34}
35
36;(async () => {
37 try {
38 const { stdout } = await runBareHarness()
39 assert.match(stdout, /\[bare\] starting in/)
40 assert.match(stdout, /\[bare\] pubkey: [0-9a-f]{64}/)
41 assert.match(stdout, /\[bare\] posted opId: [0-9a-f]{16}/)
42 assert.match(stdout, /\[bare\] listPosts returned 1 post/)
43 assert.match(stdout, /\[bare\] first title: hello from bare/)
44 assert.match(stdout, /\[bare\] ok/)
45 console.log(' ✓ backend boots under standalone Bare and answers RPC')
46 console.log('\n1/1 passed')
47 process.exit(0)
48 } catch (err) {
49 console.log(' ✗ backend boots under standalone Bare')
50 console.log(` ${err.message}`)
51 console.log('\n0/1 passed')
52 process.exit(1)
53 }
54})()
test/m11.js +144 -0
……@@ -0,0 +1,144 @@
1'use strict'
2
3// M11 — MVP feature additions:
4// - listCommentTree: returns nested comments flat, with parent pointers
5// - exportPrimaryKey: reveals the identity key
6// - edit/delete still work over RPC
7
8const assert = require('assert/strict')
9const b4a = require('b4a')
10const { Node } = require('../src/backend/node')
11const { Indexer } = require('../src/backend/indexer')
12const { createRPC } = require('../src/backend/rpc')
13
14const tests = []
15function test (name, fn) { tests.push({ name, fn }) }
16
17async function waitFor (pred, { timeoutMs = 5000, intervalMs = 20 } = {}) {
18 const t0 = Date.now()
19 while (Date.now() - t0 < timeoutMs) {
20 if (await pred()) return
21 await new Promise(r => setTimeout(r, intervalMs))
22 }
23 throw new Error('waitFor timeout')
24}
25
26test('exportPrimaryKey returns 64-char hex', async () => {
27 const node = await Node.openTemp()
28 const idx = new Indexer(node); await idx.ready()
29 const rpc = createRPC({ node, indexer: idx })
30
31 const { hex } = await rpc.dispatch('exportPrimaryKey')
32 assert.match(hex, /^[0-9a-f]{64}$/)
33 assert.equal(b4a.toString(node.primaryKey, 'hex'), hex)
34
35 await idx.close(); await node.close()
36})
37
38test('listCommentTree returns nested descendants, not just direct children', async () => {
39 const node = await Node.openTemp()
40 const idx = new Indexer(node); await idx.ready()
41 const rpc = createRPC({ node, indexer: idx })
42
43 const { opId } = await node.post('front', 't', 'root')
44 const rootHex = b4a.toString(opId, 'hex')
45
46 const c1 = await node.comment(opId, 'child of post', { ts: 1 })
47 const c2 = await node.comment(c1.opId, 'grandchild', { ts: 2 })
48 await node.comment(c2.opId, 'great-grandchild', { ts: 3 })
49 await node.comment(opId, 'sibling of c1', { ts: 4 })
50
51 await waitFor(async () => {
52 const tree = await rpc.dispatch('listCommentTree', { rootOpId: rootHex })
53 return tree.length === 4
54 })
55
56 const tree = await rpc.dispatch('listCommentTree', { rootOpId: rootHex })
57 assert.equal(tree.length, 4, '4 descendants total')
58
59 // Parent pointers should form a valid chain
60 const byId = Object.fromEntries(tree.map((c) => [c.opId, c]))
61 const parents = tree.map((c) => c.parent)
62 // Count: 2 children of root, 1 child of c1, 1 child of c2
63 const parentCounts = new Map()
64 for (const p of parents) parentCounts.set(p, (parentCounts.get(p) || 0) + 1)
65 assert.equal(parentCounts.get(rootHex), 2, 'two direct replies to root')
66
67 // The great-grandchild should exist and reference c2
68 const greats = tree.filter((c) => c.body === 'great-grandchild')
69 assert.equal(greats.length, 1)
70 assert.equal(byId[greats[0].parent].body, 'grandchild')
71
72 await idx.close(); await node.close()
73})
74
75test('listCommentTree respects maxNodes cap', async () => {
76 const node = await Node.openTemp()
77 const idx = new Indexer(node); await idx.ready()
78 const rpc = createRPC({ node, indexer: idx })
79
80 const post = await node.post('front', 't', 'b')
81 for (let i = 0; i < 20; i++) await node.comment(post.opId, `c${i}`, { ts: i })
82
83 await waitFor(async () => {
84 const tree = await rpc.dispatch('listCommentTree', {
85 rootOpId: b4a.toString(post.opId, 'hex')
86 })
87 return tree.length >= 10
88 })
89
90 const capped = await rpc.dispatch('listCommentTree', {
91 rootOpId: b4a.toString(post.opId, 'hex'),
92 maxNodes: 5
93 })
94 assert.equal(capped.length, 5)
95
96 await idx.close(); await node.close()
97})
98
99test('editOp + deleteOp via RPC surface the overlay/tombstone', async () => {
100 const node = await Node.openTemp()
101 const idx = new Indexer(node); await idx.ready()
102 const rpc = createRPC({ node, indexer: idx })
103
104 const { opId } = await node.post('front', 'original', 'body')
105 const hex = b4a.toString(opId, 'hex')
106
107 await waitFor(async () => (await rpc.dispatch('getPost', { opId: hex })) != null)
108
109 await rpc.dispatch('editOp', { opId: hex, body: 'edited body', title: 'edited title' })
110 await waitFor(async () => {
111 const p = await rpc.dispatch('getPost', { opId: hex })
112 return p.title === 'edited title'
113 })
114 const edited = await rpc.dispatch('getPost', { opId: hex })
115 assert.equal(edited.body, 'edited body')
116 assert.equal(edited.edited, true)
117
118 await rpc.dispatch('deleteOp', { opId: hex })
119 await waitFor(async () => (await idx.isTombstoned(opId)))
120
121 // Deleted posts disappear from listPosts
122 const posts = await rpc.dispatch('listPosts', { board: 'front' })
123 assert.equal(posts.find((p) => p.opId === hex), undefined)
124
125 await idx.close(); await node.close()
126})
127
128// runner
129;(async () => {
130 let pass = 0, fail = 0
131 for (const { name, fn } of tests) {
132 try {
133 await fn()
134 console.log(` ✓ ${name}`)
135 pass++
136 } catch (err) {
137 console.log(` ✗ ${name}`)
138 console.log(` ${err.stack || err.message}`)
139 fail++
140 }
141 }
142 console.log(`\n${pass}/${pass + fail} passed`)
143 process.exit(fail ? 1 : 0)
144})()
test/m2.js +155 -0
……@@ -0,0 +1,155 @@
1'use strict'
2
3// M2 integration tests: replication between two nodes.
4//
5// Two scenarios:
6// 1. direct-stream replication (fast, deterministic, no DHT)
7// 2. Hyperswarm over a local DHT testnet (real, slower)
8
9const assert = require('assert/strict')
10const b4a = require('b4a')
11const { Node } = require('../src/backend/node')
12const { connectDirect } = require('../src/backend/swarm')
13const { boardTopic } = require('../src/backend/board')
14const createTestnet = require('hyperdht/testnet')
15
16const tests = []
17function test (name, fn) { tests.push({ name, fn }) }
18
19// ---------------------------------------------------------------------------
20// Direct-stream tests (no swarm)
21// ---------------------------------------------------------------------------
22
23test('direct: B replicates A\'s posts after trackUser', async () => {
24 const a = await Node.openTemp()
25 const b = await Node.openTemp()
26
27 const { opId: postId } = await a.post('general', 'hello from A', 'replicated?')
28 await a.comment(postId, 'and a follow-up comment')
29
30 const conn = connectDirect(a.store, b.store)
31
32 const remote = await b.trackUser(a.pubkey)
33 await remote.update({ wait: true })
34
35 assert.equal(remote.length, 2, 'B sees 2 ops from A')
36
37 const collected = []
38 for await (const entry of b.userOps(a.pubkey)) collected.push(entry)
39
40 assert.equal(collected.length, 2)
41 assert.equal(collected[0].op.payload.title, 'hello from A')
42 assert.equal(collected[1].op.payload.body, 'and a follow-up comment')
43
44 conn.close()
45 await a.close()
46 await b.close()
47})
48
49test('direct: live update — new post after connection propagates', async () => {
50 const a = await Node.openTemp()
51 const b = await Node.openTemp()
52
53 const conn = connectDirect(a.store, b.store)
54 const remote = await b.trackUser(a.pubkey)
55 await remote.ready()
56
57 // A posts AFTER connection is established
58 await a.post('general', 'live post', 'body')
59
60 // wait for live update to arrive
61 await waitFor(() => remote.length >= 1, 5000)
62 assert.equal(remote.length, 1)
63
64 const op = require('../src/backend/ops').decodeOp(await remote.get(0))
65 assert.equal(op.payload.title, 'live post')
66
67 conn.close()
68 await a.close()
69 await b.close()
70})
71
72test('direct: remote pubkey round-trips via opId', async () => {
73 const a = await Node.openTemp()
74 const b = await Node.openTemp()
75 const { opId } = await a.post('general', 't', 'b')
76
77 const conn = connectDirect(a.store, b.store)
78 const remote = await b.trackUser(a.pubkey)
79 await remote.update({ wait: true })
80
81 for await (const entry of b.userOps(a.pubkey)) {
82 assert.ok(b4a.equals(entry.opId, opId))
83 break
84 }
85 conn.close()
86 await a.close()
87 await b.close()
88})
89
90// ---------------------------------------------------------------------------
91// Hyperswarm testnet tests
92// ---------------------------------------------------------------------------
93
94test('swarm: two nodes joining same board connect + replicate', async () => {
95 const testnet = await createTestnet(3, { teardown: () => {} })
96 const bootstrap = testnet.bootstrap
97
98 const a = await Node.openTemp({ swarm: { bootstrap } })
99 const b = await Node.openTemp({ swarm: { bootstrap } })
100
101 await a.post('general', 'swarm-test', 'over the wire')
102
103 // Both join the same board topic.
104 await a.joinBoard('general')
105 await b.joinBoard('general')
106
107 // Wait for them to find each other.
108 await b.swarm.waitForPeers(1, { timeoutMs: 15000 })
109
110 // B now follows A's core.
111 const remote = await b.trackUser(a.pubkey)
112 await remote.update({ wait: true })
113
114 assert.equal(remote.length, 1, 'A\'s post is visible on B')
115 const op = require('../src/backend/ops').decodeOp(await remote.get(0))
116 assert.equal(op.payload.title, 'swarm-test')
117
118 await a.close()
119 await b.close()
120 await testnet.destroy()
121})
122
123// ---------------------------------------------------------------------------
124// helpers
125// ---------------------------------------------------------------------------
126
127function waitFor (predicate, timeoutMs) {
128 return new Promise((resolve, reject) => {
129 const start = Date.now()
130 const tick = () => {
131 if (predicate()) return resolve()
132 if (Date.now() - start > timeoutMs) return reject(new Error('waitFor timeout'))
133 setTimeout(tick, 50)
134 }
135 tick()
136 })
137}
138
139// runner
140;(async () => {
141 let pass = 0; let fail = 0
142 for (const { name, fn } of tests) {
143 try {
144 await fn()
145 console.log(` ✓ ${name}`)
146 pass++
147 } catch (err) {
148 console.log(` ✗ ${name}`)
149 console.log(` ${err.stack || err.message}`)
150 fail++
151 }
152 }
153 console.log(`\n${pass}/${pass + fail} passed`)
154 process.exit(fail ? 1 : 0)
155})()
test/m3.js +109 -0
……@@ -0,0 +1,109 @@
1'use strict'
2
3// M3: peer key-exchange via the announce protomux channel.
4// Two nodes that join the same board should auto-discover each other's pubkey
5// without any manual trackUser() call.
6
7const assert = require('assert/strict')
8const b4a = require('b4a')
9const { Node } = require('../src/backend/node')
10const createTestnet = require('hyperdht/testnet')
11const { decodeOp } = require('../src/backend/ops')
12
13const tests = []
14function test (name, fn) { tests.push({ name, fn }) }
15
16test('swarm+announce: B auto-tracks A after joining same board', async () => {
17 const testnet = await createTestnet(3, { teardown: () => {} })
18 const bootstrap = testnet.bootstrap
19
20 const a = await Node.openTemp({ swarm: { bootstrap } })
21 const b = await Node.openTemp({ swarm: { bootstrap } })
22
23 await a.post('general', 'auto-discovery test', 'body')
24
25 await a.joinBoard('general')
26 await b.joinBoard('general')
27
28 // Wait for B to receive an announce for A's pubkey.
29 await waitForEvent(b, 'track', (pubkey) => b4a.equals(pubkey, a.pubkey), 15000)
30
31 const remote = b.userCore(a.pubkey)
32 assert.ok(remote, 'B has tracked A')
33 await remote.update({ wait: true })
34 assert.equal(remote.length, 1)
35 const op = decodeOp(await remote.get(0))
36 assert.equal(op.payload.title, 'auto-discovery test')
37
38 await a.close()
39 await b.close()
40 await testnet.destroy()
41})
42
43test('swarm+announce: transitive discovery (C learns A via B)', async () => {
44 const testnet = await createTestnet(3, { teardown: () => {} })
45 const bootstrap = testnet.bootstrap
46
47 const a = await Node.openTemp({ swarm: { bootstrap } })
48 const b = await Node.openTemp({ swarm: { bootstrap } })
49 const c = await Node.openTemp({ swarm: { bootstrap } })
50
51 await a.post('general', 'hello', 'from A')
52
53 // B tracks A first, A and B on 'general' board.
54 await a.joinBoard('general')
55 await b.joinBoard('general')
56 await waitForEvent(b, 'track', (pk) => b4a.equals(pk, a.pubkey), 15000)
57
58 // C joins a DIFFERENT board from A, but same as B (call it 'meta').
59 // B also joins 'meta'. Via B, C should learn about A's pubkey.
60 await b.joinBoard('meta')
61 await c.joinBoard('meta')
62
63 await waitForEvent(c, 'track', (pk) => b4a.equals(pk, a.pubkey), 15000)
64
65 const aCoreOnC = c.userCore(a.pubkey)
66 assert.ok(aCoreOnC, 'C discovered A via B')
67
68 await a.close()
69 await b.close()
70 await c.close()
71 await testnet.destroy()
72})
73
74// ---------------------------------------------------------------------------
75
76function waitForEvent (emitter, event, predicate, timeoutMs) {
77 return new Promise((resolve, reject) => {
78 const timer = setTimeout(() => {
79 emitter.off(event, handler)
80 reject(new Error(`timeout waiting for ${event}`))
81 }, timeoutMs)
82 const handler = (arg) => {
83 if (predicate(arg)) {
84 clearTimeout(timer)
85 emitter.off(event, handler)
86 resolve(arg)
87 }
88 }
89 emitter.on(event, handler)
90 })
91}
92
93// runner
94;(async () => {
95 let pass = 0; let fail = 0
96 for (const { name, fn } of tests) {
97 try {
98 await fn()
99 console.log(` ✓ ${name}`)
100 pass++
101 } catch (err) {
102 console.log(` ✗ ${name}`)
103 console.log(` ${err.stack || err.message}`)
104 fail++
105 }
106 }
107 console.log(`\n${pass}/${pass + fail} passed`)
108 process.exit(fail ? 1 : 0)
109})()
test/m4.js +224 -0
……@@ -0,0 +1,224 @@
1'use strict'
2
3// M4: local Hyperbee indexer.
4
5const assert = require('assert/strict')
6const b4a = require('b4a')
7const { Node } = require('../src/backend/node')
8const { Indexer } = require('../src/backend/indexer')
9const { connectDirect } = require('../src/backend/swarm')
10
11const tests = []
12function test (name, fn) { tests.push({ name, fn }) }
13
14async function indexed (node) {
15 const idx = new Indexer(node)
16 await idx.ready()
17 return idx
18}
19
20// tiny helper to wait until a predicate on the indexer becomes true
21async function waitFor (predicate, { timeoutMs = 5000, intervalMs = 20 } = {}) {
22 const start = Date.now()
23 while (Date.now() - start < timeoutMs) {
24 if (await predicate()) return
25 await new Promise(r => setTimeout(r, intervalMs))
26 }
27 throw new Error('waitFor timeout')
28}
29
30test('listPosts: own posts appear newest-first', async () => {
31 const node = await Node.openTemp()
32 const idx = await indexed(node)
33
34 const p1 = await node.post('general', 'first', 'a', { ts: 1000 })
35 const p2 = await node.post('general', 'second', 'b', { ts: 2000 })
36 const p3 = await node.post('other', 'elsewhere', 'c', { ts: 1500 })
37
38 await waitFor(async () => {
39 const list = []
40 for await (const p of idx.listPosts('general')) list.push(p)
41 return list.length === 2
42 })
43
44 const list = []
45 for await (const p of idx.listPosts('general')) list.push(p)
46 assert.equal(list.length, 2)
47 assert.equal(list[0].ts, 2000)
48 assert.equal(list[1].ts, 1000)
49 assert.ok(b4a.equals(list[0].opId, p2.opId))
50 assert.ok(b4a.equals(list[1].opId, p1.opId))
51
52 // other board
53 const other = []
54 for await (const p of idx.listPosts('other')) other.push(p)
55 assert.equal(other.length, 1)
56 assert.ok(b4a.equals(other[0].opId, p3.opId))
57
58 await idx.close()
59 await node.close()
60})
61
62test('listComments: ordered by ts under parent', async () => {
63 const node = await Node.openTemp()
64 const idx = await indexed(node)
65
66 const post = await node.post('general', 't', 'b', { ts: 1000 })
67 const c1 = await node.comment(post.opId, 'first comment', { ts: 1100 })
68 const c2 = await node.comment(post.opId, 'second comment', { ts: 1200 })
69
70 await waitFor(async () => {
71 const list = []
72 for await (const c of idx.listComments(post.opId)) list.push(c)
73 return list.length === 2
74 })
75
76 const list = []
77 for await (const c of idx.listComments(post.opId)) list.push(c)
78 assert.equal(list.length, 2)
79 assert.ok(b4a.equals(list[0].opId, c1.opId))
80 assert.ok(b4a.equals(list[1].opId, c2.opId))
81
82 await idx.close()
83 await node.close()
84})
85
86test('getVoteTotals: up/down tally', async () => {
87 const a = await Node.openTemp()
88 const b = await Node.openTemp()
89 const conn = connectDirect(a.store, b.store)
90
91 const idxA = await indexed(a)
92
93 const post = await a.post('general', 't', 'b')
94 await a.vote(post.opId, 1) // self-upvote
95
96 // B follows A, then upvotes then downvotes (final = -1 but we count raw votes)
97 await b.trackUser(a.pubkey)
98 const remote = b.userCore(a.pubkey)
99 await remote.update({ wait: true })
100
101 await b.vote(post.opId, 1)
102
103 // A also needs to see B's votes
104 await a.trackUser(b.pubkey)
105 const bRemote = a.userCore(b.pubkey)
106 await bRemote.update({ wait: true })
107
108 await waitFor(async () => {
109 const t = await idxA.getVoteTotals(post.opId)
110 return t.up >= 2
111 })
112
113 const totals = await idxA.getVoteTotals(post.opId)
114 assert.equal(totals.up, 2, 'A + B upvotes')
115 assert.equal(totals.down, 0)
116
117 conn.close()
118 await idxA.close()
119 await a.close()
120 await b.close()
121})
122
123test('scoreBoard: ranks by net vote count', async () => {
124 const node = await Node.openTemp()
125 const other = await Node.openTemp()
126 const conn = connectDirect(node.store, other.store)
127
128 const idx = await indexed(node)
129
130 const p1 = await node.post('general', 'p1', '.', { ts: 1000 })
131 const p2 = await node.post('general', 'p2', '.', { ts: 2000 })
132 const p3 = await node.post('general', 'p3', '.', { ts: 3000 })
133
134 await other.trackUser(node.pubkey)
135 const remote = other.userCore(node.pubkey)
136 await remote.update({ wait: true })
137
138 await other.vote(p2.opId, 1)
139 await other.vote(p2.opId, 1) // duplicate vote from same key overwrites — but counted once in index
140
141 // p1 gets one vote from self-node
142 await node.vote(p1.opId, 1)
143
144 await node.trackUser(other.pubkey)
145 const otherRemote = node.userCore(other.pubkey)
146 await otherRemote.update({ wait: true })
147 await otherRemote.download({ start: 0, end: otherRemote.length }).done()
148
149 // Wait until the indexer has seen BOTH votes (on p1 and p2)
150 await waitFor(async () => {
151 const p1Votes = await idx.getVoteTotals(p1.opId)
152 const p2Votes = await idx.getVoteTotals(p2.opId)
153 return p1Votes.up >= 1 && p2Votes.up >= 1
154 })
155
156 const ranked = await idx.scoreBoard('general', { sort: 'top', weighted: false })
157 assert.equal(ranked.length, 3)
158 // p2 has 1 vote (from other, duplicate overwrote), p1 has 1 vote (from node)
159 // tiebreak by ts desc → p2 (ts=2000) before p1 (ts=1000)
160 assert.ok(b4a.equals(ranked[0].opId, p2.opId), 'p2 first')
161 assert.ok(b4a.equals(ranked[1].opId, p1.opId))
162 assert.ok(b4a.equals(ranked[2].opId, p3.opId))
163
164 conn.close()
165 await idx.close()
166 await node.close()
167 await other.close()
168})
169
170test('getBoard + listBoards: board_create indexed', async () => {
171 const node = await Node.openTemp()
172 const idx = await indexed(node)
173
174 await node.createBoard('newboard', 'a shiny new board', { minPowBits: 20 })
175
176 await waitFor(async () => (await idx.getBoard('newboard')) != null)
177
178 const b = await idx.getBoard('newboard')
179 assert.equal(b.description, 'a shiny new board')
180 assert.equal(b.minPowBits, 20)
181 assert.ok(b4a.equals(b.creator, node.pubkey))
182
183 const boards = []
184 for await (const name of idx.listBoards()) boards.push(name)
185 assert.deepEqual(boards, ['newboard'])
186
187 await idx.close()
188 await node.close()
189})
190
191test('first board_create wins (squatting policy)', async () => {
192 const node = await Node.openTemp()
193 const idx = await indexed(node)
194
195 await node.createBoard('squat', 'original', { minPowBits: 18 }, )
196 await waitFor(async () => (await idx.getBoard('squat')) != null)
197 await node.createBoard('squat', 'imposter', { minPowBits: 22 })
198
199 // race: wait a tick for 2nd op to be indexed
200 await new Promise(r => setTimeout(r, 100))
201 const b = await idx.getBoard('squat')
202 assert.equal(b.description, 'original', 'second registration ignored')
203
204 await idx.close()
205 await node.close()
206})
207
208// runner
209;(async () => {
210 let pass = 0; let fail = 0
211 for (const { name, fn } of tests) {
212 try {
213 await fn()
214 console.log(` ✓ ${name}`)
215 pass++
216 } catch (err) {
217 console.log(` ✗ ${name}`)
218 console.log(` ${err.stack || err.message}`)
219 fail++
220 }
221 }
222 console.log(`\n${pass}/${pass + fail} passed`)
223 process.exit(fail ? 1 : 0)
224})()
test/m5.js +174 -0
……@@ -0,0 +1,174 @@
1'use strict'
2
3// M5: end-to-end HTTP + WebSocket RPC smoke test.
4
5const assert = require('assert/strict')
6const http = require('http')
7const fs = require('fs')
8const os = require('os')
9const path = require('path')
10const WebSocket = require('ws')
11const { start } = require('../src/backend/server')
12
13const tests = []
14function test (name, fn) { tests.push({ name, fn }) }
15
16function fetchText (url) {
17 return new Promise((resolve, reject) => {
18 http.get(url, (res) => {
19 let body = ''
20 res.on('data', (c) => { body += c })
21 res.on('end', () => resolve({ status: res.statusCode, body, headers: res.headers }))
22 res.on('error', reject)
23 }).on('error', reject)
24 })
25}
26
27function rpcClient (url) {
28 const ws = new WebSocket(url)
29 const pending = new Map()
30 let id = 1
31 const ready = new Promise((resolve, reject) => {
32 ws.on('open', resolve)
33 ws.on('error', reject)
34 })
35 ws.on('message', (data) => {
36 const msg = JSON.parse(data.toString())
37 const p = pending.get(msg.id)
38 if (!p) return
39 pending.delete(msg.id)
40 if (msg.error) p.reject(new Error(msg.error))
41 else p.resolve(msg.result)
42 })
43 return {
44 ready,
45 call (method, params) {
46 const reqId = id++
47 return new Promise((resolve, reject) => {
48 pending.set(reqId, { resolve, reject })
49 ws.send(JSON.stringify({ id: reqId, method, params }))
50 })
51 },
52 close () { ws.close() }
53 }
54}
55
56async function waitFor (pred, { timeoutMs = 5000, intervalMs = 20 } = {}) {
57 const t0 = Date.now()
58 while (Date.now() - t0 < timeoutMs) {
59 if (await pred()) return
60 await new Promise(r => setTimeout(r, intervalMs))
61 }
62 throw new Error('waitFor timeout')
63}
64
65test('server boots, serves index.html and static assets', async () => {
66 const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p2pbuilders-m5-'))
67 const app = await start({ dir, port: 0, swarm: null })
68 const port = app.server.address().port
69
70 const root = await fetchText(`http://localhost:${port}/`)
71 assert.equal(root.status, 200)
72 assert.ok(root.body.includes('p2pbuilders'))
73
74 const css = await fetchText(`http://localhost:${port}/styles.css`)
75 assert.equal(css.status, 200)
76 assert.ok(css.body.includes('--accent'))
77
78 const js = await fetchText(`http://localhost:${port}/app.js`)
79 assert.equal(js.status, 200)
80 assert.ok(js.body.includes('WebSocket'))
81
82 // SPA fallback: arbitrary route returns index.html
83 const fallback = await fetchText(`http://localhost:${port}/b/anything`)
84 assert.equal(fallback.status, 200)
85 assert.ok(fallback.body.includes('<title>p2pbuilders</title>'))
86
87 await app.close()
88 fs.rmSync(dir, { recursive: true, force: true })
89})
90
91test('WebSocket RPC: createBoard → createPost → listPosts → vote → comment', async () => {
92 const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'p2pbuilders-m5-'))
93 const app = await start({ dir, port: 0, swarm: null })
94 const port = app.server.address().port
95 const client = rpcClient(`ws://localhost:${port}/rpc`)
96 await client.ready
97
98 const me = await client.call('me')
99 assert.ok(me.pubkey && me.pubkey.length === 64)
100
101 await client.call('createBoard', { name: 'meta', description: 'about p2pbuilders itself' })
102
103 // createBoard is fire-and-indexed; let the indexer catch up
104 await waitFor(async () => {
105 const boards = await client.call('listBoards')
106 return boards.includes('meta')
107 })
108
109 const b = await client.call('getBoard', { name: 'meta' })
110 assert.equal(b.description, 'about p2pbuilders itself')
111
112 const { opId } = await client.call('createPost', {
113 board: 'meta',
114 title: 'hello',
115 body: 'this is the first post',
116 link: null
117 })
118 assert.ok(opId && opId.length === 80)
119
120 // Let indexer see the post
121 await waitFor(async () => {
122 const posts = await client.call('listPosts', { board: 'meta' })
123 return posts.length === 1
124 })
125
126 const posts = await client.call('listPosts', { board: 'meta' })
127 assert.equal(posts[0].title, 'hello')
128 assert.equal(posts[0].body, 'this is the first post')
129 assert.equal(posts[0].commentCount, 0)
130
131 // Vote on it
132 await client.call('vote', { targetOpId: opId, dir: 1 })
133 await waitFor(async () => {
134 const t = await client.call('getVoteTotals', { opId })
135 return t.up === 1
136 })
137
138 // Comment on it
139 const { opId: cId } = await client.call('createComment', {
140 parentOpId: opId,
141 body: 'nice'
142 })
143 assert.ok(cId)
144
145 await waitFor(async () => {
146 const cs = await client.call('listComments', { parentOpId: opId })
147 return cs.length === 1
148 })
149
150 const comments = await client.call('listComments', { parentOpId: opId })
151 assert.equal(comments[0].body, 'nice')
152
153 client.close()
154 await app.close()
155 fs.rmSync(dir, { recursive: true, force: true })
156})
157
158// runner
159;(async () => {
160 let pass = 0, fail = 0
161 for (const { name, fn } of tests) {
162 try {
163 await fn()
164 console.log(` ✓ ${name}`)
165 pass++
166 } catch (err) {
167 console.log(` ✗ ${name}`)
168 console.log(` ${err.stack || err.message}`)
169 fail++
170 }
171 }
172 console.log(`\n${pass}/${pass + fail} passed`)
173 process.exit(fail ? 1 : 0)
174})()
test/m6.js +188 -0
……@@ -0,0 +1,188 @@
1'use strict'
2
3// M6: antispam layers — PoW gate, rate limits, blocklist, weighted votes.
4
5const assert = require('assert/strict')
6const b4a = require('b4a')
7const { Node } = require('../src/backend/node')
8const { Indexer } = require('../src/backend/indexer')
9const { connectDirect } = require('../src/backend/swarm')
10const pow = require('../src/backend/pow')
11
12const tests = []
13function test (name, fn) { tests.push({ name, fn }) }
14
15async function waitFor (pred, { timeoutMs = 5000, intervalMs = 20 } = {}) {
16 const t0 = Date.now()
17 while (Date.now() - t0 < timeoutMs) {
18 if (await pred()) return
19 await new Promise(r => setTimeout(r, intervalMs))
20 }
21 throw new Error('waitFor timeout')
22}
23
24test('PoW gate: op with bits below min is dropped', async () => {
25 const node = await Node.openTemp()
26 const idx = new Indexer(node)
27 await idx.ready()
28
29 // First: a valid post at default bits
30 const good = await node.post('general', 'good', 'body')
31 // Then: a low-PoW post (bits=0 → nearly instant mint but won't meet the 18-bit min)
32 const bad = await node.post('general', 'bad', 'spam', { bits: 0 })
33
34 await waitFor(async () => {
35 const list = []
36 for await (const p of idx.listPosts('general')) list.push(p)
37 return list.length >= 1
38 })
39
40 await new Promise(r => setTimeout(r, 150)) // let bad op also attempt to index
41
42 const list = []
43 for await (const p of idx.listPosts('general')) list.push(p)
44 assert.equal(list.length, 1, 'only the good post indexed')
45 assert.ok(b4a.equals(list[0].opId, good.opId))
46
47 await idx.close()
48 await node.close()
49})
50
51test('rate limit: 11th post within an hour is dropped (limit=10)', async () => {
52 const node = await Node.openTemp()
53 const idx = new Indexer(node)
54 await idx.ready()
55
56 const baseTs = 1700000000000
57 const results = []
58 for (let i = 0; i < 11; i++) {
59 const r = await node.post('general', `p${i}`, 'b', { ts: baseTs + i * 1000 })
60 results.push(r)
61 }
62
63 await waitFor(async () => {
64 const list = []
65 for await (const p of idx.listPosts('general', { limit: 100 })) list.push(p)
66 return list.length >= 10
67 })
68
69 await new Promise(r => setTimeout(r, 200))
70 const list = []
71 for await (const p of idx.listPosts('general', { limit: 100 })) list.push(p)
72 assert.equal(list.length, 10, 'only 10 of 11 indexed')
73
74 await idx.close()
75 await node.close()
76})
77
78test('rate limit: posts spaced >1h apart all pass', async () => {
79 const node = await Node.openTemp()
80 const idx = new Indexer(node)
81 await idx.ready()
82
83 const H = 3600 * 1000
84 const base = 1700000000000
85 for (let i = 0; i < 12; i++) {
86 await node.post('general', `p${i}`, 'b', { ts: base + i * H + 1000 })
87 }
88
89 await waitFor(async () => {
90 const list = []
91 for await (const p of idx.listPosts('general', { limit: 100 })) list.push(p)
92 return list.length === 12
93 })
94
95 await idx.close()
96 await node.close()
97})
98
99test('local blocklist: blocked author hidden from lists + vote totals', async () => {
100 const a = await Node.openTemp()
101 const b = await Node.openTemp()
102 const conn = connectDirect(a.store, b.store)
103 const idxA = new Indexer(a)
104 await idxA.ready()
105
106 // A's post, then B tracks A and votes
107 const post = await a.post('general', 'visible', 'body')
108 await b.trackUser(a.pubkey)
109 await b.userCore(a.pubkey).update({ wait: true })
110 await b.vote(post.opId, 1)
111
112 // A tracks B so A's indexer sees B's vote
113 await a.trackUser(b.pubkey)
114 const bRemote = a.userCore(b.pubkey)
115 await bRemote.update({ wait: true })
116 await bRemote.download({ start: 0, end: bRemote.length }).done()
117
118 await waitFor(async () => (await idxA.getVoteTotals(post.opId)).up === 1)
119
120 // Now A blocks B → B's vote should no longer count in weighted totals
121 idxA.block(b.pubkey)
122 const weighted = await idxA.getWeightedVoteTotals(post.opId)
123 assert.equal(weighted.weightedScore, 0, 'blocked voter excluded from weighted score')
124
125 // B's own posts (if any) also hidden
126 await b.post('general', 'blocked-post', 'x')
127 await new Promise(r => setTimeout(r, 300))
128 const list = []
129 for await (const p of idxA.listPosts('general')) list.push(p)
130 const titles = []
131 for (const p of list) {
132 const op = require('../src/backend/ops').decodeOp(await a.userCore(p.author).get(p.seq))
133 titles.push(op.payload.title)
134 }
135 assert.ok(!titles.includes('blocked-post'), 'blocked author post hidden')
136
137 conn.close()
138 await idxA.close()
139 await a.close()
140 await b.close()
141})
142
143test('weighted vote: new-key voter has weight 0.02 (floor)', async () => {
144 const node = await Node.openTemp()
145 const idx = new Indexer(node)
146 await idx.ready()
147
148 const post = await node.post('general', 't', 'b')
149 await node.vote(post.opId, 1) // self-vote from a fresh key
150
151 await waitFor(async () => (await idx.getVoteTotals(post.opId)).up === 1)
152
153 const weighted = await idx.getWeightedVoteTotals(post.opId)
154 assert.equal(weighted.up, 1)
155 // Fresh key: weight floored at 0.02
156 assert.ok(weighted.weightedScore >= 0.02 - 1e-9 && weighted.weightedScore <= 0.05,
157 `new-key weight near floor, got ${weighted.weightedScore}`)
158
159 await idx.close()
160 await node.close()
161})
162
163test('reputation formula exposed directly', async () => {
164 const rep = require('../src/backend/reputation')
165 assert.equal(rep.computeWeight(0, 0) >= 0.02, true)
166 assert.equal(rep.computeWeight(365, 1000) <= 1.0, true)
167 // Spec simulation anchor: 30d + 10 upvotes ≈ 0.329
168 const w = rep.computeWeight(30, 10)
169 assert.ok(w > 0.31 && w < 0.35, `30d/10upv weight ≈ 0.329, got ${w.toFixed(3)}`)
170})
171
172// runner
173;(async () => {
174 let pass = 0, fail = 0
175 for (const { name, fn } of tests) {
176 try {
177 await fn()
178 console.log(` ✓ ${name}`)
179 pass++
180 } catch (err) {
181 console.log(` ✗ ${name}`)
182 console.log(` ${err.stack || err.message}`)
183 fail++
184 }
185 }
186 console.log(`\n${pass}/${pass + fail} passed`)
187 process.exit(fail ? 1 : 0)
188})()
test/m7.js +94 -0
……@@ -0,0 +1,94 @@
1'use strict'
2
3// M7: hiverelay — a long-running peer that holds others' cores.
4//
5// Scenario:
6// 1. A relay starts, joins 'general' board.
7// 2. User A comes online, posts, and discovers the relay.
8// Relay auto-tracks A's core (via announce protocol).
9// 3. A goes offline.
10// 4. User B comes online, joins 'general'.
11// 5. Relay replicates A's core to B — A's post is visible without A being online.
12
13const assert = require('assert/strict')
14const b4a = require('b4a')
15const createTestnet = require('hyperdht/testnet')
16const { Node } = require('../src/backend/node')
17const { startRelay } = require('../src/relay/server')
18const { decodeOp } = require('../src/backend/ops')
19const fs = require('fs'), os = require('os'), path = require('path')
20
21const tests = []
22function test (name, fn) { tests.push({ name, fn }) }
23
24function mkdir () { return fs.mkdtempSync(path.join(os.tmpdir(), 'p2pbuilders-m7-')) }
25
26async function waitFor (pred, { timeoutMs = 15000, intervalMs = 50 } = {}) {
27 const t0 = Date.now()
28 while (Date.now() - t0 < timeoutMs) {
29 if (await pred()) return
30 await new Promise(r => setTimeout(r, intervalMs))
31 }
32 throw new Error('waitFor timeout')
33}
34
35test('relay seeds offline author to late-arriving peer', async () => {
36 const testnet = await createTestnet(3, { teardown: () => {} })
37 const bootstrap = testnet.bootstrap
38
39 const relayDir = mkdir()
40 const relay = await startRelay({ dir: relayDir, boards: ['general'], bootstrap, announcePubkey: false })
41
42 const a = await Node.openTemp({ swarm: { bootstrap } })
43 await a.joinBoard('general')
44
45 // wait for the relay to see A
46 await waitFor(() => a.swarm.peerCount >= 1)
47 await a.post('general', 'while-online', 'this should survive me')
48
49 // wait for relay to learn A's pubkey via announce protocol
50 await waitFor(async () => relay.node.userCore(a.pubkey) != null)
51 const aCoreOnRelay = relay.node.userCore(a.pubkey)
52 await waitFor(() => aCoreOnRelay.length > 0, { timeoutMs: 10000 })
53 assert.equal(aCoreOnRelay.length, 1, 'relay has A\'s post')
54
55 // A goes offline
56 const aPubkey = b4a.from(a.pubkey)
57 await a.close()
58
59 // B comes online, joins same board
60 const b = await Node.openTemp({ swarm: { bootstrap } })
61 await b.joinBoard('general')
62
63 // B learns about A's pubkey via the relay's announce
64 await waitFor(() => b.userCore(aPubkey) != null, { timeoutMs: 20000 })
65 const aCoreOnB = b.userCore(aPubkey)
66 await waitFor(() => aCoreOnB.length > 0, { timeoutMs: 20000 })
67 assert.equal(aCoreOnB.length, 1, 'B received A\'s post via the relay')
68
69 const op = decodeOp(await aCoreOnB.get(0))
70 assert.equal(op.payload.title, 'while-online')
71
72 await b.close()
73 await relay.close()
74 await testnet.destroy()
75 fs.rmSync(relayDir, { recursive: true, force: true })
76})
77
78// runner
79;(async () => {
80 let pass = 0, fail = 0
81 for (const { name, fn } of tests) {
82 try {
83 await fn()
84 console.log(` ✓ ${name}`)
85 pass++
86 } catch (err) {
87 console.log(` ✗ ${name}`)
88 console.log(` ${err.stack || err.message}`)
89 fail++
90 }
91 }
92 console.log(`\n${pass}/${pass + fail} passed`)
93 process.exit(fail ? 1 : 0)
94})()
test/m8.js +147 -0
……@@ -0,0 +1,147 @@
1'use strict'
2
3// M8: edit/tombstone/profile handling in the indexer.
4
5const assert = require('assert/strict')
6const b4a = require('b4a')
7const { Node } = require('../src/backend/node')
8const { Indexer } = require('../src/backend/indexer')
9
10const tests = []
11function test (name, fn) { tests.push({ name, fn }) }
12
13async function waitFor (pred, { timeoutMs = 5000, intervalMs = 20 } = {}) {
14 const t0 = Date.now()
15 while (Date.now() - t0 < timeoutMs) {
16 if (await pred()) return
17 await new Promise(r => setTimeout(r, intervalMs))
18 }
19 throw new Error('waitFor timeout')
20}
21
22test('edit: overlay replaces title/body at read time; no history leak', async () => {
23 const node = await Node.openTemp()
24 const idx = new Indexer(node)
25 await idx.ready()
26
27 const post = await node.post('front', 'original title', 'original body')
28 await waitFor(async () => (await idx.getOverlay(post.opId)) === null)
29
30 await node.edit(post.opId, 'edited body', { title: 'edited title', ts: Date.now() + 1000 })
31 await waitFor(async () => (await idx.getOverlay(post.opId)) != null)
32
33 const overlay = await idx.getOverlay(post.opId)
34 assert.equal(overlay.title, 'edited title')
35 assert.equal(overlay.body, 'edited body')
36
37 await idx.close()
38 await node.close()
39})
40
41test('edit: only author can edit; forged edit from other key is ignored', async () => {
42 const a = await Node.openTemp()
43 const b = await Node.openTemp()
44 const { connectDirect } = require('../src/backend/swarm')
45 const conn = connectDirect(a.store, b.store)
46 const idxA = new Indexer(a)
47 await idxA.ready()
48
49 const post = await a.post('front', 't', 'b')
50 await waitFor(async () => {
51 const list = []
52 for await (const p of idxA.listPosts('front')) list.push(p)
53 return list.length === 1
54 })
55
56 // B tries to edit A's post by making its own edit op pointing at A's opId.
57 await b.trackUser(a.pubkey)
58 await a.trackUser(b.pubkey)
59 const bCore = a.userCore(b.pubkey)
60 // B edits A's post (forged)
61 await b.edit(post.opId, 'forged', { title: 'FORGED' })
62 await waitFor(() => bCore.length > 0, { timeoutMs: 8000 })
63 await new Promise(r => setTimeout(r, 300))
64
65 const overlay = await idxA.getOverlay(post.opId)
66 assert.equal(overlay, null, 'forged edit dropped by indexer')
67
68 conn.close()
69 await idxA.close()
70 await a.close()
71 await b.close()
72})
73
74test('tombstone: target hidden from listPosts; only author can tombstone', async () => {
75 const node = await Node.openTemp()
76 const idx = new Indexer(node)
77 await idx.ready()
78
79 const p1 = await node.post('front', 'keeps', 'a')
80 const p2 = await node.post('front', 'deletes', 'b')
81 await waitFor(async () => {
82 const list = []
83 for await (const p of idx.listPosts('front')) list.push(p)
84 return list.length === 2
85 })
86
87 await node.tombstone(p2.opId)
88 await waitFor(async () => (await idx.isTombstoned(p2.opId)))
89
90 const visible = []
91 for await (const p of idx.listPosts('front')) visible.push(p)
92 assert.equal(visible.length, 1, 'tombstoned post hidden')
93 assert.ok(b4a.equals(visible[0].opId, p1.opId))
94
95 // But opt-in can still surface it
96 const all = []
97 for await (const p of idx.listPosts('front', { includeTombstoned: true })) all.push(p)
98 assert.equal(all.length, 2)
99
100 await idx.close()
101 await node.close()
102})
103
104test('profile: nickname indexed + retrievable; latest wins', async () => {
105 const node = await Node.openTemp()
106 const idx = new Indexer(node)
107 await idx.ready()
108
109 await node.setProfile({ nick: 'alice', bio: 'hello', ts: 1000 })
110 await waitFor(async () => (await idx.getProfile(node.pubkey)) != null)
111 let p = await idx.getProfile(node.pubkey)
112 assert.equal(p.nick, 'alice')
113 assert.equal(p.bio, 'hello')
114
115 // update; newer ts wins
116 await node.setProfile({ nick: 'ALICE', bio: 'updated', ts: 2000 })
117 await waitFor(async () => (await idx.getProfile(node.pubkey)).nick === 'ALICE')
118 p = await idx.getProfile(node.pubkey)
119 assert.equal(p.nick, 'ALICE')
120
121 // older ts must not overwrite
122 await node.setProfile({ nick: 'stale', ts: 500 })
123 await new Promise(r => setTimeout(r, 150))
124 p = await idx.getProfile(node.pubkey)
125 assert.equal(p.nick, 'ALICE', 'older ts ignored')
126
127 await idx.close()
128 await node.close()
129})
130
131// runner
132;(async () => {
133 let pass = 0, fail = 0
134 for (const { name, fn } of tests) {
135 try {
136 await fn()
137 console.log(` ✓ ${name}`)
138 pass++
139 } catch (err) {
140 console.log(` ✗ ${name}`)
141 console.log(` ${err.stack || err.message}`)
142 fail++
143 }
144 }
145 console.log(`\n${pass}/${pass + fail} passed`)
146 process.exit(fail ? 1 : 0)
147})()
test/m9.js +97 -0
……@@ -0,0 +1,97 @@
1'use strict'
2
3// M9: hiverelay with @hyperswarm/dht-relay endpoint.
4//
5// Verifies:
6// - relay starts with a DHT-relay WebSocket listener
7// - a client connects and can wrap the WebSocket in a relayed DHT instance
8// - the relay's Bare-side swarm + our seeding behavior are unaffected
9
10const assert = require('assert/strict')
11const WebSocket = require('ws')
12const createTestnet = require('hyperdht/testnet')
13const { startRelay } = require('../src/relay/server')
14const fs = require('fs'), os = require('os'), path = require('path')
15
16const tests = []
17function test (name, fn) { tests.push({ name, fn }) }
18
19function mkdir () { return fs.mkdtempSync(path.join(os.tmpdir(), 'p2pbuilders-m9-')) }
20
21test('dht-relay WebSocket endpoint accepts connections', async () => {
22 const testnet = await createTestnet(3, { teardown: () => {} })
23 const bootstrap = testnet.bootstrap
24
25 const dir = mkdir()
26 const relay = await startRelay({
27 dir,
28 boards: ['front'],
29 bootstrap,
30 dhtRelayPort: 0, // pick a free port
31 announcePubkey: false
32 })
33 const port = relay.dhtRelayServer.address().port
34 assert.ok(port > 0)
35
36 // Raw WebSocket connect — this exercises the upgrade + dht-relay handshake.
37 const ws = new WebSocket(`ws://localhost:${port}`)
38 await new Promise((resolve, reject) => {
39 ws.once('open', resolve)
40 ws.once('error', reject)
41 })
42 assert.equal(ws.readyState, WebSocket.OPEN)
43 // Clean shutdown
44 ws.close()
45
46 await relay.close()
47 await testnet.destroy()
48 fs.rmSync(dir, { recursive: true, force: true })
49})
50
51test('relayed DHT instance can perform DHT lookups via the WebSocket', async () => {
52 const testnet = await createTestnet(3, { teardown: () => {} })
53 const bootstrap = testnet.bootstrap
54
55 const dir = mkdir()
56 const relay = await startRelay({
57 dir, boards: [], bootstrap, dhtRelayPort: 0, announcePubkey: false
58 })
59 const port = relay.dhtRelayServer.address().port
60
61 const RelayedDHT = require('@hyperswarm/dht-relay')
62 const Stream = require('@hyperswarm/dht-relay/ws')
63
64 const ws = new WebSocket(`ws://localhost:${port}`)
65 await new Promise((resolve, reject) => {
66 ws.once('open', resolve)
67 ws.once('error', reject)
68 })
69 const relayedDht = new RelayedDHT(new Stream(true, ws))
70 await relayedDht.ready()
71
72 assert.equal(typeof relayedDht.destroy, 'function')
73 assert.ok(relayedDht.defaultKeyPair, 'relayed DHT received a keypair from the relay')
74
75 await relayedDht.destroy()
76 await relay.close()
77 await testnet.destroy()
78 fs.rmSync(dir, { recursive: true, force: true })
79})
80
81// runner
82;(async () => {
83 let pass = 0, fail = 0
84 for (const { name, fn } of tests) {
85 try {
86 await fn()
87 console.log(` ✓ ${name}`)
88 pass++
89 } catch (err) {
90 console.log(` ✗ ${name}`)
91 console.log(` ${err.stack || err.message}`)
92 fail++
93 }
94 }
95 console.log(`\n${pass}/${pass + fail} passed`)
96 process.exit(fail ? 1 : 0)
97})()