From f0b1096404a2ee8281d68530320b1e9e758894dd Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 31 Mar 2026 16:20:11 -0700 Subject: [PATCH] Initial research and POC scaffolding for Fileverse + rStack integration Research docs covering crypto comparison (@fileverse/crypto vs MIT primitives vs rSpace DocCrypto), Y.js vs Automerge architecture decision, and phased integration plan. POC scaffolding includes MIT crypto primitives library with ECIES key exchange, benchmark suite, collab-server Docker Compose for Netcup deployment, and placeholder structures for IPFS storage and dSheet embed. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 5 + README.md | 63 ++++++ docs/crypto-comparison.md | 130 ++++++++++++ docs/integration-plan.md | 283 +++++++++++++++++++++++++++ docs/yjs-vs-automerge.md | 72 +++++++ poc/collab-server/Dockerfile | 24 +++ poc/collab-server/README.md | 33 ++++ poc/collab-server/docker-compose.yml | 65 ++++++ poc/crypto-eval/package.json | 25 +++ poc/crypto-eval/src/benchmark.ts | 150 ++++++++++++++ poc/crypto-eval/src/mit-crypto.ts | 154 +++++++++++++++ poc/crypto-eval/tsconfig.json | 13 ++ poc/dsheet-embed/README.md | 25 +++ poc/ipfs-storage/README.md | 36 ++++ 14 files changed, 1078 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/crypto-comparison.md create mode 100644 docs/integration-plan.md create mode 100644 docs/yjs-vs-automerge.md create mode 100644 poc/collab-server/Dockerfile create mode 100644 poc/collab-server/README.md create mode 100644 poc/collab-server/docker-compose.yml create mode 100644 poc/crypto-eval/package.json create mode 100644 poc/crypto-eval/src/benchmark.ts create mode 100644 poc/crypto-eval/src/mit-crypto.ts create mode 100644 poc/crypto-eval/tsconfig.json create mode 100644 poc/dsheet-embed/README.md create mode 100644 poc/ipfs-storage/README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71da7d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.env.local +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..034fc95 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Fileverse + rNotes Integration + +Research and proof-of-concept repo for integrating [Fileverse](https://github.com/fileverse) decentralization primitives into the rStack ecosystem (rNotes, rSpace). + +## Goal + +Evaluate and prototype how Fileverse's AGPL-licensed components can enhance rNotes/rSpace with: +- **E2E encryption** via `@fileverse/crypto` (or MIT equivalents) +- **IPFS storage** for files/images (eliminating centralized storage) +- **WebRTC P2P sync** for real-time collaboration +- **Decentralized spreadsheets** via `@fileverse-dev/dsheet` + +## Current rStack Architecture + +| Component | rnotes-online | rspace-online (rNotes module) | +|-----------|--------------|-------------------------------| +| CRDT | None | Automerge 2.2.8 | +| Sync | REST only | WebSocket Automerge sync | +| Offline | No | Full offline-first (IndexedDB) | +| Encryption | TLS only | E2E (AES-256-GCM + HKDF) | +| Real-time Collab | No | Yes (Automerge + Y.js) | +| Editor | TipTap 3.19 | TipTap 3.20 + Y.js | +| Auth | EncryptID | EncryptID + MCP | +| Import | Markdown | 6 formats (Notion, Obsidian, Logseq, Google Docs, Evernote, Roam) | + +## Fileverse Ecosystem + +| Package | Purpose | License | +|---------|---------|---------| +| [`@fileverse-dev/ddoc`](https://github.com/fileverse/fileverse-ddoc) | TipTap editor + Y.js CRDTs + WebRTC + E2E + IPFS | AGPL-3.0 | +| [`@fileverse/crypto`](https://github.com/fileverse/fileverse-cryptography) | ECIES, NaCl SecretBox, Argon2id, HKDF | AGPL-3.0 | +| [`collaboration-server`](https://github.com/fileverse/collaboration-server) | Y.js WebSocket relay, UCAN auth, ephemeral → IPFS | AGPL-3.0 | +| [`fileverse-storage`](https://github.com/fileverse/fileverse-storage) | UCAN-authorized file uploads to IPFS | AGPL-3.0 | +| [`@fileverse-dev/dsheet`](https://github.com/fileverse/fileverse-dsheet) | Decentralized spreadsheet (Y.js + WebRTC + IndexedDB) | AGPL-3.0 | + +## Integration Priorities + +See [`docs/integration-plan.md`](docs/integration-plan.md) for the detailed plan. + +1. **Crypto primitives evaluation** — Compare `@fileverse/crypto` vs rSpace's existing DocCrypto +2. **IPFS file storage** — POC for encrypted file uploads (resolves rNotes TASK-4) +3. **Collaboration server** — Self-host on Netcup for WebSocket relay +4. **dSheet module** — Prototype as `rsheet-online` in rStack + +## Repo Structure + +``` +fileverse/ +├── docs/ # Research and architecture docs +│ ├── integration-plan.md # Detailed integration roadmap +│ ├── crypto-comparison.md # @fileverse/crypto vs DocCrypto analysis +│ └── yjs-vs-automerge.md # CRDT framework decision +├── poc/ # Proof-of-concept implementations +│ ├── crypto-eval/ # Encryption primitive benchmarks +│ ├── ipfs-storage/ # IPFS file upload POC +│ ├── collab-server/ # Self-hosted collaboration server +│ └── dsheet-embed/ # dSheet React component POC +└── README.md +``` + +## License Considerations + +Fileverse repos are AGPL-3.0. Using their npm packages triggers copyleft for the combined work. Since rStack is open source on Gitea/GitHub, this is acceptable. For maximum flexibility, the crypto POC also evaluates MIT-licensed alternatives (Noble, StableLib, TweetNaCl) that provide equivalent primitives without AGPL obligations. diff --git a/docs/crypto-comparison.md b/docs/crypto-comparison.md new file mode 100644 index 0000000..c75b20f --- /dev/null +++ b/docs/crypto-comparison.md @@ -0,0 +1,130 @@ +# Crypto Comparison: @fileverse/crypto vs rSpace DocCrypto vs MIT Primitives + +## Overview + +Three options for the encryption layer: + +1. **Keep DocCrypto** — rSpace's existing AES-256-GCM + HKDF implementation +2. **Adopt @fileverse/crypto** — Drop-in AGPL library with ECIES, NaCl, Argon2id +3. **Build from MIT primitives** — Same capabilities as Fileverse but without AGPL + +## Primitive-Level Comparison + +### Symmetric Encryption + +| | DocCrypto | @fileverse/crypto | MIT Build | +|--|-----------|-------------------|-----------| +| Algorithm | AES-256-GCM | XSalsa20-Poly1305 (NaCl SecretBox) | Either | +| Implementation | Web Crypto API | TweetNaCl.js | Web Crypto or TweetNaCl | +| Key size | 256 bits | 256 bits | 256 bits | +| Nonce | 96 bits (random) | 192 bits (random) | Depends on choice | +| Auth tag | 128 bits | 128 bits (Poly1305) | Depends on choice | +| Bundle size | 0 (native) | ~7KB (tweetnacl) | 0–7KB | +| Performance | Fastest (hardware) | Fast (pure JS) | Fastest if Web Crypto | + +**Analysis:** AES-256-GCM via Web Crypto is faster (hardware acceleration) and adds 0 bundle size. NaCl SecretBox is simpler to use correctly (larger nonce = less collision risk). For rStack, Web Crypto AES-256-GCM is the better choice — it's already battle-tested in DocCrypto. + +### Asymmetric Encryption (Gap in DocCrypto) + +| | DocCrypto | @fileverse/crypto | MIT Build | +|--|-----------|-------------------|-----------| +| ECIES | Not implemented | ✅ (secp256k1) | Noble curves | +| RSA | Not implemented | ✅ (envelope) | Web Crypto | +| Key exchange | Not implemented | ECDH shared secret | Noble/StableLib x25519 | + +**Analysis:** This is the main gap. DocCrypto only does symmetric encryption — it can't share encryption keys between users without a trusted server. ECIES allows encrypting a document key so only a specific collaborator's private key can decrypt it. + +### Key Derivation + +| | DocCrypto | @fileverse/crypto | MIT Build | +|--|-----------|-------------------|-----------| +| HKDF | ✅ (Web Crypto) | ✅ (StableLib) | Web Crypto | +| Argon2id | Not implemented | ✅ (argon2-browser WASM) | argon2-browser | +| Key hierarchy | Master → Space → Doc | Flat | Custom | + +**Analysis:** DocCrypto's key hierarchy is more sophisticated. Fileverse derives keys per-operation. For rStack, keep DocCrypto's hierarchy and add Argon2id for password-protected sharing. + +## Recommendation + +### Extend DocCrypto with MIT Primitives + +```typescript +// Additions to rspace-online/shared/local-first/crypto.ts + +import { x25519 } from '@noble/curves/ed25519' +import { hkdf } from '@noble/hashes/hkdf' +import { sha256 } from '@noble/hashes/sha256' +import argon2 from 'argon2-browser' + +class DocCrypto { + // ... existing AES-256-GCM + HKDF methods ... + + // NEW: Key exchange for multi-user collaboration + static async deriveSharedSecret( + myPrivateKey: Uint8Array, + theirPublicKey: Uint8Array + ): Promise { + const shared = x25519.getSharedSecret(myPrivateKey, theirPublicKey) + return this.importKey(shared) + } + + // NEW: Encrypt document key for a specific collaborator + static async encryptKeyForUser( + docKey: Uint8Array, + recipientPublicKey: Uint8Array, + senderPrivateKey: Uint8Array + ): Promise { + const sharedSecret = x25519.getSharedSecret(senderPrivateKey, recipientPublicKey) + const wrappingKey = hkdf(sha256, sharedSecret, null, 'rstack-key-wrap', 32) + // Encrypt docKey with wrappingKey using existing AES-256-GCM + return this.encrypt(wrappingKey, docKey) + } + + // NEW: Password-protected note sharing + static async deriveKeyFromPassword( + password: string, + salt: Uint8Array + ): Promise { + const result = await argon2.hash({ + pass: password, + salt: salt, + type: argon2.ArgonType.Argon2id, + hashLen: 32, + time: 3, + mem: 65536, + parallelism: 1, + }) + return this.importKey(result.hash) + } +} +``` + +### Dependencies (All MIT) + +```json +{ + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "argon2-browser": "^1.18.0" +} +``` + +### Bundle Size Impact + +| Package | Size (min+gzip) | +|---------|----------------| +| @noble/curves (x25519 only) | ~8KB | +| @noble/hashes (sha256+hkdf) | ~4KB | +| argon2-browser (WASM) | ~30KB | +| **Total** | **~42KB** | + +vs `@fileverse/crypto` full package: ~45KB (similar, but with AGPL) + +## Interoperability + +If we build from the same MIT primitives Fileverse uses, we maintain the option to: +- Decrypt content encrypted by Fileverse apps +- Share encrypted documents with Fileverse users +- Participate in Fileverse collaboration rooms + +The key is using compatible curve parameters (secp256k1 or x25519) and matching the encryption format (nonce || ciphertext || tag). diff --git a/docs/integration-plan.md b/docs/integration-plan.md new file mode 100644 index 0000000..dc1277b --- /dev/null +++ b/docs/integration-plan.md @@ -0,0 +1,283 @@ +# Fileverse + rStack Integration Plan + +## Key Architectural Decision: Y.js vs Automerge + +rSpace already uses **both** Automerge and Y.js: +- **Automerge 2.2.8** — Document state, sync protocol, binary wire format +- **Y.js 13.6** — TipTap editor binding (`y-prosemirror`), IndexedDB persistence (`y-indexeddb`) + +Fileverse uses **Y.js exclusively** for CRDTs. + +### Assessment + +rSpace's dual approach is actually well-positioned. The Y.js layer handles editor collaboration (which is what Fileverse's components need), while Automerge handles the broader document state management. We don't need to choose one — the integration points are at the Y.js/TipTap layer. + +**Decision: Keep both. Integrate Fileverse at the Y.js/TipTap layer.** + +The Automerge sync protocol handles state replication, while Y.js handles real-time editor cursors and awareness. Fileverse's ddoc component and collaboration-server plug into the Y.js side. + +--- + +## Phase 1: Crypto Primitives Evaluation + +**Status:** Not started +**Effort:** 1–2 days +**Target:** Determine whether to use `@fileverse/crypto` or build equivalent from MIT libs + +### rSpace DocCrypto (existing) +- AES-256-GCM for document encryption +- HKDF key derivation: Master key → Space key → Doc key +- Key material from EncryptID passkey PRF +- Location: `rspace-online/shared/local-first/crypto.ts` + +### @fileverse/crypto (Fileverse) +- ECIES (asymmetric encryption via elliptic curves) +- NaCl SecretBox (symmetric authenticated encryption via TweetNaCl) +- RSA envelope encryption for large messages +- HKDF key derivation +- Argon2id password hashing + +### Comparison + +| Feature | DocCrypto | @fileverse/crypto | +|---------|-----------|-------------------| +| Symmetric encryption | AES-256-GCM (Web Crypto) | NaCl SecretBox (TweetNaCl) | +| Asymmetric encryption | Not implemented | ECIES + RSA | +| Key derivation | HKDF (Web Crypto) | HKDF + Argon2id | +| Key hierarchy | Master → Space → Doc | Flat (per-operation) | +| Sharing keys between users | Not implemented | ECIES key exchange | +| Security audit | No | No | +| License | Proprietary (rStack) | AGPL-3.0 | + +### Gap Analysis + +rSpace's DocCrypto lacks: +1. **Asymmetric encryption** — needed to share document keys between collaborators without a central server +2. **Key exchange** — ECIES allows encrypting a doc key for a specific collaborator's public key +3. **Password-derived keys** — Argon2id enables password-protected notes (useful for shared links) + +### MIT Alternatives + +The underlying primitives in `@fileverse/crypto` are all MIT-licensed: +- **Noble curves** (`@noble/curves`) — ECIES elliptic curve operations +- **StableLib** (`@stablelib/x25519`, `@stablelib/hkdf`) — Key exchange and derivation +- **TweetNaCl** (`tweetnacl`) — SecretBox symmetric encryption +- **argon2-browser** — Argon2id in WASM + +**Recommendation:** Build a thin wrapper around MIT libs, matching `@fileverse/crypto`'s API where useful. This avoids AGPL while gaining all the cryptographic capabilities. + +### Tasks +- [ ] Benchmark NaCl SecretBox vs AES-256-GCM (performance + bundle size) +- [ ] Implement ECIES key exchange using Noble curves +- [ ] Add Argon2id for password-protected notes +- [ ] Test interop: can we decrypt Fileverse-encrypted content? + +--- + +## Phase 2: IPFS File Storage + +**Status:** Not started +**Effort:** 3–5 days +**Target:** Replace centralized file uploads with IPFS-backed storage +**Resolves:** rNotes TASK-4 (file/image upload) + +### Architecture + +``` +Client Server/IPFS + │ │ + ├─ Pick file │ + ├─ Encrypt (SecretBox) │ + ├─ Upload encrypted blob ───────►│ Store on IPFS + │◄──────── Return CID ──────────┤ + ├─ Store CID + key in │ + │ document metadata │ + │ │ + ├─ To view: fetch CID ─────────►│ Return encrypted blob + │◄──────────────────────────────┤ + ├─ Decrypt locally │ + └─ Display │ +``` + +### Options + +#### A. Use Fileverse's storage service +- Requires UCAN tokens for authorization +- Depends on Fileverse infrastructure +- No self-hosting option documented + +#### B. Self-host IPFS node on Netcup +- Full control over storage +- `kubo` (go-ipfs) or `helia` (JS) +- Pin encrypted blobs, serve via gateway +- CIDs stored in Automerge documents + +#### C. Use Pinata/web3.storage +- Managed IPFS pinning +- Free tiers available +- Less operational overhead + +**Recommendation:** Start with option C (managed pinning) for POC, migrate to B (self-hosted) for production. + +### Tasks +- [ ] Set up Pinata account and API keys (store in Infisical) +- [ ] Build upload service: encrypt → pin → return CID +- [ ] Build retrieval service: fetch CID → decrypt → serve +- [ ] Add TipTap image extension that uses IPFS CIDs +- [ ] Test with rNotes note editor + +--- + +## Phase 3: Collaboration Server + +**Status:** Not started +**Effort:** 3–5 days +**Target:** Self-host Fileverse's collaboration-server on Netcup + +### Requirements +- Node.js runtime +- MongoDB (temporary update storage) +- Redis (session management) +- WebSocket support via Traefik + +### Deployment Plan + +```yaml +# docker-compose.yml on Netcup +services: + collab-server: + image: node:20-slim + # Build from github.com/fileverse/collaboration-server + environment: + PORT: 5000 + MONGODB_URI: mongodb://collab-mongo:27017/collab + CORS_ORIGINS: "https://rnotes.jeffemmett.com,https://rspace.jeffemmett.com" + labels: + - "traefik.enable=true" + - "traefik.http.routers.collab.rule=Host(`collab.jeffemmett.com`)" + - "traefik.http.routers.collab.tls.certresolver=letsencrypt" + depends_on: + - collab-mongo + - collab-redis + + collab-mongo: + image: mongo:7 + volumes: + - collab-mongo-data:/data/db + + collab-redis: + image: redis:7-alpine + volumes: + - collab-redis-data:/data +``` + +### Integration with rSpace + +The collaboration-server uses Y.js sync protocol. rSpace already has Y.js for TipTap. Integration points: +1. Connect TipTap's `y-prosemirror` binding to collab-server WebSocket +2. Use collab-server's awareness protocol for cursor presence +3. Automerge sync continues separately for document state + +### UCAN Auth Integration + +Fileverse's collab-server uses UCAN tokens. Options: +1. **Adapt:** Generate UCANs from EncryptID DIDs (preferred) +2. **Bypass:** Fork server, replace UCAN auth with EncryptID JWT +3. **Bridge:** Proxy auth through an adapter service + +### Tasks +- [ ] Fork collaboration-server, evaluate auth integration +- [ ] Create Docker Compose config for Netcup +- [ ] Deploy with Traefik routing +- [ ] Connect rSpace TipTap editor to collab-server +- [ ] Test real-time collaboration between two clients + +--- + +## Phase 4: dSheet as rSheet Module + +**Status:** Not started +**Effort:** 5–7 days +**Target:** Integrate `@fileverse-dev/dsheet` as a new rStack module + +### Architecture + +``` +rstack-online/ +└── modules/ + └── rsheet/ # New module + ├── schemas.ts # Automerge schemas for spreadsheet metadata + ├── components/ + │ └── folk-sheet-app.ts # LitElement wrapper around dSheet + ├── local-first-client.ts # Sync integration + └── converters/ + ├── csv.ts # CSV import/export + └── xlsx.ts # Excel import/export +``` + +### dSheet Component Props + +```typescript + +``` + +### Integration Points +- Use rSpace's EncryptID auth for `isAuthorized` +- Generate `dsheetId` from Automerge document ID +- Route WebRTC signaling through self-hosted collab-server +- Store spreadsheet metadata in Automerge (title, permissions, CID) +- Store spreadsheet data in IndexedDB (via dSheet's built-in support) + +### Use Cases for rStack +- DAO treasury tracking (live blockchain data queries) +- Token allocation spreadsheets +- Budget planning with E2E encryption +- Research data tables alongside rNotes + +### Tasks +- [ ] Create rSheet module scaffold in rspace-online +- [ ] Wrap dSheet React component in LitElement +- [ ] Wire auth and collaboration +- [ ] Add CSV/XLSX import/export converters +- [ ] Deploy and test + +--- + +## Phase 5: Advanced — UCAN + Decentralized Identity + +**Status:** Future +**Effort:** 5+ days + +### Goals +- Complement EncryptID with UCAN capability tokens +- Decentralized authorization without central server +- Fine-grained permissions: read, write, share, admin per document + +### UCAN Flow +``` +User (EncryptID DID) → Mint UCAN → Delegate to collaborator + ↓ + Collaborator presents UCAN to: + - Collaboration server (real-time sync) + - Storage server (file uploads) + - IPFS gateway (content retrieval) +``` + +--- + +## Timeline + +| Phase | Duration | Dependencies | +|-------|----------|-------------| +| 1. Crypto evaluation | 1–2 days | None | +| 2. IPFS storage | 3–5 days | Phase 1 | +| 3. Collab server | 3–5 days | None (parallel with Phase 2) | +| 4. dSheet module | 5–7 days | Phase 3 | +| 5. UCAN auth | 5+ days | Phases 2, 3 | diff --git a/docs/yjs-vs-automerge.md b/docs/yjs-vs-automerge.md new file mode 100644 index 0000000..f351755 --- /dev/null +++ b/docs/yjs-vs-automerge.md @@ -0,0 +1,72 @@ +# Y.js vs Automerge: Decision for rStack + Fileverse Integration + +## Current State + +rSpace uses **both** frameworks in a layered architecture: + +``` +┌─────────────────────────────────────────┐ +│ TipTap Editor │ +│ ↕ y-prosemirror binding │ +│ ↕ Y.js (editor collaboration) │ ← Fileverse integrates HERE +├─────────────────────────────────────────┤ +│ Automerge (document state + sync) │ ← State management stays HERE +│ ↕ WebSocket binary sync protocol │ +│ ↕ IndexedDB encrypted persistence │ +└─────────────────────────────────────────┘ +``` + +## Why This Dual Approach Works + +### Y.js Layer (Editor) +- `y-prosemirror`: Official TipTap/ProseMirror binding for real-time editing +- `y-indexeddb`: Client-side persistence for editor state +- Awareness protocol: Cursor positions, user presence +- **This is what Fileverse's ddoc and collaboration-server use** + +### Automerge Layer (State) +- Document metadata, permissions, notebook structure +- Binary sync protocol over WebSocket +- Encrypted storage with DocCrypto +- Migration path from PostgreSQL + +### Integration Surface + +Fileverse components connect at the Y.js layer: +- `collaboration-server` speaks Y.js sync protocol over WebSocket +- `@fileverse-dev/ddoc` uses `y-prosemirror` internally +- `@fileverse-dev/dsheet` uses Y.js for cell sync + +The Automerge layer is invisible to Fileverse — it handles higher-level state that Fileverse doesn't need to know about. + +## Decision + +**Keep the dual architecture.** No migration needed. + +### Rationale + +1. **No conflict** — Y.js and Automerge operate at different levels +2. **Fileverse compatibility** — Their components plug into the Y.js layer directly +3. **Automerge strengths preserved** — Better merge semantics for structured data, Rust/WASM performance for large documents +4. **Migration cost: zero** — No existing code needs to change + +### What This Means for Each Integration + +| Integration | CRDT Layer | Notes | +|-------------|-----------|-------| +| E2E encryption | Neither (operates on raw bytes) | Encrypts before CRDT, decrypts after | +| IPFS storage | Neither (file blobs) | CIDs stored in Automerge metadata | +| Collab server | Y.js | Handles editor sync + awareness | +| dSheet | Y.js | Self-contained Y.js-based component | +| UCAN auth | Neither (auth layer) | Wraps transport, not CRDTs | + +## Alternative Considered: Full Y.js Migration + +If we were starting fresh, using Y.js for everything would be simpler. But the migration cost outweighs the benefit: +- Automerge schemas, storage, and sync code in rSpace would all need rewriting +- The PostgreSQL → Automerge migration tool would be wasted +- Automerge's typed document schemas are more ergonomic than Y.js's untyped maps/arrays + +## Conclusion + +The existing architecture is already well-positioned for Fileverse integration. No CRDT migration needed. Focus integration efforts on the Y.js/TipTap layer where Fileverse components naturally connect. diff --git a/poc/collab-server/Dockerfile b/poc/collab-server/Dockerfile new file mode 100644 index 0000000..77fceac --- /dev/null +++ b/poc/collab-server/Dockerfile @@ -0,0 +1,24 @@ +# Build from Fileverse collaboration-server source +# https://github.com/fileverse/collaboration-server + +FROM node:20-slim AS builder + +WORKDIR /app + +# Clone and build collaboration-server +RUN apt-get update && apt-get install -y git && \ + git clone --depth 1 https://github.com/fileverse/collaboration-server.git . && \ + npm ci && \ + npm run build + +FROM node:20-slim + +WORKDIR /app + +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./ + +EXPOSE 5000 + +CMD ["node", "dist/index.js"] diff --git a/poc/collab-server/README.md b/poc/collab-server/README.md new file mode 100644 index 0000000..35c28b2 --- /dev/null +++ b/poc/collab-server/README.md @@ -0,0 +1,33 @@ +# Self-Hosted Collaboration Server + +Fileverse's [collaboration-server](https://github.com/fileverse/collaboration-server) deployed on Netcup for rStack real-time collaboration. + +## Architecture + +``` +rSpace Client ──WebSocket──► Traefik ──► collab-server ──► MongoDB (ephemeral) + ──► Redis (sessions) +``` + +## Deployment + +```bash +# On Netcup +cd /opt/apps/collab-server +docker compose up -d + +# Verify +curl https://collab.jeffemmett.com/health +``` + +## Auth Integration + +The collaboration-server uses UCAN tokens. For rStack integration, we need to either: +1. Generate UCAN tokens from EncryptID DIDs +2. Fork and replace auth with EncryptID JWT + +See `docs/integration-plan.md` Phase 3 for details. + +## Status + +Not started — Docker Compose is prepared, needs testing after DNS setup. diff --git a/poc/collab-server/docker-compose.yml b/poc/collab-server/docker-compose.yml new file mode 100644 index 0000000..94abc2b --- /dev/null +++ b/poc/collab-server/docker-compose.yml @@ -0,0 +1,65 @@ +# Fileverse Collaboration Server — Self-hosted on Netcup +# Y.js WebSocket relay for real-time document collaboration +# +# Deploy: scp to Netcup, docker compose up -d +# Requires: Traefik network, DNS for collab.jeffemmett.com + +services: + collab-server: + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + environment: + PORT: 5000 + HOST: 0.0.0.0 + NODE_ENV: production + MONGODB_URI: mongodb://collab-mongo:27017/collab + REDIS_URL: redis://collab-redis:6379 + CORS_ORIGINS: "https://rnotes.jeffemmett.com,https://rspace.jeffemmett.com,http://localhost:3000" + # SERVER_DID and other secrets via Infisical + INFISICAL_CLIENT_ID: ${INFISICAL_CLIENT_ID} + INFISICAL_CLIENT_SECRET: ${INFISICAL_CLIENT_SECRET} + networks: + - proxy + - collab-internal + labels: + - "traefik.enable=true" + # HTTP router + - "traefik.http.routers.collab.rule=Host(`collab.jeffemmett.com`)" + - "traefik.http.routers.collab.entrypoints=websecure" + - "traefik.http.routers.collab.tls.certresolver=letsencrypt" + - "traefik.http.services.collab.loadbalancer.server.port=5000" + # WebSocket support + - "traefik.http.middlewares.collab-headers.headers.customrequestheaders.X-Forwarded-Proto=https" + - "traefik.http.routers.collab.middlewares=collab-headers" + depends_on: + - collab-mongo + - collab-redis + + collab-mongo: + image: mongo:7 + restart: unless-stopped + volumes: + - collab-mongo-data:/data/db + networks: + - collab-internal + + collab-redis: + image: redis:7-alpine + restart: unless-stopped + command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru + volumes: + - collab-redis-data:/data + networks: + - collab-internal + +networks: + proxy: + external: true + collab-internal: + driver: bridge + +volumes: + collab-mongo-data: + collab-redis-data: diff --git a/poc/crypto-eval/package.json b/poc/crypto-eval/package.json new file mode 100644 index 0000000..5bb32f1 --- /dev/null +++ b/poc/crypto-eval/package.json @@ -0,0 +1,25 @@ +{ + "name": "@rstack/crypto-eval", + "version": "0.1.0", + "private": true, + "description": "Evaluate @fileverse/crypto vs MIT primitives for rStack encryption", + "type": "module", + "scripts": { + "benchmark": "tsx src/benchmark.ts", + "test:fileverse": "tsx src/test-fileverse-crypto.ts", + "test:mit": "tsx src/test-mit-crypto.ts", + "test:interop": "tsx src/test-interop.ts", + "test": "tsx src/run-all-tests.ts" + }, + "dependencies": { + "@fileverse/crypto": "^0.0.1", + "@noble/curves": "^1.8.0", + "@noble/hashes": "^1.7.0", + "argon2-browser": "^1.18.0", + "tweetnacl": "^1.0.3" + }, + "devDependencies": { + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/poc/crypto-eval/src/benchmark.ts b/poc/crypto-eval/src/benchmark.ts new file mode 100644 index 0000000..d9df2c1 --- /dev/null +++ b/poc/crypto-eval/src/benchmark.ts @@ -0,0 +1,150 @@ +/** + * Benchmark: AES-256-GCM (Web Crypto) vs NaCl SecretBox + * Tests encryption/decryption performance across payload sizes + */ + +import { + aesEncrypt, + aesDecrypt, + generateSymmetricKey, + generateKeyPair, + encryptForRecipient, + decryptFromSender, + shareDocKey, + receiveDocKey, +} from './mit-crypto.js' + +const encoder = new TextEncoder() +const decoder = new TextDecoder() + +async function benchmark(name: string, fn: () => Promise, iterations: number = 1000) { + // Warmup + for (let i = 0; i < 10; i++) await fn() + + const start = performance.now() + for (let i = 0; i < iterations; i++) await fn() + const elapsed = performance.now() - start + + console.log(`${name}: ${(elapsed / iterations).toFixed(3)}ms avg (${iterations} iterations)`) +} + +async function main() { + console.log('=== rStack Crypto Primitives Benchmark ===\n') + + // ─── Symmetric Encryption ─── + console.log('--- Symmetric Encryption (AES-256-GCM) ---') + const key = generateSymmetricKey() + + for (const size of [100, 1_000, 10_000, 100_000]) { + const data = new Uint8Array(size) + crypto.getRandomValues(data) + + await benchmark(` Encrypt ${size.toLocaleString()}B`, async () => { + await aesEncrypt(key, data) + }) + + const encrypted = await aesEncrypt(key, data) + await benchmark(` Decrypt ${size.toLocaleString()}B`, async () => { + await aesDecrypt(key, encrypted) + }) + } + + // ─── Key Exchange ─── + console.log('\n--- Key Exchange (x25519 ECDH) ---') + const alice = generateKeyPair() + const bob = generateKeyPair() + + await benchmark(' Generate key pair', async () => { + generateKeyPair() + }) + + // ─── ECIES Encryption ─── + console.log('\n--- ECIES Encryption (x25519 + AES-256-GCM) ---') + const message = encoder.encode('This is a secret document encryption key - 32 bytes!') + + await benchmark(' Encrypt for recipient', async () => { + await encryptForRecipient(message, bob.publicKey, alice.privateKey) + }) + + const { ciphertext, ephemeralPublicKey } = await encryptForRecipient( + message, bob.publicKey, alice.privateKey + ) + await benchmark(' Decrypt from sender', async () => { + await decryptFromSender(ciphertext, ephemeralPublicKey, bob.privateKey) + }) + + // ─── Document Key Sharing ─── + console.log('\n--- Document Key Sharing ---') + const docKey = generateSymmetricKey() + + await benchmark(' Share doc key', async () => { + await shareDocKey(docKey, bob.publicKey, alice.privateKey) + }) + + const shared = await shareDocKey(docKey, bob.publicKey, alice.privateKey) + await benchmark(' Receive doc key', async () => { + await receiveDocKey(shared.encryptedKey, shared.ephemeralPublicKey, bob.privateKey) + }) + + // ─── End-to-End Roundtrip ─── + console.log('\n--- Full Roundtrip: Note Encryption + Key Sharing ---') + const noteContent = encoder.encode(JSON.stringify({ + title: 'Secret Meeting Notes', + content: '

Discussion about token allocation...

'.repeat(100), + tags: ['dao', 'treasury', 'private'], + })) + + await benchmark(' Full encrypt + share flow', async () => { + // 1. Generate doc key + const dk = generateSymmetricKey() + // 2. Encrypt content + const enc = await aesEncrypt(dk, noteContent) + // 3. Share key with collaborator + await shareDocKey(dk, bob.publicKey, alice.privateKey) + }) + + await benchmark(' Full receive + decrypt flow', async () => { + // Simulated received data + const dk = generateSymmetricKey() + const enc = await aesEncrypt(dk, noteContent) + const sk = await shareDocKey(dk, bob.publicKey, alice.privateKey) + // Actual measured flow + const receivedKey = await receiveDocKey(sk.encryptedKey, sk.ephemeralPublicKey, bob.privateKey) + await aesDecrypt(receivedKey, enc) + }) + + // ─── Correctness Check ─── + console.log('\n--- Correctness Verification ---') + + // Symmetric roundtrip + const testData = encoder.encode('Hello, encrypted world!') + const testKey = generateSymmetricKey() + const testEncrypted = await aesEncrypt(testKey, testData) + const testDecrypted = await aesDecrypt(testKey, testEncrypted) + const symOk = decoder.decode(testDecrypted) === 'Hello, encrypted world!' + console.log(` Symmetric roundtrip: ${symOk ? 'PASS' : 'FAIL'}`) + + // ECIES roundtrip + const kp1 = generateKeyPair() + const kp2 = generateKeyPair() + const plaintext = encoder.encode('Secret message for key pair 2') + const { ciphertext: ct, ephemeralPublicKey: epk } = await encryptForRecipient( + plaintext, kp2.publicKey, kp1.privateKey + ) + const decrypted = await decryptFromSender(ct, epk, kp2.privateKey) + const eciesOk = decoder.decode(decrypted) === 'Secret message for key pair 2' + console.log(` ECIES roundtrip: ${eciesOk ? 'PASS' : 'FAIL'}`) + + // Doc key sharing roundtrip + const origKey = generateSymmetricKey() + const shareResult = await shareDocKey(origKey, kp2.publicKey, kp1.privateKey) + const recoveredKey = await receiveDocKey( + shareResult.encryptedKey, shareResult.ephemeralPublicKey, kp2.privateKey + ) + const keyOk = origKey.every((b, i) => b === recoveredKey[i]) + console.log(` Doc key sharing roundtrip: ${keyOk ? 'PASS' : 'FAIL'}`) + + console.log('\nDone.') +} + +main().catch(console.error) diff --git a/poc/crypto-eval/src/mit-crypto.ts b/poc/crypto-eval/src/mit-crypto.ts new file mode 100644 index 0000000..d22cedb --- /dev/null +++ b/poc/crypto-eval/src/mit-crypto.ts @@ -0,0 +1,154 @@ +/** + * MIT-licensed crypto primitives for rStack + * Equivalent functionality to @fileverse/crypto without AGPL obligations + * + * Dependencies (all MIT): + * - @noble/curves: ECDH key exchange (x25519) + * - @noble/hashes: HKDF key derivation, SHA-256 + * - tweetnacl: SecretBox symmetric encryption (for comparison) + * - Web Crypto API: AES-256-GCM (native, zero bundle cost) + */ + +import { x25519 } from '@noble/curves/ed25519' +import { hkdf } from '@noble/hashes/hkdf' +import { sha256 } from '@noble/hashes/sha256' +import { randomBytes } from '@noble/hashes/utils' + +// ─── Symmetric Encryption (AES-256-GCM via Web Crypto) ─── + +export async function aesEncrypt( + key: Uint8Array, + plaintext: Uint8Array +): Promise { + const iv = randomBytes(12) + const cryptoKey = await crypto.subtle.importKey( + 'raw', key, 'AES-GCM', false, ['encrypt'] + ) + const ciphertext = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + cryptoKey, + plaintext + ) + // Format: iv (12) || ciphertext+tag + const result = new Uint8Array(12 + ciphertext.byteLength) + result.set(iv) + result.set(new Uint8Array(ciphertext), 12) + return result +} + +export async function aesDecrypt( + key: Uint8Array, + encrypted: Uint8Array +): Promise { + const iv = encrypted.slice(0, 12) + const ciphertext = encrypted.slice(12) + const cryptoKey = await crypto.subtle.importKey( + 'raw', key, 'AES-GCM', false, ['decrypt'] + ) + const plaintext = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + cryptoKey, + ciphertext + ) + return new Uint8Array(plaintext) +} + +// ─── Key Generation ─── + +export function generateSymmetricKey(): Uint8Array { + return randomBytes(32) +} + +export function generateKeyPair(): { publicKey: Uint8Array; privateKey: Uint8Array } { + const privateKey = randomBytes(32) + const publicKey = x25519.getPublicKey(privateKey) + return { publicKey, privateKey } +} + +// ─── ECDH Key Exchange (x25519) ─── + +export function deriveSharedSecret( + myPrivateKey: Uint8Array, + theirPublicKey: Uint8Array +): Uint8Array { + return x25519.getSharedSecret(myPrivateKey, theirPublicKey) +} + +// ─── HKDF Key Derivation ─── + +export function deriveKey( + ikm: Uint8Array, + salt: Uint8Array | null, + info: string, + length: number = 32 +): Uint8Array { + return hkdf(sha256, ikm, salt ?? undefined, info, length) +} + +// ─── Key Hierarchy (matches DocCrypto pattern) ─── + +export function deriveSpaceKey(masterKey: Uint8Array, spaceId: string): Uint8Array { + return deriveKey(masterKey, null, `rstack:space:${spaceId}`) +} + +export function deriveDocKey(spaceKey: Uint8Array, docId: string): Uint8Array { + return deriveKey(spaceKey, null, `rstack:doc:${docId}`) +} + +// ─── ECIES-like Encryption (encrypt for a specific recipient) ─── + +export async function encryptForRecipient( + plaintext: Uint8Array, + recipientPublicKey: Uint8Array, + senderPrivateKey: Uint8Array +): Promise<{ ciphertext: Uint8Array; ephemeralPublicKey: Uint8Array }> { + // Generate ephemeral key pair for forward secrecy + const ephemeral = generateKeyPair() + + // Derive shared secret from ephemeral private + recipient public + const shared = deriveSharedSecret(ephemeral.privateKey, recipientPublicKey) + const encryptionKey = deriveKey(shared, null, 'rstack:ecies:encrypt') + + // Encrypt with derived key + const ciphertext = await aesEncrypt(encryptionKey, plaintext) + + return { + ciphertext, + ephemeralPublicKey: ephemeral.publicKey, + } +} + +export async function decryptFromSender( + ciphertext: Uint8Array, + ephemeralPublicKey: Uint8Array, + recipientPrivateKey: Uint8Array +): Promise { + // Derive same shared secret from ephemeral public + recipient private + const shared = deriveSharedSecret(recipientPrivateKey, ephemeralPublicKey) + const encryptionKey = deriveKey(shared, null, 'rstack:ecies:encrypt') + + return aesDecrypt(encryptionKey, ciphertext) +} + +// ─── Document Key Sharing ─── + +export async function shareDocKey( + docKey: Uint8Array, + recipientPublicKey: Uint8Array, + senderPrivateKey: Uint8Array +): Promise<{ encryptedKey: Uint8Array; ephemeralPublicKey: Uint8Array }> { + const { ciphertext, ephemeralPublicKey } = await encryptForRecipient( + docKey, + recipientPublicKey, + senderPrivateKey + ) + return { encryptedKey: ciphertext, ephemeralPublicKey } +} + +export async function receiveDocKey( + encryptedKey: Uint8Array, + ephemeralPublicKey: Uint8Array, + recipientPrivateKey: Uint8Array +): Promise { + return decryptFromSender(encryptedKey, ephemeralPublicKey, recipientPrivateKey) +} diff --git a/poc/crypto-eval/tsconfig.json b/poc/crypto-eval/tsconfig.json new file mode 100644 index 0000000..4636154 --- /dev/null +++ b/poc/crypto-eval/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/poc/dsheet-embed/README.md b/poc/dsheet-embed/README.md new file mode 100644 index 0000000..562871c --- /dev/null +++ b/poc/dsheet-embed/README.md @@ -0,0 +1,25 @@ +# dSheet Embed POC + +Prototype for embedding `@fileverse-dev/dsheet` as a new rStack module (rSheet). + +## Goal + +Wrap Fileverse's decentralized spreadsheet component in a LitElement web component compatible with rSpace's module system. + +## Key Integration Points + +- Auth: EncryptID → `isAuthorized` prop +- Collaboration: WebRTC via self-hosted collab-server +- Persistence: IndexedDB (`enableIndexeddbSync`) +- Metadata: Stored in Automerge documents + +## Use Cases + +- DAO treasury tracking +- Token allocation spreadsheets +- Budget planning with E2E encryption +- Research data tables alongside rNotes + +## Status + +Not started — depends on Phase 3 (collab server deployment). diff --git a/poc/ipfs-storage/README.md b/poc/ipfs-storage/README.md new file mode 100644 index 0000000..4af690c --- /dev/null +++ b/poc/ipfs-storage/README.md @@ -0,0 +1,36 @@ +# IPFS File Storage POC + +Proof-of-concept for encrypted file uploads to IPFS, replacing centralized storage for rNotes. + +## Flow + +``` +1. User picks file in TipTap editor +2. Client generates per-file symmetric key +3. Client encrypts file with AES-256-GCM +4. Encrypted blob uploaded to IPFS (via Pinata or self-hosted kubo) +5. CID + encrypted file key stored in Automerge document metadata +6. To view: fetch CID → decrypt with file key → display +``` + +## Setup + +```bash +# Using Pinata (managed IPFS pinning) +# Store API keys in Infisical: infisical-project=fileverse, path=/ipfs +export PINATA_API_KEY=... +export PINATA_SECRET_KEY=... + +npm install +npm run test +``` + +## Files + +- `src/ipfs-client.ts` — Upload/download encrypted files to IPFS +- `src/tiptap-image-extension.ts` — TipTap extension for IPFS-backed images +- `src/test.ts` — End-to-end test: encrypt → upload → download → decrypt + +## Status + +Not started — waiting on Phase 1 crypto evaluation results.