docs: comprehensive update for sprints 1-9
Update README, ROADMAP, and mdBook to reflect all sprint deliverables: rich messaging, file transfer, disappearing messages, Go/TypeScript SDKs, C FFI, mesh networking (identity, store-and-forward, broadcast), and security hardening. Add 6 new mdBook guides (REPL reference, Go SDK, TypeScript SDK + browser demo, rich messaging, file transfer, mesh networking). Check off 16 completed ROADMAP items across phases 3-9.
This commit is contained in:
@@ -324,7 +324,12 @@ Ensure the client has access to the server's TLS certificate. By default, both s
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Running the Client](running-the-client.md) -- full CLI reference
|
||||
- [REPL Command Reference](repl-reference.md) -- complete list of 40+ slash commands
|
||||
- [Rich Messaging](rich-messaging.md) -- reactions, typing indicators, edit/delete
|
||||
- [File Transfer](file-transfer.md) -- chunked upload/download with SHA-256 verification
|
||||
- [Go SDK](go-sdk.md) -- build Go applications against the qpq server
|
||||
- [TypeScript SDK & Browser Demo](typescript-sdk.md) -- WASM crypto in the browser
|
||||
- [Mesh Networking](mesh-networking.md) -- P2P, broadcast channels, store-and-forward
|
||||
- [MLS (RFC 9420)](../protocol-layers/mls.md) -- how the MLS group operations work
|
||||
- [GroupMember Lifecycle](../internals/group-member-lifecycle.md) -- internal state machine details
|
||||
- [Delivery Service Internals](../internals/delivery-service.md) -- how the DS queues and delivers messages
|
||||
|
||||
114
docs/src/getting-started/file-transfer.md
Normal file
114
docs/src/getting-started/file-transfer.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# File Transfer
|
||||
|
||||
quicproquo supports encrypted file transfer with chunked upload/download,
|
||||
SHA-256 content addressing, and automatic MIME type detection. Files up to
|
||||
50 MB are supported.
|
||||
|
||||
## Sending a file
|
||||
|
||||
```
|
||||
/send-file /path/to/document.pdf
|
||||
```
|
||||
|
||||
Or use the short alias:
|
||||
|
||||
```
|
||||
/sf /path/to/document.pdf
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
1. Read the file and compute its SHA-256 hash
|
||||
2. Upload the file in **256 KB chunks** via the `uploadBlob` RPC
|
||||
3. Verify the hash on the server side
|
||||
4. Send a `FileRef` message to the active conversation containing the
|
||||
blob ID (SHA-256 hash), filename, file size, and MIME type
|
||||
|
||||
A progress bar is displayed during upload.
|
||||
|
||||
## Downloading a file
|
||||
|
||||
```
|
||||
/download 7
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```
|
||||
/dl 7
|
||||
```
|
||||
|
||||
Where `7` is the history index of the FileRef message. This will:
|
||||
|
||||
1. Fetch the blob in chunks via the `downloadBlob` RPC
|
||||
2. Verify the SHA-256 hash matches the FileRef's blob ID
|
||||
3. Save the file to the current directory using the original filename
|
||||
|
||||
If a file with the same name already exists, a suffix is appended
|
||||
(`-1`, `-2`, etc.) to avoid overwriting.
|
||||
|
||||
## How it works
|
||||
|
||||
### Content-addressable storage
|
||||
|
||||
Files are stored on the server by their SHA-256 hash (`blob_id`). This
|
||||
means identical files are automatically deduplicated — uploading the same
|
||||
file twice reuses the existing blob.
|
||||
|
||||
### Chunked transfer
|
||||
|
||||
Files are split into 256 KB chunks for upload and download. This:
|
||||
|
||||
- Keeps memory usage bounded for large files
|
||||
- Allows progress reporting
|
||||
- Enables future resumable transfers
|
||||
|
||||
### FileRef message type
|
||||
|
||||
The `FileRef` (type code `0x08`) is sent as a regular MLS-encrypted message:
|
||||
|
||||
```
|
||||
[blob_id: 32 bytes (SHA-256)]
|
||||
[filename_len: 2 bytes BE][filename: UTF-8]
|
||||
[file_size: 8 bytes BE]
|
||||
[mime_len: 2 bytes BE][mime_type: UTF-8]
|
||||
```
|
||||
|
||||
The server stores a JSON `.meta` sidecar alongside each blob with metadata.
|
||||
|
||||
### MIME type detection
|
||||
|
||||
MIME types are detected automatically using the `mime_guess` crate based on
|
||||
file extension. For example:
|
||||
|
||||
- `report.pdf` → `application/pdf`
|
||||
- `photo.jpg` → `image/jpeg`
|
||||
- `data.csv` → `text/csv`
|
||||
|
||||
### Limits
|
||||
|
||||
- **Maximum file size:** 50 MB
|
||||
- **Chunk size:** 256 KB
|
||||
- **Hash algorithm:** SHA-256
|
||||
|
||||
### RPC methods
|
||||
|
||||
| Method | Ordinal | Description |
|
||||
|---|---|---|
|
||||
| `uploadBlob` | `@21` | Chunked file upload with SHA-256 content addressing |
|
||||
| `downloadBlob` | `@22` | Chunked file download with hash verification |
|
||||
|
||||
### Error codes
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| E024 | Blob upload failed |
|
||||
| E025 | Blob download failed |
|
||||
| E026 | Blob hash mismatch |
|
||||
| E027 | Blob operation error |
|
||||
|
||||
## Implementation
|
||||
|
||||
- **Server:** `crates/quicproquo-server/src/node_service/blob_ops.rs`
|
||||
- **Client REPL:** `/send-file` and `/download` in `crates/quicproquo-client/src/client/repl.rs`
|
||||
- **Message type:** `FileRef` variant in `crates/quicproquo-core/src/app_message.rs`
|
||||
152
docs/src/getting-started/go-sdk.md
Normal file
152
docs/src/getting-started/go-sdk.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Go SDK
|
||||
|
||||
The Go SDK (`sdks/go/`) provides a native QUIC + Cap'n Proto client for
|
||||
quicproquo, giving Go applications full access to the messaging API without
|
||||
any HTTP translation layer.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.21+
|
||||
- A running qpq server
|
||||
|
||||
## Installation
|
||||
|
||||
```go
|
||||
import "quicproquo.dev/sdk/go/qpq"
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"quicproquo.dev/sdk/go/qpq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Connect to the server
|
||||
client, err := qpq.Connect(ctx, qpq.Options{
|
||||
Addr: "127.0.0.1:7000",
|
||||
InsecureSkipVerify: true, // development only
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Health check
|
||||
status, err := client.Health(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Server status:", status)
|
||||
|
||||
// OPAQUE authentication
|
||||
if err := client.LoginStart(ctx, "alice", "secret"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := client.LoginFinish(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Resolve a peer
|
||||
peerKey, err := client.ResolveUser(ctx, "bob")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a DM channel
|
||||
channelID, err := client.CreateChannel(ctx, peerKey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Channel:", channelID)
|
||||
|
||||
// Send a message
|
||||
if err := client.Send(ctx, peerKey, []byte("Hello from Go!")); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Send a disappearing message (1 hour TTL)
|
||||
if err := client.SendWithTTL(ctx, peerKey, []byte("This vanishes"), 3600); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Receive messages
|
||||
msgs, err := client.Receive(ctx, peerKey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, m := range msgs {
|
||||
fmt.Println("Received:", string(m))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API reference
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `Connect(ctx, opts)` | Establish QUIC connection to server |
|
||||
| `Health(ctx)` | Server readiness probe |
|
||||
| `RegisterStart/Finish(ctx, user, pass)` | OPAQUE registration |
|
||||
| `LoginStart/Finish(ctx, user, pass)` | OPAQUE login |
|
||||
| `ResolveUser(ctx, username)` | Look up a user's identity key |
|
||||
| `CreateChannel(ctx, peerKey)` | Create a 1:1 DM channel |
|
||||
| `Send(ctx, recipientKey, payload)` | Send a message |
|
||||
| `SendWithTTL(ctx, recipientKey, payload, ttlSecs)` | Send a disappearing message |
|
||||
| `Receive(ctx, recipientKey)` | Fetch queued messages |
|
||||
| `ReceiveWait(ctx, recipientKey, timeoutMs)` | Long-poll for messages (default 5 s timeout) |
|
||||
| `DeleteAccount(ctx)` | Delete the authenticated account |
|
||||
|
||||
## Transport details
|
||||
|
||||
- **Protocol:** QUIC with TLS 1.3 via `quic-go`
|
||||
- **ALPN:** `"capnp"` (same as the Rust client)
|
||||
- **Serialisation:** Cap'n Proto RPC over a single bidirectional stream
|
||||
- **Idle timeout:** 300 seconds
|
||||
- **Auth:** Session token stored in client, sent with every RPC via the `Auth` struct
|
||||
|
||||
## Connection options
|
||||
|
||||
```go
|
||||
type Options struct {
|
||||
Addr string // "host:port"
|
||||
CACertPath string // path to CA cert (DER format)
|
||||
InsecureSkipVerify bool // skip TLS verification (dev only!)
|
||||
Token string // pre-existing auth token (optional)
|
||||
}
|
||||
```
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
sdks/go/
|
||||
├── proto/ # Generated Cap'n Proto types from node.capnp
|
||||
├── transport/ # QUIC transport layer (quic-go + TLS 1.3)
|
||||
├── qpq/ # High-level client API (QpqClient)
|
||||
├── cmd/example/ # Example CLI program
|
||||
├── go.mod
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Running the example
|
||||
|
||||
```bash
|
||||
cd sdks/go/cmd/example
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## Regenerating proto types
|
||||
|
||||
If you modify `schemas/node.capnp`, regenerate the Go types:
|
||||
|
||||
```bash
|
||||
capnp compile -ogo sdks/go/proto/node.capnp
|
||||
```
|
||||
175
docs/src/getting-started/mesh-networking.md
Normal file
175
docs/src/getting-started/mesh-networking.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Mesh Networking
|
||||
|
||||
quicproquo includes a mesh networking layer for decentralised, peer-to-peer
|
||||
messaging without central infrastructure. It is designed for community
|
||||
networks (Freifunk, BATMAN-adv, Babel routing) and offline-capable
|
||||
environments.
|
||||
|
||||
Mesh features are **feature-gated** — build the client with:
|
||||
|
||||
```bash
|
||||
cargo build -p quicproquo-client --features mesh
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Client A ── mDNS discovery ──► nearby qpq node (LAN / mesh)
|
||||
│
|
||||
Cap'n Proto federation
|
||||
│
|
||||
remote qpq node (across mesh)
|
||||
|
||||
Client B ── iroh P2P ──────► Client C (direct, NAT-traversed)
|
||||
```
|
||||
|
||||
Three layers work together:
|
||||
|
||||
1. **Server federation** — servers relay messages across mesh via Cap'n Proto
|
||||
RPC over QUIC with mTLS
|
||||
2. **mDNS discovery** — servers announce themselves, clients find nearby nodes
|
||||
3. **iroh P2P** — direct peer-to-peer QUIC connections with NAT traversal
|
||||
|
||||
## Self-sovereign mesh identity
|
||||
|
||||
In mesh mode, identity is based on an Ed25519 keypair — independent of server
|
||||
registration. No OPAQUE password auth or Authentication Service is required.
|
||||
|
||||
```
|
||||
/mesh identity
|
||||
```
|
||||
|
||||
Shows your mesh public key and known peers. The keypair and peer directory are
|
||||
persisted in a local JSON file.
|
||||
|
||||
## mDNS discovery
|
||||
|
||||
Servers announce `_quicproquo._udp.local.` via mDNS on startup with TXT
|
||||
records:
|
||||
|
||||
```
|
||||
ver=1
|
||||
server=<host:port>
|
||||
domain=<local_domain>
|
||||
```
|
||||
|
||||
Clients browse for nearby nodes:
|
||||
|
||||
```
|
||||
/mesh peers
|
||||
```
|
||||
|
||||
Note a discovered server for connection:
|
||||
|
||||
```
|
||||
/mesh server 192.168.1.42:7000
|
||||
```
|
||||
|
||||
## Direct P2P messaging
|
||||
|
||||
Send messages directly to a peer via iroh transport (QUIC with NAT
|
||||
traversal):
|
||||
|
||||
```
|
||||
/mesh send <peer_id> Hello from the mesh!
|
||||
```
|
||||
|
||||
Messages are signed with your mesh identity key. The iroh relay server
|
||||
is used as fallback when direct connections fail.
|
||||
|
||||
## Store-and-forward
|
||||
|
||||
Messages for offline peers are buffered with TTL-based expiry:
|
||||
|
||||
- **MeshEnvelope** — signed message with TTL, hop count, and max hops
|
||||
- **Deduplication** — SHA-256 message ID prevents relay loops
|
||||
- **MeshStore** — in-memory queue with per-recipient capacity limits
|
||||
- **Garbage collection** — expired messages are automatically purged
|
||||
|
||||
Forward stored messages to a newly-discovered peer:
|
||||
|
||||
```
|
||||
/mesh store # show queue statistics
|
||||
```
|
||||
|
||||
The routing table shows known paths:
|
||||
|
||||
```
|
||||
/mesh route
|
||||
```
|
||||
|
||||
## Broadcast channels
|
||||
|
||||
Lightweight pub/sub channels using symmetric ChaCha20-Poly1305 encryption
|
||||
(no MLS overhead). Suitable for community bulletin boards, announcements,
|
||||
and emergency broadcasts.
|
||||
|
||||
```
|
||||
/mesh subscribe announcements # join a channel
|
||||
/mesh broadcast announcements Hello! # publish to a channel
|
||||
```
|
||||
|
||||
Channels are created automatically on first subscription. The symmetric key is
|
||||
derived from the topic name (for open channels) or distributed out-of-band
|
||||
(for private channels).
|
||||
|
||||
## Federation
|
||||
|
||||
Servers relay messages for recipients on remote nodes:
|
||||
|
||||
- `handle_enqueue` and `handle_batch_enqueue` call
|
||||
`federation::routing::resolve_destination()`
|
||||
- Remote recipients are forwarded via `FederationClient::relay_enqueue()`
|
||||
- mTLS mutual authentication between federation peers
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
# Environment variables
|
||||
QPQ_FEDERATION_LISTEN=0.0.0.0:7001
|
||||
QPQ_LOCAL_DOMAIN=node1.mesh.local
|
||||
QPQ_FEDERATION_CERT=/path/to/cert.der
|
||||
QPQ_FEDERATION_KEY=/path/to/key.der
|
||||
QPQ_FEDERATION_CA=/path/to/ca.der
|
||||
```
|
||||
|
||||
Or in `qpq-server.toml`:
|
||||
|
||||
```toml
|
||||
federation_enabled = true
|
||||
federation_domain = "node1.mesh.local"
|
||||
federation_listen = "0.0.0.0:7001"
|
||||
```
|
||||
|
||||
### ALPN tokens
|
||||
|
||||
| Protocol | ALPN |
|
||||
|---|---|
|
||||
| Client ↔ Server | `b"capnp"` |
|
||||
| P2P transport | `b"quicproquo/p2p/1"` |
|
||||
| Federation | `b"quicproquo/federation/1"` |
|
||||
|
||||
## REPL command summary
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `/mesh peers` | Scan for nearby nodes via mDNS |
|
||||
| `/mesh server <host:port>` | Note a discovered server |
|
||||
| `/mesh send <peer_id> <msg>` | Direct P2P message |
|
||||
| `/mesh broadcast <topic> <msg>` | Publish to broadcast channel |
|
||||
| `/mesh subscribe <topic>` | Join a broadcast channel |
|
||||
| `/mesh route` | Show routing table |
|
||||
| `/mesh identity` | Show mesh identity info |
|
||||
| `/mesh store` | Show store-and-forward stats |
|
||||
|
||||
## Implementation
|
||||
|
||||
- **P2P node:** `crates/quicproquo-p2p/src/lib.rs` — `P2pNode` with iroh
|
||||
transport
|
||||
- **Mesh identity:** `crates/quicproquo-p2p/src/identity.rs`
|
||||
- **Store-and-forward:** `crates/quicproquo-p2p/src/store.rs` +
|
||||
`envelope.rs`
|
||||
- **Broadcast:** `crates/quicproquo-p2p/src/broadcast.rs`
|
||||
- **mDNS discovery:** `crates/quicproquo-client/src/client/mesh_discovery.rs`
|
||||
- **Federation routing:** `crates/quicproquo-server/src/node_service/delivery.rs`
|
||||
- **REPL commands:** mesh handlers in `crates/quicproquo-client/src/client/repl.rs`
|
||||
124
docs/src/getting-started/repl-reference.md
Normal file
124
docs/src/getting-started/repl-reference.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# REPL Command Reference
|
||||
|
||||
The qpq interactive REPL provides 40+ slash commands for messaging, group
|
||||
management, file transfer, privacy controls, and mesh networking. Launch it
|
||||
with:
|
||||
|
||||
```bash
|
||||
cargo run --bin qpq -- repl --username alice --password mypass
|
||||
```
|
||||
|
||||
Type any text without a leading `/` to send a message in the active
|
||||
conversation.
|
||||
|
||||
---
|
||||
|
||||
## Account and identity
|
||||
|
||||
| Command | Alias | Description |
|
||||
|---|---|---|
|
||||
| `/whoami` | | Show your identity key, active conversation, and group status |
|
||||
| `/verify <username>` | | Compare 60-digit safety numbers with a peer for out-of-band key verification |
|
||||
| `/update-key` | `/rotate-key` | Rotate your MLS leaf node key material (proposes self-update, auto-commits, fans out to all members) |
|
||||
| `/delete-account` | | Permanently delete your account with confirmation prompt. Purges all user data, keys, deliveries, and channel memberships from the server |
|
||||
|
||||
---
|
||||
|
||||
## Conversations
|
||||
|
||||
| Command | Alias | Description |
|
||||
|---|---|---|
|
||||
| `/dm <username>` | | Start or switch to a 1:1 DM with a peer. Creates the channel on first use |
|
||||
| `/create-group <name>` | `/cg` | Create a new MLS group |
|
||||
| `/invite <username>` | | Add a member to the current group (fetches their KeyPackage, sends Welcome) |
|
||||
| `/remove <username>` | | Remove a member from the current group |
|
||||
| `/join` | | Accept a pending group invitation (processes Welcome message) |
|
||||
| `/leave` | | Leave the current group |
|
||||
| `/switch @user` or `/switch #group` | | Switch the active conversation |
|
||||
| `/list` | `/ls` | List all conversations |
|
||||
| `/members` | | Show group members with resolved usernames |
|
||||
| `/group-info` | `/gi` | Show group type, member list, and current MLS epoch |
|
||||
| `/rename <name>` | | Rename the current conversation |
|
||||
| `/history [count]` | `/hist` | Show message history (default: 20 messages) |
|
||||
|
||||
---
|
||||
|
||||
## Messaging
|
||||
|
||||
| Command | Alias | Description |
|
||||
|---|---|---|
|
||||
| *(plain text)* | | Send a message in the active conversation |
|
||||
| `/react <emoji> [index]` | | React to a message with an emoji. Omit index to react to the latest message |
|
||||
| `/typing` | | Send a typing indicator to the active conversation |
|
||||
| `/typing-notify on\|off` | | Toggle display of other users' typing indicators |
|
||||
| `/edit <index> <text>` | | Edit one of your own messages (by history index) |
|
||||
| `/delete <index>` | | Delete one of your own messages (by history index) |
|
||||
|
||||
---
|
||||
|
||||
## File transfer
|
||||
|
||||
| Command | Alias | Description |
|
||||
|---|---|---|
|
||||
| `/send-file <path>` | `/sf` | Upload a file in 256 KB chunks (SHA-256 verified), send a FileRef to the conversation. Max 50 MB. MIME type auto-detected |
|
||||
| `/download <index>` | `/dl` | Download a received file by message index. Verifies SHA-256 hash on completion. Saves to current directory with collision avoidance |
|
||||
|
||||
---
|
||||
|
||||
## Privacy and disappearing messages
|
||||
|
||||
| Command | Alias | Description |
|
||||
|---|---|---|
|
||||
| `/disappear <duration>` | | Set message TTL for the active conversation. Duration format: `30m`, `1h`, `1d`, `7d`. Messages are deleted by the server after expiry |
|
||||
| `/disappear off` | | Disable disappearing messages for the conversation |
|
||||
|
||||
---
|
||||
|
||||
## Mesh networking
|
||||
|
||||
These commands require the client to be built with mesh support:
|
||||
|
||||
```bash
|
||||
cargo build -p quicproquo-client --features mesh
|
||||
```
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `/mesh peers` | Scan for nearby qpq nodes via mDNS (`_quicproquo._udp.local.`) |
|
||||
| `/mesh server <host:port>` | Note a discovered server address for connection |
|
||||
| `/mesh send <peer_id> <msg>` | Send a direct P2P message via iroh transport |
|
||||
| `/mesh broadcast <topic> <msg>` | Publish a message to a broadcast channel |
|
||||
| `/mesh subscribe <topic>` | Join a topic-based broadcast channel |
|
||||
| `/mesh route` | Display the current routing table |
|
||||
| `/mesh identity` | Show your mesh identity (Ed25519 public key, known peers) |
|
||||
| `/mesh store` | Show store-and-forward queue statistics |
|
||||
|
||||
---
|
||||
|
||||
## System
|
||||
|
||||
| Command | Alias | Description |
|
||||
|---|---|---|
|
||||
| `/help` | `/h` | Show command reference |
|
||||
| `/quit` | `/q`, `/exit` | Exit the REPL |
|
||||
|
||||
---
|
||||
|
||||
## Message types on the wire
|
||||
|
||||
Every application message uses a `[version: 1][type: 1][payload...]` binary
|
||||
format inside the MLS ciphertext:
|
||||
|
||||
| Type | Code | Wire format |
|
||||
|---|---|---|
|
||||
| Chat | `0x01` | `[msg_id: 16][body_len: 2 BE][body]` |
|
||||
| Reply | `0x02` | `[ref_msg_id: 16][body_len: 2 BE][body]` |
|
||||
| Reaction | `0x03` | `[ref_msg_id: 16][emoji_len: 1][emoji]` |
|
||||
| ReadReceipt | `0x04` | `[msg_id: 16]` |
|
||||
| Typing | `0x05` | `[active: 1]` (0 = stopped, 1 = typing) |
|
||||
| Edit | `0x06` | `[ref_msg_id: 16][body_len: 2 BE][body]` |
|
||||
| Delete | `0x07` | `[ref_msg_id: 16]` |
|
||||
| FileRef | `0x08` | `[blob_id: 32][filename_len: 2 BE][filename][file_size: 8 BE][mime_len: 2 BE][mime_type]` |
|
||||
|
||||
Read receipts are sent automatically when a Chat or Reply message is received.
|
||||
Typing indicators time out after 10 seconds of inactivity.
|
||||
94
docs/src/getting-started/rich-messaging.md
Normal file
94
docs/src/getting-started/rich-messaging.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Rich Messaging
|
||||
|
||||
quicproquo supports rich messaging features beyond basic text: reactions, read
|
||||
receipts, typing indicators, message editing, and message deletion. All
|
||||
message types are end-to-end encrypted inside MLS ciphertext — the server
|
||||
only sees opaque bytes.
|
||||
|
||||
## Reactions
|
||||
|
||||
React to any message with an emoji:
|
||||
|
||||
```
|
||||
/react 👍 # react to the latest message
|
||||
/react 🎉 3 # react to message at history index 3
|
||||
```
|
||||
|
||||
Reactions are displayed inline with the sender's name. Each reaction
|
||||
references the target message by its 16-byte message ID.
|
||||
|
||||
## Read receipts
|
||||
|
||||
Read receipts are sent **automatically** when you receive a Chat or Reply
|
||||
message. The sender sees a `✓ read` indicator in their message history.
|
||||
|
||||
Receipts only trigger on Chat and Reply messages (not on other receipts,
|
||||
typing indicators, or reactions) to prevent infinite loops.
|
||||
|
||||
## Typing indicators
|
||||
|
||||
Send a typing indicator to let others know you're composing:
|
||||
|
||||
```
|
||||
/typing
|
||||
```
|
||||
|
||||
Typing indicators timeout after 10 seconds of inactivity. Toggle display of
|
||||
others' typing with:
|
||||
|
||||
```
|
||||
/typing-notify on
|
||||
/typing-notify off
|
||||
```
|
||||
|
||||
The session tracks `typing_indicators` state per conversation.
|
||||
|
||||
## Editing messages
|
||||
|
||||
Edit one of your own messages by history index:
|
||||
|
||||
```
|
||||
/edit 5 Updated text here
|
||||
```
|
||||
|
||||
Only your own messages can be edited. The edit is sent as an `Edit` message
|
||||
type referencing the original message ID, and the local database is updated on
|
||||
receipt.
|
||||
|
||||
## Deleting messages
|
||||
|
||||
Delete one of your own messages:
|
||||
|
||||
```
|
||||
/delete 5
|
||||
```
|
||||
|
||||
Only your own messages can be deleted. A `Delete` message is broadcast to the
|
||||
group, and the message is removed from the local database on receipt.
|
||||
|
||||
## Wire format
|
||||
|
||||
All rich message types use the same binary envelope inside the MLS ciphertext:
|
||||
|
||||
```
|
||||
[version: 1 byte][type: 1 byte][payload...]
|
||||
```
|
||||
|
||||
| Type | Code | Payload |
|
||||
|---|---|---|
|
||||
| Chat | `0x01` | `[msg_id: 16][body_len: 2 BE][body]` |
|
||||
| Reply | `0x02` | `[ref_msg_id: 16][body_len: 2 BE][body]` |
|
||||
| Reaction | `0x03` | `[ref_msg_id: 16][emoji_len: 1][emoji UTF-8]` |
|
||||
| ReadReceipt | `0x04` | `[msg_id: 16]` |
|
||||
| Typing | `0x05` | `[active: 1]` (0 = stopped, 1 = typing) |
|
||||
| Edit | `0x06` | `[ref_msg_id: 16][body_len: 2 BE][body]` |
|
||||
| Delete | `0x07` | `[ref_msg_id: 16]` |
|
||||
|
||||
## Implementation
|
||||
|
||||
- **Core serialisation:** `crates/quicproquo-core/src/app_message.rs` — the
|
||||
`AppMessage` enum with `serialize()` / `deserialize()` methods
|
||||
- **REPL commands:** `crates/quicproquo-client/src/client/repl.rs` — slash
|
||||
command handlers
|
||||
- **Display:** `crates/quicproquo-client/src/client/display.rs` — typing
|
||||
indicator rendering
|
||||
176
docs/src/getting-started/typescript-sdk.md
Normal file
176
docs/src/getting-started/typescript-sdk.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# TypeScript SDK and Browser Demo
|
||||
|
||||
The TypeScript SDK (`sdks/typescript/`) provides `@quicproquo/client` — a
|
||||
browser-ready client with WASM-powered crypto operations and WebSocket
|
||||
transport.
|
||||
|
||||
## What you get
|
||||
|
||||
- **WASM crypto bundle** (175 KB) — Ed25519 signatures, X25519 + ML-KEM-768
|
||||
hybrid encryption, safety numbers, sealed sender, and message padding,
|
||||
compiled from the Rust `quicproquo-core` crate
|
||||
- **`QpqClient` class** — high-level API for server connectivity and crypto
|
||||
- **Offline mode** — all crypto operations work without a server connection
|
||||
- **Browser demo** — interactive HTML page for trying every crypto operation
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+ and npm
|
||||
- Rust toolchain + `wasm-pack` (for building the WASM bundle)
|
||||
|
||||
Install wasm-pack if you don't have it:
|
||||
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### 1. Build the WASM crypto bundle
|
||||
|
||||
```bash
|
||||
cd sdks/typescript/wasm-crypto
|
||||
wasm-pack build --target web --out-dir ../pkg
|
||||
```
|
||||
|
||||
This compiles `quicproquo-core`'s crypto modules to WebAssembly and produces
|
||||
JavaScript + TypeScript bindings in `sdks/typescript/pkg/`.
|
||||
|
||||
### 2. Build the TypeScript SDK
|
||||
|
||||
```bash
|
||||
cd sdks/typescript
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Running the browser demo
|
||||
|
||||
The demo is a vanilla HTML page — no build step required beyond the WASM
|
||||
bundle.
|
||||
|
||||
```bash
|
||||
cd sdks/typescript
|
||||
python3 -m http.server 8080
|
||||
# Open http://localhost:8080/demo/
|
||||
```
|
||||
|
||||
### Demo walkthrough
|
||||
|
||||
1. **Initialize WASM** — click the button to load the 175 KB module
|
||||
2. **Generate Alice and Bob** — creates Ed25519 identity keypairs, displays
|
||||
seed and public key in hex
|
||||
3. **Compute Safety Number** — derives a Signal-style 60-digit verification
|
||||
code from both public keys
|
||||
4. **Sign and Verify** — type a message, sign it with Alice's key, verify
|
||||
with Alice's public key
|
||||
5. **Hybrid Encrypt/Decrypt** — generate an X25519 + ML-KEM-768 keypair,
|
||||
encrypt and decrypt with post-quantum protection
|
||||
6. **Sealed Sender** — create an anonymous envelope wrapping a payload with
|
||||
Alice's identity
|
||||
7. **Message Padding** — pad a message to a constant-size bucket (256, 1024,
|
||||
4096, or 16384 bytes) and unpad it
|
||||
|
||||
The crypto operations work entirely offline — no server connection needed.
|
||||
|
||||
### Server connectivity
|
||||
|
||||
The Chat section of the demo connects via WebSocket. Since the native qpq
|
||||
server speaks Cap'n Proto RPC over QUIC/TCP + Noise_XX, a WebSocket bridge
|
||||
proxy is required for browser connectivity. The demo sends JSON-framed
|
||||
requests over WebSocket.
|
||||
|
||||
## Using the SDK in your code
|
||||
|
||||
### Offline crypto (no server)
|
||||
|
||||
```typescript
|
||||
import { QpqClient } from "@quicproquo/client";
|
||||
|
||||
const client = await QpqClient.offline();
|
||||
|
||||
// Generate identities
|
||||
const alice = client.generateIdentity();
|
||||
const bob = client.generateIdentity();
|
||||
|
||||
// Safety number verification
|
||||
const safetyNumber = client.computeSafetyNumber(
|
||||
alice.publicKey,
|
||||
bob.publicKey,
|
||||
);
|
||||
console.log("Safety number:", safetyNumber);
|
||||
|
||||
// Sign and verify
|
||||
const msg = new TextEncoder().encode("hello");
|
||||
const sig = client.sign(alice.seed, msg);
|
||||
console.log("Valid:", client.verify(alice.publicKey, msg, sig));
|
||||
|
||||
// Hybrid encryption (post-quantum)
|
||||
const ciphertext = client.hybridEncrypt(bob.hybridKey, msg);
|
||||
const plaintext = client.hybridDecrypt(bob.seed, ciphertext);
|
||||
```
|
||||
|
||||
### Server connection
|
||||
|
||||
```typescript
|
||||
const client = await QpqClient.connect({ addr: "wss://bridge.example.com" });
|
||||
|
||||
// Health check
|
||||
const status = await client.health();
|
||||
|
||||
// Resolve a user
|
||||
const peerKey = await client.resolveUser("bob");
|
||||
|
||||
// Create a DM channel
|
||||
const channel = await client.createChannel(peerKey);
|
||||
|
||||
// Send messages
|
||||
await client.send(peerKey, new TextEncoder().encode("Hello!"));
|
||||
await client.sendWithTTL(peerKey, payload, 3600); // 1h TTL
|
||||
|
||||
// Receive
|
||||
const messages = await client.receive(peerKey);
|
||||
|
||||
// Account deletion
|
||||
await client.deleteAccount();
|
||||
```
|
||||
|
||||
## WASM crypto functions
|
||||
|
||||
The WASM bundle exposes 13 functions via `wasm_bindgen`:
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `generate_identity()` | Generate Ed25519 seed (32 bytes) |
|
||||
| `identity_public_key(seed)` | Derive public key from seed |
|
||||
| `sign(seed, message)` | Ed25519 signature (64 bytes) |
|
||||
| `verify(pubkey, message, sig)` | Verify signature (returns boolean) |
|
||||
| `compute_safety_number(keyA, keyB)` | 60-digit verification code |
|
||||
| `hybrid_generate_keypair()` | X25519 + ML-KEM-768 keypair |
|
||||
| `hybrid_public_key(keypair)` | Extract public key from keypair |
|
||||
| `hybrid_encrypt(pubkey, plaintext)` | Hybrid encrypt |
|
||||
| `hybrid_decrypt(keypair, ciphertext)` | Hybrid decrypt |
|
||||
| `seal(seed, payload)` | Sealed sender envelope |
|
||||
| `unseal(envelope)` | Unseal (returns sender_key + payload) |
|
||||
| `pad_message(msg)` | Pad to bucket size (256/1K/4K/16K) |
|
||||
| `unpad_message(padded)` | Remove padding |
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
sdks/typescript/
|
||||
├── src/
|
||||
│ ├── index.ts # Public API exports
|
||||
│ ├── client.ts # QpqClient class (258 lines)
|
||||
│ ├── transport.ts # WebSocket transport with reconnection (206 lines)
|
||||
│ ├── crypto.ts # WASM crypto wrapper (135 lines)
|
||||
│ └── types.ts # TypeScript type definitions
|
||||
├── demo/
|
||||
│ └── index.html # Interactive browser demo (476 lines)
|
||||
├── wasm-crypto/
|
||||
│ ├── Cargo.toml # Rust WASM crate (wasm-bindgen)
|
||||
│ └── src/lib.rs # 13 wasm_bindgen functions
|
||||
├── pkg/ # WASM output (built by wasm-pack)
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
@@ -60,11 +60,19 @@ This means the WASM build works in browser environments out of the box.
|
||||
For non-browser WASM runtimes (WASI, etc.), you may need to adjust the
|
||||
`getrandom` backend.
|
||||
|
||||
## Future plans
|
||||
## wasm-bindgen and the TypeScript SDK
|
||||
|
||||
- **wasm-bindgen JS bindings**: Wrap the WASM-compatible modules with
|
||||
`#[wasm_bindgen]` annotations to provide a native JavaScript/TypeScript API.
|
||||
This would allow web frontends to perform client-side encryption without a
|
||||
server round-trip.
|
||||
- **wasm-pack integration**: Publish the WASM module as an npm package for
|
||||
easy consumption in web projects.
|
||||
The `wasm-bindgen` JS bindings are now implemented in
|
||||
`sdks/typescript/wasm-crypto/`. This provides 13 JavaScript-callable functions
|
||||
wrapping the WASM-compatible crypto modules:
|
||||
|
||||
```bash
|
||||
cd sdks/typescript/wasm-crypto
|
||||
wasm-pack build --target web --out-dir ../pkg
|
||||
```
|
||||
|
||||
The resulting 175 KB WASM bundle is used by the `@quicproquo/client`
|
||||
TypeScript SDK and the interactive browser demo.
|
||||
|
||||
See the [TypeScript SDK and Browser Demo](typescript-sdk.md) guide for
|
||||
full details on building and running the browser demo.
|
||||
|
||||
Reference in New Issue
Block a user