| … | … | @@ -0,0 +1,1176 @@ |
| 1 | # Wirecore Pear Implementation Guide |
| 2 | |
| 3 | **Date**: 2026-01-20 |
| 4 | **Status**: Planning Phase - Pre-Implementation Documentation |
| 5 | |
| 6 | --- |
| 7 | |
| 8 | ## Table of Contents |
| 9 | |
| 10 | 1. [Overview](#overview) |
| 11 | 2. [Pear Deployment Best Practices](#pear-deployment-best-practices) |
| 12 | 3. [Identity Systems](#identity-systems) |
| 13 | 4. [Sparse Replication Strategy](#sparse-replication-strategy) |
| 14 | 5. [Pear GUI Options](#pear-gui-options) |
| 15 | 6. [Implementation Plan](#implementation-plan) |
| 16 | 7. [Phase Breakdown](#phase-breakdown) |
| 17 | 8. [References](#references) |
| 18 | |
| 19 | --- |
| 20 | |
| 21 | ## Overview |
| 22 | |
| 23 | This document outlines the complete implementation strategy for building a Pear-native Wirecore application based on lessons learned from hyper-fleeky projects and holepunchto best practices. |
| 24 | |
| 25 | **Goals:** |
| 26 | - ✅ Build Pear terminal app with persistent identities |
| 27 | - ✅ Test P2P connectivity between instances |
| 28 | - ✅ Deploy reliably (avoid previous deployment issues) |
| 29 | - ✅ Integrate with existing relay infrastructure |
| 30 | - ✅ Implement sparse replication (only pull followed users) |
| 31 | - ✅ Support multi-device identities (future phase) |
| 32 | - ✅ Build web UI that integrates with P2P core |
| 33 | - ✅ Research native GUI options (Electron vs alternatives) |
| 34 | |
| 35 | --- |
| 36 | |
| 37 | ## Pear Deployment Best Practices |
| 38 | |
| 39 | ### Lessons Learned from hyper-fleeky/pear-deployment-lessons.md |
| 40 | |
| 41 | #### Critical Issues to Avoid |
| 42 | |
| 43 | 1. **Module Resolution Under pear:// Protocol** |
| 44 | - **Problem**: ESM/CommonJS interop with hoisted dependencies fails in Bare runtime |
| 45 | - **Status**: Critical bug, not fixed as of 2026-01-20 |
| 46 | - **Impact**: Apps with npm dependencies fail to load under `pear://` links |
| 47 | - **Workaround**: Use local directory deployment |
| 48 | |
| 49 | 2. **Node Modules Staging** |
| 50 | - **Problem**: Ignoring `node_modules` in `pear.stage.ignore` breaks module resolution |
| 51 | - **Solution**: Always include `node_modules` in staging |
| 52 | - **Reason**: Bare runtime needs explicit module resolution paths |
| 53 | |
| 54 | 3. **Dynamic Dependencies** |
| 55 | - **Problem**: Dynamically required modules not included in bundle |
| 56 | - **Solution**: Add all transitive dependencies explicitly to `package.json` |
| 57 | - **Example**: `require('hyperswarm')` called indirectly needs explicit listing |
| 58 | |
| 59 | 4. **Permission Model** |
| 60 | - **Problem**: Running Pear as root breaks permission model |
| 61 | - **Solution**: Run as regular user with `CAP_NET_ADMIN` capability |
| 62 | - **Systemd Example**: |
| 63 | ```ini |
| 64 | [Service] |
| 65 | User=fleeky |
| 66 | Group=fleeky |
| 67 | AmbientCapabilities=CAP_NET_ADMIN |
| 68 | Environment="PATH=/home/fleeky/.config/pear/bin:/usr/bin:/bin" |
| 69 | ``` |
| 70 | |
| 71 | 5. **Cache Corruption** |
| 72 | - **Problem**: Pear caches apps in `~/.config/pear/app-storage/`, persists across updates |
| 73 | - **Detection**: Silent failures, timeouts, module not found errors |
| 74 | - **Solution**: Clear cache with `rm -rf ~/.config/pear/app-storage/*` |
| 75 | - **Nuclear Option**: Complete reinstall `rm -rf ~/.config/pear && npx pear` |
| 76 | |
| 77 | ### Development Workflow |
| 78 | |
| 79 | #### Local Development (Recommended) |
| 80 | |
| 81 | ```bash |
| 82 | # 1. Initialize project |
| 83 | pear init -t terminal -y |
| 84 | cd my-app |
| 85 | |
| 86 | # 2. Install dependencies |
| 87 | npm install |
| 88 | |
| 89 | # 3. Develop with hot-reload |
| 90 | pear run --dev . |
| 91 | |
| 92 | # 4. Test in production-like environment |
| 93 | pear run . |
| 94 | ``` |
| 95 | |
| 96 | #### Production Deployment (Two Options) |
| 97 | |
| 98 | **Option A: pear:// Link (Dynamic, Broken for npm deps)** |
| 99 | ```bash |
| 100 | # Stage for production |
| 101 | pear stage production |
| 102 | |
| 103 | # Release to DHT |
| 104 | pear release production |
| 105 | |
| 106 | # Seed (make available) |
| 107 | pear seed production |
| 108 | |
| 109 | # Users install via |
| 110 | pear install pear://[key] |
| 111 | ``` |
| 112 | |
| 113 | **Status**: ❌ Broken for apps with npm dependencies (module resolution bug) |
| 114 | |
| 115 | **Option B: Local Directory (Reliable, Recommended)** |
| 116 | ```bash |
| 117 | # Package and extract |
| 118 | pear stage production |
| 119 | pear dump production /opt/wirecore |
| 120 | |
| 121 | # Deploy as systemd service |
| 122 | pear run /opt/wirecore |
| 123 | |
| 124 | # Or use existing systemd unit |
| 125 | systemctl start wirecore |
| 126 | ``` |
| 127 | |
| 128 | **Status**: ✅ Works reliably, recommended for production |
| 129 | |
| 130 | ### Best Practices Checklist |
| 131 | |
| 132 | - [ ] Include `node_modules` in staging (remove from `.pearignore`) |
| 133 | - [ ] List all dependencies explicitly in `package.json` |
| 134 | - [ ] Test on clean machine before release |
| 135 | - [ ] Run as regular user with capabilities, not root |
| 136 | - [ ] Set full PATH in systemd: `/home/user/.config/pear/bin:/usr/bin:/bin` |
| 137 | - [ ] Use local directory deployment for apps with npm dependencies |
| 138 | - [ ] Clear cache when testing updates |
| 139 | - [ ] Use `--checkout=latest` flag to force fresh download |
| 140 | |
| 141 | ### Common Errors and Solutions |
| 142 | |
| 143 | | Error | Cause | Solution | |
| 144 | |-------|-------|----------| |
| 145 | | `Cannot find module 'X'` | Module not in bundle | Add to `package.json` explicitly | |
| 146 | | Permission denied | Running as root | Run as user with `CAP_NET_ADMIN` | |
| 147 | | Silent failure | Corrupted cache | `rm -rf ~/.config/pear/app-storage/*` | |
| 148 | | Module resolution error under pear:// | Known bug | Use local directory deployment | |
| 149 | | Service won't start | Wrong PATH | Set full PATH in systemd unit | |
| 150 | |
| 151 | --- |
| 152 | |
| 153 | ## Identity Systems |
| 154 | |
| 155 | ### Options Evaluated |
| 156 | |
| 157 | #### 1. keet-identity-key (Recommended) |
| 158 | |
| 159 | **What it is**: Hierarchical deterministic (HD) keypair system for P2P identities |
| 160 | |
| 161 | **Key Features:** |
| 162 | - BIP39-like mnemonic generation (24 words) |
| 163 | - Device chaining with proof system |
| 164 | - Multi-device support built-in |
| 165 | - Hypercore integration via discoveryCorePath() |
| 166 | - Used in production by Keet |
| 167 | |
| 168 | **API Example:** |
| 169 | ```javascript |
| 170 | import IdentityKey from 'keet-identity-key' |
| 171 | |
| 172 | // Generate new identity |
| 173 | const mnemonic = IdentityKey.generateMnemonic() |
| 174 | const id = await IdentityKey.from({ mnemonic }) |
| 175 | |
| 176 | // Get identity keypair |
| 177 | const identityKey = id.identityKeyPair |
| 178 | |
| 179 | // Device-specific keys |
| 180 | const deviceKey = id.keyPair(0) // First device |
| 181 | const deviceKey2 = id.keyPair(1) // Second device |
| 182 | |
| 183 | // Discovery key for Hyperswarm |
| 184 | const discoveryKey = id.discoveryCorePath() |
| 185 | |
| 186 | // Bootstrap device chain |
| 187 | const proof = id.bootstrap(deviceKey.publicKey) |
| 188 | |
| 189 | // Attest additional devices |
| 190 | const proof2 = IdentityKey.attest(deviceKey2.publicKey, deviceKey, proof) |
| 191 | |
| 192 | // Verify proof |
| 193 | const info = IdentityKey.verify(proof2) |
| 194 | ``` |
| 195 | |
| 196 | **Storage:** |
| 197 | - Mnemonic stored in `~/.wirecore/identity.json` |
| 198 | - Encrypted with user password (optional) |
| 199 | - Backup-friendly (24 words restore everything) |
| 200 | |
| 201 | **Integration with Hypercore:** |
| 202 | ```javascript |
| 203 | // User's profile feed |
| 204 | const profileCore = store.get({ |
| 205 | key: id.identityKeyPair.publicKey |
| 206 | }) |
| 207 | |
| 208 | // Join swarm with discovery key |
| 209 | swarm.join(id.discoveryCorePath()) |
| 210 | |
| 211 | // Device-specific signing |
| 212 | const signature = deviceKey.sign(message) |
| 213 | ``` |
| 214 | |
| 215 | **Pros:** |
| 216 | - ✅ Multi-device support from day 1 |
| 217 | - ✅ HD key derivation (one mnemonic → many keys) |
| 218 | - ✅ Production-proven (Keet) |
| 219 | - ✅ Identity recovery via mnemonic |
| 220 | - ✅ Device proof system |
| 221 | |
| 222 | **Cons:** |
| 223 | - ⚠️ More complex than simple Ed25519 |
| 224 | - ⚠️ Requires proof management for multi-device |
| 225 | |
| 226 | #### 2. hyperident (Alternative, Local Implementation) |
| 227 | |
| 228 | **What it is**: Decentralized identity manager built in hyper-fleeky |
| 229 | |
| 230 | **Features:** |
| 231 | - Uses keet-identity-key under the hood |
| 232 | - Additional profile management |
| 233 | - Device linking/mesh networking |
| 234 | - P2P device discovery |
| 235 | |
| 236 | **Components:** |
| 237 | - `IdentityManager`: High-level identity operations |
| 238 | - `CryptoManager`: Encryption/decryption |
| 239 | - `DeviceLinkingManager`: Multi-device coordination |
| 240 | - `DeviceMeshManager`: P2P device-to-device |
| 241 | |
| 242 | **Storage Location:** `~/.hyperident/profiles.json` |
| 243 | |
| 244 | **Pros:** |
| 245 | - ✅ Higher-level abstraction over keet-identity-key |
| 246 | - ✅ Profile management built-in |
| 247 | - ✅ Device mesh networking |
| 248 | |
| 249 | **Cons:** |
| 250 | - ⚠️ More dependencies |
| 251 | - ⚠️ Larger codebase to maintain |
| 252 | - ⚠️ Overkill for simple social network |
| 253 | |
| 254 | ### Recommendation: keet-identity-key |
| 255 | |
| 256 | **Reason**: Direct, minimal, production-proven, handles multi-device without extra complexity. |
| 257 | |
| 258 | **Implementation Strategy:** |
| 259 | ```javascript |
| 260 | // lib/identity/manager.js |
| 261 | import IdentityKey from 'keet-identity-key' |
| 262 | import { readFile, writeFile } from 'fs/promises' |
| 263 | import { join } from 'path' |
| 264 | |
| 265 | export class WirecoreIdentity { |
| 266 | constructor(storagePath) { |
| 267 | this.storagePath = storagePath |
| 268 | this.identityPath = join(storagePath, 'identity.json') |
| 269 | this.id = null |
| 270 | } |
| 271 | |
| 272 | async init() { |
| 273 | try { |
| 274 | const data = await readFile(this.identityPath, 'utf8') |
| 275 | const { mnemonic } = JSON.parse(data) |
| 276 | this.id = await IdentityKey.from({ mnemonic }) |
| 277 | } catch { |
| 278 | // No identity exists, will need to create |
| 279 | this.id = null |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | async create(name = 'Anonymous') { |
| 284 | const mnemonic = IdentityKey.generateMnemonic() |
| 285 | this.id = await IdentityKey.from({ mnemonic }) |
| 286 | |
| 287 | await writeFile(this.identityPath, JSON.stringify({ |
| 288 | name, |
| 289 | mnemonic, |
| 290 | publicKey: this.id.identityKeyPair.publicKey.toString('hex'), |
| 291 | created: Date.now() |
| 292 | })) |
| 293 | |
| 294 | return { |
| 295 | mnemonic, |
| 296 | publicKey: this.getPublicKey() |
| 297 | } |
| 298 | } |
| 299 | |
| 300 | getPublicKey() { |
| 301 | return this.id.identityKeyPair.publicKey.toString('hex') |
| 302 | } |
| 303 | |
| 304 | getDiscoveryKey() { |
| 305 | return this.id.discoveryCorePath() |
| 306 | } |
| 307 | |
| 308 | sign(message) { |
| 309 | return this.id.identityKeyPair.sign(message) |
| 310 | } |
| 311 | } |
| 312 | ``` |
| 313 | |
| 314 | --- |
| 315 | |
| 316 | ## Sparse Replication Strategy |
| 317 | |
| 318 | ### Problem Statement |
| 319 | |
| 320 | **Current relay architecture**: One central feed with ALL messages from ALL users |
| 321 | - 612 messages from 89 authors |
| 322 | - User following 3 people downloads 609 irrelevant messages |
| 323 | - Wastes bandwidth and storage |
| 324 | |
| 325 | **Goal**: Only replicate feeds of followed users |
| 326 | |
| 327 | ### Solution: Per-User Feeds |
| 328 | |
| 329 | **Architecture:** |
| 330 | ``` |
| 331 | User Alice User Bob |
| 332 | ├── Alice's Feed (writable) ├── Bob's Feed (writable) |
| 333 | └── Follows: └── Follows: |
| 334 | ├── Bob's Feed (replica) ├── Alice's Feed (replica) |
| 335 | └── Carol's Feed (replica) └── Carol's Feed (replica) |
| 336 | ``` |
| 337 | |
| 338 | **No central relay feed needed** - each user's feed is discovered via Hyperswarm DHT. |
| 339 | |
| 340 | ### Implementation |
| 341 | |
| 342 | #### 1. Selective Replication via Corestore |
| 343 | |
| 344 | ```javascript |
| 345 | // Initialize corestore |
| 346 | const store = new Corestore('./storage/feeds') |
| 347 | |
| 348 | // User's own feed (writable) |
| 349 | const myFeed = store.get({ name: 'my-feed' }) |
| 350 | await myFeed.ready() |
| 351 | |
| 352 | // Follow another user (read-only replica) |
| 353 | async function follow(publicKey, name) { |
| 354 | const feedKey = Buffer.from(publicKey, 'hex') |
| 355 | const feed = store.get(feedKey) |
| 356 | await feed.ready() |
| 357 | |
| 358 | follows.set(publicKey, { feed, name }) |
| 359 | |
| 360 | // Join their topic to find peers |
| 361 | swarm.join(feed.discoveryKey, { |
| 362 | server: false, // Don't serve their data |
| 363 | client: true // Download their data |
| 364 | }) |
| 365 | } |
| 366 | |
| 367 | // Replicate all followed feeds |
| 368 | swarm.on('connection', (conn) => { |
| 369 | // This replicates myFeed + all followed feeds |
| 370 | store.replicate(conn) |
| 371 | }) |
| 372 | ``` |
| 373 | |
| 374 | **Key Points:** |
| 375 | - `store.replicate(conn)` automatically replicates all cores in corestore |
| 376 | - Each followed user = one Hypercore in corestore |
| 377 | - Hyperswarm DHT handles peer discovery per feed |
| 378 | - No manual range selection needed (Hypercore handles sparse download) |
| 379 | |
| 380 | #### 2. Sparse Downloads (Built-in) |
| 381 | |
| 382 | Hypercore automatically does sparse downloading: |
| 383 | ```javascript |
| 384 | // Read only what you need |
| 385 | for await (const entry of feed.createReadStream({ |
| 386 | live: true, |
| 387 | start: feed.length - 20 // Only last 20 entries |
| 388 | })) { |
| 389 | console.log(entry) |
| 390 | } |
| 391 | |
| 392 | // Or specific ranges |
| 393 | const entries = await feed.getBatch(0, 20) |
| 394 | ``` |
| 395 | |
| 396 | **Hyperbee also sparse downloads:** |
| 397 | ```javascript |
| 398 | const bee = new Hyperbee(feed) |
| 399 | await bee.get('key') // Only downloads blocks needed for this query |
| 400 | ``` |
| 401 | |
| 402 | #### 3. Relay Integration (Sparse) |
| 403 | |
| 404 | **Don't replicate entire relay feed:** |
| 405 | |
| 406 | ```javascript |
| 407 | // BAD: Downloads all 612 messages |
| 408 | const relayCore = store.get(relayKey) |
| 409 | swarm.join(relayCore.discoveryKey) |
| 410 | ``` |
| 411 | |
| 412 | **GOOD: Query relay API, cache locally:** |
| 413 | |
| 414 | ```javascript |
| 415 | // Query relay for followed users only |
| 416 | async function syncFromRelay(followedUsers) { |
| 417 | for (const user of followedUsers) { |
| 418 | // GET /relay/query?author=<pubkey> |
| 419 | const res = await fetch(`https://7rnx.net/relay/all?author=${user}`) |
| 420 | const messages = await res.json() |
| 421 | |
| 422 | // Store in user's local feed |
| 423 | for (const msg of messages) { |
| 424 | await userFeed.append(msg) |
| 425 | } |
| 426 | } |
| 427 | } |
| 428 | ``` |
| 429 | |
| 430 | **Even Better: Relay publishes per-user feeds:** |
| 431 | |
| 432 | ```javascript |
| 433 | // Relay exposes per-user Hypercores |
| 434 | // GET /relay/user/<pubkey>/feed-key |
| 435 | const feedKey = await fetch(`/relay/user/${pubkey}/feed-key`).then(r => r.text()) |
| 436 | |
| 437 | // Replicate just that user's feed via Hyperswarm |
| 438 | const userFeed = store.get(Buffer.from(feedKey, 'hex')) |
| 439 | swarm.join(userFeed.discoveryKey) |
| 440 | ``` |
| 441 | |
| 442 | ### Disk Space Comparison |
| 443 | |
| 444 | | Scenario | Messages Downloaded | Storage | |
| 445 | |----------|-------------------|---------| |
| 446 | | Full relay feed | 612 messages | ~2MB | |
| 447 | | Follow 3 users (sparse) | ~30 messages | ~50KB | |
| 448 | | **Savings** | **95% less** | **97% less** | |
| 449 | |
| 450 | --- |
| 451 | |
| 452 | ## Pear GUI Options |
| 453 | |
| 454 | ### 1. Terminal App (Recommended for Phase 1) |
| 455 | |
| 456 | **Technology:** Pear Terminal + bare-readline |
| 457 | |
| 458 | **Capabilities:** |
| 459 | - Line-based input/output |
| 460 | - ANSI color support |
| 461 | - Raw terminal mode for key capture |
| 462 | - Cross-platform (Linux, macOS, Windows) |
| 463 | |
| 464 | **Limitations:** |
| 465 | - No blessed (TUI framework) - not compatible with Bare |
| 466 | - No fancy UI elements (boxes, windows, etc.) |
| 467 | - Manual ANSI escape codes for formatting |
| 468 | |
| 469 | **Example:** |
| 470 | ```javascript |
| 471 | // index.js |
| 472 | import Hyperswarm from 'hyperswarm' |
| 473 | import readline from 'bare-readline' |
| 474 | import tty from 'bare-tty' |
| 475 | |
| 476 | const rl = readline.createInterface({ |
| 477 | input: new tty.ReadStream(0), |
| 478 | output: new tty.WriteStream(1) |
| 479 | }) |
| 480 | |
| 481 | // Colors via ANSI |
| 482 | const colors = { |
| 483 | cyan: '\x1b[36m', |
| 484 | green: '\x1b[32m', |
| 485 | reset: '\x1b[0m' |
| 486 | } |
| 487 | |
| 488 | console.log(`${colors.cyan}Wirecore${colors.reset} - P2P Social Network`) |
| 489 | |
| 490 | rl.on('data', (line) => { |
| 491 | const [cmd, ...args] = line.split(' ') |
| 492 | |
| 493 | switch(cmd) { |
| 494 | case 'post': |
| 495 | handlePost(args.join(' ')) |
| 496 | break |
| 497 | case 'timeline': |
| 498 | showTimeline() |
| 499 | break |
| 500 | } |
| 501 | }) |
| 502 | ``` |
| 503 | |
| 504 | **Best For:** |
| 505 | - CLI-first users |
| 506 | - Server deployments |
| 507 | - Initial testing |
| 508 | - SSH access |
| 509 | |
| 510 | ### 2. Desktop App (Phase 3+) |
| 511 | |
| 512 | **Technology:** pear-electron (Electron wrapper) |
| 513 | |
| 514 | **Capabilities:** |
| 515 | - Full HTML/CSS/JavaScript UI |
| 516 | - React, Vue, Svelte, etc. |
| 517 | - Native window controls |
| 518 | - Rich media support |
| 519 | - Notifications |
| 520 | |
| 521 | **Setup:** |
| 522 | ```bash |
| 523 | # Initialize desktop app |
| 524 | pear init -t desktop -y |
| 525 | |
| 526 | # Install pear-electron |
| 527 | npm install pear-electron |
| 528 | |
| 529 | # package.json |
| 530 | { |
| 531 | "pear": { |
| 532 | "pre": "pear-electron/pre" |
| 533 | } |
| 534 | } |
| 535 | ``` |
| 536 | |
| 537 | **Example UI:** |
| 538 | ```html |
| 539 | <!-- index.html --> |
| 540 | <!DOCTYPE html> |
| 541 | <html> |
| 542 | <head> |
| 543 | <style> |
| 544 | #titlebar { |
| 545 | -webkit-app-region: drag; |
| 546 | background: #333; |
| 547 | color: #fff; |
| 548 | padding: 10px; |
| 549 | } |
| 550 | </style> |
| 551 | </head> |
| 552 | <body> |
| 553 | <div id="titlebar"> |
| 554 | <span>Wirecore</span> |
| 555 | <pear-ctrl></pear-ctrl> <!-- Window controls --> |
| 556 | </div> |
| 557 | |
| 558 | <div id="app"> |
| 559 | <!-- React/Vue/Svelte root --> |
| 560 | </div> |
| 561 | |
| 562 | <script type="module" src="app.js"></script> |
| 563 | </body> |
| 564 | </html> |
| 565 | ``` |
| 566 | |
| 567 | ```javascript |
| 568 | // app.js |
| 569 | import Hyperswarm from 'hyperswarm' |
| 570 | import { WirecoreNode } from './lib/node.js' |
| 571 | |
| 572 | const node = new WirecoreNode('./storage') |
| 573 | await node.init() |
| 574 | |
| 575 | // UI updates |
| 576 | node.on('post', (post) => { |
| 577 | updateTimeline(post) |
| 578 | }) |
| 579 | ``` |
| 580 | |
| 581 | **Best For:** |
| 582 | - End users |
| 583 | - Rich media (images, videos) |
| 584 | - Mainstream adoption |
| 585 | - Desktop OS integration |
| 586 | |
| 587 | ### 3. Web UI (Phase 2) |
| 588 | |
| 589 | **Technology:** Traditional HTML/CSS/JS + WebSocket to relay |
| 590 | |
| 591 | **Architecture:** |
| 592 | ``` |
| 593 | [Browser] ←→ HTTP/WebSocket ←→ [Relay Gateway] ←→ Hyperswarm P2P |
| 594 | ``` |
| 595 | |
| 596 | **Capabilities:** |
| 597 | - No installation |
| 598 | - Cross-platform |
| 599 | - Mobile support |
| 600 | - Accessible URL |
| 601 | |
| 602 | **Limitations:** |
| 603 | - Requires relay gateway |
| 604 | - Not truly P2P (relay is middleman) |
| 605 | - Network dependency |
| 606 | |
| 607 | **Implementation:** |
| 608 | ```javascript |
| 609 | // public/app.js |
| 610 | const ws = new WebSocket('wss://7rnx.net/relay') |
| 611 | |
| 612 | ws.onmessage = (event) => { |
| 613 | const post = JSON.parse(event.data) |
| 614 | addToTimeline(post) |
| 615 | } |
| 616 | |
| 617 | function post(text) { |
| 618 | ws.send(JSON.stringify({ |
| 619 | type: 'post', |
| 620 | text |
| 621 | })) |
| 622 | } |
| 623 | ``` |
| 624 | |
| 625 | **Best For:** |
| 626 | - Quick access |
| 627 | - Mobile users |
| 628 | - Users without Pear installed |
| 629 | - Public discovery |
| 630 | |
| 631 | ### Recommendation by Phase |
| 632 | |
| 633 | | Phase | Technology | Priority | Reason | |
| 634 | |-------|-----------|----------|--------| |
| 635 | | **Phase 1** | Terminal App | HIGH | Test P2P, no GUI complexity | |
| 636 | | **Phase 2** | Web UI | MEDIUM | Accessibility, mobile support | |
| 637 | | **Phase 3** | Desktop App | LOW | Rich UX for committed users | |
| 638 | |
| 639 | --- |
| 640 | |
| 641 | ## Implementation Plan |
| 642 | |
| 643 | ### Architecture Overview |
| 644 | |
| 645 | ``` |
| 646 | ┌─────────────────────────────────────────────────────────┐ |
| 647 | │ WIRECORE NODE │ |
| 648 | │ (Core P2P Logic) │ |
| 649 | ├─────────────────────────────────────────────────────────┤ |
| 650 | │ lib/core/wirecore.js │ |
| 651 | │ ✅ Already exists │ |
| 652 | │ ✅ Per-user feeds │ |
| 653 | │ ✅ Follow/unfollow │ |
| 654 | │ ✅ Timeline generation │ |
| 655 | │ ✅ ANProto signing │ |
| 656 | └─────────────────────────────────────────────────────────┘ |
| 657 | ↓ ↓ |
| 658 | ┌──────────────────────┐ ┌──────────────────────┐ |
| 659 | │ PEAR TERMINAL │ │ WEB UI │ |
| 660 | │ (Phase 1) │ │ (Phase 2) │ |
| 661 | ├──────────────────────┤ ├──────────────────────┤ |
| 662 | │ pear-app/ │ │ web-ui/ │ |
| 663 | │ ├── index.js │ │ ├── index.html │ |
| 664 | │ ├── package.json │ │ ├── app.js │ |
| 665 | │ └── Uses Wirecore │ │ └── WebSocket │ |
| 666 | └──────────────────────┘ └──────────────────────┘ |
| 667 | ↓ ↓ |
| 668 | ┌─────────────────────────────────────────────────────────┐ |
| 669 | │ IDENTITY LAYER │ |
| 670 | │ (keet-identity-key) │ |
| 671 | ├─────────────────────────────────────────────────────────┤ |
| 672 | │ lib/identity/manager.js │ |
| 673 | │ ├── Create/restore identities │ |
| 674 | │ ├── HD key derivation │ |
| 675 | │ ├── Device proofs │ |
| 676 | │ └── Mnemonic storage │ |
| 677 | └─────────────────────────────────────────────────────────┘ |
| 678 | ↓ |
| 679 | ┌─────────────────────────────────────────────────────────┐ |
| 680 | │ HYPERSWARM P2P │ |
| 681 | │ (Direct peer-to-peer networking) │ |
| 682 | └─────────────────────────────────────────────────────────┘ |
| 683 | ↕ |
| 684 | ┌─────────────────────────────────────────────────────────┐ |
| 685 | │ RELAY (Optional) │ |
| 686 | │ (Bridge for web users) │ |
| 687 | ├─────────────────────────────────────────────────────────┤ |
| 688 | │ relay/wirecore-relay.js │ |
| 689 | │ ✅ Already running on 7rnx.net │ |
| 690 | │ ↔ Sparse queries only (followed users) │ |
| 691 | └─────────────────────────────────────────────────────────┘ |
| 692 | ``` |
| 693 | |
| 694 | --- |
| 695 | |
| 696 | ## Phase Breakdown |
| 697 | |
| 698 | ### Phase 1: Pear Terminal App (Week 1-2) |
| 699 | |
| 700 | **Goal:** Build and test P2P terminal app with persistent identities |
| 701 | |
| 702 | **Deliverables:** |
| 703 | 1. Pear terminal app structure |
| 704 | 2. keet-identity-key integration |
| 705 | 3. P2P connectivity between 2+ instances |
| 706 | 4. CLI commands: init, post, follow, timeline |
| 707 | 5. Sparse replication (followed users only) |
| 708 | 6. Relay integration (query only, not full sync) |
| 709 | |
| 710 | **Tasks:** |
| 711 | |
| 712 | #### 1.1 Project Setup |
| 713 | ```bash |
| 714 | # Create pear-app directory |
| 715 | mkdir pear-app |
| 716 | cd pear-app |
| 717 | |
| 718 | # Initialize Pear terminal app |
| 719 | pear init -t terminal -y |
| 720 | |
| 721 | # Install dependencies |
| 722 | npm install keet-identity-key hyperswarm hypercore corestore |
| 723 | ``` |
| 724 | |
| 725 | **File structure:** |
| 726 | ``` |
| 727 | pear-app/ |
| 728 | ├── package.json |
| 729 | ├── index.js # Main entry point |
| 730 | ├── lib/ |
| 731 | │ ├── identity.js # keet-identity-key wrapper |
| 732 | │ ├── node.js # Uses lib/core/wirecore.js |
| 733 | │ └── commands.js # CLI command handlers |
| 734 | └── .pearignore # IMPORTANT: Don't ignore node_modules |
| 735 | ``` |
| 736 | |
| 737 | #### 1.2 Identity Management |
| 738 | |
| 739 | Create `pear-app/lib/identity.js`: |
| 740 | ```javascript |
| 741 | import IdentityKey from 'keet-identity-key' |
| 742 | import { readFile, writeFile, mkdir } from 'fs/promises' |
| 743 | import { join } from 'path' |
| 744 | |
| 745 | export class WirecoreIdentity { |
| 746 | constructor(storagePath) { |
| 747 | this.storagePath = storagePath |
| 748 | this.identityPath = join(storagePath, 'identity.json') |
| 749 | this.id = null |
| 750 | } |
| 751 | |
| 752 | async init() { |
| 753 | await mkdir(this.storagePath, { recursive: true }) |
| 754 | |
| 755 | try { |
| 756 | const data = await readFile(this.identityPath, 'utf8') |
| 757 | const { mnemonic } = JSON.parse(data) |
| 758 | this.id = await IdentityKey.from({ mnemonic }) |
| 759 | return { exists: true } |
| 760 | } catch { |
| 761 | return { exists: false } |
| 762 | } |
| 763 | } |
| 764 | |
| 765 | async create(name = 'Anonymous') { |
| 766 | const mnemonic = IdentityKey.generateMnemonic() |
| 767 | this.id = await IdentityKey.from({ mnemonic }) |
| 768 | |
| 769 | await writeFile(this.identityPath, JSON.stringify({ |
| 770 | name, |
| 771 | mnemonic, |
| 772 | publicKey: this.getPublicKey(), |
| 773 | created: Date.now() |
| 774 | }, null, 2)) |
| 775 | |
| 776 | return { mnemonic, publicKey: this.getPublicKey() } |
| 777 | } |
| 778 | |
| 779 | getPublicKey() { |
| 780 | return this.id.identityKeyPair.publicKey.toString('hex') |
| 781 | } |
| 782 | |
| 783 | getKeypair() { |
| 784 | return this.id.identityKeyPair |
| 785 | } |
| 786 | |
| 787 | getDiscoveryKey() { |
| 788 | return this.id.discoveryCorePath() |
| 789 | } |
| 790 | } |
| 791 | ``` |
| 792 | |
| 793 | #### 1.3 Integrate with Wirecore Node |
| 794 | |
| 795 | Create `pear-app/lib/node.js`: |
| 796 | ```javascript |
| 797 | import { Wirecore } from '../../lib/core/wirecore.js' |
| 798 | import { WirecoreIdentity } from './identity.js' |
| 799 | |
| 800 | export class WirecorePearNode { |
| 801 | constructor(storagePath) { |
| 802 | this.storagePath = storagePath |
| 803 | this.identity = new WirecoreIdentity(storagePath) |
| 804 | this.node = null |
| 805 | } |
| 806 | |
| 807 | async init() { |
| 808 | // Initialize identity |
| 809 | const { exists } = await this.identity.init() |
| 810 | if (!exists) { |
| 811 | throw new Error('No identity found. Run "wirecore init" first.') |
| 812 | } |
| 813 | |
| 814 | // Initialize Wirecore with keet-identity keypair |
| 815 | this.node = new Wirecore(this.storagePath) |
| 816 | await this.node.init({ |
| 817 | keypair: this.identity.getKeypair() |
| 818 | }) |
| 819 | |
| 820 | return this.node |
| 821 | } |
| 822 | |
| 823 | async createIdentity(name) { |
| 824 | return await this.identity.create(name) |
| 825 | } |
| 826 | } |
| 827 | ``` |
| 828 | |
| 829 | #### 1.4 Terminal UI |
| 830 | |
| 831 | Create `pear-app/index.js`: |
| 832 | ```javascript |
| 833 | #!/usr/bin/env pear |
| 834 | |
| 835 | import readline from 'bare-readline' |
| 836 | import tty from 'bare-tty' |
| 837 | import { WirecorePearNode } from './lib/node.js' |
| 838 | |
| 839 | const STORAGE = './wirecore-storage' |
| 840 | |
| 841 | // ANSI colors |
| 842 | const colors = { |
| 843 | reset: '\x1b[0m', |
| 844 | cyan: '\x1b[36m', |
| 845 | green: '\x1b[32m', |
| 846 | yellow: '\x1b[33m', |
| 847 | gray: '\x1b[90m' |
| 848 | } |
| 849 | |
| 850 | const node = new WirecorePearNode(STORAGE) |
| 851 | |
| 852 | async function init(name) { |
| 853 | const { mnemonic, publicKey } = await node.createIdentity(name || 'Anonymous') |
| 854 | |
| 855 | console.log(`${colors.green}✓ Identity created!${colors.reset}`) |
| 856 | console.log(`${colors.cyan}Public Key:${colors.reset} ${publicKey}`) |
| 857 | console.log(`${colors.yellow}Mnemonic (SAVE THIS):${colors.reset}`) |
| 858 | console.log(mnemonic) |
| 859 | console.log() |
| 860 | console.log('Store this mnemonic safely. You\'ll need it to restore your identity.') |
| 861 | } |
| 862 | |
| 863 | async function post(text) { |
| 864 | await node.node.post({ text }) |
| 865 | console.log(`${colors.green}✓ Posted:${colors.reset} ${text}`) |
| 866 | } |
| 867 | |
| 868 | async function timeline() { |
| 869 | const posts = await node.node.getTimeline() |
| 870 | |
| 871 | console.log(`${colors.cyan}Timeline (${posts.length} posts):${colors.reset}\n`) |
| 872 | |
| 873 | for (const post of posts.slice(-20).reverse()) { |
| 874 | const date = new Date(post.timestamp).toLocaleString() |
| 875 | const author = post.author.slice(0, 8) + '...' |
| 876 | console.log(`${colors.gray}${date}${colors.reset} ${colors.cyan}@${author}${colors.reset}`) |
| 877 | console.log(post.text) |
| 878 | console.log() |
| 879 | } |
| 880 | } |
| 881 | |
| 882 | async function follow(pubkey, name) { |
| 883 | await node.node.follow(pubkey, name) |
| 884 | console.log(`${colors.green}✓ Following:${colors.reset} ${name || pubkey.slice(0, 16)}...`) |
| 885 | } |
| 886 | |
| 887 | async function whoami() { |
| 888 | const pubkey = node.identity.getPublicKey() |
| 889 | console.log(`${colors.cyan}Your Public Key:${colors.reset}`) |
| 890 | console.log(pubkey) |
| 891 | } |
| 892 | |
| 893 | // Main |
| 894 | async function main() { |
| 895 | const args = Pear.config.args |
| 896 | const [command, ...params] = args |
| 897 | |
| 898 | try { |
| 899 | if (command === 'init') { |
| 900 | await init(params[0]) |
| 901 | return |
| 902 | } |
| 903 | |
| 904 | // All other commands need initialized node |
| 905 | await node.init() |
| 906 | |
| 907 | switch (command) { |
| 908 | case 'post': |
| 909 | await post(params.join(' ')) |
| 910 | break |
| 911 | case 'timeline': |
| 912 | await timeline() |
| 913 | break |
| 914 | case 'follow': |
| 915 | await follow(params[0], params[1]) |
| 916 | break |
| 917 | case 'whoami': |
| 918 | await whoami() |
| 919 | break |
| 920 | default: |
| 921 | console.log('Commands: init, post, timeline, follow, whoami') |
| 922 | } |
| 923 | |
| 924 | await node.node.close() |
| 925 | } catch (err) { |
| 926 | console.error(`${colors.yellow}Error:${colors.reset} ${err.message}`) |
| 927 | process.exit(1) |
| 928 | } |
| 929 | } |
| 930 | |
| 931 | main() |
| 932 | ``` |
| 933 | |
| 934 | #### 1.5 Testing Plan |
| 935 | |
| 936 | **Test 1: Single Instance** |
| 937 | ```bash |
| 938 | # Initialize |
| 939 | pear run --dev . init Alice |
| 940 | |
| 941 | # Post |
| 942 | pear run --dev . post "Hello from Alice!" |
| 943 | |
| 944 | # View timeline |
| 945 | pear run --dev . timeline |
| 946 | |
| 947 | # Get public key |
| 948 | pear run --dev . whoami |
| 949 | ``` |
| 950 | |
| 951 | **Test 2: Two Instances (P2P)** |
| 952 | ```bash |
| 953 | # Terminal 1 (Alice) |
| 954 | cd pear-app-alice |
| 955 | pear run --dev . init Alice |
| 956 | pear run --dev . post "Alice's first post" |
| 957 | pear run --dev . whoami |
| 958 | # Copy Alice's pubkey |
| 959 | |
| 960 | # Terminal 2 (Bob) |
| 961 | cd pear-app-bob |
| 962 | pear run --dev . init Bob |
| 963 | pear run --dev . follow <alice-pubkey> Alice |
| 964 | pear run --dev . timeline |
| 965 | # Should see Alice's post! |
| 966 | |
| 967 | pear run --dev . post "Hi Alice, from Bob" |
| 968 | |
| 969 | # Terminal 1 (Alice) |
| 970 | pear run --dev . follow <bob-pubkey> Bob |
| 971 | pear run --dev . timeline |
| 972 | # Should see Bob's post! |
| 973 | ``` |
| 974 | |
| 975 | #### 1.6 Deployment |
| 976 | |
| 977 | **Local directory deployment:** |
| 978 | ```bash |
| 979 | # Package |
| 980 | cd pear-app |
| 981 | pear stage production |
| 982 | |
| 983 | # Extract to local directory |
| 984 | pear dump production /opt/wirecore-pear |
| 985 | |
| 986 | # Test |
| 987 | pear run /opt/wirecore-pear init TestUser |
| 988 | |
| 989 | # Create systemd service (optional) |
| 990 | sudo systemctl enable wirecore-pear |
| 991 | sudo systemctl start wirecore-pear |
| 992 | ``` |
| 993 | |
| 994 | --- |
| 995 | |
| 996 | ### Phase 2: Web UI Integration (Week 3-4) |
| 997 | |
| 998 | **Goal:** Build web UI that queries relay for followed users only |
| 999 | |
| 1000 | **Deliverables:** |
| 1001 | 1. Web UI (HTML/CSS/JS) |
| 1002 | 2. WebSocket connection to relay |
| 1003 | 3. Sparse queries (followed users only) |
| 1004 | 4. User authentication (pubkey-based) |
| 1005 | 5. Timeline view |
| 1006 | 6. Post composition |
| 1007 | |
| 1008 | **Architecture:** |
| 1009 | ``` |
| 1010 | [Browser] ←→ WebSocket ←→ [Relay] ←→ [Query followed users] ←→ Hyperswarm |
| 1011 | ``` |
| 1012 | |
| 1013 | **Key APIs:** |
| 1014 | ```javascript |
| 1015 | // GET /relay/user/<pubkey>/posts |
| 1016 | // Returns only that user's posts |
| 1017 | |
| 1018 | // WebSocket: Subscribe to followed users |
| 1019 | ws.send(JSON.stringify({ |
| 1020 | type: 'subscribe', |
| 1021 | follows: ['<pubkey1>', '<pubkey2>'] |
| 1022 | })) |
| 1023 | |
| 1024 | // WebSocket: Receive updates |
| 1025 | ws.onmessage = (event) => { |
| 1026 | const { type, author, post } = JSON.parse(event.data) |
| 1027 | if (type === 'new-post') { |
| 1028 | addToTimeline(author, post) |
| 1029 | } |
| 1030 | } |
| 1031 | ``` |
| 1032 | |
| 1033 | --- |
| 1034 | |
| 1035 | ### Phase 3: Desktop App (Week 5-6) |
| 1036 | |
| 1037 | **Goal:** Build Electron-based desktop app with rich UI |
| 1038 | |
| 1039 | **Technology:** |
| 1040 | - pear-electron |
| 1041 | - React/Vue/Svelte (TBD) |
| 1042 | - Same Wirecore core |
| 1043 | |
| 1044 | **Features:** |
| 1045 | - Rich media (images, videos) |
| 1046 | - Notifications |
| 1047 | - Native OS integration |
| 1048 | - Offline support |
| 1049 | |
| 1050 | --- |
| 1051 | |
| 1052 | ### Phase 4: Multi-Device Support (Week 7-8) |
| 1053 | |
| 1054 | **Goal:** Enable identity across multiple devices using keet-identity-key proofs |
| 1055 | |
| 1056 | **Features:** |
| 1057 | - Device linking |
| 1058 | - Proof chain verification |
| 1059 | - Sync between devices |
| 1060 | - Device-specific keys |
| 1061 | |
| 1062 | **Implementation:** |
| 1063 | ```javascript |
| 1064 | // Main device |
| 1065 | const proof = identity.id.bootstrap(mainDevice.publicKey) |
| 1066 | |
| 1067 | // Secondary device |
| 1068 | const proof2 = IdentityKey.attest(secondaryDevice.publicKey, mainDevice, proof) |
| 1069 | |
| 1070 | // Verify on any device |
| 1071 | const info = IdentityKey.verify(proof2) |
| 1072 | // Returns: list of devices in chain |
| 1073 | ``` |
| 1074 | |
| 1075 | --- |
| 1076 | |
| 1077 | ## References |
| 1078 | |
| 1079 | ### Documentation Sources |
| 1080 | |
| 1081 | 1. **hyper-fleeky Projects** |
| 1082 | - `/hyper-fleeky/pear-deployment-lessons.md` - Critical deployment lessons |
| 1083 | - `/hyper-fleeky/repos/hyperident/` - Identity implementation reference |
| 1084 | - `/hyper-fleeky/repos/wirecore/` - This project |
| 1085 | |
| 1086 | 2. **holepunchto Knowledge Base** |
| 1087 | - `/holepunchto/skills/keet-identity-key/` - HD identity system |
| 1088 | - `/holepunchto/skills/hypercore/` - Core P2P primitives |
| 1089 | - `/holepunchto/skills/hyperswarm/` - DHT networking |
| 1090 | - `/holepunchto/skills/corestore/` - Multi-core management |
| 1091 | - `/holepunchto/pear-docs/` - Pear runtime documentation |
| 1092 | |
| 1093 | 3. **Related Documentation** |
| 1094 | - `PEAR_APP_PLAN.md` - Original Pear app planning |
| 1095 | - `CODE_REVIEW.md` - Relay security review |
| 1096 | - `DEPLOYMENT_STATE.md` - Current relay status |
| 1097 | |
| 1098 | ### Key Dependencies |
| 1099 | |
| 1100 | ```json |
| 1101 | { |
| 1102 | "dependencies": { |
| 1103 | "keet-identity-key": "^1.0.0", |
| 1104 | "hyperswarm": "^4.0.0", |
| 1105 | "hypercore": "^10.0.0", |
| 1106 | "corestore": "^6.0.0", |
| 1107 | "hyperbee": "^2.26.5" |
| 1108 | } |
| 1109 | } |
| 1110 | ``` |
| 1111 | |
| 1112 | ### External Resources |
| 1113 | |
| 1114 | - Keet: https://keet.io (production P2P chat using similar architecture) |
| 1115 | - Holepunch: https://holepunch.to (Hypercore ecosystem) |
| 1116 | - Pear Runtime: https://pears.com |
| 1117 | |
| 1118 | --- |
| 1119 | |
| 1120 | ## Appendix: Common Commands Reference |
| 1121 | |
| 1122 | ### Development |
| 1123 | ```bash |
| 1124 | # Create new Pear terminal app |
| 1125 | pear init -t terminal -y |
| 1126 | |
| 1127 | # Run in development mode |
| 1128 | pear run --dev . |
| 1129 | |
| 1130 | # Run production build |
| 1131 | pear run . |
| 1132 | ``` |
| 1133 | |
| 1134 | ### Testing |
| 1135 | ```bash |
| 1136 | # Clear Pear cache |
| 1137 | rm -rf ~/.config/pear/app-storage/* |
| 1138 | |
| 1139 | # Test on clean instance |
| 1140 | pear run --checkout=latest pear://[key] |
| 1141 | |
| 1142 | # Check Pear version |
| 1143 | pear --version |
| 1144 | ``` |
| 1145 | |
| 1146 | ### Deployment |
| 1147 | ```bash |
| 1148 | # Stage for production |
| 1149 | pear stage production |
| 1150 | |
| 1151 | # Extract to directory |
| 1152 | pear dump production /opt/app |
| 1153 | |
| 1154 | # Seed on DHT (broken for npm deps) |
| 1155 | pear seed production |
| 1156 | ``` |
| 1157 | |
| 1158 | ### Debugging |
| 1159 | ```bash |
| 1160 | # View Pear logs |
| 1161 | tail -f ~/.config/pear/by-arch/[arch]/app-storage/[key]/platform.log |
| 1162 | |
| 1163 | # Check running apps |
| 1164 | pear apps |
| 1165 | |
| 1166 | # Kill app |
| 1167 | pear kill [key] |
| 1168 | ``` |
| 1169 | |
| 1170 | --- |
| 1171 | |
| 1172 | **Document Version**: 1.0 |
| 1173 | **Last Updated**: 2026-01-20 |
| 1174 | **Next Review**: After Phase 1 completion |
| 1175 | |
| 1176 | Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |