{"id":44942592,"url":"https://github.com/chad/freeq","last_synced_at":"2026-02-18T09:05:30.598Z","repository":{"id":337687166,"uuid":"1154752062","full_name":"chad/freeq","owner":"chad","description":"What if we could go back to using irc?","archived":false,"fork":false,"pushed_at":"2026-02-13T03:40:00.000Z","size":601,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-13T12:03:29.988Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chad.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-10T18:31:31.000Z","updated_at":"2026-02-13T03:42:00.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/chad/freeq","commit_stats":null,"previous_names":["chad/irc-reboot","chad/freeq"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/chad/freeq","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chad%2Ffreeq","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chad%2Ffreeq/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chad%2Ffreeq/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chad%2Ffreeq/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chad","download_url":"https://codeload.github.com/chad/freeq/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chad%2Ffreeq/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29574066,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-18T08:38:15.585Z","status":"ssl_error","status_checked_at":"2026-02-18T08:38:14.917Z","response_time":162,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-18T09:05:29.730Z","updated_at":"2026-02-18T09:05:30.586Z","avatar_url":"https://github.com/chad.png","language":"Rust","funding_links":[],"categories":["Rust"],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"freeq.png\" alt=\"freeq logo\" width=\"200\"\u003e\n\u003c/p\u003e\n\n# freeq\n\nIRC server and client with AT Protocol (Bluesky) identity authentication,\nend-to-end encrypted channels, iroh QUIC transport, peer-to-peer DMs,\nand federated server-to-server clustering.\n\nUsers authenticate with their Bluesky identity via a custom SASL mechanism\n(`ATPROTO-CHALLENGE`). Standard IRC clients connect as guests. Authenticated\nusers get their DID bound to their connection — visible via WHOIS, enforced\nfor nick ownership, and usable for DID-based bans, invites, and persistent ops.\n\n## Architecture\n\n```\nfreeq-server/     IRC server with SASL, WebSocket, iroh, S2S federation\nfreeq-sdk/     Reusable client SDK (connect, auth, events, E2EE, P2P)\nfreeq-tui/     Terminal UI client built on the SDK\n```\n\nThe SDK exposes a `(ClientHandle, Receiver\u003cEvent\u003e)` pattern — any UI or bot\ncan consume events and send commands.\n\n### Transport Stack\n\n```\n┌──────────────────────────────────────────┐\n│            IRC Wire Protocol             │\n├──────────┬──────────┬──────────┬─────────┤\n│   TCP    │   TLS    │WebSocket │  iroh   │\n│  :6667   │  :6697   │  :8080   │  QUIC   │\n└──────────┴──────────┴──────────┴─────────┘\n```\n\nAll transports feed into the same `handle_generic()` handler — the IRC\nprotocol is transport-agnostic. Each transport is zero-cost when not enabled.\n\n## Quick Start\n\n### Build\n\n```sh\ncargo build --release\n```\n\n### Run the Server\n\n```sh\n# Minimal: plain TCP only, in-memory\ncargo run --release --bin freeq-server\n\n# With persistence\ncargo run --release --bin freeq-server -- --db-path data/irc.db\n\n# With TLS\ncargo run --release --bin freeq-server -- \\\n  --tls-cert certs/cert.pem --tls-key certs/key.pem\n\n# With WebSocket + REST API\ncargo run --release --bin freeq-server -- --web-addr 0.0.0.0:8080\n\n# With iroh transport (QUIC, NAT-traversing)\ncargo run --release --bin freeq-server -- --iroh\n\n# Full production setup\ncargo run --release --bin freeq-server -- \\\n  --listen-addr 0.0.0.0:6667 \\\n  --tls-listen-addr 0.0.0.0:6697 \\\n  --tls-cert /etc/letsencrypt/live/example.com/fullchain.pem \\\n  --tls-key /etc/letsencrypt/live/example.com/privkey.pem \\\n  --db-path ./irc.db \\\n  --web-addr 0.0.0.0:8080 \\\n  --iroh\n```\n\nGenerate a self-signed cert for local development:\n\n```sh\nmkdir -p certs\nopenssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \\\n  -keyout certs/key.pem -out certs/cert.pem -days 365 -nodes \\\n  -subj \"/CN=localhost\" \\\n  -addext \"subjectAltName=DNS:localhost,IP:127.0.0.1\"\n```\n\n### Connect with the TUI Client\n\n```sh\n# Guest (no auth)\ncargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick\n\n# Bluesky OAuth (opens browser)\ncargo run --release --bin freeq-tui -- 127.0.0.1:6697 mynick \\\n  --handle alice.bsky.social\n\n# App password fallback\ncargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick \\\n  --handle alice.bsky.social --app-password xxxx-xxxx-xxxx-xxxx\n\n# Auto-join channels\ncargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick \\\n  -c '#general,#random'\n\n# Explicit iroh transport\ncargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick \\\n  --iroh-addr \u003cendpoint-id\u003e\n\n# Vi keybindings\ncargo run --release --bin freeq-tui -- 127.0.0.1:6667 mynick --vi\n```\n\n**Iroh auto-discovery**: When connecting to a server that has `--iroh`\nenabled, the TUI probes `CAP LS` for the `iroh=\u003cendpoint-id\u003e` capability\nand auto-upgrades to iroh QUIC transport. No manual endpoint ID needed.\n\nOAuth sessions are cached to `~/.config/freeq-tui/\u003chandle\u003e.session.json`\nso you don't need to re-authenticate on every launch.\n\n### Connect with a Standard IRC Client\n\nAny IRC client works as a guest — irssi, WeeChat, HexChat, LimeChat, etc.\nConnect to `127.0.0.1:6667` (plain) or `127.0.0.1:6697` (TLS). No special\nconfiguration needed.\n\n### Connect via WebSocket\n\nWhen `--web-addr` is set, the server accepts WebSocket connections at\n`ws://\u003caddr\u003e/irc`. A test HTML client is included at `freeq-server/test-client.html`.\n\n## Authentication\n\n### SASL ATPROTO-CHALLENGE\n\nThe server implements a custom SASL mechanism for AT Protocol identity:\n\n1. Client requests `CAP sasl`, then `AUTHENTICATE ATPROTO-CHALLENGE`\n2. Server sends a challenge: `base64url(json { session_id, nonce, timestamp })`\n3. Client responds with one of:\n   - **Crypto signature** (`method: \"crypto\"`): Signs challenge bytes with a\n     private key listed in the DID document\n   - **PDS session** (`method: \"pds-session\"`): Sends an app-password JWT;\n     server verifies against the PDS\n   - **PDS OAuth** (`method: \"pds-oauth\"`): Sends a DPoP-bound access token\n     with proof; server verifies against the PDS\n4. Server verifies, emits `903` (success) or `904` (failure)\n5. Client sends `CAP END`, registration completes\n\n### Security Properties\n\n- Each challenge contains a cryptographically random nonce\n- Challenges are invalidated after use (no replay)\n- Challenge validity window: configurable, default 60 seconds\n- Private keys never leave the client\n- PDS URL is verified against the DID document before accepting session tokens\n- Supported key types: secp256k1 (MUST), ed25519 (SHOULD)\n\n### What Authentication Gets You\n\n- Nick is bound to your DID — no one else can use it\n- WHOIS shows your DID and Bluesky handle\n- You can be banned or invited by DID (survives reconnect/nick changes)\n- Persistent channel ops tied to your DID (survive reconnects and work across federated servers)\n- Your identity is cryptographically verifiable\n\n## Transports\n\n### TCP / TLS (Standard)\n\nStandard IRC on port 6667 (plain) and 6697 (TLS). TLS auto-detected by port\nin the client. Always available.\n\n### WebSocket\n\nEnabled with `--web-addr`. Accepts WebSocket IRC at `/irc`. Uses the same\nIRC wire protocol — WebSocket is a transport, not a new protocol. Includes\na read-only REST API at `/api/v1/` (channels, members, topics, messages).\n\n### iroh (QUIC)\n\nEnabled with `--iroh`. Provides NAT-traversing encrypted QUIC connections\nvia [iroh](https://iroh.computer). The server generates a persistent secret\nkey (`iroh-key.secret`) on first run — endpoint ID is stable across restarts.\n\nThe server advertises its iroh endpoint ID in `CAP LS`:\n```\nCAP * LS :sasl message-tags iroh=44f1415c9db30989...\n```\n\nClients auto-discover and upgrade to iroh when available.\n\n## End-to-End Encryption (E2EE)\n\nClient-side channel encryption using AES-256-GCM with HKDF-SHA256 key\nderivation from a shared passphrase. The server relays ciphertext unchanged.\n\n```\n/encrypt \u003cpassphrase\u003e    Enable E2EE for current channel\n/decrypt                 Disable E2EE for current channel\n```\n\nWire format: `ENC1:\u003cnonce-b64\u003e:\u003cciphertext-b64\u003e` — version-tagged, uses the\nmessage body for robustness. All channel members must use the same passphrase.\n\n## Peer-to-Peer Encrypted DMs\n\nDirect encrypted messaging between clients via iroh QUIC, bypassing the\nserver entirely.\n\n```\n/p2p start               Start your P2P endpoint\n/p2p id                  Show your P2P endpoint ID\n/p2p connect \u003cid\u003e        Connect to a peer\n/p2p msg \u003cid\u003e \u003cmessage\u003e  Send a direct message\n```\n\nP2P conversations appear in dedicated `p2p:\u003cshort-id\u003e` buffers. Wire format\nis newline-delimited JSON (not IRC protocol). ALPN: `freeq/p2p-dm/1`.\n\nP2P endpoint IDs are visible in WHOIS (numeric `672`).\n\n## Server-to-Server Federation (S2S)\n\nServers cluster over iroh QUIC connections. Each server maintains its own\nlocal state and syncs channel membership, messages, topics, and DID-based\nops across the federation.\n\n### Setup\n\n```sh\n# Server A: just enable iroh (accepts incoming S2S connections)\ncargo run --release --bin freeq-server -- --iroh\n\n# Server B: enable iroh + connect to Server A\ncargo run --release --bin freeq-server -- --iroh \\\n  --s2s-peers \u003cserver-a-endpoint-id\u003e\n```\n\nServer A doesn't need `--s2s-peers` — it accepts incoming S2S connections\nautomatically when `--iroh` is enabled.\n\n### What Syncs\n\n| Feature | Sync behavior |\n|---------|---------------|\n| JOIN/PART/QUIT | Membership tracked per origin server |\n| PRIVMSG | Channel messages relayed to all peers |\n| TOPIC | Topic changes propagate |\n| DID-based ops | Persistent ops sync via CRDT |\n| Founder | First-write-wins CRDT resolution |\n| NAMES | Includes both local and remote members |\n| WHOIS | Shows DID, handle, and origin for remote users |\n\n### CRDT-Based State Convergence\n\nChannel authority (founder, DID-based ops) uses Automerge CRDTs for\nconflict-free convergence. **Presence is NOT in the CRDT** — it's S2S\nevent-driven to avoid ghost users when servers crash.\n\n- **Founder resolution**: Deterministic min-actor-wins — concurrent claims\n  converge deterministically, late entrants cannot overwrite after sync\n- **DID ops**: Union merge — grants propagate, revocations propagate\n- **Provenance tracking**: All CRDT writes carry origin peer + authorizing DID\n- **Authority boundaries**: Soft enforcement validates who can write each key-space\n- **Event dedup**: S2S events carry unique IDs; bounded LRU prevents replay\n- **Peer identity**: CRDT sync keyed by iroh endpoint ID (cryptographic), not\n  server name (untrusted). Hello handshake binds transport to logical identity.\n- **Compaction**: Periodic snapshot + reload bounds doc growth in long-lived deployments\n- **Async-safe**: CRDT uses `tokio::sync::Mutex` — no runtime thread blocking\n- No timestamps in authority decisions (spoofable by rogue servers)\n\n### S2S Acceptance Tests\n\n```sh\n# Run against two live servers\nLOCAL_SERVER=localhost:6667 REMOTE_SERVER=irc.freeq.at:6667 \\\n  cargo test -p freeq-server --test s2s_acceptance -- --nocapture --test-threads=1\n```\n\n9 tests verify: connectivity, bidirectional message relay, NAMES sync,\ntopic sync, PART/QUIT cleanup, and late-joiner state.\n\n## IRC Features\n\n### Standard IRC\n\nFull compatibility with RFC 1459/2812 basics:\n\n- NICK, USER, JOIN, PART, PRIVMSG, NOTICE, QUIT\n- NAMES (query channel membership on demand)\n- PING/PONG (client and server keepalive)\n- WHOIS (shows DID, handle, iroh ID for authenticated users)\n- CTCP ACTION (`/me`)\n- Multiple channels, private messages\n\n### Channel Modes\n\n| Mode | Description |\n|------|-------------|\n| `+o nick` | Channel operator |\n| `+v nick` | Voice |\n| `+b mask` | Ban (hostmask `*!*@host` or DID `did:plc:xyz`) |\n| `+i` | Invite-only |\n| `+t` | Topic lock (ops only) |\n| `+k key` | Channel key (password) |\n\n### DID-Aware Features\n\n- **DID bans** (`MODE #chan +b did:plc:xyz`): Bans by identity, not just\n  hostmask. Survives nick changes and reconnects.\n- **DID invites** (`INVITE nick #chan`): If the user is authenticated, the\n  invite is stored by DID and survives reconnect.\n- **Nick ownership**: Once an authenticated user claims a nick, guests and\n  other DIDs cannot use it. If an unauthenticated user tries to take a\n  registered nick during SASL negotiation, they're renamed to `GuestXXXX`\n  at registration time.\n- **Persistent DID-based ops**: When an authenticated user is opped, their DID\n  is recorded. They're auto-opped on rejoin — even on a different server in\n  the federation. Channel founders (first authenticated user to create a channel)\n  can never be de-opped.\n\n### Message History\n\nThe server stores the last 100 messages per channel. When you join, recent\nhistory is replayed as standard PRIVMSG — works with any IRC client, no\nspecial protocol extension needed.\n\n### Rich Media (IRCv3 Message Tags)\n\nRich media is supported through IRCv3 message tags, giving **multipart/alternative\nsemantics** — the same content in two representations:\n\n- **Tags**: Structured metadata (content-type, URL, dimensions, alt text)\n- **Body**: Plain text fallback (description + URL)\n\n```\n@content-type=image/jpeg;media-url=https://cdn.bsky.app/img/...;media-alt=Sunset;media-w=1200;media-h=800 :alice!a@host PRIVMSG #photos :Sunset https://cdn.bsky.app/img/...\n```\n\n| Client | What they see |\n|--------|--------------|\n| irssi, WeeChat | `Sunset https://cdn.bsky.app/img/...` (clickable link) |\n| freeq-tui | `🖼 [image/jpeg] Sunset 1200×800 https://cdn.bsky.app/img/...` |\n\nMedia is hosted externally (AT Protocol PDS blob storage). The IRC server\nnever handles media bytes — it just relays tagged messages.\n\n**Supported tag keys:**\n\n| Tag | Description |\n|-----|-------------|\n| `content-type` | MIME type (e.g. `image/jpeg`, `video/mp4`) |\n| `media-url` | URL where the media can be fetched |\n| `media-alt` | Alt text / description |\n| `media-w` | Width in pixels |\n| `media-h` | Height in pixels |\n| `media-blurhash` | Blurhash placeholder |\n| `media-size` | File size in bytes |\n| `media-filename` | Original filename |\n\n### Rate Limiting\n\nToken bucket rate limiter (10 commands/second) kicks in after registration.\nThe initial connection burst is not rate-limited, so clients that send many\ncommands on connect (like LimeChat) work correctly.\n\n## TUI Client\n\n### Status Bar\n\nThe status bar shows:\n- **Transport badge**: Colored indicator (red=TCP, green=TLS, cyan=WS, magenta=Iroh)\n- **Nick**: Your current nick\n- **Auth**: Authenticated DID or \"guest\"\n- **Uptime**: Connection duration\n\n### Keybindings\n\n**Emacs mode** (default):\n\n| Key | Action |\n|-----|--------|\n| Ctrl-A / Home | Beginning of line |\n| Ctrl-E / End | End of line |\n| Ctrl-F / Right | Forward char |\n| Ctrl-B / Left | Back char |\n| Alt-F | Forward word |\n| Alt-B | Back word |\n| Ctrl-D | Delete char |\n| Ctrl-H / Backspace | Delete back |\n| Ctrl-K | Kill to end of line |\n| Ctrl-U | Kill to beginning |\n| Ctrl-W | Kill word back |\n| Alt-D | Kill word forward |\n| Ctrl-Y | Yank (paste kill ring) |\n| Ctrl-T | Transpose chars |\n| Alt-U | Uppercase word |\n| Alt-L | Lowercase word |\n| Alt-C | Capitalize word |\n| Tab | Nick completion |\n| Up / Down | Input history |\n| Ctrl-N / Alt-N | Next buffer |\n| Ctrl-P / Alt-P | Previous buffer |\n| BackTab (Shift-Tab) | Previous buffer |\n| PageUp / PageDown | Scroll messages |\n| Ctrl-C / Ctrl-Q | Quit |\n\n**Vi mode** (`--vi`):\n\nNormal mode: `h/l` move, `w/b/e` word motion, `0/$` line edges,\n`i/a/I/A` enter insert, `x/X/D/C/S/s` delete/change, `p/P` paste,\n`k/j` history, `dd` clear line. Insert mode: standard typing, Esc to\nexit to normal mode.\n\n### Commands\n\n```\n/join #channel          Join a channel\n/part [#channel]        Leave current or named channel\n/msg nick message       Private message\n/me action              CTCP ACTION\n/topic [text]           View or set channel topic\n/mode +o/-o nick        Op/deop\n/mode +v/-v nick        Voice/devoice\n/mode +b [mask]         Ban (or list bans)\n/mode +i/-i             Invite-only\n/mode +t/-t             Topic lock\n/mode +k/-k [key]       Channel key\n/op nick                Shortcut for /mode +o\n/deop nick              Shortcut for /mode -o\n/voice nick             Shortcut for /mode +v\n/kick nick [reason]     Kick from channel\n/ban mask               Ban user\n/unban mask             Remove ban\n/invite nick            Invite to current channel\n/whois nick             Query user info\n/names [#channel]       List channel members\n/raw \u003cline\u003e             Send raw IRC line\n/encrypt \u003cpassphrase\u003e   Enable E2EE for current channel\n/decrypt                Disable E2EE for current channel\n/p2p start              Start P2P endpoint\n/p2p id                 Show your P2P endpoint ID\n/p2p connect \u003cid\u003e       Connect to a peer\n/p2p msg \u003cid\u003e \u003ctext\u003e    Send P2P direct message\n/net                    Show/hide network info popup\n/debug                  Toggle raw IRC line display\n/quit [message]         Disconnect\n/help                   Show commands\n```\n\n### Network Info Popup (`/net`)\n\nShows: transport type, server address, connection state, uptime, nick,\nauthenticated DID, iroh endpoint ID, E2EE channels, P2P DM status.\nClose with Esc or `q`.\n\n### Debug Mode (`/debug`)\n\nToggles raw IRC line display in the status buffer (prefixed with `←`).\nUseful for diagnosing protocol issues.\n\n## REST API\n\nWhen `--web-addr` is set, a read-only REST API is available:\n\n| Endpoint | Description |\n|----------|-------------|\n| `GET /api/v1/channels` | List all channels |\n| `GET /api/v1/channels/{name}` | Channel info (topic, modes, member count) |\n| `GET /api/v1/channels/{name}/members` | Channel member list |\n| `GET /api/v1/channels/{name}/topic` | Channel topic |\n| `GET /api/v1/channels/{name}/messages` | Recent messages (with pagination) |\n| `GET /api/v1/stats` | Server stats |\n\nAll writes go through IRC — the REST API is strictly read-only.\n\n## Server Configuration\n\n```\nfreeq-server [OPTIONS]\n\nOptions:\n  --listen-addr \u003cADDR\u003e            Plain TCP address [default: 127.0.0.1:6667]\n  --tls-listen-addr \u003cADDR\u003e        TLS address [default: 127.0.0.1:6697]\n  --tls-cert \u003cPATH\u003e               TLS certificate PEM file\n  --tls-key \u003cPATH\u003e                TLS private key PEM file\n  --server-name \u003cNAME\u003e            Server name [default: freeq]\n  --challenge-timeout-secs \u003cN\u003e    SASL challenge validity [default: 60]\n  --db-path \u003cPATH\u003e                SQLite database path (omit for in-memory)\n  --web-addr \u003cADDR\u003e               HTTP/WebSocket listener address\n  --iroh                          Enable iroh QUIC transport\n  --iroh-port \u003cPORT\u003e              UDP port for iroh (default: random)\n  --s2s-peers \u003cID,ID,...\u003e         S2S peer iroh endpoint IDs\n```\n\n### Persistence\n\nWhen `--db-path` is set, the server persists:\n\n- **Message history** — all channel messages, queryable with pagination\n- **Channel state** — topics, modes (+t, +i, +k), channel keys\n- **Bans** — hostmask and DID bans survive restarts\n- **DID-nick bindings** — nick ownership persists across server restarts\n\nWithout `--db-path`, the server runs entirely in-memory.\nThe database uses SQLite with WAL mode for good concurrent read performance.\nPersistence failures are logged but do not crash the server.\n\n## Tests\n\n```sh\n# Unit + integration tests\ncargo test\n\n# S2S federation acceptance tests (9 tests, requires two live servers)\nLOCAL_SERVER=localhost:6667 REMOTE_SERVER=irc.freeq.at:6667 \\\n  cargo test -p freeq-server --test s2s_acceptance -- --nocapture --test-threads=1\n```\n\n**153 tests** covering:\n\n- **SDK (44)**: IRC parsing (with tag support), tag escaping roundtrip, DID\n  document parsing, key generation/signing/verification, multibase/multicodec,\n  challenge response encoding, SASL signer variants, media attachment roundtrip,\n  link preview roundtrip, media type detection\n- **Server unit (33 + 12 CRDT)**: Message parsing (with tags), tag escaping, SASL challenge\n  store (create, take, replay, expiry, forged nonce), channel state, database\n  roundtrips (channels, bans, messages, identities), CRDT tests (founder\n  deterministic min-actor, founder not overwritten after sync, DID ops sync,\n  topic provenance, authority validation, compaction, metrics, ban provenance)\n- **Integration (27)**: Guest connection, secp256k1 auth, ed25519 auth, wrong key\n  rejection, unknown DID rejection, expired challenge rejection, replayed nonce\n  rejection, channel messaging, mixed auth/guest, nick collision, channel topic,\n  topic lock, channel ops/kick, hostmask bans, DID bans, invite-only, message\n  history replay, nick ownership, quit broadcast, channel key (+k), TLS\n  connection, rich media tag passthrough, persistence (messages, topics, bans,\n  nick ownership survive restart)\n- **S2S acceptance (9)**: Connectivity, bidirectional message relay, NAMES sync,\n  topic sync, PART/QUIT cleanup, late-joiner state\n\n## Protocol Notes\n\n### Deviations from the Spec\n\n- Challenge uses JSON encoding (not a binary format) for debuggability\n- PDS session verification is an additional auth method beyond the spec's\n  crypto-only approach — it enables OAuth login without requiring users to\n  manage raw signing keys\n- History replay uses standard PRIVMSG (no custom extension or batch)\n\n### IRCv3 Compatibility\n\n- CAP negotiation follows IRCv3 `CAP LS 302` / `CAP REQ` / `CAP END`\n- SASL flow follows IRCv3 SASL specification with a custom mechanism name\n- `message-tags` capability follows the IRCv3 message tags specification\n- Media tags use vendor-prefixed names (`content-type`, `media-url`, etc.)\n- Server advertises `iroh=\u003cendpoint-id\u003e` in `CAP LS` for transport discovery\n- `ATPROTO-CHALLENGE` could be proposed as an IRCv3 WG mechanism\n\n## Plugins\n\nFreeq supports a plugin system for custom server behavior. Plugins hook into\nevents like authentication, message delivery, and channel joins.\n\n```sh\n# Load a plugin via CLI\nfreeq-server --plugin \"identity-override:handle=timesync.bsky.social,display_id=3|337\"\n\n# Load plugins from a directory of TOML configs\nfreeq-server --plugin-dir ./examples/plugins/\n```\n\nSee `examples/plugins/` for example configurations and `docs/PROTOCOL.md`\nfor the full plugin hook reference.\n\n## Documentation\n\n- [Features](docs/Features.md) — Complete feature catalog\n- [Protocol Notes](docs/PROTOCOL.md) — SASL mechanism, DID extensions, transport details\n- [Known Limitations](docs/KNOWN-LIMITATIONS.md) — Explicit list of gaps\n- [Architecture Decisions](docs/architecture-decisions.md) — Design rationale\n- [S2S Audit](docs/s2s-audit.md) — Federation protocol analysis\n- [CRDT Federation Audit](docs/crdt-federation-audit.md) — CRDT convergence issues \u0026 fix plan\n- [Future Direction](docs/FutureDirection.md) — Roadmap\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchad%2Ffreeq","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchad%2Ffreeq","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchad%2Ffreeq/lists"}