| … | … | @@ -0,0 +1,31 @@ |
| 1 | # deps | |
| 2 | node_modules/ | |
| 3 | ||
| 4 | # local storage / secrets (corestore data + identity keys) | |
| 5 | .data/ | |
| 6 | .hiverelay-publish/ | |
| 7 | .hiverelay-seed/ | |
| 8 | .hiverelay-probe/ | |
| 9 | .p2pbuilders.key | |
| 10 | primary.key | |
| 11 | ||
| 12 | # iOS build artifacts | |
| 13 | ios-app/build/ | |
| 14 | ios-app/P2PBuilders.xcodeproj/ | |
| 15 | ios-app/Pods/ | |
| 16 | ios-app/P2PBuilders/Resources/backend.bundle.mjs | |
| 17 | ios-app/P2PBuilders/Resources/index.html | |
| 18 | ios-app/P2PBuilders/Resources/app.js | |
| 19 | ios-app/P2PBuilders/Resources/styles.css | |
| 20 | ios-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 | |
| 31 | npm-debug.log* |
| … | … | @@ -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. |
| … | … | @@ -0,0 +1,126 @@ |
| 1 | # p2pbuilders | |
| 2 | ||
| 3 | A 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 | ||
| 7 | Install Pear once: | |
| 8 | ||
| 9 | ``` | |
| 10 | npm i -g pear | |
| 11 | pear # bootstraps the runtime | |
| 12 | export PATH="$HOME/Library/Application Support/pear/bin:$PATH" | |
| 13 | ``` | |
| 14 | ||
| 15 | Run p2pbuilders: | |
| 16 | ||
| 17 | ``` | |
| 18 | pear run pear://gopfpwat99tcuaakasfnftrds3j6t7srdmi3qidbhm9xeizt1a5y | |
| 19 | ``` | |
| 20 | ||
| 21 | Type `help` at the prompt. | |
| 22 | ||
| 23 | ## What's in here | |
| 24 | ||
| 25 | ``` | |
| 26 | src/ | |
| 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 | ||
| 47 | public/ HTML/CSS/JS for the browser/desktop UI (dev mode) | |
| 48 | ios-app/ minimal Xcode project using bare-kit-pear (parked) | |
| 49 | scripts/ | |
| 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 | |
| 53 | test/ 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 | ``` | |
| 75 | git clone <this-repo> | |
| 76 | cd p2pbuilders | |
| 77 | npm install | |
| 78 | ||
| 79 | # run the Node dev server (browser at http://localhost:8787) | |
| 80 | npm 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) | |
| 86 | npm 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) | |
| 94 | ln -sfn "$(pwd)" /tmp/p2pbuilders | |
| 95 | ||
| 96 | # 2. stage from the space-free path | |
| 97 | cd /tmp/p2pbuilders | |
| 98 | pear stage --no-ask dev . # prints pear://0.<n>.<key> | |
| 99 | ||
| 100 | # 3. run it | |
| 101 | pear run pear://<that-key> | |
| 102 | ``` | |
| 103 | ||
| 104 | ## Seed an app or user to hiverelay | |
| 105 | ||
| 106 | ``` | |
| 107 | node scripts/publish.js # publish ./public as a hyperdrive | |
| 108 | node scripts/seed-pear.js pear://<pear-key> | |
| 109 | node 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 | ||
| 126 | Apache 2.0. See [LICENSE](./LICENSE). |
| … | … | @@ -0,0 +1,328 @@ |
| 1 | # P2PBuilders — Spec v0.1 | |
| 2 | ||
| 3 | A permissionless, peer-to-peer, Reddit-style messageboard on the Pear/Bare stack. | |
| 4 | ||
| 5 | No 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 | ||
| 58 | Every block in a user's core is one op. Version byte is first to allow forward-compatible migrations. | |
| 59 | ||
| 60 | ``` | |
| 61 | Op := { | |
| 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 | ``` | |
| 79 | post: { board: string, title: string, body: string, link?: string } | |
| 80 | comment: { parent: opId, body: string } // parent is post OR comment | |
| 81 | vote: { target: opId, dir: int8 } // +1 or -1; 0 = retract | |
| 82 | edit: { target: opId, body: string, title?: string } | |
| 83 | tombstone: { target: opId } | |
| 84 | follow: { target: pubkey } | |
| 85 | block: { target: pubkey, public: bool } // public=true → gossipable | |
| 86 | profile: { nick: string, bio?: string, avatar?: blobKey } | |
| 87 | board_create: { name: string, description: string, minPowBits: uint8 } | |
| 88 | blocklist_publish: { list: [pubkey], version: uint32 } | |
| 89 | ``` | |
| 90 | ||
| 91 | Board 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 | ||
| 97 | Fast and simple. Applied to `post`, `comment`, and `board_create` ops. | |
| 98 | ||
| 99 | ### 5.1 Algorithm | |
| 100 | ||
| 101 | ``` | |
| 102 | digest = blake2b(cenc.encode(opWithoutPow)) | |
| 103 | target = 2^(256 - bits) | |
| 104 | find 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 | ||
| 115 | Boards MAY declare `minPowBits` higher than defaults (e.g. for busy boards). Peers refuse to index ops below the board's declared minimum. | |
| 116 | ||
| 117 | PoW 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 | ||
| 121 | We 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 | ||
| 129 | Default 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 | ||
| 135 | Violations 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 | ``` | |
| 140 | rep(key) = log2(1 + max(ageDays, 1/24)) * sqrt(1 + receivedUpvotes) | |
| 141 | voteWeight(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 | ||
| 146 | Sort 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 | ||
| 160 | The **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 | ||
| 170 | Reference: 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 | ||
| 174 | The 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 | ||
| 179 | This 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 | ||
| 199 | 1. Exchange capabilities (version, supported op types). | |
| 200 | 2. Exchange **core-key manifests**: "I have these author cores at these lengths." | |
| 201 | 3. Replicate cores of interest (any core active in subscribed boards). | |
| 202 | 4. 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 | ||
| 214 | Runs in a Bare worklet. Consumes ops from all replicated cores, writes to a local Hyperbee. | |
| 215 | ||
| 216 | ### 8.1 Keyspaces | |
| 217 | ||
| 218 | ``` | |
| 219 | post/<board>/new/<ts>/<opId> → opId | |
| 220 | post/<board>/hot/<score>/<opId> → opId // periodically rescored | |
| 221 | comment/<parentOpId>/<ts>/<opId> → opId | |
| 222 | vote/<targetOpId>/<voterPubkey> → dir // last-write-wins by blockSeq | |
| 223 | user/<pubkey>/posts/<ts> → opId | |
| 224 | board/<name> → board_create op | |
| 225 | rep/<pubkey> → cached reputation | |
| 226 | ``` | |
| 227 | ||
| 228 | ### 8.2 Scoring | |
| 229 | ||
| 230 | Hot score (reddit-like, weighted): | |
| 231 | ``` | |
| 232 | score = log10(max(1, |weightedVotes|)) * sign(weightedVotes) | |
| 233 | + (ts - epoch) / 45000 | |
| 234 | ``` | |
| 235 | Recomputed 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 | ``` | |
| 255 | hot(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 | ||
| 268 | 1. **Board squatting** — accepted. First `board_create` wins. Boards are just names; social pressure sorts out the good ones. | |
| 269 | 2. **Vote privacy** — votes are public and signed. | |
| 270 | 3. **Edit history** — UI shows latest only; full history available via "view source" / raw op inspection. | |
| 271 | 4. **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. | |
| 272 | 5. **Hiverelay defaults** — minimum 2 relays per user recommended in onboarding (not enforced). | |
| 273 | ||
| 274 | ### Still open | |
| 275 | ||
| 276 | 6. **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 | ||
| 285 | Shipped in v0.1 (all green, see `npm test` — 41 tests): | |
| 286 | ||
| 287 | 1. **M1 — Core.** Identity, op encoding, PoW, single-user local write/read. No networking. | |
| 288 | 2. **M2 — Replication.** Corestore + Hyperswarm; two nodes see each other's posts on a shared board. | |
| 289 | 3. **M3 — Peer discovery.** Protomux announce channel; auto-trackUser on board join; transitive discovery (C learns A via B). | |
| 290 | 4. **M4 — Indexer.** Local Hyperbee views — hot/new/top, comment threads, vote tallies, board registry. | |
| 291 | 5. **M5 — UI.** HTTP + WebSocket RPC dev transport; HN-shaped SPA frontend (browser/mobile-safe). | |
| 292 | 6. **M6 — Antispam.** PoW gate, per-pubkey rate limits, reputation weighting, local blocklists. | |
| 293 | 7. **M7 — Hiverelay (self-hosted).** Relay mode seeds announced cores 24/7 for offline authors. | |
| 294 | 8. **M8 — Hardening.** Edit/tombstone overlays (author-only enforcement), profile op indexing. | |
| 295 | 9. **M9 — Transports.** Pear desktop worker + IPC (no HTTP bridge); `@hyperswarm/dht-relay` WebSocket endpoint on hiverelay. | |
| 296 | 10. **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. | |
| 297 | 11. **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 | ||
| 312 | Since 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 | ||
| 314 | 1. Load `src/bare/ios-entry.js` into the embedded Bare runtime. | |
| 315 | 2. Hand us a duplex pipe (`bare-ipc` or `Bare.IPC.connect()`). | |
| 316 | 3. Drive the UI natively (or via a webview) and send framed JSON-RPC over the pipe. | |
| 317 | ||
| 318 | The 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 | ||
| 322 | Without 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 | ||
| 328 | Parked. Revisit if real demand for non-Pear-browser access shows up. |
| … | … | @@ -0,0 +1,19 @@ |
| 1 | import UIKit | |
| 2 | ||
| 3 | @main | |
| 4 | class 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 | } |
| … | … | @@ -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 | } |
| … | … | @@ -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 | } |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | { | |
| 2 | "info": { | |
| 3 | "version": 1, | |
| 4 | "author": "xcode" | |
| 5 | } | |
| 6 | } |
| … | … | @@ -0,0 +1,99 @@ |
| 1 | import Foundation | |
| 2 | import 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 | ||
| 16 | final 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 | } |
| … | … | @@ -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> |
| … | … | @@ -0,0 +1,16 @@ |
| 1 | import UIKit | |
| 2 | ||
| 3 | class 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 | } |
| … | … | @@ -0,0 +1,90 @@ |
| 1 | import UIKit | |
| 2 | import 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 | ||
| 21 | final 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 | } |
| … | … | @@ -0,0 +1,161 @@ |
| 1 | # P2PBuilders — iOS app shell | |
| 2 | ||
| 3 | Minimal iOS app that embeds the p2pbuilders Bare backend (via bare-kit-pear) | |
| 4 | alongside a WKWebView showing the frontend. Designed to run in the iOS | |
| 5 | Simulator 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 | ||
| 37 | The frontend, backend, and RPC protocol are all unchanged from the Node / | |
| 38 | Pear versions. This app is just a host. | |
| 39 | ||
| 40 | ## Prereqs | |
| 41 | ||
| 42 | Install these once: | |
| 43 | ||
| 44 | ```bash | |
| 45 | brew install xcodegen xcbeautify | |
| 46 | xcode-select --install | |
| 47 | ``` | |
| 48 | ||
| 49 | Also make sure the monorepo's Node deps are installed: | |
| 50 | ||
| 51 | ```bash | |
| 52 | cd .. # project root | |
| 53 | npm install | |
| 54 | ``` | |
| 55 | ||
| 56 | ## First-time setup | |
| 57 | ||
| 58 | ### 1. Generate the Xcode project | |
| 59 | ||
| 60 | ```bash | |
| 61 | cd ios-app | |
| 62 | xcodegen | |
| 63 | open P2PBuilders.xcodeproj | |
| 64 | ``` | |
| 65 | ||
| 66 | ### 2. Add bare-kit-pear as a Swift Package dependency | |
| 67 | ||
| 68 | In Xcode: | |
| 69 | ||
| 70 | 1. File → Add Package Dependencies… | |
| 71 | 2. Paste the bare-kit-pear URL: `https://github.com/bigdestiny2/bare-kit-pear` | |
| 72 | 3. Choose a version rule (branch `main` works) | |
| 73 | 4. Select the `BareKit` library product and add it to the `P2PBuilders` target. | |
| 74 | ||
| 75 | If the module name isn't literally `BareKit`, change the `import BareKit` | |
| 76 | line in `BareBridge.swift` to match. | |
| 77 | ||
| 78 | ### 3. Prepare frontend + backend bundles | |
| 79 | ||
| 80 | From the project root: | |
| 81 | ||
| 82 | ```bash | |
| 83 | ./ios-app/scripts/prepare-resources.sh | |
| 84 | ``` | |
| 85 | ||
| 86 | This copies `public/*` into `ios-app/P2PBuilders/Resources/` and runs | |
| 87 | `bare-pack` to produce `backend.bundle.mjs`. | |
| 88 | ||
| 89 | Re-run whenever you change frontend files or anything under `src/bare` or | |
| 90 | `src/backend`. | |
| 91 | ||
| 92 | ### 4. Build + run in the simulator | |
| 93 | ||
| 94 | From the project root: | |
| 95 | ||
| 96 | ```bash | |
| 97 | ./ios-app/scripts/build-sim.sh | |
| 98 | ``` | |
| 99 | ||
| 100 | This: prepares resources, builds the app, finds (or boots) a simulator, | |
| 101 | installs, and launches. When it finishes you should see the orange | |
| 102 | p2pbuilders header on the iOS simulator window. | |
| 103 | ||
| 104 | Alternatively, click Run in Xcode. | |
| 105 | ||
| 106 | ## What to expect on first launch | |
| 107 | ||
| 108 | 1. Welcome screen (first-launch onboarding) | |
| 109 | 2. Enter a nickname, tap "get started" | |
| 110 | 3. Front page — empty at first; submit a post | |
| 111 | 4. PoW pill animates while the backend mints (~80ms on simulator) | |
| 112 | 5. Thread view with upvote / reply / edit / delete | |
| 113 | ||
| 114 | The backend joins Hyperswarm on the `front` board topic, so if another | |
| 115 | peer is posting right now you should start seeing their posts after a | |
| 116 | few 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 | ``` | |
| 142 | ios-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 | ``` |
| … | … | @@ -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 | ||
| 15 | name: P2PBuilders | |
| 16 | options: | |
| 17 | bundleIdPrefix: com.p2pbuilders | |
| 18 | deploymentTarget: | |
| 19 | iOS: "16.0" | |
| 20 | ||
| 21 | settings: | |
| 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 | ||
| 28 | targets: | |
| 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). |
| … | … | @@ -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 | ||
| 13 | set -euo pipefail | |
| 14 | ||
| 15 | ROOT="$(cd "$(dirname "$0")/../.." && pwd)" | |
| 16 | IOS="$ROOT/ios-app" | |
| 17 | SCHEME="P2PBuilders" | |
| 18 | BUNDLE_ID="com.p2pbuilders.app" | |
| 19 | DERIVED="$IOS/build" | |
| 20 | ||
| 21 | echo "[build] preparing resources" | |
| 22 | "$IOS/scripts/prepare-resources.sh" | |
| 23 | ||
| 24 | cd "$IOS" | |
| 25 | if [[ ! -d P2PBuilders.xcodeproj ]]; then | |
| 26 | echo "[build] generating Xcode project via xcodegen" | |
| 27 | xcodegen | |
| 28 | fi | |
| 29 | ||
| 30 | echo "[build] xcodebuild for iOS Simulator" | |
| 31 | xcodebuild \ | |
| 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 | |
| 42 | APP_PATH="$(find "$DERIVED/Build/Products/Debug-iphonesimulator" -name "$SCHEME.app" -maxdepth 2 -type d | head -n1)" | |
| 43 | if [[ -z "$APP_PATH" || ! -d "$APP_PATH" ]]; then | |
| 44 | echo "[build] error: built app not found under $DERIVED/Build/Products/Debug-iphonesimulator" | |
| 45 | exit 1 | |
| 46 | fi | |
| 47 | echo "[build] built: $APP_PATH" | |
| 48 | ||
| 49 | # Boot a simulator (pick the first booted one; otherwise boot the default) | |
| 50 | BOOTED="$(xcrun simctl list devices booted | awk -F '[()]' '/\(Booted\)/ { print $2; exit }' || true)" | |
| 51 | if [[ -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 | |
| 64 | fi | |
| 65 | echo "[build] installing on simulator $BOOTED" | |
| 66 | xcrun simctl install "$BOOTED" "$APP_PATH" | |
| 67 | echo "[build] launching" | |
| 68 | xcrun simctl launch "$BOOTED" "$BUNDLE_ID" | |
| 69 | echo "[build] done. logs: xcrun simctl spawn $BOOTED log stream --predicate 'process == \"$SCHEME\"'" |
| … | … | @@ -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 | ||
| 13 | set -euo pipefail | |
| 14 | ||
| 15 | ROOT="$(cd "$(dirname "$0")/../.." && pwd)" | |
| 16 | RESOURCES="$ROOT/ios-app/P2PBuilders/Resources" | |
| 17 | ||
| 18 | echo "[prepare] using project root: $ROOT" | |
| 19 | echo "[prepare] destination: $RESOURCES" | |
| 20 | ||
| 21 | mkdir -p "$RESOURCES" | |
| 22 | ||
| 23 | # 1. Copy frontend files | |
| 24 | cp "$ROOT/public/index.html" "$RESOURCES/" | |
| 25 | cp "$ROOT/public/app.js" "$RESOURCES/" | |
| 26 | cp "$ROOT/public/styles.css" "$RESOURCES/" | |
| 27 | cp "$ROOT/public/transport.js" "$RESOURCES/" | |
| 28 | ||
| 29 | echo "[prepare] copied public/* to Resources/" | |
| 30 | ||
| 31 | # 2. Bundle the Bare backend | |
| 32 | cd "$ROOT" | |
| 33 | BUNDLE_OUT="$RESOURCES/backend.bundle.mjs" | |
| 34 | echo "[prepare] bundling backend → $BUNDLE_OUT" | |
| 35 | npx --no-install bare-pack --linked --host ios-arm64 src/bare/ios-entry.js -o "$BUNDLE_OUT" | |
| 36 | ||
| 37 | echo "[prepare] done." | |
| 38 | ls -lh "$RESOURCES/" |
| … | … | @@ -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 | } |
| … | … | @@ -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 | } |
| … | … | @@ -0,0 +1,576 @@ |
| 1 | // p2pbuilders frontend — HN-shaped single feed. Vanilla JS, no build step. | |
| 2 | ||
| 3 | import { createTransport } from './transport.js' | |
| 4 | ||
| 5 | const DEFAULT_BOARD = 'front' | |
| 6 | const ONBOARD_FLAG = 'p2pbuilders.onboarded' | |
| 7 | ||
| 8 | // Auto-detects environment: Pear IPC pipe if available, else localhost WebSocket. | |
| 9 | const rpc = createTransport() | |
| 10 | ||
| 11 | // Cached identity, populated on boot. Used for ownership checks. | |
| 12 | let me = null | |
| 13 | ||
| 14 | // ----- helpers ----- | |
| 15 | ||
| 16 | const 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 | } | |
| 30 | const $view = () => document.getElementById('view') | |
| 31 | const shortHex = (s, n = 8) => (s ? s.slice(0, n) : '') | |
| 32 | const displayName = (obj) => obj.authorNick || shortHex(obj.author || obj.pubkey) | |
| 33 | const isMine = (obj) => me && obj.author && obj.author === me.pubkey | |
| 34 | ||
| 35 | function hostOf (url) { | |
| 36 | try { return new URL(url).host.replace(/^www\./, '') } catch { return null } | |
| 37 | } | |
| 38 | ||
| 39 | function 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 | ||
| 48 | function render (node) { | |
| 49 | const v = $view(); v.replaceChildren(); v.appendChild(node) | |
| 50 | } | |
| 51 | function loading (msg = 'loading…') { return h('div', { class: 'empty' }, msg) } | |
| 52 | function errorBox (msg) { return h('div', { class: 'err' }, msg) } | |
| 53 | ||
| 54 | function 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 | } | |
| 62 | const 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. | |
| 66 | function 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 | ||
| 91 | async 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 | ||
| 105 | const routes = [] | |
| 106 | function route (pattern, handler) { | |
| 107 | const parts = pattern.split('/').filter(Boolean) | |
| 108 | routes.push({ parts, handler }) | |
| 109 | } | |
| 110 | ||
| 111 | async 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 | } | |
| 132 | window.addEventListener('hashchange', dispatch) | |
| 133 | ||
| 134 | // ----- feed views ----- | |
| 135 | ||
| 136 | async 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 | ||
| 154 | route('/', () => renderFeed('hot')) | |
| 155 | route('/new', () => renderFeed('new')) | |
| 156 | ||
| 157 | function 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 | ||
| 201 | route('/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 | ||
| 218 | function 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). | |
| 247 | function 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 | ||
| 266 | function 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 | ||
| 275 | function 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 | ||
| 298 | function 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 | ||
| 306 | function 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 | ||
| 330 | function 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 | ||
| 360 | async 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 | ||
| 370 | route('/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 | ||
| 411 | function 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 | ||
| 420 | route('/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 | ||
| 480 | route('/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 | ||
| 508 | function 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 | ||
| 517 | route('/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 | ||
| 557 | rpc.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 | })() |
| … | … | @@ -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> |
| … | … | @@ -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 | ||
| 32 | html, 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 | } | |
| 43 | body { display: flex; flex-direction: column; min-height: 100vh; } | |
| 44 | ||
| 45 | a { color: var(--link); text-decoration: none; } | |
| 46 | a:hover { text-decoration: underline; } | |
| 47 | a: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 | ||
| 220 | button.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 | } | |
| 230 | button.primary:hover { filter: brightness(1.05); } | |
| 231 | button.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 | ||
| 373 | table.kv { width: 100%; border-collapse: collapse; font-size: 13px; } | |
| 374 | table.kv td { padding: 4px 8px; border-bottom: 1px solid var(--border); vertical-align: top; } | |
| 375 | table.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 | } |
| … | … | @@ -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 | ||
| 10 | export 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 | ||
| 36 | function 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 | ||
| 92 | function 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 | ||
| 144 | function 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 | } |
| … | … | @@ -0,0 +1,18 @@ |
| 1 | import { HiveRelayClient } from 'p2p-hiverelay/client' | |
| 2 | import b4a from 'b4a' | |
| 3 | import hypercoreId from 'hypercore-id-encoding' | |
| 4 | ||
| 5 | const client = new HiveRelayClient('./.hiverelay-probe') | |
| 6 | await client.start() | |
| 7 | await new Promise(r => setTimeout(r, 4000)) | |
| 8 | ||
| 9 | const k = hypercoreId.decode('gopfpwat99tcuaakasfnftrds3j6t7srdmi3qidbhm9xeizt1a5y') | |
| 10 | const acceptances = await client.seed(k, { replicas: 8, timeout: 25000 }) | |
| 11 | console.log(`\nseeded on ${acceptances.length} relay(s):\n`) | |
| 12 | for (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 | } | |
| 17 | await client.close?.() | |
| 18 | process.exit(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 | ||
| 18 | const path = require('path') | |
| 19 | const fs = require('fs') | |
| 20 | ||
| 21 | async 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. | |
| 101 | function 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 | ||
| 118 | main().catch((err) => { | |
| 119 | console.error('[publish] fatal:', err.stack || err.message) | |
| 120 | process.exit(1) | |
| 121 | }) |
| … | … | @@ -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 | ||
| 14 | const path = require('path') | |
| 15 | ||
| 16 | async 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 | ||
| 83 | main().catch((err) => { | |
| 84 | console.error('[seed] fatal:', err.stack || err.message) | |
| 85 | process.exit(1) | |
| 86 | }) |
| … | … | @@ -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. | |
| 4 | module.exports = process.env |
| … | … | @@ -0,0 +1,2 @@ |
| 1 | 'use strict' | |
| 2 | module.exports = require('events') |
| … | … | @@ -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). | |
| 4 | module.exports = require('fs') |
| … | … | @@ -0,0 +1,2 @@ |
| 1 | 'use strict' | |
| 2 | module.exports = require('os') |
| … | … | @@ -0,0 +1,2 @@ |
| 1 | 'use strict' | |
| 2 | module.exports = require('path') |
| … | … | @@ -0,0 +1,3 @@ |
| 1 | 'use strict' | |
| 2 | // Node-only shim exposing `process` for the #process subpath import. | |
| 3 | module.exports = process |
| … | … | @@ -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 | ||
| 9 | const inBare = typeof Bare !== 'undefined' | |
| 10 | ||
| 11 | module.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 | } |
| … | … | @@ -0,0 +1,41 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const cenc = require('compact-encoding') | |
| 4 | const b4a = require('b4a') | |
| 5 | ||
| 6 | const PROTOCOL = 'p2pbuilders/announce/v1' | |
| 7 | const PUBKEY_BYTES = 32 | |
| 8 | const MAX_KEYS_PER_ANNOUNCE = 256 | |
| 9 | ||
| 10 | const 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 | |
| 17 | function 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 | ||
| 41 | module.exports = { setupAnnounce, PROTOCOL, MAX_KEYS_PER_ANNOUNCE } |
| … | … | @@ -0,0 +1,38 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const sodium = require('sodium-universal') | |
| 4 | const b4a = require('b4a') | |
| 5 | ||
| 6 | const BOARD_NAME_RE = /^[a-z0-9_-]{2,32}$/ | |
| 7 | const TOPIC_BYTES = 32 | |
| 8 | const BOARD_TOPIC_PREFIX = 'p2pbuilders:board:v1:' | |
| 9 | const ROSTER_TOPIC_STR = 'p2pbuilders:roster:v1' | |
| 10 | ||
| 11 | function 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 | ||
| 18 | function hashTopic (str) { | |
| 19 | const out = b4a.alloc(TOPIC_BYTES) | |
| 20 | sodium.crypto_generichash(out, b4a.from(str)) | |
| 21 | return out | |
| 22 | } | |
| 23 | ||
| 24 | function boardTopic (name) { | |
| 25 | validateBoardName(name) | |
| 26 | return hashTopic(BOARD_TOPIC_PREFIX + name) | |
| 27 | } | |
| 28 | ||
| 29 | const rosterTopic = hashTopic(ROSTER_TOPIC_STR) | |
| 30 | ||
| 31 | module.exports = { | |
| 32 | BOARD_NAME_RE, | |
| 33 | TOPIC_BYTES, | |
| 34 | validateBoardName, | |
| 35 | boardTopic, | |
| 36 | rosterTopic, | |
| 37 | hashTopic | |
| 38 | } |
| … | … | @@ -0,0 +1,54 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const { fs, path } = require('./_rt') | |
| 4 | const sodium = require('sodium-universal') | |
| 5 | const b4a = require('b4a') | |
| 6 | ||
| 7 | const PRIMARY_KEY_BYTES = 32 | |
| 8 | const USER_CORE_NAME = 'p2pbuilders/user' | |
| 9 | ||
| 10 | function generatePrimaryKey () { | |
| 11 | const key = b4a.alloc(PRIMARY_KEY_BYTES) | |
| 12 | sodium.randombytes_buf(key) | |
| 13 | return key | |
| 14 | } | |
| 15 | ||
| 16 | function 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 | ||
| 49 | module.exports = { | |
| 50 | PRIMARY_KEY_BYTES, | |
| 51 | USER_CORE_NAME, | |
| 52 | generatePrimaryKey, | |
| 53 | loadOrCreatePrimaryKey | |
| 54 | } |
| … | … | @@ -0,0 +1,498 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const b4a = require('b4a') | |
| 4 | const Hyperbee = require('hyperbee') | |
| 5 | const cenc = require('compact-encoding') | |
| 6 | const { TYPE, TYPE_NAME, REQUIRES_POW, decodeOp, makeOpId, parseOpId, OP_ID_BYTES } = require('./ops') | |
| 7 | const pow = require('./pow') | |
| 8 | const reputation = require('./reputation') | |
| 9 | ||
| 10 | const INDEX_CORE_NAME = 'p2pbuilders/index' | |
| 11 | ||
| 12 | // Default indexer policy (SPEC §6.1) | |
| 13 | const 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 | ||
| 22 | const MS_PER_HOUR = 3600 * 1000 | |
| 23 | ||
| 24 | const TS_PAD = 15 // enough for ms timestamps through year 33658 | |
| 25 | const tsKey = (ts) => String(ts).padStart(TS_PAD, '0') | |
| 26 | const hex = (buf) => b4a.toString(buf, 'hex') | |
| 27 | const fromHex = (s) => b4a.from(s, 'hex') | |
| 28 | ||
| 29 | const voteEnc = cenc.int8 | |
| 30 | const 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 | |
| 60 | function postNewKey (boardName, ts, opId) { return `post/${boardName}/new/${tsKey(ts)}/${hex(opId)}` } | |
| 61 | function commentKey (parentOpId, ts, opId) { return `comment/${hex(parentOpId)}/${tsKey(ts)}/${hex(opId)}` } | |
| 62 | function voteKey (targetOpId, voter) { return `vote/${hex(targetOpId)}/${hex(voter)}` } | |
| 63 | function userPostKey (author, ts, opId) { return `user/${hex(author)}/posts/${tsKey(ts)}/${hex(opId)}` } | |
| 64 | function boardKey (name) { return `board/${name}` } | |
| 65 | function seenKey (pubkey) { return `_seen/${hex(pubkey)}` } | |
| 66 | function opMetaKey (opId) { return `_opmeta/${hex(opId)}` } | |
| 67 | function overlayKey (opId) { return `overlay/${hex(opId)}` } | |
| 68 | function tombKey (opId) { return `tomb/${hex(opId)}` } | |
| 69 | function profileKey (pubkey) { return `profile/${hex(pubkey)}` } | |
| 70 | ||
| 71 | const 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 | ||
| 91 | const 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 | ||
| 114 | class 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 | ||
| 494 | function encodeMeta (m) { return cenc.encode(opMetaEnc, m) } | |
| 495 | function readUint32 (buf) { return buf.readUInt32BE ? buf.readUInt32BE(0) : new DataView(buf.buffer, buf.byteOffset, 4).getUint32(0, false) } | |
| 496 | function writeUint32 (n) { const b = b4a.alloc(4); new DataView(b.buffer, b.byteOffset, 4).setUint32(0, n, false); return b } | |
| 497 | ||
| 498 | module.exports = { Indexer, INDEX_CORE_NAME } |
| … | … | @@ -0,0 +1,194 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const b4a = require('b4a') | |
| 4 | const { EventEmitter } = require('#events') | |
| 5 | const { openDiskNode, openTempNode } = require('./store') | |
| 6 | const { SwarmHub } = require('./swarm') | |
| 7 | const { setupAnnounce } = require('./announce') | |
| 8 | const board = require('./board') | |
| 9 | const ops = require('./ops') | |
| 10 | const pow = require('./pow') | |
| 11 | ||
| 12 | const { TYPE, REQUIRES_POW, SCHEMA_VERSION, makeOpId, encodeOp, decodeOp } = ops | |
| 13 | ||
| 14 | const 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 | ||
| 20 | class 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 | ||
| 194 | module.exports = { Node, DEFAULT_POW_BITS_BY_TYPE } |
| … | … | @@ -0,0 +1,270 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const cenc = require('compact-encoding') | |
| 4 | const b4a = require('b4a') | |
| 5 | ||
| 6 | const SCHEMA_VERSION = 1 | |
| 7 | ||
| 8 | const 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 | ||
| 21 | const TYPE_NAME = Object.fromEntries(Object.entries(TYPE).map(([k, v]) => [v, k.toLowerCase()])) | |
| 22 | ||
| 23 | const REQUIRES_POW = new Set([TYPE.POST, TYPE.COMMENT, TYPE.BOARD_CREATE]) | |
| 24 | ||
| 25 | const OP_ID_BYTES = 40 // 32-byte pubkey + 8-byte seq | |
| 26 | ||
| 27 | function 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 | ||
| 36 | function 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 | ||
| 44 | const 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 | ||
| 58 | const opIdEnc = cenc.fixed(OP_ID_BYTES) | |
| 59 | const pubkeyEnc = cenc.fixed(32) | |
| 60 | ||
| 61 | const 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 | ||
| 197 | const NONCE_BYTES = 8 | |
| 198 | const nonceEnc = cenc.fixed(NONCE_BYTES) | |
| 199 | ||
| 200 | const 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. | |
| 239 | const 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 | ||
| 254 | function encodeOp (o) { return cenc.encode(op, o) } | |
| 255 | function decodeOp (buf) { return cenc.decode(op, buf) } | |
| 256 | function encodePreimage (o) { return cenc.encode(preimage, o) } | |
| 257 | ||
| 258 | module.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 | } |
| … | … | @@ -0,0 +1,82 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const sodium = require('sodium-universal') | |
| 4 | const b4a = require('b4a') | |
| 5 | const { encodePreimage, NONCE_BYTES } = require('./ops') | |
| 6 | ||
| 7 | const HASH_BYTES = 32 | |
| 8 | ||
| 9 | const DEFAULT_BITS = { | |
| 10 | comment: 16, | |
| 11 | post: 18, | |
| 12 | board_create: 22 | |
| 13 | } | |
| 14 | ||
| 15 | function blake2b (input, out) { | |
| 16 | out = out || b4a.alloc(HASH_BYTES) | |
| 17 | sodium.crypto_generichash(out, input) | |
| 18 | return out | |
| 19 | } | |
| 20 | ||
| 21 | function 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 | ||
| 36 | function digestPreimage (op) { | |
| 37 | return blake2b(encodePreimage(op)) | |
| 38 | } | |
| 39 | ||
| 40 | function 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 | ||
| 47 | function 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 | ||
| 53 | function 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 | ||
| 67 | function 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 | ||
| 75 | module.exports = { | |
| 76 | DEFAULT_BITS, | |
| 77 | HASH_BYTES, | |
| 78 | leadingZeroBits, | |
| 79 | digestPreimage, | |
| 80 | mint, | |
| 81 | verify | |
| 82 | } |
| … | … | @@ -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 | ||
| 7 | const FLOOR = 0.02 | |
| 8 | const CAP = 1.0 | |
| 9 | const CAP_DENOM = 50 | |
| 10 | ||
| 11 | function 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 | ||
| 16 | function computeWeight (ageDays, receivedUpvotes) { | |
| 17 | const r = computeRep(ageDays, receivedUpvotes) | |
| 18 | return Math.max(FLOOR, Math.min(CAP, r / CAP_DENOM)) | |
| 19 | } | |
| 20 | ||
| 21 | module.exports = { computeRep, computeWeight, FLOOR, CAP, CAP_DENOM } |
| … | … | @@ -0,0 +1,281 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const b4a = require('b4a') | |
| 4 | const { decodeOp, makeOpId, parseOpId } = require('./ops') | |
| 5 | const pow = require('./pow') | |
| 6 | ||
| 7 | const DEFAULT_BOARD = 'front' | |
| 8 | ||
| 9 | // Converts a Buffer/Uint8Array into hex string. null → null. | |
| 10 | const hex = (b) => (b ? b4a.toString(b, 'hex') : null) | |
| 11 | const fromHex = (s) => (s ? b4a.from(s, 'hex') : null) | |
| 12 | ||
| 13 | // Create an RPC dispatcher bound to a Node + Indexer. | |
| 14 | function 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 | ||
| 281 | module.exports = { createRPC } |
| … | … | @@ -0,0 +1,93 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const http = require('http') | |
| 4 | const fs = require('fs') | |
| 5 | const path = require('path') | |
| 6 | const { WebSocketServer } = require('ws') | |
| 7 | ||
| 8 | const { Node } = require('./node') | |
| 9 | const { Indexer } = require('./indexer') | |
| 10 | const { createRPC } = require('./rpc') | |
| 11 | ||
| 12 | const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public') | |
| 13 | const 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 | ||
| 23 | function 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 | ||
| 42 | async 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 | ||
| 79 | module.exports = { start } | |
| 80 | ||
| 81 | // CLI entry | |
| 82 | if (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 | } |
| … | … | @@ -0,0 +1,48 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const { fs, os, path } = require('./_rt') | |
| 4 | const Corestore = require('corestore') | |
| 5 | const { USER_CORE_NAME, loadOrCreatePrimaryKey, generatePrimaryKey } = require('./identity') | |
| 6 | ||
| 7 | function 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 | ||
| 13 | async 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. | |
| 23 | async 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. | |
| 32 | async 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 | ||
| 48 | module.exports = { openStore, openUserCore, openDiskNode, openTempNode } |
| … | … | @@ -0,0 +1,86 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | const 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). | |
| 8 | class 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. | |
| 74 | function 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 | ||
| 86 | module.exports = { SwarmHub, connectDirect } |
| … | … | @@ -0,0 +1,137 @@ |
| 1 | # iOS integration — bare-kit-pear ↔ p2pbuilders | |
| 2 | ||
| 3 | This document describes how to wire the p2pbuilders Bare backend into an iOS | |
| 4 | app 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 | |
| 6 | public API + `node_modules/bare-ipc`. | |
| 7 | ||
| 8 | ## 1. Bundle the JS | |
| 9 | ||
| 10 | The app loads `src/bare/ios-entry.js` which in turn requires the full backend. | |
| 11 | You need to bundle: | |
| 12 | ``` | |
| 13 | src/bare/ios-entry.js | |
| 14 | src/bare/harness.js | |
| 15 | src/backend/**/*.js (everything) | |
| 16 | node_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 | ||
| 22 | Two 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 — | |
| 25 | simplest, biggest binary. | |
| 26 | ||
| 27 | **(b) pre-bundle with `bare-pack`** (the Bare-native bundler analogous to esbuild): | |
| 28 | ``` | |
| 29 | npx bare-pack src/bare/ios-entry.js > bundle.js | |
| 30 | ``` | |
| 31 | then ship the single `bundle.js`. Faster startup, smaller install. | |
| 32 | ||
| 33 | ## 2. Start the worklet | |
| 34 | ||
| 35 | ```swift | |
| 36 | import BareKit | |
| 37 | ||
| 38 | let worklet = BareWorklet() | |
| 39 | try 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. | |
| 47 | let ipc = BareIPC(worklet: worklet) | |
| 48 | ``` | |
| 49 | ||
| 50 | ## 3. Speak JSON-RPC over the pipe | |
| 51 | ||
| 52 | The JS backend treats the IPC stream as line-delimited JSON-RPC. Frame protocol: | |
| 53 | ||
| 54 | ``` | |
| 55 | request: {"id": <int>, "method": "<name>", "params": { ... }}\n | |
| 56 | response: {"id": <int>, "result": ...}\n | |
| 57 | {"id": <int>, "error": "<message>"}\n | |
| 58 | event: {"event": "ready", "pubkey": "<hex64>", "dir": "<path>"}\n | |
| 59 | ``` | |
| 60 | ||
| 61 | The first frame you'll see (unsolicited) is the `ready` event — our backend | |
| 62 | emits it when the node + indexer are booted. | |
| 63 | ||
| 64 | ### Swift call example | |
| 65 | ||
| 66 | ```swift | |
| 67 | // Send: | |
| 68 | let request = #"{"id":1,"method":"listPosts","params":{"sort":"hot"}}"# + "\n" | |
| 69 | try ipc.write(data: request.data(using: .utf8)!) | |
| 70 | ||
| 71 | // Read (async): | |
| 72 | ipc.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 | ||
| 80 | All 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 | ||
| 107 | When the iOS app backgrounds: | |
| 108 | ```swift | |
| 109 | worklet.suspend() // JS side: Bare.on('suspend') | |
| 110 | // ... later ... | |
| 111 | worklet.resume() // JS side: Bare.on('resume') | |
| 112 | ``` | |
| 113 | Our `ios-entry.js` already calls `pipe.unref()` on suspend and `pipe.ref()` on | |
| 114 | resume so the Bare event loop doesn't keep the pipe hot while backgrounded. | |
| 115 | ||
| 116 | ## 6. Minimal round-trip test | |
| 117 | ||
| 118 | 1. Swift: start worklet, attach IPC. | |
| 119 | 2. Swift: read one line, parse it → expect `{"event":"ready","pubkey":"…"}`. | |
| 120 | 3. Swift: `ipc.write("{\"id\":1,\"method\":\"me\",\"params\":{}}\n")`. | |
| 121 | 4. Swift: read one line → `{"id":1,"result":{"pubkey":"…","length":0}}`. | |
| 122 | ||
| 123 | If 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`. |
| … | … | @@ -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 | ||
| 17 | const { path } = require('../backend/_rt') | |
| 18 | const { Node } = require('../backend/node') | |
| 19 | const { Indexer } = require('../backend/indexer') | |
| 20 | const { createRPC } = require('../backend/rpc') | |
| 21 | ||
| 22 | async 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 | ||
| 52 | function 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 | ||
| 73 | module.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. | |
| 78 | if (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 | } |
| … | … | @@ -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 | ||
| 24 | const { path, os, env, exit } = require('../backend/_rt') | |
| 25 | const { start } = require('./harness') | |
| 26 | ||
| 27 | function 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 | ||
| 40 | function 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 | }) |
| … | … | @@ -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 | ||
| 10 | const { path } = require('../backend/_rt') | |
| 11 | const { Node } = require('../backend/node') | |
| 12 | const { Indexer } = require('../backend/indexer') | |
| 13 | const { createRPC } = require('../backend/rpc') | |
| 14 | ||
| 15 | // Pear exposes global `Pear` when this module runs as a worker. | |
| 16 | /* global Pear */ | |
| 17 | ||
| 18 | async 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 | ||
| 54 | main().catch((err) => { | |
| 55 | // eslint-disable-next-line no-console | |
| 56 | console.error('p2pbuilders worker fatal:', err) | |
| 57 | process.exit(1) | |
| 58 | }) |
| … | … | @@ -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 | ||
| 22 | const { path, os } = require('../backend/_rt') | |
| 23 | const b4a = require('b4a') | |
| 24 | const { Node } = require('../backend/node') | |
| 25 | ||
| 26 | let dhtRelayModule = null | |
| 27 | try { | |
| 28 | dhtRelayModule = require('@hyperswarm/dht-relay') | |
| 29 | } catch { /* optional: relay still works without the WS endpoint */ } | |
| 30 | ||
| 31 | async 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 | ||
| 77 | async 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 | ||
| 95 | module.exports = { startRelay, startDhtRelayEndpoint } | |
| 96 | ||
| 97 | if (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 | } |
| … | … | @@ -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). | |
| 14 | const process = (typeof Bare !== 'undefined') ? require('bare-process') : global.process | |
| 15 | ||
| 16 | console.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 | |
| 24 | const 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. | |
| 30 | const PINNED_OPIDS = [ | |
| 31 | '16752aeb998f33904cfa0165526d7f217590e85096a964a07bd29c347edbe86d0000000000000001' | |
| 32 | ] | |
| 33 | ||
| 34 | const { path } = require('../backend/_rt') | |
| 35 | const { Node } = require('../backend/node') | |
| 36 | const { Indexer } = require('../backend/indexer') | |
| 37 | const { createRPC } = require('../backend/rpc') | |
| 38 | const b4a = require('b4a') | |
| 39 | ||
| 40 | // ---- ANSI helpers ----------------------------------------------------------- | |
| 41 | ||
| 42 | const 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 | ||
| 54 | const short = (s, n = 8) => (s ? s.slice(0, n) : '') | |
| 55 | const 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 | ||
| 68 | async 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 | ||
| 108 | async 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 | ||
| 490 | main().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 | }) |
| … | … | @@ -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 | ||
| 6 | const assert = require('assert/strict') | |
| 7 | const b4a = require('b4a') | |
| 8 | const { Node } = require('../src/backend/node') | |
| 9 | const { TYPE, parseOpId, encodeOp, decodeOp, makeOpId } = require('../src/backend/ops') | |
| 10 | const pow = require('../src/backend/pow') | |
| 11 | ||
| 12 | const tests = [] | |
| 13 | function test (name, fn) { tests.push({ name, fn }) } | |
| 14 | ||
| 15 | test('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 | ||
| 28 | test('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 | ||
| 57 | test('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 | ||
| 70 | test('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 | ||
| 84 | test('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 | ||
| 102 | test('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 | ||
| 116 | test('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 | ||
| 125 | test('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 | ||
| 134 | test('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 | })() |
| … | … | @@ -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 | ||
| 10 | const assert = require('assert/strict') | |
| 11 | const { spawn } = require('child_process') | |
| 12 | const path = require('path') | |
| 13 | ||
| 14 | const BARE = path.join(__dirname, '..', 'node_modules', 'bare', 'bin', 'bare') | |
| 15 | const HARNESS = path.join(__dirname, '..', 'src', 'bare', 'harness.js') | |
| 16 | ||
| 17 | function 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 | })() |
| … | … | @@ -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 | ||
| 8 | const assert = require('assert/strict') | |
| 9 | const b4a = require('b4a') | |
| 10 | const { Node } = require('../src/backend/node') | |
| 11 | const { Indexer } = require('../src/backend/indexer') | |
| 12 | const { createRPC } = require('../src/backend/rpc') | |
| 13 | ||
| 14 | const tests = [] | |
| 15 | function test (name, fn) { tests.push({ name, fn }) } | |
| 16 | ||
| 17 | async 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 | ||
| 26 | test('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 | ||
| 38 | test('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 | ||
| 75 | test('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 | ||
| 99 | test('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 | })() |
| … | … | @@ -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 | ||
| 9 | const assert = require('assert/strict') | |
| 10 | const b4a = require('b4a') | |
| 11 | const { Node } = require('../src/backend/node') | |
| 12 | const { connectDirect } = require('../src/backend/swarm') | |
| 13 | const { boardTopic } = require('../src/backend/board') | |
| 14 | const createTestnet = require('hyperdht/testnet') | |
| 15 | ||
| 16 | const tests = [] | |
| 17 | function test (name, fn) { tests.push({ name, fn }) } | |
| 18 | ||
| 19 | // --------------------------------------------------------------------------- | |
| 20 | // Direct-stream tests (no swarm) | |
| 21 | // --------------------------------------------------------------------------- | |
| 22 | ||
| 23 | test('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 | ||
| 49 | test('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 | ||
| 72 | test('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 | ||
| 94 | test('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 | ||
| 127 | function 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 | })() |
| … | … | @@ -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 | ||
| 7 | const assert = require('assert/strict') | |
| 8 | const b4a = require('b4a') | |
| 9 | const { Node } = require('../src/backend/node') | |
| 10 | const createTestnet = require('hyperdht/testnet') | |
| 11 | const { decodeOp } = require('../src/backend/ops') | |
| 12 | ||
| 13 | const tests = [] | |
| 14 | function test (name, fn) { tests.push({ name, fn }) } | |
| 15 | ||
| 16 | test('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 | ||
| 43 | test('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 | ||
| 76 | function 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 | })() |
| … | … | @@ -0,0 +1,224 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | // M4: local Hyperbee indexer. | |
| 4 | ||
| 5 | const assert = require('assert/strict') | |
| 6 | const b4a = require('b4a') | |
| 7 | const { Node } = require('../src/backend/node') | |
| 8 | const { Indexer } = require('../src/backend/indexer') | |
| 9 | const { connectDirect } = require('../src/backend/swarm') | |
| 10 | ||
| 11 | const tests = [] | |
| 12 | function test (name, fn) { tests.push({ name, fn }) } | |
| 13 | ||
| 14 | async 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 | |
| 21 | async 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 | ||
| 30 | test('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 | ||
| 62 | test('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 | ||
| 86 | test('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 | ||
| 123 | test('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 | ||
| 170 | test('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 | ||
| 191 | test('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 | })() |
| … | … | @@ -0,0 +1,174 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | // M5: end-to-end HTTP + WebSocket RPC smoke test. | |
| 4 | ||
| 5 | const assert = require('assert/strict') | |
| 6 | const http = require('http') | |
| 7 | const fs = require('fs') | |
| 8 | const os = require('os') | |
| 9 | const path = require('path') | |
| 10 | const WebSocket = require('ws') | |
| 11 | const { start } = require('../src/backend/server') | |
| 12 | ||
| 13 | const tests = [] | |
| 14 | function test (name, fn) { tests.push({ name, fn }) } | |
| 15 | ||
| 16 | function 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 | ||
| 27 | function 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 | ||
| 56 | async 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 | ||
| 65 | test('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 | ||
| 91 | test('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 | })() |
| … | … | @@ -0,0 +1,188 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | // M6: antispam layers — PoW gate, rate limits, blocklist, weighted votes. | |
| 4 | ||
| 5 | const assert = require('assert/strict') | |
| 6 | const b4a = require('b4a') | |
| 7 | const { Node } = require('../src/backend/node') | |
| 8 | const { Indexer } = require('../src/backend/indexer') | |
| 9 | const { connectDirect } = require('../src/backend/swarm') | |
| 10 | const pow = require('../src/backend/pow') | |
| 11 | ||
| 12 | const tests = [] | |
| 13 | function test (name, fn) { tests.push({ name, fn }) } | |
| 14 | ||
| 15 | async 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 | ||
| 24 | test('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 | ||
| 51 | test('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 | ||
| 78 | test('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 | ||
| 99 | test('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 | ||
| 143 | test('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 | ||
| 163 | test('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 | })() |
| … | … | @@ -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 | ||
| 13 | const assert = require('assert/strict') | |
| 14 | const b4a = require('b4a') | |
| 15 | const createTestnet = require('hyperdht/testnet') | |
| 16 | const { Node } = require('../src/backend/node') | |
| 17 | const { startRelay } = require('../src/relay/server') | |
| 18 | const { decodeOp } = require('../src/backend/ops') | |
| 19 | const fs = require('fs'), os = require('os'), path = require('path') | |
| 20 | ||
| 21 | const tests = [] | |
| 22 | function test (name, fn) { tests.push({ name, fn }) } | |
| 23 | ||
| 24 | function mkdir () { return fs.mkdtempSync(path.join(os.tmpdir(), 'p2pbuilders-m7-')) } | |
| 25 | ||
| 26 | async 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 | ||
| 35 | test('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 | })() |
| … | … | @@ -0,0 +1,147 @@ |
| 1 | 'use strict' | |
| 2 | ||
| 3 | // M8: edit/tombstone/profile handling in the indexer. | |
| 4 | ||
| 5 | const assert = require('assert/strict') | |
| 6 | const b4a = require('b4a') | |
| 7 | const { Node } = require('../src/backend/node') | |
| 8 | const { Indexer } = require('../src/backend/indexer') | |
| 9 | ||
| 10 | const tests = [] | |
| 11 | function test (name, fn) { tests.push({ name, fn }) } | |
| 12 | ||
| 13 | async 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 | ||
| 22 | test('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 | ||
| 41 | test('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 | ||
| 74 | test('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 | ||
| 104 | test('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 | })() |
| … | … | @@ -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 | ||
| 10 | const assert = require('assert/strict') | |
| 11 | const WebSocket = require('ws') | |
| 12 | const createTestnet = require('hyperdht/testnet') | |
| 13 | const { startRelay } = require('../src/relay/server') | |
| 14 | const fs = require('fs'), os = require('os'), path = require('path') | |
| 15 | ||
| 16 | const tests = [] | |
| 17 | function test (name, fn) { tests.push({ name, fn }) } | |
| 18 | ||
| 19 | function mkdir () { return fs.mkdtempSync(path.join(os.tmpdir(), 'p2pbuilders-m9-')) } | |
| 20 | ||
| 21 | test('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 | ||
| 51 | test('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 | })() |