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 <noreply@anthropic.com>
This commit is contained in:
commit
f0b1096404
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
|
|
@ -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.
|
||||
|
|
@ -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<CryptoKey> {
|
||||
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<Uint8Array> {
|
||||
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<CryptoKey> {
|
||||
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).
|
||||
|
|
@ -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
|
||||
<DSheet
|
||||
isAuthorized={true}
|
||||
dsheetId="room-id" // Collaboration room
|
||||
enableWebrtc={true} // P2P sync
|
||||
enableIndexeddbSync={true} // Offline persistence
|
||||
isCollaborative={true} // Multi-user
|
||||
onChange={handleChange} // Data callback
|
||||
/>
|
||||
```
|
||||
|
||||
### 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 |
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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.
|
||||
|
|
@ -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:
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<void>, 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: '<p>Discussion about token allocation...</p>'.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)
|
||||
|
|
@ -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<Uint8Array> {
|
||||
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<Uint8Array> {
|
||||
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<Uint8Array> {
|
||||
// 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<Uint8Array> {
|
||||
return decryptFromSender(encryptedKey, ephemeralPublicKey, recipientPrivateKey)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
@ -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.
|
||||
Loading…
Reference in New Issue