commits
Enable users to embed Bluesky posts by pasting bsky.app URLs. Posts are
resolved at response time with text, author info, and engagement stats.
## New Package: internal/core/blueskypost/
Core service following the unfurl package pattern:
- types.go: BlueskyPostResult, Author structs with ErrCircuitOpen sentinel
- interfaces.go: Service and Repository interfaces
- repository.go: PostgreSQL cache with TTL (1 hour) and AT-URI validation
- url_parser.go: bsky.app URL → AT-URI conversion with rkey validation
- fetcher.go: Bluesky public API client using SSRF-safe HTTP client
- circuit_breaker.go: Failure protection (3 failures, 5min open)
- service.go: Cache-first resolution with circuit breaker integration
## Features
- Detect bsky.app URLs in post creation, convert to social.coves.embed.post
- Resolve Bluesky posts at feed response time via TransformPostEmbeds()
- Support for quoted posts (1 level deep)
- Media indicators (hasMedia, mediaCount) without rendering (Phase 2)
- Typed error handling with retryable flag for transient failures
- Debug logging for embed processing traceability
## Integration
- Updated discover, timeline, communityFeed handlers
- Wired blueskypost service in cmd/server/main.go
- Database migration for bluesky_post_cache table
## Test Coverage: 73.1%
- url_parser_test.go: URL parsing, validation, edge cases
- circuit_breaker_test.go: State transitions, thread safety
- service_test.go: Cache hit/miss, circuit breaker integration
- fetcher_test.go: Post mapping, media detection, quotes
- repository_test.go: AT-URI validation
## Out of Scope (Phase 2)
- Rendering embedded images/videos
- Moderation labels (NSFW handling)
- Deep quote chains (>1 level nesting)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update GetCommunity() to accept DIDs, canonical handles (c-name.domain),
scoped identifiers (!name@domain), and at-identifiers (@handle)
- Fix subscribe/unsubscribe handlers to let service handle identifier resolution
(removes redundant ResolveCommunityIdentifier calls)
- Add community error handling to aggregator handlers to properly return
404/400 instead of 500 for community errors
- Add communityService dependency to listForCommunity handler for identifier
resolution
- Preserve original identifier in error messages for better debugging
- Add comprehensive unit tests for subscribe/unsubscribe handlers
- Add GetCommunity identifier resolution integration tests
- Add handle format tests for listForCommunity E2E tests
Fixes issue where endpoints only accepted DIDs and rejected valid handles
like c-worldnews.coves.social
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test was using AddUser() which creates a mock token, but subscription
and other write operations need the real PDS access token. Using
AddUserWithPDSToken() stores the actual PDS token so write-forward works.
All tests now pass without --short flag.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix concurrent modification test to accept either ErrConflict (409)
or CID mismatch error (400 "Record was at")
- Shorten e2epost community name to stay under handle length limit
Remaining: TestFullUserJourney_E2E fails due to PDS token validation
issue ("InvalidToken") - this is a test infrastructure issue where the
community's stored access token is no longer valid with the PDS.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes:
- Exclude deleted top-level comments from ListByParentWithHotRank query
(nested deleted comments still preserved via ListByParentsBatch)
- Fix OAuth E2E tests: unwrap MobileAwareStoreWrapper for cleanup methods
- Fix hostedby security tests: conditionally skip DID verification
- Fix concurrent_scenarios_test: use correct column name (commenter_did)
- Fix user_journey_e2e_test: use correct column name (commenter_did)
- Fix handle length issues: use shorter prefixes with 6-digit timestamps
to stay under ATProto's 32-character handle limit
Pre-commit hook:
- Add go vet check that rejects commits with static analysis issues
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix TestHandleClientMetadata to expect full metadata URL per atproto OAuth spec
- Fix TestVoteRepo_Delete to match GetByURI behavior (excludes soft-deleted votes)
- Fix TestPostgresOAuthStore_CleanupExpiredSessions test isolation
- Fix lexicon IDs to use lowerCamelCase (getProfile, updateProfile) per atproto spec
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change community handle format to simplify DNS/Caddy configuration:
- Old: gaming.community.coves.social
- New: c-gaming.coves.social
This works with single-level wildcard certificates (*.coves.social)
while keeping the same user-facing display format (!gaming@coves.social).
Changes:
- Add migration 022 to update existing handles in database
- Update handle generation in pds_provisioning.go
- Update GetDisplayHandle() parsing in community.go
- Update scoped identifier resolution in service.go
- Update PDS_SERVICE_HANDLE_DOMAINS in docker-compose.dev.yml
Also addresses PR review feedback:
- Fix LRU cache error handling (panic on critical failure)
- Add logging for JSON marshal failures in facets
- Add DEBUG logging for domain extraction fallbacks
- Add DEBUG logging for GetDisplayHandle parse failures
- Add WARNING log for non-did:web hostedBy
- Add edge case tests for malformed handle inputs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add true E2E integration tests that verify the full write-forward flow
through real PDS and Jetstream infrastructure:
- Comment E2E tests: create, update, delete with real Jetstream indexing
- Comment authorization tests: verify users cannot modify others' comments
- Comment validation tests: verify proper error handling
- Community update E2E tests: single and multiple updates via Jetstream
These tests require dev infrastructure (make dev-up) and are skipped
in short mode. Fixed race condition where Jetstream subscription started
after create event was emitted.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add optional sources array to create_external_embed() for including
article source citations in post embeds. Sources are passed through
from parsed Kagi news stories to the embed structure.
- Add sources parameter to CovesClient.create_external_embed()
- Pass story sources to embed in Aggregator.run()
- Add comprehensive unit tests for create_external_embed()
- Add integration tests for posting with/without sources
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a user changes their handle on their PDS, all active OAuth sessions
are now updated to reflect the new handle. This ensures mobile/web apps
display the correct handle without requiring re-authentication.
Implementation:
- Add UpdateHandleByDID method to PostgresOAuthStore
- Add SessionHandleUpdater interface for dependency injection
- Use functional options pattern for consumer configuration
- Pass verified handle through mobile callback flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add --remove-orphans flag and network cleanup to prevent stale
containers from accumulating during development.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add missing mock methods to vote handler tests (EnsureCachePopulated,
GetViewerVote, GetViewerVotesForSubjects)
- Add //go:build ignore to standalone scripts to exclude from package lint
- Remove deprecated skip-dirs from golangci config
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Claude hook (.claude/settings.json) to run gofumpt after editing .go files
- Fix .golangci.yml Go version to 1.24 to match golangci-lint build version
- Pre-commit hook updated to use full path for golangci-lint
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use ON CONFLICT DO NOTHING for comment indexing to handle race
conditions from duplicate Jetstream events (at-least-once delivery).
This eliminates duplicate key constraint errors when the same event
is delivered multiple times.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add optional authentication support to the Discover feed endpoint so
authenticated users can see their existing likes/votes on posts.
Changes:
- Add voteService and OptionalAuth middleware to Discover handler
- Extract shared PopulateViewerVoteState helper to reduce duplication
- Add FeedPostProvider interface with GetPost() method to all feed types
- Add comprehensive integration tests for viewer vote state
The Discover feed remains public (unauthenticated users can still view it),
but authenticated users now receive their vote state (vote direction and URI)
on each post, matching the behavior of Timeline and Community Feed handlers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement PutRecord in PDS client with swapRecord CID validation:
- Add ErrConflict error type for HTTP 409 responses
- Add PutRecord method to Client interface with optimistic locking
- Map 409 status to ErrConflict in wrapAPIError
Migrate UpdateComment to use PutRecord:
- Use existingRecord.CID as swapRecord for concurrent modification detection
- Add ErrConcurrentModification error type in comments package
- Return proper error when PDS detects CID mismatch
Testing:
- Add PutRecord unit tests (success, conflict, typed errors)
- Add PutRecord to mockPDSClient for unit test compatibility
- Add integration test for concurrent modification detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement comment deletion that preserves thread structure by keeping
tombstone records with blanked content instead of hiding comments entirely.
Features:
- Add deletion_reason enum (author, moderator) and deleted_by column
- Blank content on delete but preserve threading references
- Include deleted comments in thread queries as "[deleted]" placeholders
- Add RepositoryTx interface for atomic delete + count updates
- Add validation for deletion reason constants
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add SoftDeleteWithReasonTx to mockCommentRepo implementing RepositoryTx.
Update SoftDeleteWithReason to validate deletion reason and delegate
to the Tx method.
Update test for deleted comments to verify they appear as placeholders
with IsDeleted=true instead of being filtered out.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace inline SQL in deleteCommentAndUpdateCounts with call to
SoftDeleteWithReasonTx via type assertion to RepositoryTx interface.
This eliminates duplicate deletion logic between consumer and repo
while maintaining atomic transaction for delete + count updates.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add buildDeletedCommentView method that creates placeholder views
for deleted comments with blanked content but preserved threading.
Update buildThreadViews to include deleted comments instead of
skipping them, enabling child comments to remain visible with
their parent showing "[deleted]".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add SoftDeleteWithReason and SoftDeleteWithReasonTx methods that:
- Blank content, facets, embed, and labels on deletion
- Set deletion_reason and deleted_by metadata
- Validate deletion reason is author or moderator
- Support optional transaction for atomic operations
- Return rows affected for idempotency checks
Update all thread queries to include deleted comments by removing
deleted_at IS NULL filter, preserving thread structure.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add SoftDeleteWithReason to Repository interface for content-blanking
soft deletes that preserve thread structure.
Add RepositoryTx interface with SoftDeleteWithReasonTx method for
transactional operations, enabling atomic delete + count updates
in the Jetstream consumer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add IsDeleted, DeletionReason, and DeletedAt fields to CommentView
for rendering deleted comment placeholders in thread views.
Frontend can display "[deleted]" or "[removed by moderator]" based
on the deletion_reason field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add DeletionReasonAuthor and DeletionReasonModerator constants.
Add DeletionReason and DeletedBy fields to Comment struct for
tracking who deleted a comment and why.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add migration 021 that:
- Creates deletion_reason enum type (author, moderator)
- Adds deletion_reason and deleted_by columns to comments table
- Backfills existing deleted comments as author-deleted
- Removes deleted_at IS NULL filter from thread indexes to preserve structure
- Adds indexes for deletion_reason and deleted_by for moderation queries
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add complete comment write operations (create/update/delete) with:
- XRPC lexicons for all three operations
- Service layer with validation and authorization
- HTTP handlers with proper error mapping
- Comprehensive unit and integration tests
- Proper grapheme counting with uniseg library
Follows write-forward architecture: Client → Handler → Service → PDS → Jetstream → DB
Add two P3 technical debt items for future optimistic locking:
1. Implement PutRecord in PDS Client
- Add swapRecord/swapCommit for optimistic locking
- Handle conflict responses gracefully
2. Migrate UpdateComment to Use PutRecord
- Blocked by PutRecord implementation
- Will prevent concurrent update races
These items are not urgent since concurrent comment updates are rare,
but will improve robustness when implemented.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add github.com/rivo/uniseg for proper Unicode grapheme cluster counting.
This replaces utf8.RuneCountInString() with uniseg.GraphemeClusterCount()
to correctly handle complex Unicode characters like emojis with modifiers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add integration tests for comment write operations testing:
- CreateComment XRPC endpoint validation
- UpdateComment authorization and validation
- DeleteComment authorization and success
Fix existing integration tests to use NewCommentServiceWithPDSFactory
with nil factory for read-only test scenarios. This allows tests that
only exercise the read path (GetComments) to work without OAuth setup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive unit tests for CreateComment, UpdateComment, and
DeleteComment service methods including:
- Validation tests (empty content, content too long, invalid URIs)
- Authorization tests (ownership verification for update/delete)
- Collection validation to prevent cross-collection attacks
- Success path tests with mock PDS client
Also updates existing comment service tests for constructor changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add RegisterCommentRoutes for comment write XRPC endpoints
- Wire comment service with OAuth dependencies in main.go
- All write endpoints require OAuth authentication middleware
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement XRPC-style HTTP handlers for comment write operations:
- CreateCommentHandler: POST /xrpc/social.coves.community.comment.create
- UpdateCommentHandler: POST /xrpc/social.coves.community.comment.update
- DeleteCommentHandler: POST /xrpc/social.coves.community.comment.delete
Features:
- Request body size limit (100KB) for DoS prevention
- OAuth session extraction from middleware context
- Proper error mapping to lexicon-defined error types
- Labels validation with explicit error handling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement comment write operations following write-forward architecture:
- CreateComment: Write new comments/replies to user's PDS
- UpdateComment: Modify existing comment content (preserves reply refs)
- DeleteComment: Remove comments via PDS deleteRecord
Key features:
- Proper grapheme counting with unicode/uniseg library
- Collection validation to prevent cross-collection attacks
- Ownership verification before update/delete
- OAuth session-based PDS client creation
- PDSClientFactory for testability with password auth
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Define AT Protocol lexicon schemas for comment write operations:
- social.coves.community.comment.create: Create new comments/replies
- social.coves.community.comment.update: Update existing comment content
- social.coves.community.comment.delete: Delete comments by URI
All procedures require OAuth authentication and follow atProto conventions
with proper error definitions (ContentTooLong, ContentEmpty, NotAuthorized, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add stale vote cleanup in Jetstream consumer: when indexing a vote,
detect and soft-delete any existing active vote with a different URI
for the same user/subject (handles missed delete events)
- Change indexVoteAndUpdateCounts to return (bool, error) to indicate
if vote was newly inserted vs already existed
- Remove noisy "Vote already indexed" log for idempotent cases
- Only log "✓ Indexed vote" when vote is actually new
- Update CLAUDE.md with PR reviewer instructions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add vote caching to solve eventual consistency issues when displaying
user vote state in community feeds and timeline. The cache is populated
from the user's PDS (source of truth) on first authenticated request,
avoiding stale data from the AppView database.
Changes:
- Add VoteCache with TTL-based expiration and incremental updates
- Integrate cache into feed and timeline handlers for viewer vote state
- Add EnsureCachePopulated and GetViewerVotesForSubjects to vote service
- Add reindex-votes CLI tool for rebuilding vote counts from PDS
- Update CLAUDE.md to PR reviewer persona
- Fix E2E tests to properly simulate Jetstream events
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
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>
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>
The atproto OAuth spec requires client_id to be the URL of the client
metadata document, not just the domain. Changed from:
https://coves.social
To:
https://coves.social/oauth/client-metadata.json
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Without this, OAuth client uses localhost which causes PAR request
to fail with "localhost hostname is not allowed (RFC 8252)".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Required for sealing session tokens in production.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Delete internal/atproto/auth/ directory (JWT/DPoP verification - unused)
- Delete cmd/genjwks/ (confidential client key generator - unused)
- Remove ClientSecret/ClientKID from OAuthConfig (public client only)
- Remove HandleJWKS endpoint and routes (not needed for public clients)
- Remove OAUTH_PRIVATE_JWK from docker-compose.prod.yml
- Update tests and integration helpers
Coves is a public OAuth client - this cleanup removes ~1,500 lines of
dead code that was never being used.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Remove unused OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI, OAUTH_PRIVATE_JWK from .env.prod.example
- Add OAUTH_SEAL_SECRET to .env.dev for local development
- Clarify that OAUTH_SEAL_SECRET is required, client secret/kid are optional
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add OAuth and Universal Links env vars to example
- Update go.mod/go.sum with required dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Update test helpers for new OAuth flow
- Adapt aggregator, community, post tests
- Update user journey tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Enable users to embed Bluesky posts by pasting bsky.app URLs. Posts are
resolved at response time with text, author info, and engagement stats.
## New Package: internal/core/blueskypost/
Core service following the unfurl package pattern:
- types.go: BlueskyPostResult, Author structs with ErrCircuitOpen sentinel
- interfaces.go: Service and Repository interfaces
- repository.go: PostgreSQL cache with TTL (1 hour) and AT-URI validation
- url_parser.go: bsky.app URL → AT-URI conversion with rkey validation
- fetcher.go: Bluesky public API client using SSRF-safe HTTP client
- circuit_breaker.go: Failure protection (3 failures, 5min open)
- service.go: Cache-first resolution with circuit breaker integration
## Features
- Detect bsky.app URLs in post creation, convert to social.coves.embed.post
- Resolve Bluesky posts at feed response time via TransformPostEmbeds()
- Support for quoted posts (1 level deep)
- Media indicators (hasMedia, mediaCount) without rendering (Phase 2)
- Typed error handling with retryable flag for transient failures
- Debug logging for embed processing traceability
## Integration
- Updated discover, timeline, communityFeed handlers
- Wired blueskypost service in cmd/server/main.go
- Database migration for bluesky_post_cache table
## Test Coverage: 73.1%
- url_parser_test.go: URL parsing, validation, edge cases
- circuit_breaker_test.go: State transitions, thread safety
- service_test.go: Cache hit/miss, circuit breaker integration
- fetcher_test.go: Post mapping, media detection, quotes
- repository_test.go: AT-URI validation
## Out of Scope (Phase 2)
- Rendering embedded images/videos
- Moderation labels (NSFW handling)
- Deep quote chains (>1 level nesting)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update GetCommunity() to accept DIDs, canonical handles (c-name.domain),
scoped identifiers (!name@domain), and at-identifiers (@handle)
- Fix subscribe/unsubscribe handlers to let service handle identifier resolution
(removes redundant ResolveCommunityIdentifier calls)
- Add community error handling to aggregator handlers to properly return
404/400 instead of 500 for community errors
- Add communityService dependency to listForCommunity handler for identifier
resolution
- Preserve original identifier in error messages for better debugging
- Add comprehensive unit tests for subscribe/unsubscribe handlers
- Add GetCommunity identifier resolution integration tests
- Add handle format tests for listForCommunity E2E tests
Fixes issue where endpoints only accepted DIDs and rejected valid handles
like c-worldnews.coves.social
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test was using AddUser() which creates a mock token, but subscription
and other write operations need the real PDS access token. Using
AddUserWithPDSToken() stores the actual PDS token so write-forward works.
All tests now pass without --short flag.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix concurrent modification test to accept either ErrConflict (409)
or CID mismatch error (400 "Record was at")
- Shorten e2epost community name to stay under handle length limit
Remaining: TestFullUserJourney_E2E fails due to PDS token validation
issue ("InvalidToken") - this is a test infrastructure issue where the
community's stored access token is no longer valid with the PDS.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes:
- Exclude deleted top-level comments from ListByParentWithHotRank query
(nested deleted comments still preserved via ListByParentsBatch)
- Fix OAuth E2E tests: unwrap MobileAwareStoreWrapper for cleanup methods
- Fix hostedby security tests: conditionally skip DID verification
- Fix concurrent_scenarios_test: use correct column name (commenter_did)
- Fix user_journey_e2e_test: use correct column name (commenter_did)
- Fix handle length issues: use shorter prefixes with 6-digit timestamps
to stay under ATProto's 32-character handle limit
Pre-commit hook:
- Add go vet check that rejects commits with static analysis issues
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix TestHandleClientMetadata to expect full metadata URL per atproto OAuth spec
- Fix TestVoteRepo_Delete to match GetByURI behavior (excludes soft-deleted votes)
- Fix TestPostgresOAuthStore_CleanupExpiredSessions test isolation
- Fix lexicon IDs to use lowerCamelCase (getProfile, updateProfile) per atproto spec
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change community handle format to simplify DNS/Caddy configuration:
- Old: gaming.community.coves.social
- New: c-gaming.coves.social
This works with single-level wildcard certificates (*.coves.social)
while keeping the same user-facing display format (!gaming@coves.social).
Changes:
- Add migration 022 to update existing handles in database
- Update handle generation in pds_provisioning.go
- Update GetDisplayHandle() parsing in community.go
- Update scoped identifier resolution in service.go
- Update PDS_SERVICE_HANDLE_DOMAINS in docker-compose.dev.yml
Also addresses PR review feedback:
- Fix LRU cache error handling (panic on critical failure)
- Add logging for JSON marshal failures in facets
- Add DEBUG logging for domain extraction fallbacks
- Add DEBUG logging for GetDisplayHandle parse failures
- Add WARNING log for non-did:web hostedBy
- Add edge case tests for malformed handle inputs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add true E2E integration tests that verify the full write-forward flow
through real PDS and Jetstream infrastructure:
- Comment E2E tests: create, update, delete with real Jetstream indexing
- Comment authorization tests: verify users cannot modify others' comments
- Comment validation tests: verify proper error handling
- Community update E2E tests: single and multiple updates via Jetstream
These tests require dev infrastructure (make dev-up) and are skipped
in short mode. Fixed race condition where Jetstream subscription started
after create event was emitted.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add optional sources array to create_external_embed() for including
article source citations in post embeds. Sources are passed through
from parsed Kagi news stories to the embed structure.
- Add sources parameter to CovesClient.create_external_embed()
- Pass story sources to embed in Aggregator.run()
- Add comprehensive unit tests for create_external_embed()
- Add integration tests for posting with/without sources
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a user changes their handle on their PDS, all active OAuth sessions
are now updated to reflect the new handle. This ensures mobile/web apps
display the correct handle without requiring re-authentication.
Implementation:
- Add UpdateHandleByDID method to PostgresOAuthStore
- Add SessionHandleUpdater interface for dependency injection
- Use functional options pattern for consumer configuration
- Pass verified handle through mobile callback flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add missing mock methods to vote handler tests (EnsureCachePopulated,
GetViewerVote, GetViewerVotesForSubjects)
- Add //go:build ignore to standalone scripts to exclude from package lint
- Remove deprecated skip-dirs from golangci config
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Claude hook (.claude/settings.json) to run gofumpt after editing .go files
- Fix .golangci.yml Go version to 1.24 to match golangci-lint build version
- Pre-commit hook updated to use full path for golangci-lint
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use ON CONFLICT DO NOTHING for comment indexing to handle race
conditions from duplicate Jetstream events (at-least-once delivery).
This eliminates duplicate key constraint errors when the same event
is delivered multiple times.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add optional authentication support to the Discover feed endpoint so
authenticated users can see their existing likes/votes on posts.
Changes:
- Add voteService and OptionalAuth middleware to Discover handler
- Extract shared PopulateViewerVoteState helper to reduce duplication
- Add FeedPostProvider interface with GetPost() method to all feed types
- Add comprehensive integration tests for viewer vote state
The Discover feed remains public (unauthenticated users can still view it),
but authenticated users now receive their vote state (vote direction and URI)
on each post, matching the behavior of Timeline and Community Feed handlers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement PutRecord in PDS client with swapRecord CID validation:
- Add ErrConflict error type for HTTP 409 responses
- Add PutRecord method to Client interface with optimistic locking
- Map 409 status to ErrConflict in wrapAPIError
Migrate UpdateComment to use PutRecord:
- Use existingRecord.CID as swapRecord for concurrent modification detection
- Add ErrConcurrentModification error type in comments package
- Return proper error when PDS detects CID mismatch
Testing:
- Add PutRecord unit tests (success, conflict, typed errors)
- Add PutRecord to mockPDSClient for unit test compatibility
- Add integration test for concurrent modification detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement comment deletion that preserves thread structure by keeping
tombstone records with blanked content instead of hiding comments entirely.
Features:
- Add deletion_reason enum (author, moderator) and deleted_by column
- Blank content on delete but preserve threading references
- Include deleted comments in thread queries as "[deleted]" placeholders
- Add RepositoryTx interface for atomic delete + count updates
- Add validation for deletion reason constants
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add SoftDeleteWithReasonTx to mockCommentRepo implementing RepositoryTx.
Update SoftDeleteWithReason to validate deletion reason and delegate
to the Tx method.
Update test for deleted comments to verify they appear as placeholders
with IsDeleted=true instead of being filtered out.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace inline SQL in deleteCommentAndUpdateCounts with call to
SoftDeleteWithReasonTx via type assertion to RepositoryTx interface.
This eliminates duplicate deletion logic between consumer and repo
while maintaining atomic transaction for delete + count updates.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add buildDeletedCommentView method that creates placeholder views
for deleted comments with blanked content but preserved threading.
Update buildThreadViews to include deleted comments instead of
skipping them, enabling child comments to remain visible with
their parent showing "[deleted]".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add SoftDeleteWithReason and SoftDeleteWithReasonTx methods that:
- Blank content, facets, embed, and labels on deletion
- Set deletion_reason and deleted_by metadata
- Validate deletion reason is author or moderator
- Support optional transaction for atomic operations
- Return rows affected for idempotency checks
Update all thread queries to include deleted comments by removing
deleted_at IS NULL filter, preserving thread structure.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add SoftDeleteWithReason to Repository interface for content-blanking
soft deletes that preserve thread structure.
Add RepositoryTx interface with SoftDeleteWithReasonTx method for
transactional operations, enabling atomic delete + count updates
in the Jetstream consumer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add IsDeleted, DeletionReason, and DeletedAt fields to CommentView
for rendering deleted comment placeholders in thread views.
Frontend can display "[deleted]" or "[removed by moderator]" based
on the deletion_reason field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add migration 021 that:
- Creates deletion_reason enum type (author, moderator)
- Adds deletion_reason and deleted_by columns to comments table
- Backfills existing deleted comments as author-deleted
- Removes deleted_at IS NULL filter from thread indexes to preserve structure
- Adds indexes for deletion_reason and deleted_by for moderation queries
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add complete comment write operations (create/update/delete) with:
- XRPC lexicons for all three operations
- Service layer with validation and authorization
- HTTP handlers with proper error mapping
- Comprehensive unit and integration tests
- Proper grapheme counting with uniseg library
Follows write-forward architecture: Client → Handler → Service → PDS → Jetstream → DB
Add two P3 technical debt items for future optimistic locking:
1. Implement PutRecord in PDS Client
- Add swapRecord/swapCommit for optimistic locking
- Handle conflict responses gracefully
2. Migrate UpdateComment to Use PutRecord
- Blocked by PutRecord implementation
- Will prevent concurrent update races
These items are not urgent since concurrent comment updates are rare,
but will improve robustness when implemented.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add github.com/rivo/uniseg for proper Unicode grapheme cluster counting.
This replaces utf8.RuneCountInString() with uniseg.GraphemeClusterCount()
to correctly handle complex Unicode characters like emojis with modifiers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add integration tests for comment write operations testing:
- CreateComment XRPC endpoint validation
- UpdateComment authorization and validation
- DeleteComment authorization and success
Fix existing integration tests to use NewCommentServiceWithPDSFactory
with nil factory for read-only test scenarios. This allows tests that
only exercise the read path (GetComments) to work without OAuth setup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive unit tests for CreateComment, UpdateComment, and
DeleteComment service methods including:
- Validation tests (empty content, content too long, invalid URIs)
- Authorization tests (ownership verification for update/delete)
- Collection validation to prevent cross-collection attacks
- Success path tests with mock PDS client
Also updates existing comment service tests for constructor changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement XRPC-style HTTP handlers for comment write operations:
- CreateCommentHandler: POST /xrpc/social.coves.community.comment.create
- UpdateCommentHandler: POST /xrpc/social.coves.community.comment.update
- DeleteCommentHandler: POST /xrpc/social.coves.community.comment.delete
Features:
- Request body size limit (100KB) for DoS prevention
- OAuth session extraction from middleware context
- Proper error mapping to lexicon-defined error types
- Labels validation with explicit error handling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement comment write operations following write-forward architecture:
- CreateComment: Write new comments/replies to user's PDS
- UpdateComment: Modify existing comment content (preserves reply refs)
- DeleteComment: Remove comments via PDS deleteRecord
Key features:
- Proper grapheme counting with unicode/uniseg library
- Collection validation to prevent cross-collection attacks
- Ownership verification before update/delete
- OAuth session-based PDS client creation
- PDSClientFactory for testability with password auth
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Define AT Protocol lexicon schemas for comment write operations:
- social.coves.community.comment.create: Create new comments/replies
- social.coves.community.comment.update: Update existing comment content
- social.coves.community.comment.delete: Delete comments by URI
All procedures require OAuth authentication and follow atProto conventions
with proper error definitions (ContentTooLong, ContentEmpty, NotAuthorized, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add stale vote cleanup in Jetstream consumer: when indexing a vote,
detect and soft-delete any existing active vote with a different URI
for the same user/subject (handles missed delete events)
- Change indexVoteAndUpdateCounts to return (bool, error) to indicate
if vote was newly inserted vs already existed
- Remove noisy "Vote already indexed" log for idempotent cases
- Only log "✓ Indexed vote" when vote is actually new
- Update CLAUDE.md with PR reviewer instructions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add vote caching to solve eventual consistency issues when displaying
user vote state in community feeds and timeline. The cache is populated
from the user's PDS (source of truth) on first authenticated request,
avoiding stale data from the AppView database.
Changes:
- Add VoteCache with TTL-based expiration and incremental updates
- Integrate cache into feed and timeline handlers for viewer vote state
- Add EnsureCachePopulated and GetViewerVotesForSubjects to vote service
- Add reindex-votes CLI tool for rebuilding vote counts from PDS
- Update CLAUDE.md to PR reviewer persona
- Fix E2E tests to properly simulate Jetstream events
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
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>
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>
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>
The atproto OAuth spec requires client_id to be the URL of the client
metadata document, not just the domain. Changed from:
https://coves.social
To:
https://coves.social/oauth/client-metadata.json
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Delete internal/atproto/auth/ directory (JWT/DPoP verification - unused)
- Delete cmd/genjwks/ (confidential client key generator - unused)
- Remove ClientSecret/ClientKID from OAuthConfig (public client only)
- Remove HandleJWKS endpoint and routes (not needed for public clients)
- Remove OAUTH_PRIVATE_JWK from docker-compose.prod.yml
- Update tests and integration helpers
Coves is a public OAuth client - this cleanup removes ~1,500 lines of
dead code that was never being used.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Remove unused OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI, OAUTH_PRIVATE_JWK from .env.prod.example
- Add OAUTH_SEAL_SECRET to .env.dev for local development
- Clarify that OAUTH_SEAL_SECRET is required, client secret/kid are optional
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>