iOS v1 prerequisites: Idempotency-Key + ASCII QR pairing #8

Merged
arne merged 2 commits from ios-v1-prereqs into main 2026-05-10 18:00:34 +02:00
Owner

Summary

Two server-side changes the iOS v1 client depends on, both small and self-contained (per TODO.md).

  • Idempotency-Key on POST /api/v1/messages. Migration v4 adds idempotency_keys (token_id, idempotency_key, row_id, payload_hash, created_at). A new LookupOrEnqueueIdempotent does lookup-or-insert atomically in one tx, returns the row's current state on replay, and returns ErrIdempotencyMismatch (→ 409) when the payload diverges from the stored hash. Records older than 24h are swept opportunistically on each call. The handler reads the optional Idempotency-Key header; on replay it reuses the response shape but skips the outbound-state SSE publish so subscribers don't see the same row twice.
  • ASCII QR in posta-server token create. After the plaintext token, the CLI prints a posta+v1://<host>#token=<token> URI and a UTF-8 block QR encoding the same URI. Host comes from the per-identity manifest entry via daemon.IdentityHost. --no-qr suppresses both for scripted use.

CLIENT_API.md updated for both — new Idempotency section under POST /messages, and the Pairing section reflects the new CLI output.

Test plan

  • go vet ./... clean
  • go test ./... clean
  • LookupOrEnqueueIdempotent: replay returns same row + current state; mismatched payload → ErrIdempotencyMismatch; same key from a different token produces an independent row; records older than IdempotencyTTL are swept on next call
  • sendMessage over httptest: same key + same body → identical 202 twice with exactly one SSE publish; same key + different body → 409 idempotency-mismatch; same key + different bearer → independent rows
  • renderQR: dimensions and quiet zone are right; every module's dark/light agrees with rsc.io/qr bit-for-bit
  • Manual: scan the printed QR with iPhone Camera.app and confirm it parses to `posta+v1://…#token=…`

🤖 Generated with Claude Code

## Summary Two server-side changes the iOS v1 client depends on, both small and self-contained (per `TODO.md`). - **`Idempotency-Key` on `POST /api/v1/messages`.** Migration v4 adds `idempotency_keys (token_id, idempotency_key, row_id, payload_hash, created_at)`. A new `LookupOrEnqueueIdempotent` does lookup-or-insert atomically in one tx, returns the row's current state on replay, and returns `ErrIdempotencyMismatch` (→ 409) when the payload diverges from the stored hash. Records older than 24h are swept opportunistically on each call. The handler reads the optional `Idempotency-Key` header; on replay it reuses the response shape but skips the `outbound-state` SSE publish so subscribers don't see the same row twice. - **ASCII QR in `posta-server token create`.** After the plaintext token, the CLI prints a `posta+v1://<host>#token=<token>` URI and a UTF-8 block QR encoding the same URI. Host comes from the per-identity manifest entry via `daemon.IdentityHost`. `--no-qr` suppresses both for scripted use. `CLIENT_API.md` updated for both — new Idempotency section under `POST /messages`, and the Pairing section reflects the new CLI output. ## Test plan - [x] `go vet ./...` clean - [x] `go test ./...` clean - [x] `LookupOrEnqueueIdempotent`: replay returns same row + current state; mismatched payload → `ErrIdempotencyMismatch`; same key from a different token produces an independent row; records older than `IdempotencyTTL` are swept on next call - [x] `sendMessage` over `httptest`: same key + same body → identical 202 twice with exactly one SSE publish; same key + different body → 409 `idempotency-mismatch`; same key + different bearer → independent rows - [x] `renderQR`: dimensions and quiet zone are right; every module's dark/light agrees with `rsc.io/qr` bit-for-bit - [ ] Manual: scan the printed QR with iPhone Camera.app and confirm it parses to \`posta+v1://…#token=…\` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Two server-side changes the iOS v1 client depends on, both small and
self-contained.

Idempotency-Key on POST /api/v1/messages
- Migration v4 adds idempotency_keys (token_id, key, row_id,
  payload_hash, created_at) keyed on (token_id, key) and FK-cascading
  to messages(id), so a deleted row drops its key with it
- New store method LookupOrEnqueueIdempotent does lookup-or-insert in
  a single transaction; returns IsReplay=true on hit, ErrIdempotency-
  Mismatch when payload diverges, and lazily sweeps records older than
  IdempotencyTTL (24h) on each call
- sendMessage handler reads the optional Idempotency-Key header,
  sha256s the request body, routes through the new method, and maps
  mismatch to 409. Replay path reuses the response shape but skips
  the SSE outbound-state publish (the original POST already fired one)
- CLIENT_API.md gains an Idempotency section under POST /messages

ASCII QR in posta-server token create
- New cmd/posta-server/qr.go renders rsc.io/qr output as `██`/spaces
  with a 1-module quiet zone, two cells per module so terminals don't
  squish it; golden-ish unit test pins the bit-perfect mapping against
  the encoder
- token create prints the plaintext, then `posta+v1://<host>#token=…`,
  then the ASCII QR. Host comes from the per-identity manifest URL via
  daemon.IdentityHost. --no-qr suppresses both URI and QR for scripts
- CLIENT_API.md Pairing section updated with the new flow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Idempotency hash now covers (peer, inReplyTo, payload) NUL-separated,
  not just payload. A client reusing the same Idempotency-Key across
  different peers now gets 409 instead of a wrong replay carrying the
  original peer's rowId
- LookupOrEnqueueIdempotent retries once on idempotency_keys uniqueness
  violation, so concurrent same-key writers no longer surface a 500 to
  the loser — the second iteration's SELECT finds the committed row
  and returns it as a normal replay
- CLIENT_API.md updated to describe the broader hash semantics
- go mod tidy moves rsc.io/qr from indirect to direct (it is)
- Added a regression test that pins the new hash semantics: same key,
  same payload, different peer → 409

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arne merged commit f8f7676b31 into main 2026-05-10 18:00:34 +02:00
arne deleted branch ios-v1-prereqs 2026-05-10 18:00:34 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
posta/server!8
No description provided.