commits
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>
- E2E tests for OAuth flows
- Session fixation attack prevention tests
- Token verification tests
- Rate limiting tests
- Remove obsolete JWT verification test (merged into new suite)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Simplify auth middleware implementation
- Update routes to use consistent auth patterns
- Improve test coverage for auth flows
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Register OAuth handlers at /oauth/* endpoints
- Register well-known routes for mobile verification
- Add /app/oauth/callback for Universal Links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- /.well-known/apple-app-site-association for iOS
- /.well-known/assetlinks.json for Android
- Configurable via APPLE_APP_ID and ANDROID_* env vars
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- OAuth client for atproto authentication flow
- Session store with CSRF protection and secure token sealing
- Mobile-specific handlers with Universal Links redirect
- Database migrations for OAuth sessions and CSRF tokens
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add secp256k1 (ES256K) support to JWT access token verification using
Bluesky's indigo crypto package. This enables authentication from
external PDSes that use ES256K-signed tokens.
Changes:
- jwt.go: Add ES256K detection and verification using indigo's crypto
- New verifyES256KToken() for ES256K-specific verification
- New parseJWKMapToIndigoPublicKey() to convert JWK to indigo key
- New verifyJWTSignatureWithIndigoKey() for indigo signature verification
- New parseJWTClaimsManually() to parse claims without golang-jwt
- Update ToPublicKey() to return JWK map for secp256k1 curves
- did_key_fetcher.go: Return indigo PublicKey for secp256k1 keys
- FetchPublicKey now returns indigoCrypto.PublicKey for secp256k1
- NIST curves (P-256, P-384, P-521) still return *ecdsa.PublicKey
This complements the DPoP ES256K support added earlier, completing
full ES256K support across the authentication stack.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add ES256K (secp256k1) algorithm support using indigo's crypto package
- Add algorithm-curve binding validation to prevent algorithm confusion attacks
- Restore exp/nbf claim validation for DPoP proofs (security regression fix)
- Replace golang-jwt parsing with manual JWT parsing to support ES256K
- Add comprehensive test coverage for ES256K and security validations
- Update Caddyfile with proper Host headers for DPoP htu matching
Security fixes:
- Validate JWK curve matches claimed algorithm (ES256K->secp256k1, ES256->P-256, etc.)
- Validate exp claim if present (with clock skew tolerance)
- Validate nbf claim if present (with clock skew tolerance)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
DPoP security improvements addressing PR review findings:
- feat(auth): comprehensive DPoP security improvements
- Access token hash (ath) validation per RFC 9449
- Proxy header support (X-Forwarded-Host, RFC 7239 Forwarded)
- EscapedPath for percent-encoded URLs
- Case-insensitive DPoP scheme per RFC 7235
- fix(auth): prevent goroutine leak from DPoP replay cache
- Graceful server shutdown with signal handling
- Proper cleanup in integration tests
- docs: update authentication documentation for DPoP scheme
Add comment clarifying that PDS uploadBlob calls use Bearer scheme
(standard atproto server auth) rather than DPoP (AppView client auth).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update documentation to reflect the transition from Bearer tokens
to DPoP-bound tokens for client authentication:
- federation-prd.md: Update auth examples to use DPoP scheme
- Add note about Bearer vs DPoP for server-to-server auth
- Update request examples with DPoP header
- COMMENT_SYSTEM_IMPLEMENTATION.md: Update auth references
- "Bearer token" → "DPoP-bound access token"
- Document DPoP proof validation in OptionalAuth
- FEED_SYSTEM_IMPLEMENTATION.md: Update curl examples
- Add DPoP header alongside Authorization header
- Update auth requirement description
- PRD_OAUTH.md, aggregators/SETUP_GUIDE.md, auth/README.md:
- Minor terminology updates for consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The DPoP verifier starts a background goroutine for nonce cache cleanup.
Without calling Stop(), this goroutine persists and accumulates across
server reloads and test runs.
Changes:
- cmd/server/main.go: Add graceful shutdown with signal handling
- Listen for SIGINT/SIGTERM
- Call authMiddleware.Stop() during shutdown
- Use http.Server.Shutdown() for graceful connection draining
- Integration tests: Add defer authMiddleware.Stop() after creation
- user_journey_e2e_test.go
- post_e2e_test.go
- community_e2e_test.go
- aggregator_e2e_test.go
- jwt_verification_test.go (2 locations)
This prevents NonceCache cleanup goroutines from leaking in both
production and test environments.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit addresses multiple security findings from PR review:
1. Access Token Hash (ath) Validation (RFC 9449 Section 4.2)
- Added VerifyAccessTokenHash() to verify DPoP proof's ath claim
- If ath is present, it MUST match SHA-256 hash of access token
- Prevents proof reuse across different tokens
2. Proxy Header Support for htu Verification
- Added extractSchemeAndHost() for X-Forwarded-Proto/Host support
- RFC 7239 Forwarded header parsing with mixed-case keys and quotes
- Critical for DPoP verification behind TLS-terminating proxies
3. Percent-Encoded Path Handling
- Use r.URL.EscapedPath() instead of r.URL.Path
- Preserves percent-encoding for accurate htu matching
4. Case-Insensitive DPoP Scheme (RFC 7235)
- Added extractDPoPToken() helper with strings.EqualFold()
- Accepts "DPoP", "dpop", "DPOP" per HTTP auth spec
Tests added for all security improvements:
- TestVerifyDPoPBinding_UsesForwardedHost
- TestVerifyDPoPBinding_UsesStandardForwardedHeader
- TestVerifyDPoPBinding_ForwardedMixedCaseAndQuotes
- TestVerifyDPoPBinding_AthValidation
- TestRequireAuth_CaseInsensitiveScheme
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The latest indigo (Nov 27, 2025) requires Go 1.25 which isn't available
in Docker Hub yet. Pin to the Oct 10, 2025 commit which is the last
Go 1.24-compatible version.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The local Go 1.25.1 is a pre-release version not available in Docker Hub.
Set minimum go version to 1.24 with toolchain directive for local dev.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement DPoP (RFC 9449) token binding for OAuth access tokens.
Features:
- DPoP proof verification with ES256 signing
- NonceCache for jti-based replay protection
- JWK thumbprint calculation per RFC 7638
- Middleware integration with Stop() for clean shutdown
- X-Forwarded-Proto support for reverse proxy deployments
Security:
- DPoP is additional security, never a fallback
- Tokens with cnf.jkt require valid DPoP proof
- 5-minute proof validity window
- Replay attack prevention via jti tracking
- Add github.com/google/uuid for DPoP proof jti generation
- Add .cache/ to .gitignore for Go build cache
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Document DPoP token binding implementation:
- Explain DPoP security model and why it's not a fallback
- Add flow diagrams for DPoP verification process
- Document replay protection with NonceCache
- Add code examples for DPoP verification
- List implemented security features and future enhancements
🤖 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>
- OAuth client for atproto authentication flow
- Session store with CSRF protection and secure token sealing
- Mobile-specific handlers with Universal Links redirect
- Database migrations for OAuth sessions and CSRF tokens
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add secp256k1 (ES256K) support to JWT access token verification using
Bluesky's indigo crypto package. This enables authentication from
external PDSes that use ES256K-signed tokens.
Changes:
- jwt.go: Add ES256K detection and verification using indigo's crypto
- New verifyES256KToken() for ES256K-specific verification
- New parseJWKMapToIndigoPublicKey() to convert JWK to indigo key
- New verifyJWTSignatureWithIndigoKey() for indigo signature verification
- New parseJWTClaimsManually() to parse claims without golang-jwt
- Update ToPublicKey() to return JWK map for secp256k1 curves
- did_key_fetcher.go: Return indigo PublicKey for secp256k1 keys
- FetchPublicKey now returns indigoCrypto.PublicKey for secp256k1
- NIST curves (P-256, P-384, P-521) still return *ecdsa.PublicKey
This complements the DPoP ES256K support added earlier, completing
full ES256K support across the authentication stack.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add ES256K (secp256k1) algorithm support using indigo's crypto package
- Add algorithm-curve binding validation to prevent algorithm confusion attacks
- Restore exp/nbf claim validation for DPoP proofs (security regression fix)
- Replace golang-jwt parsing with manual JWT parsing to support ES256K
- Add comprehensive test coverage for ES256K and security validations
- Update Caddyfile with proper Host headers for DPoP htu matching
Security fixes:
- Validate JWK curve matches claimed algorithm (ES256K->secp256k1, ES256->P-256, etc.)
- Validate exp claim if present (with clock skew tolerance)
- Validate nbf claim if present (with clock skew tolerance)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
DPoP security improvements addressing PR review findings:
- feat(auth): comprehensive DPoP security improvements
- Access token hash (ath) validation per RFC 9449
- Proxy header support (X-Forwarded-Host, RFC 7239 Forwarded)
- EscapedPath for percent-encoded URLs
- Case-insensitive DPoP scheme per RFC 7235
- fix(auth): prevent goroutine leak from DPoP replay cache
- Graceful server shutdown with signal handling
- Proper cleanup in integration tests
- docs: update authentication documentation for DPoP scheme
Update documentation to reflect the transition from Bearer tokens
to DPoP-bound tokens for client authentication:
- federation-prd.md: Update auth examples to use DPoP scheme
- Add note about Bearer vs DPoP for server-to-server auth
- Update request examples with DPoP header
- COMMENT_SYSTEM_IMPLEMENTATION.md: Update auth references
- "Bearer token" → "DPoP-bound access token"
- Document DPoP proof validation in OptionalAuth
- FEED_SYSTEM_IMPLEMENTATION.md: Update curl examples
- Add DPoP header alongside Authorization header
- Update auth requirement description
- PRD_OAUTH.md, aggregators/SETUP_GUIDE.md, auth/README.md:
- Minor terminology updates for consistency
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The DPoP verifier starts a background goroutine for nonce cache cleanup.
Without calling Stop(), this goroutine persists and accumulates across
server reloads and test runs.
Changes:
- cmd/server/main.go: Add graceful shutdown with signal handling
- Listen for SIGINT/SIGTERM
- Call authMiddleware.Stop() during shutdown
- Use http.Server.Shutdown() for graceful connection draining
- Integration tests: Add defer authMiddleware.Stop() after creation
- user_journey_e2e_test.go
- post_e2e_test.go
- community_e2e_test.go
- aggregator_e2e_test.go
- jwt_verification_test.go (2 locations)
This prevents NonceCache cleanup goroutines from leaking in both
production and test environments.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit addresses multiple security findings from PR review:
1. Access Token Hash (ath) Validation (RFC 9449 Section 4.2)
- Added VerifyAccessTokenHash() to verify DPoP proof's ath claim
- If ath is present, it MUST match SHA-256 hash of access token
- Prevents proof reuse across different tokens
2. Proxy Header Support for htu Verification
- Added extractSchemeAndHost() for X-Forwarded-Proto/Host support
- RFC 7239 Forwarded header parsing with mixed-case keys and quotes
- Critical for DPoP verification behind TLS-terminating proxies
3. Percent-Encoded Path Handling
- Use r.URL.EscapedPath() instead of r.URL.Path
- Preserves percent-encoding for accurate htu matching
4. Case-Insensitive DPoP Scheme (RFC 7235)
- Added extractDPoPToken() helper with strings.EqualFold()
- Accepts "DPoP", "dpop", "DPOP" per HTTP auth spec
Tests added for all security improvements:
- TestVerifyDPoPBinding_UsesForwardedHost
- TestVerifyDPoPBinding_UsesStandardForwardedHeader
- TestVerifyDPoPBinding_ForwardedMixedCaseAndQuotes
- TestVerifyDPoPBinding_AthValidation
- TestRequireAuth_CaseInsensitiveScheme
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement DPoP (RFC 9449) token binding for OAuth access tokens.
Features:
- DPoP proof verification with ES256 signing
- NonceCache for jti-based replay protection
- JWK thumbprint calculation per RFC 7638
- Middleware integration with Stop() for clean shutdown
- X-Forwarded-Proto support for reverse proxy deployments
Security:
- DPoP is additional security, never a fallback
- Tokens with cnf.jkt require valid DPoP proof
- 5-minute proof validity window
- Replay attack prevention via jti tracking
- Add github.com/google/uuid for DPoP proof jti generation
- Add .cache/ to .gitignore for Go build cache
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Document DPoP token binding implementation:
- Explain DPoP security model and why it's not a fallback
- Add flow diagrams for DPoP verification process
- Document replay protection with NonceCache
- Add code examples for DPoP verification
- List implemented security features and future enhancements
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>