commit 018f7075aa1470e844f323371b8b6f68cf249a73
parent 05c83b36bfe40eba783b690f9353c89cd69cc305
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 26 Aug 2026 05:09:31 -0700
Plan key-authenticated Textlog server
Diffstat:
1 file changed, 106 insertions(+), 0 deletions(-)
diff --git a/doc/plan/key-auth-server.md b/doc/plan/key-auth-server.md
@@ -0,0 +1,106 @@
+# Key-authenticated Textlog server
+
+## Cross-repository contract
+
+The server lives at `../gotextlog-srv` and the client remains in this repository.
+The server implements the API consumed in `doc/api.md`, except that account
+creation and login use SSH Ed25519 keys. The existing email-code methods remain
+available in the client for other Textlog instances.
+
+Key authentication uses these endpoints under `/api/v1`:
+
+- `POST /auth/key/challenge` accepts `{"public_key": <OpenSSH authorized-key string>, "handle": <optional string>}`. It rejects algorithms other than `ssh-ed25519`. An unknown key requires a handle and creates the account only after proof succeeds. It returns `{"data":{"challenge_id":string,"message":base64,"expires_at":RFC3339,"registered":bool}}`.
+- `POST /auth/key/verify` accepts `{"challenge_id":string,"signature":base64}`, where signature is the SSH wire encoding of `ssh.Signature` over the decoded message. It consumes the challenge and returns the existing session envelope.
+- The challenge message is server-generated and domain-separated with `gotextlog-auth-v1`, the configured public origin, challenge ID, nonce, purpose, and expiry. Challenges expire after five minutes and are single-use.
+
+The TUI discovers `ssh-ed25519` public keys from `~/.ssh/*.pub`. A selected
+public key is signed through `SSH_AUTH_SOCK` when available, otherwise through
+the matching unencrypted private-key file. Encrypted private keys must be loaded
+into an agent. The login screen allows switching between existing email-code
+login and key login, selecting a discovered key, and entering a handle for
+first use.
+
+Bearer sessions remain unchanged. The server returns empty `email` and false
+`email_verified` fields so the existing current-user decoder remains compatible.
+Opaque cursors represent stable `(created_at,id)` pagination positions.
+
+## Ownership and boundaries
+
+- `gotextlog-srv/internal/store` owns schema execution, SQL, transactions,
+ persistence models, cursor ordering inputs, relationship invariants, and
+ activity records.
+- `gotextlog-srv/internal/server` owns HTTP routing, validation, auth
+ middleware, API JSON shapes, URL derivation, cursor encoding, and SSE fanout.
+- `gotextlog-srv/internal/keyauth` owns challenge message construction and SSH
+ public-key/signature verification.
+- `gotextlog/internal/textlog` owns wire types and key-auth HTTP methods.
+- `gotextlog/internal/sshkey` owns local key discovery and signing.
+- `gotextlog/internal/tui` owns login-mode state and presentation.
+
+## W1: Complete server
+
+Files: all files under `../gotextlog-srv`. Do not edit this repository.
+
+Deliverables:
+
+- Initialize a Go repository and command at `cmd/gotextlog-srv`.
+- Start with a coarse failing HTTP integration test covering registration by
+ key, login, two users, posts/replies, feeds/search, profiles, follow/block,
+ tags, activities, reports, session revocation, and SSE. Make it pass.
+- Implement the full endpoint inventory in `doc/api.md`, including cursor
+ pagination and response fields consumed by the client.
+- Use SQLite through cgo. Vendor `github.com/mattn/go-sqlite3`, including its
+ amalgamation, and make normal builds use the vendor directory.
+- Add `doc/schema.md` with tables, columns, constraints, indexes, ownership,
+ and migration policy. Add `doc/api.md` for the key-auth difference.
+- Add migrations, foreign keys, WAL/busy timeout, graceful shutdown, public
+ origin/listen/database flags, JSON errors, and tests.
+- Add a justfile and companion scripts under `dev/` for build, test, and an
+ end-to-end smoke test.
+
+Verification: `CGO_ENABLED=1 go test -race ./...`, `just build`, and the
+server smoke script.
+
+## W2: Client key-auth transport and local SSH keys
+
+Files: `internal/textlog/client.go`, `internal/textlog/client_test.go`,
+`internal/textlog/types.go`, `internal/textlog/types_test.go`, new
+`internal/sshkey/*`, and `go.mod`/`go.sum`. Do not edit TUI, docs, command,
+or config files.
+
+Deliverables:
+
+- Start with a coarse failing test that performs challenge and verify against
+ an `httptest.Server` using a temporary Ed25519 private key.
+- Add wire types and client methods for the fixed key-auth contract.
+- Discover sorted Ed25519 public keys and sign with an injected signer source;
+ production supports ssh-agent and matching unencrypted private files.
+- Keep email-code APIs and types intact.
+
+Verification: `go test -race ./internal/textlog ./internal/sshkey`.
+
+## W3: TUI key-login flow
+
+Depends on W2. Files: `internal/tui/backend.go`,
+`internal/tui/backend_test.go`, `internal/tui/model.go`,
+`internal/tui/model_test.go`, and `internal/tui/view.go`. Do not edit client,
+SSH-key, config, docs, or command files.
+
+Deliverables:
+
+- Start with a coarse failing model test covering mode switch, key selection,
+ first-use handle entry, successful signing/login, token save, and account
+ transition.
+- Add backend operations that request a challenge, sign its exact message, and
+ verify it.
+- Preserve the email-code flow and make key-login errors actionable.
+
+Verification: `go test -race ./internal/tui`.
+
+## Integration and acceptance
+
+Handled after W1-W3. Add durable client API/usage documentation, then run two
+real TUI backends against a temporary server/database using generated keys.
+The acceptance test registers two accounts, logs both in, creates a post and
+reply, follows, observes feeds/activity, searches, blocks, and revokes sessions.
+Run all tests in both repositories and remove this completed plan.