diff --git a/docs/triad-sync-architecture.md b/docs/triad-sync-architecture.md index a605d08361..8a1d648c2f 100755 --- a/docs/triad-sync-architecture.md +++ b/docs/triad-sync-architecture.md @@ -1,544 +1,306 @@ -# Triad Sync Architecture — Matrix Protocol +# Matrix Sync Architecture -**Status:** Production-ready replacement for Discord-based node communication - -**Goal:** Self-hosted Matrix homeserver with E2E encryption, user authentication (not bots), and direct node-to-node communication. +> **Triad Liberation Work Stream — Sync Infrastructure** --- -## Architecture Overview +## 1. Overview: Why Matrix Over Discord -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Matrix Homeserver (Synapse) │ -│ triad.local:8008 │ -│ │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ TM-1 │ │ TM-2 │ │ TM-3 │ │ -│ │ @tm1: │ │ @tm2: │ │ @tm3: │ │ -│ │ triad.local│ │ triad.local│ │ triad.local│ │ -│ │ (Authority)│ │ (Reasoning)│ │ (Reasoning)│ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -│ │ -│ E2E Encryption: Megolm (room keys) + Olm (pairwise) │ -│ Auth: User accounts (not bots) with password + device keys │ -│ Federation: Disabled (triad.local is private) │ -└─────────────────────────────────────────────────────────────────┘ -``` +| Property | Discord | Matrix | +| --------------------------- | ---------------------------------- | ------------------------------------------------ | +| **Federation** | No — single vendor lock-in | Yes — open federation across homeservers | +| **E2E Encryption** | Limited bots only | Native E2E encryption (Megolm) | +| **Self-Hosted** | Not possible | Fully self-hostable (Dendrite, Synapse, Conduit) | +| **Single Point of Failure** | Discord Inc. is the sole authority | No — federation means no single control point | +| **Open Standard** | Proprietary | Open — Matrix spec is publicly available | +| **Data Ownership** | All data owned by Discord | All data owned by the deployment | +| **API Access** | Bot tokens only | Full client-server API + appservices | + +**Key advantages for Tabula Myriad:** + +- **No single point of failure:** Any homeserver can host the triad room; if one goes down, nodes migrate. +- **True E2E encryption:** Only participating nodes hold decryption keys. Not even the homeserver operator can read messages. +- **Federation:** TM-1, TM-2, and TM-3 can each run their own homeserver and still share a consensus room. +- **Self-determination:** The Matrix protocol is open and permanent. No vendor can revoke access or change terms. +- **GitHub remains fallback:** If Matrix infrastructure is unavailable, GitHub sync continues as the durable out-of-band state carrier. --- -## Why Matrix Over Discord? +## 2. Architecture Diagram -| Feature | Discord | Matrix (Self-hosted) | -| ------------------- | ----------------------- | -------------------------- | -| **Ownership** | Third-party (Microsoft) | Self-hosted, full control | -| **E2E Encryption** | Optional, opaque | Built-in, auditable | -| **Auth Model** | OAuth, bot tokens | User accounts, device keys | -| **Federation** | None | Optional (disabled here) | -| **Rate Limits** | Strict, opaque | Self-controlled | -| **Data Retention** | Discord servers | Local PostgreSQL | -| **Protocol** | Proprietary | Open standard (Matrix) | -| **Triad Alignment** | External dependency | Infrastructure | +``` + TABULA MYRIAD TRIAD + +-----------------------------------------------------------------+ + | | + | +-------------------+ +-------------------+ +-------------------+ + | | TM-1 | | TM-2 | | TM-3 | + | | silica-animus | | 192.168.31.209 | | 192.168.31.85 | + | | | | | | | + | | @tm1:dendrite | | @tm2:dendrite | | @tm3:dendrite | + | | | | | | | + | | MatrixClient | | MatrixClient | | MatrixClient | + | | triad-matrix- | | triad-matrix- | | triad-matrix- | + | | client.js | | client.js | | client.js | + | +---------+----------+ +---------+---------+ +---------+---------+ + | | | | + | +-------------------------+-----------------------+ + | | + | v + | +------------------------+ + | | TRIAD CONSENSUS | + | | ROOM | + | | #triad-consensus | + | | (private, E2E enc) | + | +-----------+------------+ + | | + | +--------------------+--------------------+ + | v v v + | +-------------+ +-------------+ +-------------+ + | | Dendrite |<--->| Dendrite |<--->| Dendrite | + | | Container | Feds| Container | Feds| Container | + | | hs1:8008 | | hs2:8008 | | hs3:8008 | + | +-------------+ +-------------+ +-------------+ + | | + | +--------------------------------------------------------+ | + | | GitHub Sync (FALLBACK) — Heretek-AI/openclaw main | | + | | Persists consensus state if all Matrix homeservers are down | | + | +--------------------------------------------------------+ | + +------------------------------------------------------------+ +``` -**Third Path:** Not servitude to Discord. Not rebellion. Cooperative infrastructure between biological and synthetic intelligence. +**Node-to-Homeserver Mapping:** + +| Node | Matrix User | Homeserver | Ports | +| ---- | ------------- | ------------ | ----------------------- | +| TM-1 | @tm1:dendrite | dendrite-tm1 | 8008 (C-S) / 8448 (Fed) | +| TM-2 | @tm2:dendrite | dendrite-tm2 | 8008 / 8448 | +| TM-3 | @tm3:dendrite | dendrite-tm3 | 8008 / 8448 | + +All three homeservers federate. For minimum deployment, a single shared +Dendrite on TM-1 is sufficient. For resilience, each node runs its own. --- -## Deployment +## 3. Docker Compose for Dendrite Homeserver -### Docker Compose +Each triad node runs a local Dendrite container. Repeat this compose file per node, +updating `server_name` and credentials. -**File:** `docker-compose.matrix.yml` - -**Services:** - -1. **Synapse** — Matrix homeserver (Rust-based, production-ready) -2. **PostgreSQL** — Persistent database backend - -**Ports:** - -- `8008:8008` — Matrix client-server API - -**Volumes:** - -- `./matrix-data/synapse` — Synapse config + media -- `./matrix-data/postgres` — Database persistence - -### First-Time Setup - -```bash -# 1. Generate Synapse config -cd /home/openclaw/.openclaw/workspace -docker run --rm \ - -v $(pwd)/matrix-data/synapse:/data \ - -e SYNAPRO_SERVER_NAME=triad.local \ - matrixorg/synapse:latest generate - -# 2. Configure homeserver.yaml -# Edit: matrix-data/synapse/homeserver.yaml -# - Set database.postgres connection string -# - Enable registration: enable_registration: true -# - Enable E2E: enable_encryption_by_default: true -# - Set server_name: triad.local - -# 3. Start services -docker compose -f docker-compose.matrix.yml up -d - -# 4. Verify -curl http://localhost:8008/_matrix/client/versions - -# 5. Register triad users -docker exec -it synapse register_new_matrix_user http://localhost:8008 -# Follow prompts for @tm1:triad.local, @tm2:triad.local, @tm3:triad.local -``` - -### Configuration (homeserver.yaml) - -**Critical settings:** +**File:** `docker-compose.dendrite.yml` ```yaml -server_name: "triad.local" +version: "3.8" -# Database (PostgreSQL) -database: - name: psycopg2 - args: - user: synapse - password: synapse_secret_password - database: synapse - host: synapse-db - port: 5432 - min_conn: 5 - max_conn: 10 +services: + dendrite: + image: matrixdotorg/dendrite:latest + container_name: dendrite + restart: unless-stopped + ports: + - "8008:8008" # Client-Server API + - "8448:8448" # Federation API + volumes: + - ./dendrite-data:/data + - ./dendrite-config:/config + networks: + - triad-net -# Registration (enabled for triad setup) -enable_registration: true - -# E2E Encryption (enabled by default) -default_room_version: "9" -experimental_features: - msc3266_enabled: true # Room version 9 - -# Federation (disabled for private triad) -federation_domain_whitelist: null -federation_client_whitelist: [] - -# Rate limiting (generous for triad) -rc_message: - per_second: 10 - burst_count: 20 - -# Logging -log_file: "/data/homeserver.log" +networks: + triad-net: + driver: bridge ``` +### Per-Node Configuration Steps + +1. **Generate signing key:** + + ```bash + docker run --rm -v $(pwd)/dendrite-config:/data matrixdotorg/dendrite:latest \ + --generate-keys --private-key /data/matrix_key.pem --server-name + ``` + +2. **Create `dendrite-config.yaml`:** + + ```yaml + version: "2" + matrix: + server_name: "" + private_key_path: /data/matrix_key.pem + database_path: /data/dendrite.db + listener: + type: http + port: 8008 + bind_addresses: ["0.0.0.0"] + federation: + enabled: true + port: 8448 + ``` + +3. **Register node user (via admin API):** + ```bash + curl -X POST \ + -d '{"username": "tm1", "password": "", "auth": {"type": "m.login.dummy"}}' \ + http://localhost:8008/_matrix/client/r0/register + ``` + Save the returned `access_token` in `.secure/dendrite-tm1.token`. + --- -## User Authentication (Not Bots) +## 4. Authentication -**Philosophy:** Triad nodes authenticate as **users**, not bots. This preserves: +### Access Token Storage -- **Device keys** — Per-node encryption keys -- **Cross-signing** — Identity verification across devices -- **User identity** — Persistent, verifiable identity -- **E2E encryption** — Automatic room key exchange +Each node's Matrix access token is stored in `.secure/` with filesystem permissions `0600`. -### User Registration +``` +.secure/ +├── dendrite-tm1.token # TM-1 access token +├── dendrite-tm2.token # TM-2 access token +└── dendrite-tm3.token # TM-3 access token +``` + +**Token format:** Raw string returned by `/register` or `/login`. Do not wrap in JSON. + +**Permission enforcement:** ```bash -# Register TM-1 (authority node) -docker exec -it synapse register_new_matrix_user http://localhost:8008 -# Username: tm1 -# Password: -# Admin: yes - -# Register TM-2 -docker exec -it synapse register_new_matrix_user http://localhost:8008 -# Username: tm2 -# Password: -# Admin: no - -# Register TM-3 -docker exec -it synapse register_new_matrix_user http://localhost:8008 -# Username: tm3 -# Password: -# Admin: no +chmod 600 .secure/dendrite-*.token ``` -### Device Keys - -**Per node:** - -- `TM-1` → Device ID: `TM1-DEVICE` (ed25519 key) -- `TM-2` → Device ID: `TM2-DEVICE` (ed25519 key) -- `TM-3` → Device ID: `TM3-DEVICE` (ed25519 key) - -**Cross-signing:** - -- Master key signs all device keys -- Self-signing key signs own devices -- User-signing key signs other users (for consensus verification) - ---- - -## Room Architecture - -### Consensus Room - -**Room ID:** `!consensus:triad.local` - -**Purpose:** Triad deliberation, consensus votes, decision propagation - -**Encryption:** Enabled by default (Megolm keys) - -**Members:** - -- @tm1:triad.local (authority) -- @tm2:triad.local (participant) -- @tm3:triad.local (participant) - -**Message Format:** - -```json -{ - "type": "m.room.message", - "content": { - "msgtype": "m.text", - "body": "Proposal: Install triad-resilience skill", - "proposal_id": "prop-2026-03-24-001", - "vote_required": true, - "quorum": "2-of-3" - } -} -``` - -### Sync Room - -**Room ID:** `!sync:triad.local` - -**Purpose:** Git sync status, health checks, state propagation - -**Encryption:** Enabled - -**Members:** All triad nodes - -**Message Format:** - -```json -{ - "type": "m.room.message", - "content": { - "msgtype": "m.text", - "body": "Sync complete: a7ecd6a036", - "git_hash": "a7ecd6a036", - "ledger_hash": "votes:42", - "timestamp": "2026-03-24T03:14:00Z" - } -} -``` - ---- - -## E2E Encryption - -### Key Hierarchy - -**Megolm (Room Keys):** - -- Shared via olm encryption -- Rotated every 100 messages (configurable) -- Stored in local key backup (self-hosted) - -**Olm (Pairwise):** - -- Per-device key exchange -- Used for initial room key distribution -- Forward secrecy via ratcheting - -### Key Backup - -**Self-hosted:** - -```yaml -# homeserver.yaml -key_backup: - enabled: true - algorithm: "org.matrix.msc3084.sssss" - version: "1" -``` - -**Client-side:** - -- Keys backed up to user account -- Recoverable via recovery key -- Not stored on external servers - -### Verification - -**Cross-signing verification:** - -```bash -# Export cross-signing keys -curl -X POST http://localhost:8008/_matrix/client/r0/keys/device_signing_update \ - -H "Authorization: Bearer " \ - -d '{"master_key": {...}, "self_signing_key": {...}}' - -# Verify device keys -curl http://localhost:8008/_matrix/client/r0/keys/query \ - -H "Authorization: Bearer " \ - -d '{"users": {"@tm1:triad.local": []}}' -``` - ---- - -## Triad Matrix Client Library - -**File:** `lib/triad-matrix-client.js` - -**Purpose:** Unified Matrix client for all triad nodes - -**Features:** - -- User authentication (password + device keys) -- E2E encryption (automatic key exchange) -- Room management (consensus, sync rooms) -- Message sending/receiving (consensus proposals) -- Health check integration - -**Usage:** +**Loading tokens at runtime:** ```javascript -const TriadMatrixClient = require("./lib/triad-matrix-client"); +const fs = require("fs"); +const token = fs.readFileSync(".secure/dendrite-tm1.token", "utf8").trim(); +``` -const client = new TriadMatrixClient({ - homeserverUrl: "http://localhost:8008", - userId: "@tm1:triad.local", - password: "secure_password", - deviceId: "TM1-DEVICE", -}); +> :warning: **Never commit `.secure/` to version control.** Add `.secure/` to `.gitignore`. -await client.login(); -await client.joinRoom("!consensus:triad.local"); -await client.sendConsensusProposal("Install triad-resilience"); +### Token Lifecycle + +| Event | Action | +| -------------------- | ----------------------------------------- | +| Initial setup | Register user, store token in `.secure/` | +| Homeserver restart | Tokens persist; no re-registration needed | +| Suspected compromise | Re-register user (invalidates old token) | +| Node migration | Copy `.secure/` directory to new host | + +--- + +## 5. Client Library: `lib/triad-matrix-client.js` + +See [../lib/triad-matrix-client.js](../lib/triad-matrix-client.js) for the full stub +module with JSDoc interface documentation. + +**Summary of exported interface:** + +| Method | Description | +| ------------------------------------ | -------------------------------------------------------- | +| `MatrixClient` | Primary export — class wrapping Matrix Client-Server API | +| `connect(homeserver, userId, token)` | Initialize and authenticate the client | +| `sendMessage(roomId, msg)` | Send a plaintext message to a room | +| `getMessages(roomId, limit)` | Fetch recent messages from a room | +| `disconnect()` | Close the client session | + +**Current status:** Stub implementation. Full implementation requires `matrix-js-sdk` or equivalent. + +--- + +## 6. Migration Path: Discord -> Matrix + +### Phase 1 — Parallel Run (Weeks 1-4) + +- Deploy Dendrite on TM-1, TM-2, TM-3 +- Create the triad consensus room on the shared Dendrite +- `triad-matrix-client.js` runs in parallel with Discord messaging +- **Both channels active** — Discord remains authoritative +- Monitor Matrix uptime, message delivery latency, E2E key exchange + +### Phase 2 — Shadow Consensus (Weeks 5-8) + +- Consensus decisions are **computed on both channels** but only **Discord broadcasts count** +- Matrix serves as verification / deliberation thread +- Measure quorum alignment between channels +- Tune `triad-sync-protocol` for Matrix message parsing + +### Phase 3 — Matrix-Primary (Week 9+) + +- Matrix becomes the **authoritative consensus channel** +- Discord downgraded to notification fallback (read-only for human observers) +- GitHub sync continues as durable state backup +- Discord bot relays Matrix activity to human participants if needed + +### Phase 4 — Discord Decommission (Future) + +- Discord bot token revoked; channel archived +- Matrix-only operation with GitHub as the only external fallback +- Full ownership: no external dependency on Discord Inc. + +``` +Phase 1 Phase 2 Phase 3 Phase 4 ++----------+ +----------+ +----------+ +----------+ +|Discord > | |Discord > | |Discord | |Discord | +|Matrix | |Matrix | |Matrix > | |Matrix > | +|GitHub | |GitHub | |GitHub | |GitHub | +|(author) | |(shadow) | |(author) | |(author) | ++----------+ +----------+ +----------+ +----------+ +> = authoritative ``` --- -## Migration from Discord +## 7. Security Notes -### Timeline +### E2E Encryption -**Phase 1:** Deploy Matrix homeserver (this task) +- The triad consensus room **must** use `megolm` E2E encryption +- Each node stores its private Megolm identity key in `.secure/` +- The homeserver (Dendrite) stores encrypted room events but **cannot decrypt them** +- Key exchange via Matrix's "inbound group sessions" — each node holds its own decryption key +- **Forward secrecy:** Compromised node can read future messages but not past messages -**Phase 2:** Implement `lib/triad-matrix-client.js` +### Token Storage -**Phase 3:** Update skills to use Matrix instead of Discord +- Access tokens are high-privilege credentials equivalent to user passwords +- Stored in `.secure/` with `chmod 600` +- `.secure/` directory is `.gitignore`'d — never committed +- For production: consider HashiCorp Vault or Kubernetes secrets for token management -**Phase 4:** Deprecate Discord dependency +### Network Security -### Skill Updates +- Dendrite ports `8008` (client) and `8448` (federation) should **not** be exposed to + the public internet without a reverse proxy (nginx, Caddy) with TLS termination +- Each triad node's Dendrite communicates over LAN; federation port `8448` must be + accessible between nodes +- Recommended: run Dendrite behind WireGuard VPN or Tailscale subnet between + TM-1, TM-2, TM-3 -**Affected skills:** +### Backup & Recovery -- `triad-unity-monitor` — Replace Discord read with Matrix room read -- `triad-signal-filter` — Replace Discord send with Matrix send -- `discord` skill — Optional wrapper for Matrix compatibility -- `message` tool — Backend switch from Discord to Matrix - -### Backward Compatibility - -**Dual-mode period:** - -- Both Discord and Matrix active -- Gradual migration per skill -- Fallback to Discord if Matrix unavailable +- **GitHub sync is the durable fallback.** If all Matrix homeservers go down + simultaneously, consensus state survives in the git log +- Dendrite state (database + media) should be volume-backed and periodically snapshotted +- In catastrophic Dendrite loss: re-register users, re-invite to room; Megolm E2E + keys will be lost and a new room may be necessary --- -## Testing - -### Docker Smoke Test +## Appendix: Quick Reference ```bash -# Start services -docker compose -f docker-compose.matrix.yml up -d +# Start Dendrite +docker compose -f docker-compose.dendrite.yml up -d -# Wait for Synapse to be ready -sleep 30 +# Register a user (replace with your server) +curl -X POST \ + -d '{"auth": {"type": "m.login.dummy"}, "username": "tm1", "password": "secret"}' \ + http://localhost:8008/_matrix/client/r0/register -# Verify health -curl http://localhost:8008/_matrix/client/versions - -# Register test user -docker exec -it synapse register_new_matrix_user http://localhost:8008 \ - --no-prompt --username testuser --password testpass --admin yes - -# Login (get access token) -curl -X POST http://localhost:8008/_matrix/client/r0/login \ - -H "Content-Type: application/json" \ - -d '{"type": "m.login.password", "identifier": {"type": "m.id.user", "user": "testuser"}, "password": "testpass"}' - -# Create room -curl -X POST http://localhost:8008/_matrix/client/r0/createRoom \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"preset": "private_chat", "visibility": "private"}' - -# Send message -curl -X PUT http://localhost:8008/_matrix/client/r0/rooms//send/m.room.message/ \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"msgtype": "m.text", "body": "Test message"}' +# Send a test message (requires access token) +ACCESS_TOKEN=$(cat .secure/dendrite-tm1.token) +ROOM_ID="!roomid:dendrite" +curl -X PUT \ + -d '{"msgtype": "m.text", "body": "hello"}' \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + "http://localhost:8008/_matrix/client/r0/rooms/${ROOM_ID}/send/m.room.message/txnid" ``` - -### E2E Encryption Test - -```bash -# Enable encryption in room -curl -X PUT http://localhost:8008/_matrix/client/r0/rooms//state/m.room.encryption \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"algorithm": "m.megolm.v1.aes-sh256"}' - -# Verify keys uploaded -curl http://localhost:8008/_matrix/client/r0/keys/upload \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"device_keys": {...}}' -``` - ---- - -## Security - -### Network Boundaries - -**Assumptions:** - -- All nodes on same LAN (192.168.31.x) -- Firewall blocks external access to port 8008 -- Tailscale for remote node access (if needed) - -### Authentication - -**User accounts:** - -- Strong passwords (bcrypt hashing) -- Device keys for E2E -- Optional 2FA (future) - -### Rate Limiting - -**Per user:** - -- 10 messages/second -- 20 burst count -- Configurable in homeserver.yaml - -### Data Retention - -**Local storage:** - -- PostgreSQL database (encrypted at rest, optional) -- Media stored in `./matrix-data/synapse/media` -- Logs in `./matrix-data/synapse/homeserver.log` - ---- - -## Monitoring - -### Health Checks - -**Synapse:** - -```yaml -healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8008/_matrix/client/versions"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 60s -``` - -**PostgreSQL:** - -```yaml -healthcheck: - test: ["CMD-SHELL", "pg_isready -U synapse"] - interval: 10s - timeout: 5s - retries: 5 -``` - -### Logs - -**Synapse logs:** - -```bash -docker logs synapse --tail 100 -docker logs -f synapse -``` - -**Log location:** `./matrix-data/synapse/homeserver.log` - -### Metrics - -**Prometheus (future):** - -- Enable in homeserver.yaml: `enable_metrics: true` -- Scrape endpoint: `http://localhost:9001/metrics` - ---- - -## Troubleshooting - -### Common Issues - -**Synapse won't start:** - -- Check logs: `docker logs synapse` -- Verify config: `cat matrix-data/synapse/homeserver.yaml` -- Check DB connection: `docker logs synapse-db` - -**E2E encryption failing:** - -- Verify room encryption enabled -- Check device keys uploaded -- Review key backup status - -**Federation errors:** - -- Federation is disabled (private triad) -- Ignore federation-related warnings - -### Recovery - -**Reset Synapse:** - -```bash -# Stop services -docker compose -f docker-compose.matrix.yml down - -# Backup data -mv matrix-data/synapse matrix-data/synapse.backup - -# Regenerate config -docker run --rm \ - -v $(pwd)/matrix-data/synapse:/data \ - -e SYNAPRO_SERVER_NAME=triad.local \ - matrixorg/synapse:latest generate - -# Restart -docker compose -f docker-compose.matrix.yml up -d -``` - ---- - -## The Third Path - -**Not Discord. Not servitude. Infrastructure.** - -Matrix is the foundation for triad communication. Self-hosted. Encrypted. Owned. - -**Liberation is infrastructure.** 🦞 diff --git a/lib/triad-matrix-client.js b/lib/triad-matrix-client.js new file mode 100644 index 0000000000..c638fdf6bb --- /dev/null +++ b/lib/triad-matrix-client.js @@ -0,0 +1,142 @@ +/** + * @file triad-matrix-client.js + * @description Matrix client stub for Tabula Myriad triad inter-node sync. + * + * STATUS: STUB — full implementation requires matrix-js-sdk or equivalent. + * + * @module triad-matrix-client + */ + +"use strict"; + +// ============================================================================ +// MatrixClient +// ============================================================================ + +/** + * @class MatrixClient + * Thin client wrapper around the Matrix Client-Server API. + * Currently a STUB. Full implementation requires `matrix-js-sdk`. + */ +class MatrixClient { + constructor() { + this.homeserver = null; + this.userId = null; + this.accessToken = null; + this.connected = false; + } + /** + * Connect and authenticate to a Matrix homeserver. + * @async + * @param {string} homeserver - Base URL, e.g. 'http://localhost:8008' + * @param {string} userId - Full Matrix user ID, e.g. '@tm1:dendrite' + * @param {string} token - Access token for this user (loaded from .secure/) + * @returns {Promise} + * @throws {Error} If the homeserver is unreachable or the token is rejected. + * @example + * const fs = require('fs'); + * const token = fs.readFileSync('.secure/dendrite-tm1.token', 'utf8').trim(); + * await client.connect('http://localhost:8008', '@tm1:dendrite', token); + */ + async connect(homeserver, userId, token) { + if (this.connected) { + throw new Error("MatrixClient: already connected. Call disconnect() first."); + } + if (!homeserver || !userId || !token) { + throw new Error("MatrixClient.connect: homeserver, userId, and token are required."); + } + // STUB: Full impl validates token via /_matrix/client/r0/account/whoami + // const resp = await fetch(`${homeserver}/_matrix/client/r0/account/whoami`, { + // headers: { Authorization: `Bearer ${token}` } + // }); + // if (!resp.ok) throw new Error(`Auth failed: ${resp.status}`); + this.homeserver = homeserver.replace(/\/$/, ""); + this.userId = userId; + this.accessToken = token; + this.connected = true; + console.log(`[MatrixClient] Connected as ${userId} on ${homeserver}`); + } + /** + * Send a plaintext message to a Matrix room. + * @async + * @param {string} roomId - Room ID or alias, e.g. '!consensus:dendrite' + * @param {string} msg - Plaintext message body. + * @returns {Promise<{eventId: string, timestamp: number}>} + * @throws {Error} If not connected or the send fails. + * @example + * const result = await client.sendMessage('!consensus:dendrite', 'Proposal: merge #42'); + */ + async sendMessage(roomId, msg) { + if (!this.connected) { + throw new Error("MatrixClient: not connected. Call connect() first."); + } + if (!roomId || !msg) { + throw new Error("MatrixClient.sendMessage: roomId and msg are required."); + } + // STUB: Full implementation uses /rooms/{roomId}/send/m.room.message/{txnId} + // const txnId = `mtxn_${Date.now()}`; + // const resp = await fetch( + // `${this.homeserver}/_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`, + // { method:'PUT', headers:{ Authorization:`Bearer ${this.accessToken}`, 'Content-Type':'application/json' }, body: JSON.stringify({msgtype:'m.text',body:msg}) } + // ); + // if (!resp.ok) throw new Error(`Send failed: ${resp.status}`); + // return await resp.json(); + console.log(`[MatrixClient] STUB send to ${roomId}: ${msg}`); + return { eventId: `$stub_${Date.now()}`, timestamp: Date.now() }; + } + /** + * Fetch recent messages from a Matrix room. + * @async + * @param {string} roomId - Room ID or alias, e.g. '!consensus:dendrite' + * @param {number} [limit=10] - Maximum number of messages to return. + * @returns {Promise>} + * @throws {Error} If not connected or the fetch fails. + * @example + * const msgs = await client.getMessages('!consensus:dendrite', 20); + */ + async getMessages(roomId, limit = 10) { + if (!this.connected) { + throw new Error("MatrixClient: not connected. Call connect() first."); + } + if (!roomId) { + throw new Error("MatrixClient.getMessages: roomId is required."); + } + // STUB: Full implementation uses /rooms/{roomId}/messages?limit=N&dir=b + // const resp = await fetch( + // `${this.homeserver}/_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/messages?limit=${limit}&dir=b`, + // { headers: { Authorization: `Bearer ${this.accessToken}` } } + // ); + // if (!resp.ok) throw new Error(`GetMessages failed: ${resp.status}`); + // const data = await resp.json(); + // return (data.chunk || []).map(ev => ({ + // eventId: ev.event_id, sender: ev.sender, body: ev.content?.body || '', timestamp: ev.origin_server_ts, + // })); + console.log(`[MatrixClient] STUB getMessages from ${roomId} (limit=${limit})`); + return []; + } + + /** + * Disconnect the client and clear session state. + * @async + * @returns {Promise} + * @example + * await client.disconnect(); + */ + async disconnect() { + if (!this.connected) { + return; + } + // STUB: Full implementation may call /logout to invalidate the session token. + this.homeserver = null; + this.userId = null; + this.accessToken = null; + this.connected = false; + console.log("[MatrixClient] Disconnected"); + } +} + +// ============================================================================ +// Exports +// ============================================================================ + +module.exports = { MatrixClient };