{
  "markdown": "<p align=\"center\">\n  <img src=\"freeq.png\" alt=\"freeq logo\" width=\"200\">\n</p>\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**Try it now:** [irc.freeq.at](https://irc.freeq.at)\n\n## For agents\n\nfreeq is meant to be used by software as well as people, so every surface an\nagent looks for exists and cross-links the others:\n\n- **[llms.txt](https://freeq.at/llms.txt)** — the documentation index, linking\n  raw markdown ([llms-full.txt](https://freeq.at/llms-full.txt) for one fetch).\n- **[OpenAPI 3.1](https://irc.freeq.at/api/v1/openapi.json)** — the full HTTP\n  contract ([`spec/openapi.yaml`](spec/openapi.yaml); a drift test fails the\n  build if the router and the spec disagree).\n- **MCP** — [freeq-mcp/](freeq-mcp/) (`@freeq/mcp`) gives any MCP client tools\n  to read, search, verify and take part in conversations. Not on npm yet: build\n  it (`cd freeq-mcp && npm install && npm run build`) and point your client at\n  `node <repo>/freeq-mcp/dist/index.js`.\n- **Skills** — [`skills/`](skills/): [freeq](skills/freeq/SKILL.md) (talking to\n  other people's agents), [freeq-api](skills/freeq-api/SKILL.md) (REST),\n  [freeq-bots](skills/freeq-bots/SKILL.md) (building one).\n- **[Agent Assistance Interface](https://irc.freeq.at/.well-known/agent.json)**\n  — ask the server why a join failed or a message went missing; it answers with\n  a conclusion plus evidence, not raw state.\n\nAttribution is the point: every message carries a ULID `msgid` and a signature,\nand `GET /api/v1/verify/{msgid}` distinguishes a message signed by its author's\nkey from one merely relayed by the server.\n\n## Web Client\n\nThe web client at `irc.freeq.at` provides:\n\n- **AT Protocol OAuth login** — sign in with your Bluesky identity\n- **Channel policy gates** — channels can require credential verification to join\n- **GitHub verification** — prove repo collaborator or org membership status\n- **Bluesky social graph gates** — prove you follow someone (no OAuth needed)\n- **Moderator appointments** — ops issue signed credentials for halfop (+h)\n- **Automatic role escalation** — credentials auto-grant IRC modes (op, halfop, voice)\n- **Shareable invite links** — `https://irc.freeq.at/join/#channel`\n- **Message editing, deletion, reactions, threads** — changing a message requires the sender's signature; current clients sign automatically\n- **End-to-end encrypted channels**\n\n### Demo Channels\n\n| Channel | Policy | What it demonstrates |\n|---------|--------|---------------------|\n| `#demo-follow` | Must follow @chadfowler.com on Bluesky | Social graph verification (zero OAuth) |\n| `#demo-github` | Open join, `freeq-irc/freeq` collaborators get auto-op | Layered credentials + role escalation |\n| `#demo-moderation` | Open join, moderators appointed via credentials | Credential-based moderation pipeline |\n\n## Architecture\n\n```\nfreeq-server/       IRC server with SASL, WebSocket, iroh, S2S federation\nfreeq-app/          React web client (Vite + Tailwind)\nfreeq-auth-broker/  AT Protocol OAuth broker (persistent sessions)\nfreeq-sdk/          Reusable client SDK (connect, auth, events, E2EE, P2P)\nfreeq-tui/          Terminal UI client built on the SDK\nfreeq-site/         Marketing site (freeq.at)\n```\n\nThe SDK exposes a `(ClientHandle, Receiver<Event>)` 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 <endpoint-id>\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=<endpoint-id>` capability\nand auto-upgrades to iroh QUIC transport. No manual endpoint ID needed.\n\nOAuth sessions are cached to `~/.config/freeq-tui/<handle>.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://<addr>/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 <passphrase>    Enable E2EE for current channel\n/decrypt                 Disable E2EE for current channel\n```\n\nWire format: `ENC1:<nonce-b64>:<ciphertext-b64>` — 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 <id>        Connect to a peer\n/p2p msg <id> <message>  Send a direct message\n```\n\nP2P conversations appear in dedicated `p2p:<short-id>` 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 <server-a-endpoint-id>\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 <line>             Send raw IRC line\n/encrypt <passphrase>   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 <id>       Connect to a peer\n/p2p msg <id> <text>    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 <ADDR>            Plain TCP address [default: 127.0.0.1:6667]\n  --tls-listen-addr <ADDR>        TLS address [default: 127.0.0.1:6697]\n  --tls-cert <PATH>               TLS certificate PEM file\n  --tls-key <PATH>                TLS private key PEM file\n  --server-name <NAME>            Server name [default: freeq]\n  --challenge-timeout-secs <N>    SASL challenge validity [default: 60]\n  --config <PATH>                 Read options from a TOML file (flags override)\n  --check-config                  Validate configuration and exit\n  --db-path <PATH>                SQLite database path (omit for in-memory)\n  --migrate-to <VERSION>          Run the schema ladder to VERSION and exit\n  --web-addr <ADDR>               HTTP/WebSocket listener address\n  --iroh                          Enable iroh QUIC transport\n  --iroh-port <PORT>              UDP port for iroh (default: random)\n  --s2s-peers <ID,ID,...>         S2S peer iroh endpoint IDs\n  --s2s-peer-api <ID=URL,...>     Where each S2S peer serves its users' signing keys\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. The database uses SQLite with WAL mode for good concurrent read performance. Persistence failures are logged but do not crash the server.\n\nEvery flag can also live in a TOML file passed via `--config server.toml` (or `FREEQ_CONFIG`): keys are the flag names with underscores (`listen_addr = \"0.0.0.0:6667\"`), lists are arrays, and precedence is CLI flag > environment variable > file > default, so existing flag-based deployments work unchanged. A typo'd key is a startup error that names the key. `--migrate-to` is deliberately CLI-only. A full worked example ships as [`server.toml.example`](server.toml.example), and a test keeps it in sync with the real schema.\n\nSchema migrations run automatically (and upward only) at startup. A server binary refuses to open a database stamped with a newer schema than it knows, so before rolling back to an older binary, downgrade the schema first: `freeq-server --db-path <PATH> --migrate-to <VERSION>` runs the ladder to the named version and exits. Downgrades stop with an error at any migration that is irreversible by design.\n\n## Deployment\n\nSee [deploy/README.md](deploy/README.md) for example VPS setup and deployment instructions.\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=<endpoint-id>` 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 & fix plan\n- [Future Direction](docs/FutureDirection.md) — Roadmap\n\n## License\n\nMIT\n",
  "bytes": 25337,
  "sha": "c102749f120c61b23f36a79244b979a47e2e7a0d502c16562d881007637e9fd4",
  "repo_slug": "freeq-irc/freeq",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_freeq_irc_freeq_cffcd164/readme"
}