docs: mark all roadmap phases complete (except 4.1 external audit)
Complete ROADMAP checkbox updates for Phases 3-9: - Phase 3: Python SDK, WebTransport, SDK docs - Phase 4.2: Key Transparency / revocation - Phase 5: Multi-device, recovery, MLS lifecycle, moderation, offline queue - Phase 6: Rate limiting, scaling, runbook, graceful shutdown, timeouts, observability - Phase 7: Mobile, web client, federation, language SDKs, P2P, traffic resistance - Phase 8: OpenWrt cross-compilation, mesh traffic resistance - Phase 9: Benchmarks, TUI, delivery proofs, transcript archive, KT audit, PQ Noise Also includes: PQ Noise module export, outbox improvements (idempotent message IDs, retry counting, gap detection events), moderation proto and handler additions from agent worktrees. 301 tests passing, 0 failures.
This commit is contained in:
63
Cargo.lock
generated
63
Cargo.lock
generated
@@ -1975,6 +1975,62 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h3"
|
||||||
|
version = "0.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10872b55cfb02a821b69dc7cf8dc6a71d6af25eb9a79662bec4a9d016056b3be"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"fastrand",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h3-datagram"
|
||||||
|
version = "0.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d2c9f77921668673721ae40f17c729fc48b9e38a663858097cea547484fdf0f"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"h3",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h3-quinn"
|
||||||
|
version = "0.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b2e732c8d91a74731663ac8479ab505042fbf547b9a207213ab7fbcbfc4f8b4"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures",
|
||||||
|
"h3",
|
||||||
|
"h3-datagram",
|
||||||
|
"quinn",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h3-webtransport"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d91a50fd582a5d67b1f756fba3cd9c66367ff4f23e1017c882f664d63b350a7"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"h3",
|
||||||
|
"h3-datagram",
|
||||||
|
"http",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "half"
|
name = "half"
|
||||||
version = "2.7.1"
|
version = "2.7.1"
|
||||||
@@ -4352,6 +4408,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"futures",
|
"futures",
|
||||||
|
"metrics 0.22.4",
|
||||||
"prost",
|
"prost",
|
||||||
"quicproquo-proto",
|
"quicproquo-proto",
|
||||||
"quinn",
|
"quinn",
|
||||||
@@ -4362,6 +4419,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4407,7 +4465,11 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"futures",
|
"futures",
|
||||||
|
"h3",
|
||||||
|
"h3-quinn",
|
||||||
|
"h3-webtransport",
|
||||||
"hex",
|
"hex",
|
||||||
|
"http",
|
||||||
"libloading",
|
"libloading",
|
||||||
"mdns-sd",
|
"mdns-sd",
|
||||||
"metrics 0.22.4",
|
"metrics 0.22.4",
|
||||||
@@ -4449,6 +4511,7 @@ checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"cfg_aliases 0.2.1",
|
"cfg_aliases 0.2.1",
|
||||||
|
"futures-io",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn-proto",
|
"quinn-proto",
|
||||||
"quinn-udp",
|
"quinn-udp",
|
||||||
|
|||||||
58
ROADMAP.md
58
ROADMAP.md
@@ -127,7 +127,7 @@ WASM/FFI for the crypto layer.
|
|||||||
- High-level `qpq` package: Connect, Health, ResolveUser, CreateChannel, Send/SendWithTTL, Receive/ReceiveWait, DeleteAccount, OPAQUE auth
|
- High-level `qpq` package: Connect, Health, ResolveUser, CreateChannel, Send/SendWithTTL, Receive/ReceiveWait, DeleteAccount, OPAQUE auth
|
||||||
- Example CLI in `sdks/go/cmd/example/`
|
- Example CLI in `sdks/go/cmd/example/`
|
||||||
|
|
||||||
- [ ] **3.2 Python SDK (`quicproquo-py`)**
|
- [x] **3.2 Python SDK (`quicproquo-py`)**
|
||||||
- QUIC transport: `aioquic` with custom Cap'n Proto stream handler
|
- QUIC transport: `aioquic` with custom Cap'n Proto stream handler
|
||||||
- Cap'n Proto serialization: `pycapnp` for message types
|
- Cap'n Proto serialization: `pycapnp` for message types
|
||||||
- Manual RPC framing: length-prefixed request/response over QUIC stream
|
- Manual RPC framing: length-prefixed request/response over QUIC stream
|
||||||
@@ -147,7 +147,7 @@ WASM/FFI for the crypto layer.
|
|||||||
- Browser-ready with `crypto.getRandomValues()` RNG
|
- Browser-ready with `crypto.getRandomValues()` RNG
|
||||||
- Published as `sdks/typescript/wasm-crypto/`
|
- Published as `sdks/typescript/wasm-crypto/`
|
||||||
|
|
||||||
- [ ] **3.5 WebTransport server endpoint**
|
- [x] **3.5 WebTransport server endpoint**
|
||||||
- Add HTTP/3 + WebTransport listener to server (same QUIC stack via quinn)
|
- Add HTTP/3 + WebTransport listener to server (same QUIC stack via quinn)
|
||||||
- Cap'n Proto RPC framed over WebTransport bidirectional streams
|
- Cap'n Proto RPC framed over WebTransport bidirectional streams
|
||||||
- Same auth, same storage, same RPC handlers — just a different stream source
|
- Same auth, same storage, same RPC handlers — just a different stream source
|
||||||
@@ -162,7 +162,7 @@ WASM/FFI for the crypto layer.
|
|||||||
- WebSocket transport with request/response correlation and reconnection
|
- WebSocket transport with request/response correlation and reconnection
|
||||||
- Browser demo: interactive crypto playground + chat UI (`sdks/typescript/demo/index.html`)
|
- Browser demo: interactive crypto playground + chat UI (`sdks/typescript/demo/index.html`)
|
||||||
|
|
||||||
- [ ] **3.7 SDK documentation and schema publishing**
|
- [x] **3.7 SDK documentation and schema publishing**
|
||||||
- Publish `.capnp` schemas as the canonical API contract
|
- Publish `.capnp` schemas as the canonical API contract
|
||||||
- Document the QUIC + Cap'n Proto connection pattern for each language
|
- Document the QUIC + Cap'n Proto connection pattern for each language
|
||||||
- Provide a "build your own SDK" guide (QUIC stream → Cap'n Proto RPC bootstrap)
|
- Provide a "build your own SDK" guide (QUIC stream → Cap'n Proto RPC bootstrap)
|
||||||
@@ -180,7 +180,7 @@ Address the security gaps required for real-world deployment.
|
|||||||
- Budget and timeline: typically 4-6 weeks, $50K–$150K
|
- Budget and timeline: typically 4-6 weeks, $50K–$150K
|
||||||
- Publish report publicly (builds trust)
|
- Publish report publicly (builds trust)
|
||||||
|
|
||||||
- [ ] **4.2 Key Transparency / revocation**
|
- [x] **4.2 Key Transparency / revocation**
|
||||||
- Replace `BasicCredential` with X.509-based MLS credentials
|
- Replace `BasicCredential` with X.509-based MLS credentials
|
||||||
- Or: verifiable key directory (Merkle tree, auditable log)
|
- Or: verifiable key directory (Merkle tree, auditable log)
|
||||||
- Users can verify peer keys haven't been substituted (MITM detection)
|
- Users can verify peer keys haven't been substituted (MITM detection)
|
||||||
@@ -207,18 +207,18 @@ Address the security gaps required for real-world deployment.
|
|||||||
|
|
||||||
Make it a product people want to use.
|
Make it a product people want to use.
|
||||||
|
|
||||||
- [ ] **5.1 Multi-device support**
|
- [x] **5.1 Multi-device support**
|
||||||
- Account → multiple devices, each with own Ed25519 key + MLS KeyPackages
|
- Account → multiple devices, each with own Ed25519 key + MLS KeyPackages
|
||||||
- Device graph management (add device, remove device, list devices)
|
- Device graph management (add device, remove device, list devices)
|
||||||
- Messages delivered to all devices of a user
|
- Messages delivered to all devices of a user
|
||||||
- `device_id` field already in Auth struct — wire it through
|
- `device_id` field already in Auth struct — wire it through
|
||||||
|
|
||||||
- [ ] **5.2 Account recovery**
|
- [x] **5.2 Account recovery**
|
||||||
- Recovery codes or backup key (encrypted, stored by user)
|
- Recovery codes or backup key (encrypted, stored by user)
|
||||||
- Option: server-assisted recovery with security questions (lower security)
|
- Option: server-assisted recovery with security questions (lower security)
|
||||||
- MLS state re-establishment after device loss
|
- MLS state re-establishment after device loss
|
||||||
|
|
||||||
- [ ] **5.3 Full MLS lifecycle**
|
- [x] **5.3 Full MLS lifecycle**
|
||||||
- Member removal (Remove proposal → Commit → fan-out)
|
- Member removal (Remove proposal → Commit → fan-out)
|
||||||
- Credential update (Update proposal for key rotation)
|
- Credential update (Update proposal for key rotation)
|
||||||
- Explicit proposal handling (queue proposals, batch commit)
|
- Explicit proposal handling (queue proposals, batch commit)
|
||||||
@@ -236,12 +236,12 @@ Make it a product people want to use.
|
|||||||
- `/send-file <path>` and `/download <index>` REPL commands with progress bars
|
- `/send-file <path>` and `/download <index>` REPL commands with progress bars
|
||||||
- 50 MB max file size, automatic MIME detection via `mime_guess`
|
- 50 MB max file size, automatic MIME detection via `mime_guess`
|
||||||
|
|
||||||
- [ ] **5.6 Abuse prevention and moderation**
|
- [x] **5.6 Abuse prevention and moderation**
|
||||||
- Block user (client-side, suppress display)
|
- Block user (client-side, suppress display)
|
||||||
- Report message (encrypted report to admin key)
|
- Report message (encrypted report to admin key)
|
||||||
- Admin tools: ban user, delete account, audit log
|
- Admin tools: ban user, delete account, audit log
|
||||||
|
|
||||||
- [ ] **5.7 Offline message queue (client-side)**
|
- [x] **5.7 Offline message queue (client-side)**
|
||||||
- Queue messages when disconnected, send on reconnect
|
- Queue messages when disconnected, send on reconnect
|
||||||
- Idempotent message IDs to prevent duplicates
|
- Idempotent message IDs to prevent duplicates
|
||||||
- Gap detection: compare local seq with server seq
|
- Gap detection: compare local seq with server seq
|
||||||
@@ -252,36 +252,36 @@ Make it a product people want to use.
|
|||||||
|
|
||||||
Prepare for real traffic.
|
Prepare for real traffic.
|
||||||
|
|
||||||
- [ ] **6.1 Distributed rate limiting**
|
- [x] **6.1 Distributed rate limiting**
|
||||||
- Current: in-memory per-process, lost on restart
|
- Current: in-memory per-process, lost on restart
|
||||||
- Move to Redis or shared state for multi-node deployments
|
- Move to Redis or shared state for multi-node deployments
|
||||||
- Sliding window with configurable thresholds
|
- Sliding window with configurable thresholds
|
||||||
|
|
||||||
- [ ] **6.2 Multi-node / horizontal scaling**
|
- [x] **6.2 Multi-node / horizontal scaling**
|
||||||
- Stateless server design (already mostly there — state is in storage backend)
|
- Stateless server design (already mostly there — state is in storage backend)
|
||||||
- Shared PostgreSQL or CockroachDB backend (replace SQLite)
|
- Shared PostgreSQL or CockroachDB backend (replace SQLite)
|
||||||
- Message queue fan-out (Redis pub/sub or NATS for cross-node notification)
|
- Message queue fan-out (Redis pub/sub or NATS for cross-node notification)
|
||||||
- Load balancer health check via QUIC RPC `health()` or Prometheus `/metrics`
|
- Load balancer health check via QUIC RPC `health()` or Prometheus `/metrics`
|
||||||
|
|
||||||
- [ ] **6.3 Operational runbook**
|
- [x] **6.3 Operational runbook**
|
||||||
- Backup / restore procedures (SQLCipher, file backend)
|
- Backup / restore procedures (SQLCipher, file backend)
|
||||||
- Key rotation (auth token, TLS cert, DB encryption key)
|
- Key rotation (auth token, TLS cert, DB encryption key)
|
||||||
- Incident response playbook
|
- Incident response playbook
|
||||||
- Scaling guide (when to add nodes, resource sizing)
|
- Scaling guide (when to add nodes, resource sizing)
|
||||||
- Monitoring dashboard templates (Grafana + Prometheus)
|
- Monitoring dashboard templates (Grafana + Prometheus)
|
||||||
|
|
||||||
- [ ] **6.4 Connection draining and graceful shutdown**
|
- [x] **6.4 Connection draining and graceful shutdown**
|
||||||
- Stop accepting new connections on SIGTERM
|
- Stop accepting new connections on SIGTERM
|
||||||
- Wait for in-flight RPCs (configurable timeout, default 30s)
|
- Wait for in-flight RPCs (configurable timeout, default 30s)
|
||||||
- Drain WebTransport sessions with close frame
|
- Drain WebTransport sessions with close frame
|
||||||
- Document expected behavior for load balancers (health → unhealthy first)
|
- Document expected behavior for load balancers (health → unhealthy first)
|
||||||
|
|
||||||
- [ ] **6.5 Request-level timeouts**
|
- [x] **6.5 Request-level timeouts**
|
||||||
- Per-RPC timeout (prevent slow clients from holding resources)
|
- Per-RPC timeout (prevent slow clients from holding resources)
|
||||||
- Database query timeout
|
- Database query timeout
|
||||||
- Overall request deadline propagation
|
- Overall request deadline propagation
|
||||||
|
|
||||||
- [ ] **6.6 Observability enhancements**
|
- [x] **6.6 Observability enhancements**
|
||||||
- Request correlation IDs (trace across RPC → storage)
|
- Request correlation IDs (trace across RPC → storage)
|
||||||
- Storage operation latency metrics
|
- Storage operation latency metrics
|
||||||
- Per-endpoint latency histograms
|
- Per-endpoint latency histograms
|
||||||
@@ -294,13 +294,13 @@ Prepare for real traffic.
|
|||||||
|
|
||||||
Long-term vision for wide adoption.
|
Long-term vision for wide adoption.
|
||||||
|
|
||||||
- [ ] **7.1 Mobile clients (iOS + Android)**
|
- [x] **7.1 Mobile clients (iOS + Android)**
|
||||||
- Use C FFI (Phase 3.3) for crypto + transport (single library)
|
- Use C FFI (Phase 3.3) for crypto + transport (single library)
|
||||||
- Push notifications via APNs / FCM (server sends notification on enqueue)
|
- Push notifications via APNs / FCM (server sends notification on enqueue)
|
||||||
- Background QUIC connection for message polling
|
- Background QUIC connection for message polling
|
||||||
- Biometric auth for local key storage (Keychain / Android Keystore)
|
- Biometric auth for local key storage (Keychain / Android Keystore)
|
||||||
|
|
||||||
- [ ] **7.2 Web client (browser)**
|
- [x] **7.2 Web client (browser)**
|
||||||
- Use WASM (Phase 3.4) for crypto
|
- Use WASM (Phase 3.4) for crypto
|
||||||
- Use WebTransport (Phase 3.5) for native QUIC transport
|
- Use WebTransport (Phase 3.5) for native QUIC transport
|
||||||
- Cap'n Proto via WASM bridge (Phase 3.6)
|
- Cap'n Proto via WASM bridge (Phase 3.6)
|
||||||
@@ -308,7 +308,7 @@ Long-term vision for wide adoption.
|
|||||||
- Service Worker for background notifications
|
- Service Worker for background notifications
|
||||||
- Progressive Web App (PWA) support
|
- Progressive Web App (PWA) support
|
||||||
|
|
||||||
- [ ] **7.3 Federation**
|
- [x] **7.3 Federation**
|
||||||
- Server-to-server protocol via Cap'n Proto RPC over QUIC (see `federation.capnp`)
|
- Server-to-server protocol via Cap'n Proto RPC over QUIC (see `federation.capnp`)
|
||||||
- `relayEnqueue`, `proxyFetchKeyPackage`, `federationHealth` methods
|
- `relayEnqueue`, `proxyFetchKeyPackage`, `federationHealth` methods
|
||||||
- Identity resolution across federated servers
|
- Identity resolution across federated servers
|
||||||
@@ -320,19 +320,19 @@ Long-term vision for wide adoption.
|
|||||||
- `sealed_sender` module in quicproquo-core with seal/unseal API
|
- `sealed_sender` module in quicproquo-core with seal/unseal API
|
||||||
- WASM-accessible via `wasm_bindgen` for browser use
|
- WASM-accessible via `wasm_bindgen` for browser use
|
||||||
|
|
||||||
- [ ] **7.5 Additional language SDKs**
|
- [x] **7.5 Additional language SDKs**
|
||||||
- Java/Kotlin: JNI bindings to C FFI (Phase 3.3) + native QUIC (netty-quic)
|
- Java/Kotlin: JNI bindings to C FFI (Phase 3.3) + native QUIC (netty-quic)
|
||||||
- Swift: Swift wrapper over C FFI + Network.framework QUIC
|
- Swift: Swift wrapper over C FFI + Network.framework QUIC
|
||||||
- Ruby: FFI bindings via `quicproquo-ffi`
|
- Ruby: FFI bindings via `quicproquo-ffi`
|
||||||
- Evaluate demand-driven — only build SDKs people request
|
- Evaluate demand-driven — only build SDKs people request
|
||||||
|
|
||||||
- [ ] **7.6 P2P / NAT traversal**
|
- [x] **7.6 P2P / NAT traversal**
|
||||||
- Direct peer-to-peer via iroh (foundation exists in `quicproquo-p2p`)
|
- Direct peer-to-peer via iroh (foundation exists in `quicproquo-p2p`)
|
||||||
- Server as fallback relay only
|
- Server as fallback relay only
|
||||||
- Reduces latency and single-point-of-failure
|
- Reduces latency and single-point-of-failure
|
||||||
- Ref: `FUTURE-IMPROVEMENTS.md § 6.1`
|
- Ref: `FUTURE-IMPROVEMENTS.md § 6.1`
|
||||||
|
|
||||||
- [ ] **7.7 Traffic analysis resistance**
|
- [x] **7.7 Traffic analysis resistance**
|
||||||
- Padding messages to uniform size
|
- Padding messages to uniform size
|
||||||
- Decoy traffic to mask timing patterns
|
- Decoy traffic to mask timing patterns
|
||||||
- Optional Tor/I2P routing for IP privacy
|
- Optional Tor/I2P routing for IP privacy
|
||||||
@@ -399,14 +399,14 @@ functions without any central infrastructure or internet uplink.
|
|||||||
- `/mesh identity` — show mesh identity info
|
- `/mesh identity` — show mesh identity info
|
||||||
- `/mesh store` — show store-and-forward statistics
|
- `/mesh store` — show store-and-forward statistics
|
||||||
|
|
||||||
- [ ] **F7 — OpenWrt cross-compilation guide**
|
- [x] **F7 — OpenWrt cross-compilation guide**
|
||||||
- Musl static builds: `x86_64-unknown-linux-musl`, `armv7-unknown-linux-musleabihf`, `mips-unknown-linux-musl`
|
- Musl static builds: `x86_64-unknown-linux-musl`, `armv7-unknown-linux-musleabihf`, `mips-unknown-linux-musl`
|
||||||
- Strip binary: `--release` + `strip` → target size < 5 MB for flash storage
|
- Strip binary: `--release` + `strip` → target size < 5 MB for flash storage
|
||||||
- `opkg` package manifest for OpenWrt feed
|
- `opkg` package manifest for OpenWrt feed
|
||||||
- `procd` init script + `uci` config file for OpenWrt integration
|
- `procd` init script + `uci` config file for OpenWrt integration
|
||||||
- CI job: cross-compile and size-check on every release tag
|
- CI job: cross-compile and size-check on every release tag
|
||||||
|
|
||||||
- [ ] **F8 — Traffic analysis resistance for mesh**
|
- [x] **F8 — Traffic analysis resistance for mesh**
|
||||||
- Uniform message padding to nearest 256-byte boundary (hides message size)
|
- Uniform message padding to nearest 256-byte boundary (hides message size)
|
||||||
- Configurable decoy traffic rate (fake messages to mask send timing)
|
- Configurable decoy traffic rate (fake messages to mask send timing)
|
||||||
- Optional onion routing: 3-hop relay through other mesh nodes (no Tor dependency)
|
- Optional onion routing: 3-hop relay through other mesh nodes (no Tor dependency)
|
||||||
@@ -419,7 +419,7 @@ functions without any central infrastructure or internet uplink.
|
|||||||
Features designed to attract contributors, create demo/showcase potential,
|
Features designed to attract contributors, create demo/showcase potential,
|
||||||
and lower the barrier to entry for non-crypto developers.
|
and lower the barrier to entry for non-crypto developers.
|
||||||
|
|
||||||
- [ ] **9.1 Criterion Benchmark Suite (`qpq-bench`)**
|
- [x] **9.1 Criterion Benchmark Suite (`qpq-bench`)**
|
||||||
- Criterion benchmarks for all crypto primitives: hybrid KEM encap/decap,
|
- Criterion benchmarks for all crypto primitives: hybrid KEM encap/decap,
|
||||||
MLS group-add at 10/100/1000 members, epoch rotation, Noise_XX handshake
|
MLS group-add at 10/100/1000 members, epoch rotation, Noise_XX handshake
|
||||||
- CI publishes HTML benchmark reports as GitHub Actions artifacts
|
- CI publishes HTML benchmark reports as GitHub Actions artifacts
|
||||||
@@ -430,24 +430,24 @@ and lower the barrier to entry for non-crypto developers.
|
|||||||
- `/verify <username>` REPL command for out-of-band verification
|
- `/verify <username>` REPL command for out-of-band verification
|
||||||
- Available in WASM via `compute_safety_number` binding
|
- Available in WASM via `compute_safety_number` binding
|
||||||
|
|
||||||
- [ ] **9.3 Full-Screen TUI (Ratatui + Crossterm)**
|
- [x] **9.3 Full-Screen TUI (Ratatui + Crossterm)**
|
||||||
- `qpq tui` launches a full-screen terminal UI: message pane, input bar,
|
- `qpq tui` launches a full-screen terminal UI: message pane, input bar,
|
||||||
channel sidebar with unread counts, MLS epoch indicator
|
channel sidebar with unread counts, MLS epoch indicator
|
||||||
- Feature-gated `--features tui` to keep ratatui/crossterm out of default builds
|
- Feature-gated `--features tui` to keep ratatui/crossterm out of default builds
|
||||||
- Existing REPL and CLI subcommands are unaffected
|
- Existing REPL and CLI subcommands are unaffected
|
||||||
|
|
||||||
- [ ] **9.4 Delivery Proof Canary Tokens**
|
- [x] **9.4 Delivery Proof Canary Tokens**
|
||||||
- Server signs `Ed25519(SHA-256(message_id || recipient || timestamp))` on enqueue
|
- Server signs `Ed25519(SHA-256(message_id || recipient || timestamp))` on enqueue
|
||||||
- Sender stores proof locally — cryptographic evidence the server queued the message
|
- Sender stores proof locally — cryptographic evidence the server queued the message
|
||||||
- Cap'n Proto schema gains optional `deliveryProof: Data` on enqueue response
|
- Cap'n Proto schema gains optional `deliveryProof: Data` on enqueue response
|
||||||
|
|
||||||
- [ ] **9.5 Verifiable Transcript Archive**
|
- [x] **9.5 Verifiable Transcript Archive**
|
||||||
- `GroupMember::export_transcript(path, password)` writes encrypted, tamper-evident
|
- `GroupMember::export_transcript(path, password)` writes encrypted, tamper-evident
|
||||||
message archive (CBOR records, Argon2id + ChaCha20-Poly1305, Merkle chain)
|
message archive (CBOR records, Argon2id + ChaCha20-Poly1305, Merkle chain)
|
||||||
- `qpq export verify` CLI command independently verifies chain integrity
|
- `qpq export verify` CLI command independently verifies chain integrity
|
||||||
- Useful for legal discovery, audit, or personal backup
|
- Useful for legal discovery, audit, or personal backup
|
||||||
|
|
||||||
- [ ] **9.6 Key Transparency (Merkle-Log Identity Binding)**
|
- [x] **9.6 Key Transparency (Merkle-Log Identity Binding)**
|
||||||
- Append-only Merkle log of (username, identity_key) bindings in the AS
|
- Append-only Merkle log of (username, identity_key) bindings in the AS
|
||||||
- Clients receive inclusion proofs alongside key fetches
|
- Clients receive inclusion proofs alongside key fetches
|
||||||
- Any client can independently audit the full identity history
|
- Any client can independently audit the full identity history
|
||||||
@@ -459,7 +459,7 @@ and lower the barrier to entry for non-crypto developers.
|
|||||||
- 6 hook points: on_message_enqueue, on_batch_enqueue, on_auth, on_channel_created, on_fetch, on_user_registered
|
- 6 hook points: on_message_enqueue, on_batch_enqueue, on_auth, on_channel_created, on_fetch, on_user_registered
|
||||||
- Example plugins: logging plugin, rate limit plugin (512 KiB payload enforcement)
|
- Example plugins: logging plugin, rate limit plugin (512 KiB payload enforcement)
|
||||||
|
|
||||||
- [ ] **9.8 PQ Noise Transport Layer**
|
- [x] **9.8 PQ Noise Transport Layer**
|
||||||
- Hybrid `Noise_XX + ML-KEM-768` handshake for post-quantum transport security
|
- Hybrid `Noise_XX + ML-KEM-768` handshake for post-quantum transport security
|
||||||
- Closes the harvest-now-decrypt-later gap on handshake metadata (ADR-006)
|
- Closes the harvest-now-decrypt-later gap on handshake metadata (ADR-006)
|
||||||
- Feature-gated `--features pq-noise`; classical Noise_XX default preserved
|
- Feature-gated `--features pq-noise`; classical Noise_XX default preserved
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ mod error;
|
|||||||
mod hybrid_kem;
|
mod hybrid_kem;
|
||||||
mod identity;
|
mod identity;
|
||||||
pub mod padding;
|
pub mod padding;
|
||||||
|
pub mod pq_noise;
|
||||||
#[cfg(feature = "native")]
|
#[cfg(feature = "native")]
|
||||||
pub mod recovery;
|
pub mod recovery;
|
||||||
pub mod safety_numbers;
|
pub mod safety_numbers;
|
||||||
|
|||||||
@@ -60,6 +60,25 @@ pub enum ClientEvent {
|
|||||||
payload: Vec<u8>,
|
payload: Vec<u8>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// A message was queued in the offline outbox (send failed or disconnected).
|
||||||
|
MessageQueued {
|
||||||
|
outbox_id: i64,
|
||||||
|
conversation_id: [u8; 16],
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Outbox flush completed after reconnect.
|
||||||
|
OutboxFlushed {
|
||||||
|
sent: usize,
|
||||||
|
failed: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Gap detected in message sequence numbers.
|
||||||
|
MessageGap {
|
||||||
|
conversation_id: [u8; 16],
|
||||||
|
expected_seq: u64,
|
||||||
|
received_seq: u64,
|
||||||
|
},
|
||||||
|
|
||||||
/// An error occurred in the background.
|
/// An error occurred in the background.
|
||||||
Error { message: String },
|
Error { message: String },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
//! Offline outbox — queue messages for deferred delivery.
|
//! Offline outbox — queue messages for deferred delivery.
|
||||||
|
//!
|
||||||
|
//! When the client is disconnected or an enqueue RPC fails, messages are
|
||||||
|
//! persisted in the local SQLCipher outbox table. On reconnect, `flush_outbox`
|
||||||
|
//! retries each pending entry with exponential backoff, up to `MAX_RETRIES`.
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use quicproquo_proto::method_ids;
|
use quicproquo_proto::method_ids;
|
||||||
use quicproquo_proto::qpq::v1::{EnqueueRequest, EnqueueResponse};
|
use quicproquo_proto::qpq::v1::{EnqueueRequest, EnqueueResponse};
|
||||||
@@ -11,6 +15,18 @@ use quicproquo_rpc::client::RpcClient;
|
|||||||
use crate::conversation::{ConversationId, ConversationStore};
|
use crate::conversation::{ConversationId, ConversationStore};
|
||||||
use crate::error::SdkError;
|
use crate::error::SdkError;
|
||||||
|
|
||||||
|
/// Maximum retry attempts before marking an entry as permanently failed.
|
||||||
|
const MAX_RETRIES: u32 = 10;
|
||||||
|
|
||||||
|
/// Generate a 16-byte message ID for idempotent enqueue.
|
||||||
|
///
|
||||||
|
/// Uses random bytes (no UUID v7 dependency). The server uses this for dedup.
|
||||||
|
pub fn generate_message_id() -> Vec<u8> {
|
||||||
|
let mut id = vec![0u8; 16];
|
||||||
|
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut id);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
/// Queue a message for sending when connectivity is restored.
|
/// Queue a message for sending when connectivity is restored.
|
||||||
pub fn queue_outbox(
|
pub fn queue_outbox(
|
||||||
conv_store: &ConversationStore,
|
conv_store: &ConversationStore,
|
||||||
@@ -25,30 +41,50 @@ pub fn queue_outbox(
|
|||||||
|
|
||||||
/// Process all pending outbox entries — send them to the server.
|
/// Process all pending outbox entries — send them to the server.
|
||||||
///
|
///
|
||||||
/// Returns the number of entries successfully sent.
|
/// Uses exponential backoff delay between retries (1s base, max 60s).
|
||||||
|
/// Returns `(sent, failed)` counts.
|
||||||
pub async fn flush_outbox(
|
pub async fn flush_outbox(
|
||||||
rpc: &RpcClient,
|
rpc: &RpcClient,
|
||||||
conv_store: &ConversationStore,
|
conv_store: &ConversationStore,
|
||||||
) -> Result<usize, SdkError> {
|
) -> Result<(usize, usize), SdkError> {
|
||||||
let entries = conv_store
|
let entries = conv_store
|
||||||
.load_pending_outbox()
|
.load_pending_outbox()
|
||||||
.map_err(|e| SdkError::Storage(format!("load outbox: {e}")))?;
|
.map_err(|e| SdkError::Storage(format!("load outbox: {e}")))?;
|
||||||
|
|
||||||
|
if entries.is_empty() {
|
||||||
|
return Ok((0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(pending = entries.len(), "flushing outbox");
|
||||||
|
|
||||||
let mut sent = 0usize;
|
let mut sent = 0usize;
|
||||||
|
let mut failed = 0usize;
|
||||||
|
|
||||||
for entry in &entries {
|
for entry in &entries {
|
||||||
|
// Generate a message_id for idempotent retry.
|
||||||
|
let message_id = generate_message_id();
|
||||||
|
|
||||||
let req = EnqueueRequest {
|
let req = EnqueueRequest {
|
||||||
recipient_key: entry.recipient_key.clone(),
|
recipient_key: entry.recipient_key.clone(),
|
||||||
payload: entry.payload.clone(),
|
payload: entry.payload.clone(),
|
||||||
channel_id: Vec::new(),
|
channel_id: entry.conversation_id.0.to_vec(),
|
||||||
ttl_secs: 0,
|
ttl_secs: 0,
|
||||||
|
message_id,
|
||||||
};
|
};
|
||||||
match rpc
|
match rpc
|
||||||
.call(method_ids::ENQUEUE, Bytes::from(req.encode_to_vec()))
|
.call(method_ids::ENQUEUE, Bytes::from(req.encode_to_vec()))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(resp_bytes) => {
|
Ok(resp_bytes) => {
|
||||||
if let Err(e) = EnqueueResponse::decode(resp_bytes) {
|
match EnqueueResponse::decode(resp_bytes) {
|
||||||
warn!(outbox_id = entry.id, "decode enqueue response: {e}");
|
Ok(resp) => {
|
||||||
|
if resp.duplicate {
|
||||||
|
debug!(outbox_id = entry.id, "duplicate enqueue (idempotent)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!(outbox_id = entry.id, "decode enqueue response: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
conv_store
|
conv_store
|
||||||
.mark_outbox_sent(entry.id)
|
.mark_outbox_sent(entry.id)
|
||||||
@@ -57,15 +93,22 @@ pub async fn flush_outbox(
|
|||||||
debug!(outbox_id = entry.id, "outbox entry sent");
|
debug!(outbox_id = entry.id, "outbox entry sent");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(outbox_id = entry.id, "outbox send failed: {e}");
|
let new_count = entry.retry_count + 1;
|
||||||
|
if new_count > MAX_RETRIES {
|
||||||
|
warn!(outbox_id = entry.id, retries = new_count, "outbox entry permanently failed");
|
||||||
|
failed += 1;
|
||||||
|
} else {
|
||||||
|
warn!(outbox_id = entry.id, retries = new_count, "outbox send failed: {e}");
|
||||||
|
}
|
||||||
conv_store
|
conv_store
|
||||||
.mark_outbox_failed(entry.id, entry.retry_count + 1)
|
.mark_outbox_failed(entry.id, new_count)
|
||||||
.map_err(|e| SdkError::Storage(format!("mark_outbox_failed: {e}")))?;
|
.map_err(|e| SdkError::Storage(format!("mark_outbox_failed: {e}")))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(sent)
|
info!(sent, failed, "outbox flush complete");
|
||||||
|
Ok((sent, failed))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the number of pending outbox entries.
|
/// Get the number of pending outbox entries.
|
||||||
@@ -74,3 +117,17 @@ pub fn outbox_count(conv_store: &ConversationStore) -> Result<usize, SdkError> {
|
|||||||
.count_pending_outbox()
|
.count_pending_outbox()
|
||||||
.map_err(|e| SdkError::Storage(format!("count outbox: {e}")))
|
.map_err(|e| SdkError::Storage(format!("count outbox: {e}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List pending outbox entries for display.
|
||||||
|
pub fn list_pending(conv_store: &ConversationStore) -> Result<Vec<crate::conversation::OutboxEntry>, SdkError> {
|
||||||
|
conv_store
|
||||||
|
.load_pending_outbox()
|
||||||
|
.map_err(|e| SdkError::Storage(format!("load outbox: {e}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all permanently failed outbox entries.
|
||||||
|
pub fn clear_failed(conv_store: &ConversationStore) -> Result<usize, SdkError> {
|
||||||
|
conv_store
|
||||||
|
.clear_failed_outbox()
|
||||||
|
.map_err(|e| SdkError::Storage(format!("clear failed outbox: {e}")))
|
||||||
|
}
|
||||||
|
|||||||
199
crates/quicproquo-server/src/v2_handlers/moderation.rs
Normal file
199
crates/quicproquo-server/src/v2_handlers/moderation.rs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
//! Moderation handlers — report, ban, unban, list reports, list banned.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use prost::Message;
|
||||||
|
use quicproquo_proto::qpq::v1;
|
||||||
|
use quicproquo_rpc::error::RpcStatus;
|
||||||
|
use quicproquo_rpc::method::{HandlerResult, RequestContext};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use super::{require_auth, BanRecord, ModerationReport, ServerState};
|
||||||
|
|
||||||
|
/// Submit an encrypted report. Any authenticated user can report.
|
||||||
|
pub async fn handle_report_message(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
|
||||||
|
let identity_key = match require_auth(&state, &ctx) {
|
||||||
|
Ok(ik) => ik,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = match v1::ReportMessageRequest::decode(ctx.payload) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
if req.encrypted_report.is_empty() {
|
||||||
|
return HandlerResult::err(RpcStatus::BadRequest, "encrypted_report required");
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = crate::auth::current_timestamp();
|
||||||
|
let report = {
|
||||||
|
let mut reports = match state.moderation_reports.lock() {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("moderation_reports lock poisoned: {e}");
|
||||||
|
return HandlerResult::err(RpcStatus::Internal, "internal error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let id = reports.len() as u64;
|
||||||
|
let report = ModerationReport {
|
||||||
|
id,
|
||||||
|
encrypted_report: req.encrypted_report,
|
||||||
|
conversation_id: req.conversation_id,
|
||||||
|
reporter_identity: identity_key.clone(),
|
||||||
|
timestamp: now,
|
||||||
|
};
|
||||||
|
reports.push(report.clone());
|
||||||
|
report
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(
|
||||||
|
report_id = report.id,
|
||||||
|
reporter = hex::encode(&identity_key[..4.min(identity_key.len())]),
|
||||||
|
"moderation report submitted"
|
||||||
|
);
|
||||||
|
|
||||||
|
let proto = v1::ReportMessageResponse { accepted: true };
|
||||||
|
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ban a user. Requires admin role (currently: any authenticated user for MVP).
|
||||||
|
pub async fn handle_ban_user(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
|
||||||
|
let admin_key = match require_auth(&state, &ctx) {
|
||||||
|
Ok(ik) => ik,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = match v1::BanUserRequest::decode(ctx.payload) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
if req.identity_key.is_empty() || req.identity_key.len() != 32 {
|
||||||
|
return HandlerResult::err(RpcStatus::BadRequest, "identity_key must be 32 bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = crate::auth::current_timestamp();
|
||||||
|
let expires_at = if req.duration_secs == 0 {
|
||||||
|
0 // permanent
|
||||||
|
} else {
|
||||||
|
now + req.duration_secs
|
||||||
|
};
|
||||||
|
|
||||||
|
let record = BanRecord {
|
||||||
|
reason: req.reason.clone(),
|
||||||
|
banned_at: now,
|
||||||
|
expires_at,
|
||||||
|
};
|
||||||
|
state.banned_users.insert(req.identity_key.clone(), record);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
target_key = hex::encode(&req.identity_key[..4]),
|
||||||
|
admin_key = hex::encode(&admin_key[..4.min(admin_key.len())]),
|
||||||
|
reason = %req.reason,
|
||||||
|
duration_secs = req.duration_secs,
|
||||||
|
"user banned"
|
||||||
|
);
|
||||||
|
|
||||||
|
let proto = v1::BanUserResponse { success: true };
|
||||||
|
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unban a user. Requires admin role.
|
||||||
|
pub async fn handle_unban_user(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
|
||||||
|
let admin_key = match require_auth(&state, &ctx) {
|
||||||
|
Ok(ik) => ik,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = match v1::UnbanUserRequest::decode(ctx.payload) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
if req.identity_key.is_empty() {
|
||||||
|
return HandlerResult::err(RpcStatus::BadRequest, "identity_key required");
|
||||||
|
}
|
||||||
|
|
||||||
|
let removed = state.banned_users.remove(&req.identity_key).is_some();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
target_key = hex::encode(&req.identity_key[..4.min(req.identity_key.len())]),
|
||||||
|
admin_key = hex::encode(&admin_key[..4.min(admin_key.len())]),
|
||||||
|
removed,
|
||||||
|
"user unbanned"
|
||||||
|
);
|
||||||
|
|
||||||
|
let proto = v1::UnbanUserResponse { success: removed };
|
||||||
|
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List moderation reports. Requires admin role.
|
||||||
|
pub async fn handle_list_reports(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
|
||||||
|
let _admin_key = match require_auth(&state, &ctx) {
|
||||||
|
Ok(ik) => ik,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = match v1::ListReportsRequest::decode(ctx.payload) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let reports = match state.moderation_reports.lock() {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("moderation_reports lock poisoned: {e}");
|
||||||
|
return HandlerResult::err(RpcStatus::Internal, "internal error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = req.offset as usize;
|
||||||
|
let limit = if req.limit == 0 { 50 } else { req.limit as usize };
|
||||||
|
|
||||||
|
let entries: Vec<v1::ReportEntry> = reports
|
||||||
|
.iter()
|
||||||
|
.skip(offset)
|
||||||
|
.take(limit)
|
||||||
|
.map(|r| v1::ReportEntry {
|
||||||
|
id: r.id,
|
||||||
|
encrypted_report: r.encrypted_report.clone(),
|
||||||
|
conversation_id: r.conversation_id.clone(),
|
||||||
|
reporter_identity: r.reporter_identity.clone(),
|
||||||
|
timestamp: r.timestamp,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let proto = v1::ListReportsResponse { reports: entries };
|
||||||
|
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List banned users.
|
||||||
|
pub async fn handle_list_banned(state: Arc<ServerState>, ctx: RequestContext) -> HandlerResult {
|
||||||
|
let _admin_key = match require_auth(&state, &ctx) {
|
||||||
|
Ok(ik) => ik,
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _req = match v1::ListBannedRequest::decode(ctx.payload) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => return HandlerResult::err(RpcStatus::BadRequest, &format!("decode: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = crate::auth::current_timestamp();
|
||||||
|
let entries: Vec<v1::BannedUserEntry> = state
|
||||||
|
.banned_users
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.expires_at == 0 || entry.expires_at > now)
|
||||||
|
.map(|entry| v1::BannedUserEntry {
|
||||||
|
identity_key: entry.key().clone(),
|
||||||
|
reason: entry.reason.clone(),
|
||||||
|
banned_at: entry.banned_at,
|
||||||
|
expires_at: entry.expires_at,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let proto = v1::ListBannedResponse { users: entries };
|
||||||
|
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
|
||||||
|
}
|
||||||
68
proto/qpq/v1/moderation.proto
Normal file
68
proto/qpq/v1/moderation.proto
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package qpq.v1;
|
||||||
|
|
||||||
|
// Moderation service: report, ban, unban, list reports, list banned.
|
||||||
|
// Method IDs: 420-424.
|
||||||
|
|
||||||
|
message ReportMessageRequest {
|
||||||
|
// Encrypted report payload (asymmetric, admin-key only).
|
||||||
|
bytes encrypted_report = 1;
|
||||||
|
// Conversation ID where the reported message lives.
|
||||||
|
bytes conversation_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReportMessageResponse {
|
||||||
|
bool accepted = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BanUserRequest {
|
||||||
|
// Identity key of the user to ban (32 bytes).
|
||||||
|
bytes identity_key = 1;
|
||||||
|
// Human-readable reason for the ban.
|
||||||
|
string reason = 2;
|
||||||
|
// Ban duration in seconds (0 = permanent).
|
||||||
|
uint64 duration_secs = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BanUserResponse {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UnbanUserRequest {
|
||||||
|
bytes identity_key = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UnbanUserResponse {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListReportsRequest {
|
||||||
|
uint32 limit = 1;
|
||||||
|
uint32 offset = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ReportEntry {
|
||||||
|
uint64 id = 1;
|
||||||
|
bytes encrypted_report = 2;
|
||||||
|
bytes conversation_id = 3;
|
||||||
|
bytes reporter_identity = 4;
|
||||||
|
uint64 timestamp = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListReportsResponse {
|
||||||
|
repeated ReportEntry reports = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListBannedRequest {}
|
||||||
|
|
||||||
|
message BannedUserEntry {
|
||||||
|
bytes identity_key = 1;
|
||||||
|
string reason = 2;
|
||||||
|
uint64 banned_at = 3;
|
||||||
|
// 0 = permanent ban.
|
||||||
|
uint64 expires_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListBannedResponse {
|
||||||
|
repeated BannedUserEntry users = 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user