code
Clone this repository
https://tangled.org/bretton.dev/coves
git@knot.bretton.dev:bretton.dev/coves
For self-hosted knots, clone URLs may differ based on your setup.
This PR addresses two issues from code review:
1. **Fragile string matching for error detection** - The vote service was
using `strings.Contains(err.Error(), "401")` which is brittle. Now uses
typed errors (`pds.ErrUnauthorized`, `pds.ErrForbidden`) with `errors.Is()`.
2. **Auth error handling regression** - The PDS client refactor removed
401/403 mapping for write operations, causing expired sessions to return
500 errors instead of prompting re-authentication. This is now fixed.
Changes:
- Add internal/atproto/pds package with Client interface abstraction
- Add typed errors: ErrUnauthorized, ErrForbidden, ErrNotFound, ErrBadRequest
- Add wrapAPIError() that inspects atclient.APIError status codes
- Add IsAuthError() convenience helper
- Update vote service to use pds.IsAuthError() for all PDS operations
- Add comprehensive unit tests for error handling
- Add PasswordAuthPDSClientFactory for E2E test compatibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add //go:build dev tags to dev_resolver.go and dev_auth_resolver.go
- Create dev_stubs.go with production stubs (//go:build !dev)
- Fix mobile OAuth flow: localhost→127.0.0.1 redirect for cookie consistency
- Fix handle verification via local PDS in callback handler
- Use config.PublicURL for OAuth callback instead of hardcoded localhost
- Add build-dev Makefile target for dev builds
- Update dev-run.sh to use -tags dev
- Create .env.dev.example template (safe to commit)
- Document dev mode configuration in .env.dev
Dev mode code is now physically excluded from production builds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add `deleted_at IS NULL` filter to GetByURI() in vote repository
to properly exclude soft-deleted votes (matching GetByVoterAndSubject behavior)
- Fix TestVoteE2E_ToggleDifferentDirection to simulate correct event sequence:
When changing vote direction, the service DELETEs old vote and CREATEs new one
with a new rkey (not UPDATE). Test now simulates DELETE + CREATE events.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Remove SubjectValidator and SubjectNotFound error - votes on non-existent
or deleted subjects are now allowed. This aligns with atproto's design:
- User's PDS accepts the vote regardless (they own their repo)
- Jetstream emits the event regardless
- AppView consumer correctly handles orphaned votes by only updating
counts for non-deleted subjects
Benefits:
- Reduced latency (no extra DB queries per vote)
- No race conditions (subject could be deleted between validation and PDS write)
- No eventual consistency issues (subject might not be indexed yet)
- Simpler code and fewer failure modes
Changes:
- Remove SubjectValidator interface and CompositeSubjectValidator
- Remove ErrSubjectNotFound from errors and lexicon
- Update NewService signature to remove validator parameter
- Update tests to remove SubjectNotFound test cases
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
P1 fixes:
- Use errors.Is() in handler to match wrapped errors (ErrNotAuthorized
was returning 500 instead of 403 when wrapped)
P2 fixes:
- Add SubjectValidator interface to check post/comment existence
- Service now validates subject exists before creating vote
- Returns ErrSubjectNotFound per lexicon if subject doesn't exist
- Prevents dangling votes on non-existent content
Also:
- Add CompositeSubjectValidator for checking both posts and comments
- Wire up subject validation in main.go with post/comment repos
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Wire up vote service and routes in main server:
- Initialize VoteService with OAuth client for PDS authentication
- Register vote XRPC routes with auth middleware
Also adds E2E test helpers:
- AddSessionWithPDS: Store session with specific PDS URL
- AddUserWithPDSToken: Register user with real PDS access token
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Comprehensive E2E tests covering:
- TestVoteE2E_CreateUpvote: Full create flow with PDS verification
- TestVoteE2E_ToggleSameDirection: Toggle off behavior
- TestVoteE2E_ToggleDifferentDirection: Vote direction change
- TestVoteE2E_DeleteVote: Explicit delete via XRPC
- TestVoteE2E_JetstreamIndexing: Real Jetstream firehose consumption
Tests verify:
- Vote records written to PDS correctly
- Jetstream consumer indexes votes
- Post vote counts updated
- Empty object response for delete per lexicon
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Register vote XRPC endpoints on the router:
- POST /xrpc/social.coves.feed.vote.create (authenticated)
- POST /xrpc/social.coves.feed.vote.delete (authenticated)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add HTTP handlers for vote XRPC endpoints:
- HandleCreateVote: POST /xrpc/social.coves.feed.vote.create
- HandleDeleteVote: POST /xrpc/social.coves.feed.vote.delete
Error handling matches lexicon exactly:
- VoteNotFound, SubjectNotFound, InvalidSubject, NotAuthorized
- Delete returns empty object {} per lexicon spec
Includes comprehensive unit tests for all handlers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements vote service that queries PDS directly for existing votes,
avoiding eventual consistency issues with the AppView database.
Key features:
- CreateVote with toggle behavior (same direction = delete)
- DeleteVote for explicit vote removal
- getVoteFromPDS: Paginates through all vote records to handle >100 votes
- Proper auth error handling (401/403 -> ErrNotAuthorized)
Architecture:
- Queries PDS via com.atproto.repo.listRecords (source of truth)
- Writes via com.atproto.repo.createRecord/deleteRecord
- AppView indexes from Jetstream for aggregate counts only
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add XRPC procedure lexicons for voting on posts/comments:
- social.coves.feed.vote.create: Create/toggle votes with up/down direction
- social.coves.feed.vote.delete: Remove existing votes
Follows atproto lexicon best practices:
- Uses knownValues for direction (not closed enum)
- References com.atproto.repo.strongRef for subject
- UpperCamelCase error names per spec
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Instead of redirecting directly to the custom scheme (social.coves:/),
serve an intermediate HTML page that:
- Shows "Login Complete" with Coves branding
- Displays the user's handle
- Redirects to the app via JavaScript + meta refresh
- Attempts to close the browser tab
- Shows friendly fallback message if app doesn't open
This prevents users from seeing stale PDS error pages when the
Custom Tab doesn't close immediately after the OAuth redirect.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>