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.
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>
Per atproto OAuth spec, native mobile apps can use custom URL schemes
where the scheme matches the client_id hostname in reverse-domain order.
For coves.social, the allowed scheme is "social.coves".
Supported redirect URIs:
- social.coves:/callback (custom scheme per atproto spec)
- social.coves://callback
- social.coves:/oauth/callback
- social.coves://oauth/callback
- https://coves.social/app/oauth/callback (Universal Link)
馃 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- assetlinks.json for Android App Links
- apple-app-site-association for iOS Universal Links
Note: iOS file needs TEAM_ID replaced with actual Apple Developer Team ID
馃 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When Universal Links don't intercept the redirect to /app/oauth/callback,
return a clear error instead of trying to process it as an OAuth callback.
馃 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The static callback.html was intercepting OAuth callbacks before they
reached the Go handler. This prevented proper token exchange and caused
"Sign in successful" HTML to be shown instead of redirecting to the
mobile app's Universal Link callback URL.
Now all /oauth/callback requests go through the Go handler which:
- Exchanges OAuth code for tokens
- Creates sealed session tokens
- Redirects mobile flows to Universal Link URL with credentials
馃 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>