code
Clone this repository
https://tangled.org/bretton.dev/coves-mobile
git@knot.bretton.dev:bretton.dev/coves-mobile
For self-hosted knots, clone URLs may differ based on your setup.
Add try-catch blocks with graceful fallbacks for all FlutterKey.fromJwk() calls:
- oauth_client.dart:683 (callback/token exchange)
- Throws descriptive exception on key corruption
- User prompted to re-authenticate
- oauth_client.dart:851 (session restore)
- Deletes corrupted session with delStored()
- Forces clean re-authentication flow
- oauth_client.dart:923 (session revoke)
- Skips server-side revocation if key corrupted
- Still deletes local session in finally block
- Logs warning in debug mode
- session_getter.dart:265 (token refresh)
- Throws TokenRefreshError for corrupted keys
- Triggers session deletion via existing error handling
Also reduces DPoP key logging verbosity:
- Removes detailed key structure logging that exposed implementation
- Simplified to basic confirmation messages
- Improves security posture
Handles edge case where JWK data becomes corrupted in secure storage,
preventing cryptic errors and providing clear recovery path.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
CRITICAL FIXES:
- [P0] Use requestUri instead of request path in DPoP retry (fetch_dpop.dart)
- Was using relative path, now uses absolute URI
- Prevents endpoint resolution failures during retry
- [CRITICAL] Fix token refresh to preserve DPoP key (session_getter.dart:293)
- Was using undefined newDpopKey.bareJwk variable
- Now correctly preserves storedSession.dpopKey
- [CRITICAL] Fix onStoreError to use stored key for revocation (session_getter.dart:178)
- Was generating new key instead of using stored key
- Now properly restores key with FlutterKey.fromJwk()
- Remove duplicate dpopKey declaration (session_getter.dart:234)
- Add automatic nonce retry in onResponse handler (fetch_dpop.dart)
- Handles 401 responses when validateStatus: true
- Implements same retry logic as onError handler
These fixes ensure DPoP keys persist correctly across the entire
OAuth lifecycle, preventing "DPoP proof does not match JKT" errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Complete voting system improvements including lexicon migration,
optimistic UI updates, initial vote state loading, and performance
optimizations from PR review.
All 119 tests passing ✅
This PR implements several improvements to the voting system:
**1. Vote Lexicon Migration**
- Migrate from social.coves.interaction.vote to social.coves.feed.vote
- Aligns with backend migration (commit 7a87d6b)
- Follows atProto conventions (like app.bsky.feed.like)
**2. Optimistic Score Updates**
- Vote counts update immediately when users vote
- Score adjustments tracked per post:
- Create upvote: +1
- Remove upvote: -1
- Create downvote: -1
- Remove downvote: +1
- Switch up→down: -2
- Switch down→up: +2
- Automatic rollback on API errors
- 7 new tests covering all scenarios
**3. Initial Vote State Loading**
- Added VoteService.getUserVotes() to query PDS for user's votes
- Added VoteProvider.loadInitialVotes() to bulk-load vote state
- FeedProvider loads vote state after fetching posts
- Hearts now fill correctly on app reload
**4. Performance Optimization** (PR Review)
- Added vote state cache to avoid O(n) PDS lookups
- VoteProvider passes cached state (rkey + direction) to VoteService
- Eliminates 5-10 API calls for users with many votes
- Performance: O(1) instead of O(n)
**5. Code Quality Improvements** (PR Review)
- Fix: Unsafe force unwrap in Provider initialization (main.dart:57)
- Fix: Added specific catch types (on Exception catch)
- Fix: Reset score adjustments when loading votes (prevents double-counting)
- All 119 tests passing ✅
**Breaking Changes:** None
**Migration Required:** None (backward compatible)
**Test Results:**
- 119/119 tests passing
- 0 errors, 0 warnings
- Updated test mocks for new optional parameters
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes critical bug where users could not unlike posts after voting on
more than 100 posts, leading to duplicate vote records accumulating on
their PDS.
## Problem
The _findExistingVote method only checked the first 100 vote records
(limit=100). Once a user voted on >100 posts, older votes fell off the
first page, causing:
- _findExistingVote to return null for older votes
- Creation of duplicate vote records instead of deleting originals
- Users unable to unlike older posts
## Solution
Implemented cursor-based pagination to search through ALL vote records:
- Added do-while loop to fetch pages until cursor is null
- Constructs URL with cursor parameter for subsequent pages
- Searches each page for matching vote before moving to next
- Early exit when vote is found for efficiency
## Changes
- lib/services/vote_service.dart:
- _findExistingVote now uses cursor-based pagination (lines 198-273)
- Added pageSize constant (100 records per page)
- Loop continues while cursor != null
- Updated doc comment to explain pagination
- test/services/vote_service_test.dart:
- Added 3 comprehensive pagination tests:
1. Vote found in first page
2. Vote found on second page (validates cursor following)
3. Vote not found after exhausting all pages
- Uses mockito to mock OAuthSession.fetchHandler
- Verifies correct number of listRecords calls
## Testing
All 112 tests pass (up from 109, +3 new pagination tests).
## Impact
✅ Users can now unlike posts regardless of vote count
✅ No duplicate vote records created
✅ Proper atProto cursor-based pagination pattern
✅ Early exit optimization when vote found
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implemented full DPoP (Demonstrating Proof of Possession) authentication
for voting records using the local atproto_oauth_flutter package's built-in
capabilities.
**DPoP Implementation**:
- Uses OAuthSession.fetchHandler for all PDS requests
- Automatic token refresh on expiry
- Nonce management for replay protection
- Proper Authorization: DPoP <access_token> headers
- DPoP: <proof> signed JWT headers
**VoteService Changes**:
- Removed manual Dio HTTP client and interceptors
- Now uses session.fetchHandler for all XRPC calls:
- com.atproto.repo.createRecord
- com.atproto.repo.deleteRecord
- com.atproto.repo.listRecords
- Simplified authentication - all handled by OAuthSession
**Testing**:
- All 109 tests passing
- Successfully tested on real PDS (localhost:3001)
- Vote records properly created with correct schema:
- $type: social.coves.interaction.vote
- Strong references (uri + cid)
- Proper timestamps and direction
**Production Ready**:
✅ DPoP authentication working
✅ Direct-to-PDS writes successful
✅ Records match backend lexicon
✅ Ready for Jetstream integration testing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>