### Major changes
- **Client-side identity** — New session key store (`sessionKey.ts`) backed by
`sessionStorage` with a module-level caching, a `crypto.subtle` cache, a `useIdentityLock`
hook for decrypt-once signing, `followSignature.ts` for signed follows, and
two new UI modals (`IdentityBackup.tsx`, `UnlockIdentityModal.tsx`).
`CreateIdentity.tsx` is rewritten to generate BIP-39 mnemonics and encrypt the
Ed25519 keypair with AES-256-GCM via PBKDF2 (600k iterations) before storing
in IndexedDB.
- **Rate limiting** — New `rate-limit-config.ts` and `rate-limit.ts` provide a
per-IP sliding-window rate limiter backed by Redis. All external-facing routes
(`/discover`, `/discover/rotate/*`, `/proxy`, social API endpoints) now have
conservative defaults wired into the custom HTTP server before requests reach
Next.js handlers.
- **Proxy route hardening** — The `/proxy` route now enforces a 256 KB payload
limit (HTTP 413), validates JSON before parsing, applies a per-origin rate
limit (100 req/min), and imports the `blocks` table to reject requests from
blocked servers.
- **Docker integration-test cluster** — New `Dockerfile`, `.dockerignore`, and
`tests/docker-compose.yml` orchestrate three SiPher instances (A, B, C) plus
shared PostgreSQL and Redis. Key generation (`generate-keys.ts`) and discovery
setup (`setup-discovery.ts`) scripts automate cluster bootstrap. Three example
env files document required per-instance configuration.
- **Full test suite overhaul** — Replaces the old attack/auth/discover/key/proxy
tests with a structured suite:
* `tests/federation/` — Keytools unit tests + key-rotation e2e test
* `tests/proxy/` — Proxy relay e2e tests (single-server validation)
* `tests/integration/` — Multi-instance integration tests for discover,
proxy-chain relay, and federated post delivery via BullMQ
* `tests/helpers/` — Reusable DB, identity, and auth-user utilities
* Playwright config updated to match new file conventions
* Unused helpers (`tests/helpers/queue.ts`) removed
- **Social plugin endpoints** — Rewritten `follows.ts`, `blocks.ts`, `mutes.ts`,
and `posts.ts` with proper federation integration. `social.ts` gains helpers
for looking up posts by federation URL.
### Minor changes
- **README** — Expanded from a 42-line stub to a full architecture guide with
tables for every layer (auth, DB, queues, storage, real-time), API route
documentation, setup instructions, environment variables, test coverage, and
the updated roadmap.
- **Federation helpers** — `keytools.ts` refactors imports and cleans up the public surface.
`fetch.ts`, `registry.ts`, and `proxy-helpers/federated-post.ts` pick up small
improvements. `PostFederationSchema` simplifies its encryption type assertion.
- **Plugin infrastructure** — Oven plugin schema and server index gain minor
refactors. Social client adds a `muteUser` method.
- **UI components** — `switch.tsx` and `tooltip.tsx` rewritten for Radix v2 /
Tailwind 4; `accordion.tsx`, `dropdown-menu.tsx`, `form`, `button`, `card` get
minor consistency fixes. `dialog.tsx` removes unused `DialogHeader`.
- **Server bootstrap** — `server.ts` imports DB schema before `instrumentation`
for correct Drizzle initialization, rate-limiting routes are wired, and CORS
allows federation origins. `auth.ts` regenerates Oven and social plugin schemas.
- **Dependencies** — Added `@noble/ciphers` and `@noble/hashes` (crypto
primitives). Removed `@signalapp/libsignal-client`, `base58-js`, `nanostores`,
`tweetnacl-util`, `dexie-react-hooks`, `socket.io-client`. Updated all Better
Auth packages to 1.6.11, BullMQ to 5.76.10, and various dev deps across the
board.
- **.gitignore** — Added `/audits` and `tests/docker/*.env` to prevent secret
leakage.
- **DB schema** — `blocks` table imported in `src/lib/db/schema/index.ts`.
Co-authored-by: Cursor <cursoragent@cursor.com>
242 lines
8.7 KiB
YAML
242 lines
8.7 KiB
YAML
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Sipher federation test cluster
|
|
#
|
|
# Three independent Sipher instances (A, B, C) sharing one Postgres server
|
|
# (separate databases) and one Redis server (separate logical DBs 0/1/2).
|
|
#
|
|
# All commands below should be run from the repository root with the explicit
|
|
# compose file (-f tests/docker-compose.yml) — `package.json` already wraps
|
|
# the common ones (docker:generate-keys, docker:build, docker:setup-discovery).
|
|
#
|
|
# Quick start
|
|
# ───────────
|
|
# 1. bun run docker:generate-keys # writes tests/docker/sipher-{a,b,c}.env
|
|
# 2. docker compose -f tests/docker-compose.yml \
|
|
# --profile init up # push DB schema (exits when done)
|
|
# 3. docker compose -f tests/docker-compose.yml up -d # start the cluster
|
|
# 4. docker compose -f tests/docker-compose.yml \
|
|
# --profile setup up # mutual discovery (exits when done)
|
|
#
|
|
# Running integration tests
|
|
# ─────────────────────────
|
|
# docker compose -f tests/docker-compose.yml run --rm test-runner \
|
|
# tests/integration/proxy-chain.ts \
|
|
# --proxy http://sipher-b:3001 --target http://sipher-c:3002
|
|
#
|
|
# docker compose -f tests/docker-compose.yml run --rm test-runner \
|
|
# tests/integration/federation-post-delivery.ts \
|
|
# --proxy http://sipher-b:3001 --target http://sipher-c:3002
|
|
#
|
|
# Both integration scripts now auto-create their own Better Auth users + identity
|
|
# keys + follow rows via HTTP, so you no longer need to pass --bearer.
|
|
#
|
|
# Teardown
|
|
# ────────
|
|
# docker compose -f tests/docker-compose.yml down # stop (keeps volumes)
|
|
# docker compose -f tests/docker-compose.yml down -v # stop + wipe all data volumes
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
# Pinned project name so the docker objects (containers, network, volumes) keep
|
|
# stable names regardless of the directory containing this compose file. Docker
|
|
# would otherwise default to the parent directory of the compose file.
|
|
name: sipher-noai
|
|
|
|
services:
|
|
|
|
# ── Infrastructure ──────────────────────────────────────────────────────────
|
|
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_USER: sipher
|
|
POSTGRES_PASSWORD: sipher_test
|
|
# sipher_a is created by POSTGRES_DB; init.sql creates sipher_b + sipher_c.
|
|
POSTGRES_DB: sipher_a
|
|
volumes:
|
|
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
|
- postgres_data:/var/lib/postgresql/data
|
|
# No host port binding — containers reach Postgres over the Docker network.
|
|
# To connect from the host for debugging: docker compose exec postgres psql -U sipher sipher_a
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U sipher -d sipher_a"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 12
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
restart: unless-stopped
|
|
# noeviction: BullMQ requires keys to never be silently dropped.
|
|
command: redis-server --maxmemory-policy noeviction
|
|
volumes:
|
|
- redis_data:/data
|
|
# No host port binding — containers reach Redis over the Docker network.
|
|
# To connect from the host for debugging: docker compose exec redis redis-cli
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 12
|
|
|
|
# ── Migrations (profile: init) ───────────────────────────────────────────────
|
|
# Run once: docker compose -f tests/docker-compose.yml --profile init up
|
|
# Each service pushes the Drizzle schema to its database and exits.
|
|
|
|
migrate-a:
|
|
build:
|
|
context: ..
|
|
dockerfile: Dockerfile
|
|
env_file: docker/sipher-a.env
|
|
command: ["bunx", "drizzle-kit", "push", "--force"]
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
profiles: [init]
|
|
restart: "no"
|
|
|
|
migrate-b:
|
|
build:
|
|
context: ..
|
|
dockerfile: Dockerfile
|
|
env_file: docker/sipher-b.env
|
|
command: ["bunx", "drizzle-kit", "push", "--force"]
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
profiles: [init]
|
|
restart: "no"
|
|
|
|
migrate-c:
|
|
build:
|
|
context: ..
|
|
dockerfile: Dockerfile
|
|
env_file: docker/sipher-c.env
|
|
command: ["bunx", "drizzle-kit", "push", "--force"]
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
profiles: [init]
|
|
restart: "no"
|
|
|
|
# ── Sipher instances ─────────────────────────────────────────────────────────
|
|
# Each instance:
|
|
# • has its own Postgres database (sipher_a / _b / _c)
|
|
# • has its own Redis logical DB (0 / 1 / 2) — isolates BullMQ queues and
|
|
# rate-limit buckets so instances don't interfere with each other
|
|
# • has a unique federation Ed25519 + X25519 keypair (generated by docker:generate-keys)
|
|
# • lists sipher-a/b/c in DEV_ALLOWED_HOSTNAMES so the SSRF url-guard allows
|
|
# outbound federation requests to the other instances over the Docker network
|
|
|
|
sipher-a:
|
|
build:
|
|
context: ..
|
|
dockerfile: Dockerfile
|
|
env_file: docker/sipher-a.env
|
|
ports:
|
|
- "3000:3000"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -fs http://localhost:3000/discover > /dev/null"]
|
|
interval: 15s
|
|
timeout: 10s
|
|
retries: 6
|
|
start_period: 30s
|
|
|
|
sipher-b:
|
|
build:
|
|
context: ..
|
|
dockerfile: Dockerfile
|
|
env_file: docker/sipher-b.env
|
|
ports:
|
|
- "3001:3001"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -fs http://localhost:3001/discover > /dev/null"]
|
|
interval: 15s
|
|
timeout: 10s
|
|
retries: 6
|
|
start_period: 30s
|
|
|
|
sipher-c:
|
|
build:
|
|
context: ..
|
|
dockerfile: Dockerfile
|
|
env_file: docker/sipher-c.env
|
|
ports:
|
|
- "3002:3002"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -fs http://localhost:3002/discover > /dev/null"]
|
|
interval: 15s
|
|
timeout: 10s
|
|
retries: 6
|
|
start_period: 30s
|
|
|
|
# ── Discovery setup (profile: setup) ────────────────────────────────────────
|
|
# Run once after `docker compose up -d` to register all instances with each other.
|
|
# Waits for all three sipher instances to pass their healthcheck before running.
|
|
|
|
setup-discovery:
|
|
build:
|
|
context: ..
|
|
dockerfile: Dockerfile
|
|
env_file: docker/sipher-a.env
|
|
command: ["tests/docker/setup-discovery.ts"]
|
|
depends_on:
|
|
sipher-a:
|
|
condition: service_healthy
|
|
sipher-b:
|
|
condition: service_healthy
|
|
sipher-c:
|
|
condition: service_healthy
|
|
profiles: [setup]
|
|
restart: "no"
|
|
entrypoint: ["bun", "run"]
|
|
|
|
# ── Integration test runner (profile: test) ──────────────────────────────────
|
|
# Runs tests/integration scripts inside the Docker network so service names
|
|
# (sipher-a, sipher-b, sipher-c) resolve correctly.
|
|
#
|
|
# Usage:
|
|
# docker compose -f tests/docker-compose.yml run --rm test-runner \
|
|
# tests/integration/proxy-chain.ts \
|
|
# --proxy http://sipher-b:3001 --target http://sipher-c:3002
|
|
#
|
|
# The test runner uses Server A's env (DATABASE_URL → sipher_a, federation keys)
|
|
# because the integration scripts import @/lib/db and read those credentials.
|
|
|
|
test-runner:
|
|
build:
|
|
context: ..
|
|
dockerfile: Dockerfile
|
|
env_file: docker/sipher-a.env
|
|
depends_on:
|
|
sipher-a:
|
|
condition: service_healthy
|
|
sipher-b:
|
|
condition: service_healthy
|
|
sipher-c:
|
|
condition: service_healthy
|
|
profiles: [test]
|
|
restart: "no"
|
|
entrypoint: ["bun", "run"]
|
|
|
|
volumes:
|
|
postgres_data:
|
|
redis_data:
|