| … | … | @@ -1,58 +1,350 @@ |
| 1 | 1 | # Hypernote |
| 2 | 2 | |
| 3 | | P2P note-taking server with per-notebook Hyperdrives, end-to-end encryption, wiki-links, and multi-writer collaboration. |
| 3 | P2P note-taking server with per-notebook Hyperdrives, end-to-end encryption, wiki-links, and a built-in WYSIWYG editor. |
| 4 | 4 | |
| 5 | | Built on [Holepunch](https://holepunch.to): Hypercore, Hyperdrive, Hyperbee, Hyperswarm, Autobase. |
| 5 | Built on the [Holepunch](https://holepunch.to) stack: Hypercore, Hyperdrive, Hyperbee, and Hyperswarm. |
| 6 | 6 | |
| 7 | 7 | ## Features |
| 8 | 8 | |
| 9 | | - Per-notebook Hyperdrives with selective P2P replication |
| 10 | | - Multi-writer collaboration via Autobase + Yjs CRDTs |
| 11 | | - End-to-end encryption (Argon2id + XSalsa20-Poly1305) |
| 12 | | - Ed25519 browser identity with challenge-response auth |
| 13 | | - Wiki-links, backlinks, and knowledge graph |
| 14 | | - WYSIWYG editor (Toast UI), media upload, gallery |
| 15 | | - Hyper:// site publishing for Agregore browser |
| 16 | | - Zero CDN dependencies (works offline) |
| 9 | - **Per-notebook Hyperdrives** -- each notebook is an isolated Hyperdrive that can be selectively shared and replicated |
| 10 | - **P2P replication** -- announce notebooks on Hyperswarm for real-time peer-to-peer sync |
| 11 | - **Hyper:// site publishing** -- auto-publishes a read/write site drive accessible via [Agregore](https://agregore.mauve.moe/) browser |
| 12 | - **End-to-end encryption** -- per-notebook encryption with Argon2id key derivation and XSalsa20-Poly1305 |
| 13 | - **Ed25519 identity** -- passwordless auth via auto-generated browser keypairs with challenge-response signing |
| 14 | - **Wiki-links & backlinks** -- `[[Note Title]]` syntax with automatic backlink indexing and knowledge graph |
| 15 | - **WYSIWYG editor** -- Toast UI Editor with markdown, image upload, and video support |
| 16 | - **Content-addressed media** -- SHA-256 dedup via Hyperblobs, streaming serve with HTTP range requests (206 Partial Content) |
| 17 | - **HLS adaptive video** -- upload video and it auto-transcodes to 480p/720p/1080p HLS segments (requires ffmpeg) |
| 18 | - **Data export** -- export individual notebooks or the entire server as JSON |
| 19 | - **Public sharing** -- `/:username/:notebook/:noteId` URLs for shared notebooks |
| 20 | - **Note merge & split** -- combine notes or split at heading boundaries |
| 21 | - **Full-text search** -- search across all notebooks with context snippets |
| 22 | - **Zero CDN dependencies** -- all assets served locally (works offline and in hyper://) |
| 23 | - **CLI + Web UI + REST API** -- three ways to interact |
| 17 | 24 | |
| 18 | | ## Quick Start |
| 25 | ## Install |
| 26 | |
| 27 | ### From pear-git (P2P) |
| 19 | 28 | |
| 20 | 29 | ```bash |
| 30 | # 1. Install pear-git (if you don't have pgh) |
| 31 | npm install -g pear |
| 32 | pear dump pear://dpjhhzqgm6wqxc9nmw1y3m7eaptfufr6o5p7r4xegjghju8mkweo ~/.local/share/pear-git |
| 33 | cat > ~/.local/bin/pgh << 'EOF' |
| 34 | #!/bin/sh |
| 35 | exec bare ~/.local/share/pear-git/index.js pgh "$@" |
| 36 | EOF |
| 37 | chmod +x ~/.local/bin/pgh |
| 38 | |
| 39 | # 2. Clone hypernote |
| 40 | pgh repo clone punch://1290678432f9573b463d4472a9426c5408b34579bf8576e26957a1321fd5e836/hyper-note |
| 41 | |
| 42 | # 3. Install and run |
| 43 | cd hyper-note |
| 21 | 44 | npm install |
| 22 | 45 | bare hypernote-server.js serve |
| 46 | ``` |
| 23 | 47 | |
| 24 | | # LAN accessible |
| 25 | | bare hypernote-server.js serve 4040 --network |
| 48 | ### System Dependencies |
| 26 | 49 | |
| 27 | | # Behind reverse proxy |
| 28 | | bare hypernote-server.js serve 4040 --network --prefix=/hypernote |
| 29 | | ``` |
| 50 | - **[Bare runtime](https://github.com/nicknisi/bare)** -- JavaScript runtime for P2P applications |
| 51 | - **ffmpeg** -- required for HLS video transcoding (`apt install ffmpeg` / `brew install ffmpeg`) |
| 30 | 52 | |
| 31 | | On first start, an admin token is printed to the console. Open `http://localhost:4040` -- an ed25519 identity is generated automatically and the first user is promoted to admin. |
| 53 | ### From local checkout |
| 32 | 54 | |
| 33 | | ## Install via pear-git |
| 55 | ```bash |
| 56 | git clone <repo-url> hyper-note |
| 57 | cd hyper-note |
| 58 | npm install |
| 59 | bare hypernote-server.js serve |
| 60 | ``` |
| 61 | |
| 62 | ### As a Pear app (auto-updates) |
| 34 | 63 | |
| 35 | 64 | ```bash |
| 36 | 65 | pgh app install pear://p84snzmpxca3ii7nfdqjbd5wxjdqzccpoj9xjwzz96wpe4m557zo --name hypernote |
| 37 | 66 | hypernote serve |
| 38 | 67 | ``` |
| 39 | 68 | |
| 69 | ## Quick Start |
| 70 | |
| 71 | ```bash |
| 72 | # Start server (localhost:4040) |
| 73 | bare hypernote-server.js serve |
| 74 | |
| 75 | # Start on LAN (accessible from other devices) |
| 76 | bare hypernote-server.js serve 4040 --network |
| 77 | |
| 78 | # Custom port, storage, and URL prefix |
| 79 | bare hypernote-server.js serve 5555 --storage=/path/to/data --prefix=/hypernote |
| 80 | |
| 81 | # Promote a user to admin (CLI) |
| 82 | bare hypernote-server.js promote <publicKey|username> |
| 83 | ``` |
| 84 | |
| 85 | On first start, the server prints an admin bearer token to the console. Open `http://localhost:4040` in your browser -- an ed25519 identity is generated automatically and the first user is promoted to admin. |
| 86 | |
| 87 | ### Reverse Proxy |
| 88 | |
| 89 | Use `--prefix` when running behind a reverse proxy: |
| 90 | |
| 91 | ```bash |
| 92 | bare hypernote-server.js serve 4040 --network --prefix=/hypernote |
| 93 | ``` |
| 94 | |
| 95 | Access at `http://yourhost/hypernote/`. The prefix is stripped from API routes automatically. |
| 96 | |
| 97 | ### Agregore (hyper://) |
| 98 | |
| 99 | The server auto-publishes a site drive on Hyperswarm. Access via Agregore browser: |
| 100 | |
| 101 | ``` |
| 102 | hyper://SITE_KEY/ |
| 103 | ``` |
| 104 | |
| 105 | The site key is shown in the server logs and available at `GET /api/site`. When accessed via `hyper://`, the app connects back to the local HTTP server for full read/write access (not read-only). |
| 106 | |
| 40 | 107 | ## CLI |
| 41 | 108 | |
| 42 | 109 | ```bash |
| 110 | # Notebooks |
| 43 | 111 | hn notebook list |
| 44 | 112 | hn notebook create "Work Notes" |
| 113 | hn notebook delete work-notes |
| 114 | hn notebook rename work-notes new-slug |
| 115 | |
| 116 | # Notes |
| 117 | hn note list work-notes |
| 45 | 118 | hn note create work-notes --title "Meeting" --content "# Agenda\n..." |
| 46 | | hn search "query" |
| 119 | hn note get work-notes <noteId> |
| 120 | hn note update work-notes <noteId> --title "Updated" |
| 121 | hn note delete work-notes <noteId> |
| 122 | hn note move work-notes <noteId> --to personal |
| 123 | |
| 124 | # Search |
| 125 | hn search "query" --notebook work-notes |
| 126 | |
| 127 | # Encryption |
| 128 | hn share encrypt work-notes --password secret |
| 129 | hn share decrypt work-notes --password secret |
| 130 | hn share unlock work-notes --password secret |
| 131 | |
| 132 | # Links |
| 133 | hn links work-notes <noteId> |
| 134 | |
| 135 | # P2P Sync |
| 47 | 136 | hn sync status |
| 137 | hn sync announce work-notes |
| 138 | hn sync add <driveKey> --slug remote-notes |
| 139 | hn sync remove remote-notes |
| 140 | |
| 141 | # Migration (Hyperdrive media → content-addressed MediaStore) |
| 142 | bare hypernote-server.js migrate verify [--storage=path] |
| 143 | bare hypernote-server.js migrate cleanup [--storage=path] |
| 144 | ``` |
| 145 | |
| 146 | Configure the CLI: |
| 147 | |
| 148 | ```bash |
| 149 | # Stored in ~/.config/hypernote/cli.json |
| 150 | hn config set server http://localhost:4040 |
| 151 | hn config set token <bearer-token> |
| 152 | ``` |
| 153 | |
| 154 | ## Architecture |
| 155 | |
| 156 | ``` |
| 157 | Browser (SPA) Server (Bare runtime) |
| 158 | identity.js AuthManager |
| 159 | ed25519 keypair WebIdentityProvider |
| 160 | challenge signing BearerTokenProvider |
| 161 | fetch interception KeetIdentityProvider |
| 162 | api.js NoteManager |
| 163 | HTTP client per-notebook CRUD |
| 164 | hyper:// dual-mode DriveManager |
| 165 | app.js Hyperdrive lifecycle |
| 166 | hash router ReplicationManager |
| 167 | Toast UI Editor Hyperswarm P2P sync |
| 168 | IdentityRegistry |
| 169 | user accounts + permissions |
| 170 | LinkManager |
| 171 | wiki-links + backlinks |
| 172 | NotebookCrypto |
| 173 | per-notebook encryption |
| 174 | MediaStore |
| 175 | SHA-256 content-addressed blobs |
| 176 | VideoProcessor |
| 177 | HLS transcoding (ffmpeg) |
| 178 | SitePublisher |
| 179 | hyper:// site drive |
| 48 | 180 | ``` |
| 49 | 181 | |
| 50 | | Configure: `hn config set server http://localhost:4040` |
| 182 | ### Storage Layout |
| 183 | |
| 184 | ``` |
| 185 | ~/.config/hypernote/ |
| 186 | storage-locator.json # breadcrumb to find storage |
| 187 | server-data/ |
| 188 | server-identity.json # device ed25519 keypair |
| 189 | device-mnemonic.enc # encrypted BIP39 mnemonic vault |
| 190 | corestore/ |
| 191 | registry/ # Hyperbee (users, tokens, metadata) |
| 192 | notebook-general/ # Hyperdrive for "general" notebook |
| 193 | notebook-work/ # Hyperdrive for "work" notebook |
| 194 | site-public/ # Hyperdrive for hyper:// site |
| 195 | ... |
| 196 | ``` |
| 197 | |
| 198 | Media is stored in a shared, content-addressed Hyperblobs core (MediaStore). Duplicate files are automatically deduplicated by SHA-256 hash. Legacy per-notebook Hyperdrive media paths are supported via backward-compatible filename-to-hash mappings. |
| 199 | |
| 200 | Each notebook Hyperdrive contains: |
| 201 | |
| 202 | ``` |
| 203 | /metadata.json # notebook name, description, flags |
| 204 | /notes/{noteId}.json # individual notes (title, content, tags) |
| 205 | ``` |
| 206 | |
| 207 | ### Authentication |
| 208 | |
| 209 | Three auth methods, tried in order: |
| 210 | |
| 211 | 1. **Web Identity** -- browser generates ed25519 keypair **client-side** using @noble/ed25519 (loaded locally, no CDN). Signs server challenges. Compatible with wirecore/wiredove ANProto format. Headers: `X-Wirecore-PublicKey`, `X-Wirecore-Signature`, `X-Wirecore-Challenge` |
| 212 | 2. **Bearer Token** -- `Authorization: Bearer <token>` for CLI and API clients |
| 213 | 3. **Keet Identity** -- BIP39 mnemonic-based, for cross-device portability |
| 214 | |
| 215 | ### P2P Replication |
| 216 | |
| 217 | Each notebook is a Hyperdrive. To share: |
| 218 | |
| 219 | 1. **Announce** a notebook on Hyperswarm (`POST /api/notebooks/:slug/announce`) |
| 220 | 2. Peers discover it via the discovery key |
| 221 | 3. Remote peers add it by drive key (`POST /api/sync/add`) |
| 222 | 4. Hyperdrive handles incremental replication automatically |
| 223 | |
| 224 | Only announced notebooks are discoverable. The registry (users, tokens, permissions) is never replicated. |
| 225 | |
| 226 | ### Multi-Writer / Device Linking |
| 227 | |
| 228 | Hypernote supports linking multiple instances via [Autobase](https://github.com/holepunchto/autobase) for multi-device and multi-user collaboration. |
| 229 | |
| 230 | **How it works:** |
| 231 | 1. An admin on instance A creates a link invite (`POST /api/multiwriter/link`) and gets an invite hex |
| 232 | 2. An admin on instance B joins with that invite (`POST /api/multiwriter/join`) |
| 233 | 3. Both instances share a replicated registry (users, permissions) via Autobase |
| 234 | 4. Mirror sync copies notebooks between instances in the background |
| 235 | |
| 236 | **Key endpoints:** |
| 237 | - `GET /api/multiwriter/status` -- check linking state and peers |
| 238 | - `POST /api/multiwriter/link` / `POST /api/multiwriter/join` -- instance linking |
| 239 | - `POST /api/multiwriter/sync` -- trigger manual mirror sync |
| 240 | - `POST /api/multiwriter/mirror` -- start/stop background mirror sync |
| 241 | - `POST /api/notebooks/:nb/share` -- toggle Autobase sharing on a notebook |
| 242 | - Shared notebooks get their own CRDT-based note endpoints (`/shared-notes`, Yjs updates) |
| 243 | |
| 244 | See [docs/api.md](docs/api.md) for the complete multiwriter API reference. |
| 245 | |
| 246 | ### Site Publisher |
| 247 | |
| 248 | The server automatically publishes a Hyperdrive containing: |
| 249 | - All web UI files (`/index.html`, `/web/*`) |
| 250 | - Notebook data as static JSON (`/api/notebooks.json`, `/api/notebooks/:slug/*`) |
| 251 | - Media files (`/api/media/:slug/*`) |
| 252 | - Recent notes feed (`/api/recent.json`) |
| 253 | |
| 254 | The site drive is announced on Hyperswarm and accessible via `hyper://SITE_KEY/` in Agregore. When the app detects hyper:// protocol, it routes API calls to the configured local HTTP server for full read/write access. |
| 255 | |
| 256 | ## API |
| 257 | |
| 258 | See [docs/api.md](docs/api.md) for the complete REST API reference. |
| 259 | |
| 260 | ### Quick Reference |
| 261 | |
| 262 | | Method | Endpoint | Description | |
| 263 | |--------|----------|-------------| |
| 264 | | GET | `/api/health` | Server health check | |
| 265 | | GET | `/api/status` | Server status + identity + site key | |
| 266 | | GET | `/api/whoami` | Current user info | |
| 267 | | GET | `/api/site` | Site drive key + hyper:// URL | |
| 268 | | POST | `/api/site/publish` | Force republish site (admin) | |
| 269 | | GET | `/api/recent` | Recent notes feed | |
| 270 | | GET | `/api/notebooks` | List notebooks (?mine=true, ?owner=) | |
| 271 | | POST | `/api/notebooks` | Create notebook | |
| 272 | | GET | `/api/notebooks/:nb` | Get notebook metadata | |
| 273 | | PATCH | `/api/notebooks/:nb` | Update notebook | |
| 274 | | DELETE | `/api/notebooks/:nb` | Delete notebook | |
| 275 | | POST | `/api/notebooks/:nb/rename` | Rename notebook slug | |
| 276 | | GET | `/api/notebooks/:nb/notes` | List notes (?limit, ?offset) | |
| 277 | | POST | `/api/notebooks/:nb/notes` | Create note | |
| 278 | | GET | `/api/notebooks/:nb/notes/:id` | Get note | |
| 279 | | PATCH | `/api/notebooks/:nb/notes/:id` | Update note | |
| 280 | | DELETE | `/api/notebooks/:nb/notes/:id` | Delete note | |
| 281 | | POST | `/api/notebooks/:nb/notes/:id/move` | Move note to another notebook | |
| 282 | | POST | `/api/notebooks/:nb/media` | Upload media (content-addressed) | |
| 283 | | GET | `/api/notebooks/:nb/media/:identifier` | Serve media (streaming, range requests) | |
| 284 | | GET | `/api/notebooks/:nb/media/:hash/stream` | HLS master playlist | |
| 285 | | GET | `/api/notebooks/:nb/media/:hash/stream/:seg` | HLS segment | |
| 286 | | GET | `/api/media/:hash/status` | Video processing status | |
| 287 | | GET | `/api/export/:notebook` | Export notebook (read auth) | |
| 288 | | GET | `/api/export` | Export all notebooks (admin) | |
| 289 | | GET | `/api/search?q=term` | Full-text search | |
| 290 | | POST | `/api/notebooks/:nb/encrypt` | Encrypt notebook | |
| 291 | | POST | `/api/notebooks/:nb/decrypt` | Decrypt notebook | |
| 292 | | POST | `/api/notebooks/:nb/unlock` | Unlock encrypted notebook | |
| 293 | | POST | `/api/notebooks/:nb/share` | Share notebook (token or multiwriter) | |
| 294 | | GET | `/api/notebooks/:nb/shares` | List share tokens (admin) | |
| 295 | | DELETE | `/api/notebooks/:nb/shares/:hash` | Revoke share token (admin) | |
| 296 | | GET | `/api/notebooks/:nb/notes/:id/links` | Get outgoing wiki-links | |
| 297 | | GET | `/api/notebooks/:nb/notes/:id/backlinks` | Get backlinks | |
| 298 | | GET | `/api/graph` | Knowledge graph (?notebook=) | |
| 299 | | POST | `/api/notebooks/:nb/merge` | Merge two notes | |
| 300 | | POST | `/api/notebooks/:nb/notes/:id/split` | Split note at headings | |
| 301 | | POST | `/api/notebooks/:nb/announce` | Announce on Hyperswarm | |
| 302 | | POST | `/api/notebooks/:nb/unannounce` | Stop announcing | |
| 303 | | GET | `/api/notebooks/:nb/drive-info` | Hyperdrive info | |
| 304 | | GET | `/api/sync/status` | P2P sync status | |
| 305 | | POST | `/api/sync/add` | Add remote notebook | |
| 306 | | DELETE | `/api/sync/remote/:nb` | Remove remote notebook | |
| 307 | | POST | `/api/auth/challenge` | Get signing challenge | |
| 308 | | GET | `/api/user/profile` | Get user profile | |
| 309 | | PATCH | `/api/user/profile` | Update display name/theme | |
| 310 | | POST | `/api/user/username` | Set username | |
| 311 | | GET | `/api/user/username/:u/available` | Check username availability | |
| 312 | | GET | `/api/users` | Public user directory | |
| 313 | | GET | `/api/admin/users` | List all users (admin) | |
| 314 | | DELETE | `/api/admin/users/:id` | Delete user (admin) | |
| 315 | | GET | `/api/admin/stats` | Server stats (admin) | |
| 316 | | POST | `/api/admin/promote` | Promote user (admin) | |
| 317 | | POST | `/api/admin/demote` | Demote user (admin) | |
| 318 | | POST | `/api/admin/notebooks/:nb/transfer` | Transfer ownership (admin) | |
| 319 | | GET | `/api/multiwriter/status` | Multi-writer status (admin) | |
| 320 | | POST | `/api/multiwriter/link` | Create link invite (admin) | |
| 321 | | POST | `/api/multiwriter/join` | Join with invite (admin) | |
| 322 | | POST | `/api/multiwriter/add-writer` | Add writer key (admin) | |
| 323 | | POST | `/api/multiwriter/sync` | Trigger mirror sync (admin) | |
| 324 | | POST | `/api/multiwriter/mirror` | Start/stop mirror (admin) | |
| 325 | | GET | `/api/notebooks/:nb/shared-notes` | List shared notes | |
| 326 | | POST | `/api/notebooks/:nb/shared-notes` | Create shared note | |
| 327 | | POST | `/api/notebooks/:nb/shared-notes/:id/yjs` | Apply Yjs update | |
| 328 | | GET | `/api/notebooks/:nb/shared-notes/:id/content` | Get shared note content | |
| 329 | | GET | `/api/debug/identity-chain` | Identity chain (localhost) | |
| 51 | 330 | |
| 52 | 331 | ## Deployment |
| 53 | 332 | |
| 333 | ### pear-git CI/CD |
| 334 | |
| 335 | Hypernote deploys automatically via pear-git: |
| 336 | |
| 54 | 337 | ```bash |
| 55 | | git push pear master # triggers auto-deploy via pear-git CI/CD |
| 338 | git push pear master # triggers auto-deploy |
| 339 | pgh deploy log hyper-note # check deploy status |
| 340 | ``` |
| 341 | |
| 342 | The CI pipeline (`.pear-git/ci.yaml`) installs dependencies, then pear-git runs `pear stage` -> `pear release` -> `pear seed`. |
| 343 | |
| 344 | ### Manual |
| 345 | |
| 346 | ```bash |
| 347 | bare hypernote-server.js serve 4040 --network |
| 56 | 348 | ``` |
| 57 | 349 | |
| 58 | 350 | For production, run in tmux or as a systemd service. |
| … | … | @@ -60,13 +352,89 @@ For production, run in tmux or as a systemd service. |
| 60 | 352 | ## Testing |
| 61 | 353 | |
| 62 | 354 | ```bash |
| 63 | | npm test # 150+ assertions across 17 test files |
| 355 | # Run all tests (150+ assertions) |
| 356 | npm test |
| 357 | |
| 358 | # Run specific test |
| 359 | bare node_modules/.bin/brittle test/unit/note-manager.test.js |
| 64 | 360 | ``` |
| 65 | 361 | |
| 66 | | ## Documentation |
| 362 | Test coverage: identity, auth, notebooks, notes, encryption, sharing, links, merge/split, replication, rate limiting, storage, CLI config, web routes. |
| 363 | |
| 364 | ## Project Structure |
| 67 | 365 | |
| 68 | | - [REST API Reference](docs/api.md) -- all 65+ endpoints |
| 69 | | - [Architecture](docs/architecture.md) -- system design, storage layout, auth, security model, project structure |
| 366 | ``` |
| 367 | hypernote/ |
| 368 | hypernote-server.js # server entry point |
| 369 | package.json |
| 370 | bin/hn # CLI entry point |
| 371 | .pear-git/ci.yaml # pear-git CI config |
| 372 | lib/ |
| 373 | auth.js # AuthManager (multi-method) |
| 374 | note-manager.js # notebook + note CRUD |
| 375 | drive-manager.js # per-notebook Hyperdrive lifecycle |
| 376 | replication.js # Hyperswarm P2P sync |
| 377 | site-publisher.js # hyper:// site drive publisher |
| 378 | notebook-crypto.js # per-notebook encryption |
| 379 | link-manager.js # wiki-links + backlinks + graph |
| 380 | media-store.js # SHA-256 content-addressed blob storage |
| 381 | export.js # data export (notebook + full server) |
| 382 | anproto.js # ANProto ed25519 signatures |
| 383 | storage-locator.js # storage breadcrumb system |
| 384 | errors.js # custom error classes |
| 385 | response.js # HTTP response helpers |
| 386 | media/ |
| 387 | video-processor.js # HLS transcoding via ffmpeg |
| 388 | multiwriter/ |
| 389 | manager.js # multi-writer orchestration |
| 390 | instance-linker.js # invite-based instance linking |
| 391 | shared-registry.js # Autobase-backed shared user registry |
| 392 | registry-apply.js # registry operation application |
| 393 | auth/ |
| 394 | base-provider.js # auth provider interface |
| 395 | bearer-token.js # bearer token auth |
| 396 | web-identity.js # browser ed25519 auth |
| 397 | keet-identity.js # BIP39 mnemonic auth |
| 398 | challenge-manager.js # challenge-response protocol |
| 399 | identity-registry.js # user + identity storage |
| 400 | rate-limiter.js # per-IP rate limiting |
| 401 | identity/ |
| 402 | identity-manager.js # device keypair management |
| 403 | secure-storage.js # encrypted mnemonic vault |
| 404 | routes/ |
| 405 | index.js # HTTP router |
| 406 | auth.js # identity + auth endpoints |
| 407 | notebooks.js # notebook + note + media endpoints |
| 408 | sharing.js # encryption + sharing endpoints |
| 409 | links.js # links + backlinks + graph endpoints |
| 410 | sync.js # P2P sync endpoints |
| 411 | multiwriter.js # instance linking + shared notebooks |
| 412 | web.js # static files + vendor + public URLs |
| 413 | cli/ |
| 414 | client.js # HTTP client for CLI |
| 415 | config.js # CLI config file |
| 416 | output.js # CLI output formatting |
| 417 | commands/ # CLI subcommands |
| 418 | web/ |
| 419 | index.html # SPA entry point |
| 420 | js/ |
| 421 | app.js # main app + router + views |
| 422 | api.js # HTTP client (dual-mode: HTTP + hyper://) |
| 423 | identity.js # ed25519 auth + fetch interception |
| 424 | markdown.js # markdown rendering |
| 425 | css/ |
| 426 | app.css # dark theme styles |
| 427 | vendor/ |
| 428 | toastui-editor-all.min.js # Toast UI editor (local, no CDN) |
| 429 | toastui-editor.min.css # Toast UI styles (local, no CDN) |
| 430 | hls.min.js # HLS.js video player (local, no CDN) |
| 431 | test/ |
| 432 | unit/ # 17 test files (brittle) |
| 433 | helpers/ |
| 434 | fixtures.js # test helpers |
| 435 | docs/ |
| 436 | api.md # REST API reference |
| 437 | ``` |
| 70 | 438 | |
| 71 | 439 | ## License |
| 72 | 440 | |