gotextlog

gotextlog
git clone https://git.ryansepassi.com/git/gotextlog.git
Log | Files | Refs | README

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:
Adoc/plan/key-auth-server.md | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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.