commits
New Tests:
- TestGetCommunityFeed_BasicRetrieval: Verifies feed endpoint works
- TestGetCommunityFeed_SortOrders: Tests hot/new/top algorithms
- TestGetCommunityFeed_Pagination: Validates cursor-based paging
- TestGetCommunityFeed_EmptyFeed: Handles communities with no posts
- TestGetCommunityFeed_LimitValidation: Ensures limit clamping works
Test Fix:
- community_consumer_test.go: Changed collection from app.bsky.feed.post
to app.bsky.communityFeed.post to better demonstrate non-Coves namespace
filtering (the test verifies we ignore non-social.coves.community.* events)
Test Coverage:
- Feed retrieval with real post data
- Sort algorithm behavior
- Pagination edge cases
- Input validation
- Error handling
All tests use the shared test infrastructure (SetupTestDB, test user helpers)
and follow integration test patterns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Comprehensive documentation for feed systems and future aggregator features:
New Documentation:
- COMMUNITY_FEEDS.md: Complete guide to feed architecture and implementation
- aggregators/PRD_AGGREGATORS.md: Product spec for RSS/aggregator features
- aggregators/PRD_KAGI_NEWS_RSS.md: Kagi News integration design
Updated:
- PRD_POSTS.md: Refined post creation flow and security model
Feed Documentation Coverage:
- Architecture overview (service → repo → postgres)
- Sort algorithms (hot, new, top)
- Query optimization and indexing strategy
- Security considerations
- API examples and usage
Aggregator PRDs:
- RSS feed generation per community
- External content aggregation
- Kagi News integration patterns
- Federation considerations
These docs provide context for current feed implementation and roadmap
for future aggregator features.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extends post types to support feed views and updates lexicon definitions:
Post Types Added:
- PostView: Full post representation with all metadata for feeds
- AuthorView: Author info (DID, handle, displayName, avatar, reputation)
- CommunityRef: Minimal community reference in posts
- PostStats: Aggregated statistics (votes, comments, shares)
- ViewerState: User's relationship with post (votes, saves, tags)
Lexicon Updates:
- social.coves.post.get: Restructured postView definition
- social.coves.feed.getCommunity: Updated query parameters and response
These types align with atProto patterns:
- Follows app.bsky.feed.defs#feedViewPost structure
- Supports viewer-specific state
- Enables efficient feed rendering
- Provides all data clients need in single request
The PostView types are used by feed endpoints to return rich post data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Integrates community feed functionality into the main server:
- Initialize CommunityFeedRepository with database connection
- Initialize CommunityFeedService with dependencies
- Register feed XRPC routes (public endpoints)
- Add startup logging for feed service
Dependency injection flow:
feedRepo ← db
feedService ← feedRepo + communityService
feedHandler ← feedService
Routes registered:
- GET /xrpc/social.coves.feed.getCommunity (public)
The feed service is now live and ready to serve community feeds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements the HTTP handler layer for social.coves.feed.getCommunity:
- GetCommunityHandler: XRPC endpoint handler with proper validation
- Query parameter parsing: community, sort, limit, cursor
- Error handling: Proper XRPC error responses (InvalidRequest, NotFound)
- Route registration: Public endpoint (no auth required for reading)
Security:
- Input validation for all query parameters
- Limit clamping (max 100 posts per request)
- Community existence verification
- No sensitive data exposure in error messages
Handler flow:
1. Parse and validate query parameters
2. Call feed service
3. Transform to XRPC response format
4. Return JSON with proper headers
This implements the read path for community feeds per atProto patterns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements the core service layer and database repository for community feeds:
- CommunityFeedService: Orchestrates feed retrieval with sorting and pagination
- FeedRepository: PostgreSQL queries for post aggregation and filtering
- Feed types: FeedOptions, FeedResponse, SortOrder enums
- Error types: Proper error handling for feed operations
Architecture:
- Service layer handles business logic and coordinates with community service
- Repository performs efficient SQL queries with proper indexing
- Supports multiple sort algorithms (hot, new, top)
- Pagination via cursor-based approach
Security:
- All queries use parameterized statements
- Input validation in service layer
- Proper error wrapping for debugging
This is the foundation for social.coves.feed.getCommunity XRPC endpoint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements Alpha post creation feature with full write-forward to community
PDS and real-time Jetstream indexing.
Features:
- POST /xrpc/social.coves.post.create endpoint
- Write-forward architecture (posts written to community's PDS repository)
- Real-time AppView indexing via Jetstream consumer
- Comprehensive security validation (auth, repository ownership, FK integrity)
- Support for all 4 at-identifier formats (DIDs, canonical, @-prefixed, scoped)
- Database schema with proper indexing (migration 011)
- Full integration test suite (service, repository, handler, E2E with live PDS)
Implementation:
- Domain layer: Post models, service, validation
- Repository layer: PostgreSQL with JSON support
- Handler layer: XRPC endpoint with OAuth auth
- Consumer layer: Jetstream real-time indexing with security checks
- 13 commits, 30+ files, ~2,700 lines
Deferred to Beta:
- Content rules validation
- Post read operations (get, list)
- Post update/delete operations
- Voting system
See docs/PRD_POSTS.md for complete status.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update lexicon validation tests to handle post record types:
- Add social.coves.post.record to test cases
- Verify at-identifier format for community field
- Validate author field (required DID)
Ensures lexicon validation works correctly for new post records.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Move duplicate helper functions from multiple test files to helpers.go:
- authenticateWithPDS() - Used by post e2e tests
- contains() / anySubstring() - String utilities
- Import standardization across test files
Benefits:
- Eliminates code duplication across 6+ test files
- Centralizes test utilities for easier maintenance
- Improves test readability (focus on test logic, not helpers)
All tests continue to pass with consolidated helpers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Minor formatting cleanup:
- Align struct field comments consistently
- No functional changes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add SetTestUserDID() function to inject user DID into context for testing.
Purpose:
- Mock authenticated users in integration tests without full OAuth flow
- Used by post handler tests to simulate authenticated requests
- Marked with comment: "ONLY be used in tests"
This enables testing authenticated endpoints (like post creation)
without requiring real PDS authentication in test environment.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update PRD_POSTS.md with implementation status:
- Add "Implementation Status" section showing completed work
- Mark Alpha CREATE features as complete (✅)
- Mark Beta features as deferred (⚠️)
- Update all sections with checkmarks and status
- Add database schema status (migration 011 complete)
- Update success metrics (Alpha checklist complete)
- Reference IMPLEMENTATION_POST_CREATION.md for details
Completed (Alpha):
✅ Post creation endpoint with write-forward to community PDS
✅ Handler with authentication, validation, security checks
✅ Service layer with token refresh and community resolution
✅ PostgreSQL repository with proper indexing
✅ Jetstream consumer for real-time indexing
✅ E2E tests (service, repository, handler, live PDS+Jetstream)
✅ All 4 at-identifier formats supported
Deferred (Beta):
⚠️ Content rules validation
⚠️ Post read operations (get, list)
⚠️ Post update/edit operations
⚠️ Post deletion
⚠️ Voting system
Update other PRDs:
- PRD_BACKLOG: Add post creation to completed items
- PRD_COMMUNITIES: Reference post integration
- PRD_GOVERNANCE: Note content rules deferred to Beta
PRDs now accurately reflect codebase state.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add social.coves.post lexicon definitions:
1. social.coves.post.record
- Post record schema for community repositories
- Fields: community (at-identifier), author (did), title, content
- Rich text support: facets for mentions/links
- Embed support: images, video, external, record
- Content labels: nsfw, spoiler, violence
- Federation fields: originalAuthor, federatedFrom (future)
- Location support (future)
- Author field REQUIRED (added after PR review)
2. social.coves.post.create
- XRPC procedure for post creation
- Input: matches record schema (minus author - server-populated)
- Output: uri (AT-URI), cid (content ID)
- Errors: InvalidRequest, AuthRequired, NotAuthorized, Banned
3. social.coves.post.get
- XRPC query for fetching single post (future)
- Input: uri (AT-URI)
- Output: post view with stats
Update community profile and feed lexicons:
- Reference post record type
- Update descriptions for post integration
All lexicons follow atProto conventions and use at-identifier
format for community references (supports DIDs and handles).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add 4 test files covering full post creation flow:
1. post_creation_test.go - Service layer tests (11 subtests)
- Happy path with DID and handle resolution
- Validation: missing fields, invalid formats, length limits
- Content label validation (nsfw, spoiler, violence)
- Repository tests: create, duplicate URI handling
2. post_e2e_test.go - TRUE end-to-end test
- Part 1: Write-forward to live PDS
- Part 2: Real Jetstream WebSocket consumption
- Verifies complete cycle: HTTP → PDS → Jetstream → AppView DB
- Tests ~1 second indexing latency
- Requires live PDS and Jetstream services
3. post_handler_test.go - Handler security tests (10+ subtests)
- Reject client-provided authorDid (impersonation prevention)
- Require authentication (401 on missing token)
- Request body size limit (1MB DoS prevention)
- Malformed JSON handling
- All 4 at-identifier formats (DIDs, canonical, @-prefixed, scoped)
- Unicode/emoji support
- SQL injection prevention
4. helpers.go - Test utilities
- JWT token generation for test users
All tests passing. Coverage includes security, validation,
business logic, and real-time indexing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Wire up post creation feature in main server:
1. Initialize post service
- Create post repository (PostgreSQL)
- Create post service with community service integration
- Configure with default PDS URL
2. Register XRPC routes
- POST /xrpc/social.coves.post.create
- Requires OAuth authentication via middleware
3. Start Jetstream consumer for posts
- WebSocket URL: ws://localhost:6008/subscribe
- Collection filter: social.coves.post.record
- Runs in background goroutine
- Indexes CREATE operations (UPDATE/DELETE deferred)
Environment variables:
- POST_JETSTREAM_URL: Override default Jetstream endpoint
Initialization order ensures postRepo created before consumer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Export community service methods for post creation:
- EnsureFreshToken() - Auto-refresh PDS tokens before write operations
- GetByDID() - Direct repository access for post service
These methods enable posts service to:
1. Fetch community from AppView by DID
2. Ensure fresh PDS credentials before writing to community repo
3. Use community's access token for PDS write-forward
Changes:
- Made ensureFreshToken() public as EnsureFreshToken()
- Added GetByDID() wrapper for repository access
- No functional changes, just visibility
Supports write-forward architecture where posts are written to
community's PDS repository using community's credentials.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add PostEventConsumer for AppView indexing:
- Listen for social.coves.post.record CREATE events via WebSocket
- Parse post records from Jetstream firehose
- Index posts into AppView database
- Handle UPDATE/DELETE deferred until those features exist
Security validation:
- Verify repository DID matches community DID (prevents fake posts)
- Verify community exists in AppView (foreign key integrity)
- Verify author exists in AppView (foreign key integrity)
- Idempotent indexing (safe for Jetstream replays)
Add PostJetstreamConnector:
- Dedicated WebSocket connector for post events
- Collection filter: social.coves.post.record
- Separate from CommunityJetstreamConnector (different event types)
Posts are indexed with ~1 second latency from PDS write.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add POST /xrpc/social.coves.post.create endpoint:
- Authentication required via OAuth middleware
- Validate request body size (1MB limit for DoS prevention)
- Validate at-identifier format for community field
- Reject client-provided authorDid (security)
- Set authorDid from authenticated JWT
- Error mapping for lexicon-compliant responses
Handler security features:
- Body size limit prevents DoS
- Author impersonation prevented
- All 4 at-identifier formats supported
- Proper error codes (400, 401, 403, 404, 500)
Route registration integrates with auth middleware.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add migration 011: posts table with indexes
- Add foreign keys to users and communities
- Add indexes for common query patterns (community feed, author, score)
- Add PostgreSQL repository implementation
- Add Create() and GetByURI() methods
- Add JSON serialization for facets, embeds, labels
Posts table supports:
- AT-URI, CID, rkey for atProto compliance
- Title, content, facets, embed, labels
- Vote counts and score (denormalized for performance)
- Soft delete with deleted_at timestamp
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add Post domain model with AppView database representation
- Add CreatePostRequest/Response for XRPC endpoint
- Add PostRecord for PDS write-forward
- Add Service and Repository interfaces
- Add error types (ValidationError, ContentRuleViolation)
- Add service implementation with PDS write-forward
- Add validation for content length, labels, at-identifiers
Part of Alpha post creation feature.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Complete implementation of community handle naming convention change
with comprehensive PR review fixes.
This merge includes:
- Critical bug fixes for identifier resolution
- Comprehensive test coverage (31 test cases, all passing)
- Database migration for .communities → .community transition
- Input validation and error handling improvements
- Support for self-hosted instances
All PR review comments addressed:
✅ GetDisplayHandle() bug fixed for multi-part domains
✅ Case sensitivity bug fixed in resolveScopedIdentifier
✅ Comprehensive input validation (DNS labels, domain format)
✅ 100% test coverage for new code
✅ Database migration script with rollback
✅ Improved error messages with identifier context
✅ NewPDSAccountProvisioner argument order corrected
✅ Removed hardcoded coves.social from tests
Test Results:
- 31 identifier resolution tests: PASS
- All integration tests: PASS
- E2E tests with real PDS: PASS
- Regression tests: PASS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update development environment configuration and Go module dependencies.
Changes:
- Update .env.dev with latest environment variables
- Update docker-compose.dev.yml configuration
- Run go mod tidy to clean up dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update Product Requirements Document to document the singular
.community. naming convention.
Changes:
- Update handle format examples
- Document identifier resolution features
- Update API examples with new handle format
This ensures documentation matches the implemented handle format
and provides clear examples for developers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update all integration tests to use singular .community. naming convention
instead of .communities.
Tests updated:
- E2E community creation and XRPC endpoint tests
- HostedBy security validation tests
- Community provisioning tests
- Service integration tests
- V2 validation tests
- Token refresh tests
Changes:
- Update expected handle formats in assertions
- Update test fixtures to use new convention
- Ensure regex patterns match .community. format
All tests passing with new handle format.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update PDS provisioning and Jetstream consumer to generate handles
using singular .community. instead of .communities.
Changes:
- PDSAccountProvisioner: Generate {name}.community.{domain} handles
- JetstreamConsumer: Parse and validate new handle format
- Update handle extraction logic for consistency
Example handle formats:
- gardening.community.coves.social
- gaming.community.coves.social
This maintains consistency with the lexicon schema update and aligns
with AT Protocol naming conventions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update AT Protocol lexicon schema and test data to use singular
.community. instead of .communities. in handle format.
Changes:
- Update profile.json lexicon schema with new handle pattern
- Update test fixtures to use singular convention
- Update lexicon validation tests
This aligns with AT Protocol naming standards where all record types
use singular form (e.g., app.bsky.feed.post, app.bsky.graph.follow).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add production-ready migration script to update existing community handles
from plural (.communities.) to singular (.community.) format.
Migration features:
- Safe UPDATE using REPLACE for targeted changes
- Verification checks to ensure migration completion
- Rollback script for emergency reversion
- Error handling with informative messages
Example transformations:
- gardening.communities.coves.social → gardening.community.coves.social
- gaming.communities.coves.social → gaming.community.coves.social
This migration is required for the .communities → .community naming
convention change to align with AT Protocol lexicon standards
(all record types use singular form).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add 31 test cases covering all identifier resolution code paths:
TestCommunityIdentifierResolution (14 E2E tests):
- DID format resolution (3 tests)
- Canonical handle format (3 tests)
- At-identifier format (2 tests)
- Scoped format !name@instance (5 tests)
- Edge cases (4 tests)
TestResolveScopedIdentifier_InputValidation (9 tests):
- Reject special characters, spaces, invalid DNS labels
- Reject names starting/ending with hyphens
- Reject names exceeding 63 character DNS limit
- Accept valid alphanumeric names with hyphens/numbers
- Validate domain format
TestGetDisplayHandle (5 tests):
- Standard two-part domains
- Multi-part TLDs (e.g., .co.uk)
- Subdomain instances
- Malformed input graceful fallback
TestIdentifierResolution_ErrorContext (3 tests):
- Verify error messages include identifier for debugging
- Verify DID, handle, and scoped errors provide context
Fixes:
- Use environment variables for configuration (no hardcoded coves.social)
- Correct NewPDSAccountProvisioner argument order (instanceDomain, pdsURL)
- Support self-hosted instances via INSTANCE_DOMAIN env var
All tests passing with real PDS integration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Critical fixes:
- Fix GetDisplayHandle() to handle multi-part domains correctly using string.Index
- Add DNS label validation (RFC 1035) with isValidDNSLabel helper
- Add domain format validation with isValidDomain helper
- Normalize instanceDomain to lowercase for case-insensitive lookup
- Improve error messages to include identifier context for debugging
Validation improvements:
- Reject special characters in community names
- Enforce DNS label length limits (1-63 chars)
- Prevent names starting/ending with hyphens
- Validate domain format before lookup
Bug fixes:
- GetDisplayHandle now correctly parses handles with multi-part TLDs (e.g., coves.co.uk)
- resolveScopedIdentifier properly normalizes domain case (!gardening@Coves.social works)
- Error messages now include the identifier that failed resolution
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This file was already removed in the feature branch but needs to be
cleaned up from the working directory.
Membership tracking is AppView-only data, not atProto records.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Comprehensive lexicon extensibility fixes to ensure alpha-readiness
and future-proof schema evolution without requiring V2 migrations.
Key Changes:
- Fixed all closed enums → knownValues (moderationType, visibility, sort)
- Made moderator roles and permissions extensible
- Added authentication documentation to all endpoints
- Removed invalid membership record references
- Documented technical decisions in PRD_GOVERNANCE.md
This locks down the lexicon schemas for alpha while enabling future
beta features (sortition moderation, new visibility modes, moderator
tiers) without breaking changes.
Per atProto style guide (bluesky-social/atproto#4245): enum sets
cannot be extended without breaking schema evolution rules.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Membership tracking is AppView-only data, not atProto records.
Changes:
- Removed membershipUri field from community.get viewerState
- Updated member field description to clarify it's AppView-computed
- Removed membership lexicon file (already deleted)
- Removed membership test data files (already deleted)
Rationale:
- Membership/reputation is indexed from user activity, not explicit records
- No need for AT-URI reference to non-existent record type
- Clarifies that membership status is computed by AppView, not stored in repo
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Per atProto style guide: endpoint descriptions should mention if
authentication is required and whether responses are personalized.
Changes:
- create.json: Added "Requires authentication."
- update.json: Added "Requires authentication and moderator/admin permissions."
- subscribe.json: Added "Requires authentication."
- unsubscribe.json: Added "Requires authentication."
- get.json: Added "Authentication optional; viewer state will be included if authenticated."
- list.json: Added "Authentication optional; viewer state will be included if authenticated."
This improves developer experience by making auth requirements
explicit without requiring documentation lookup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
BREAKING: This is a pre-alpha schema fix. Must be applied before any
moderator records are created.
Changes to social.coves.community.moderator:
- Change role from enum to knownValues (enables future role types)
- Change permissions from enum to knownValues (enables new permissions)
- Add maxLength: 64 to both fields per atProto style guide
Future extensibility examples:
- Roles: "owner", "trainee", "emeritus"
- Permissions: "manage_bots", "manage_flairs", "manage_automoderator"
Documented in PRD_GOVERNANCE.md:
- Technical decision rationale
- atProto style guide reference
- Future beta phase extensibility plan
- Security considerations
This enables Beta Phase 2 (Moderator Tiers & Permissions) without
requiring V2 schema migration or breaking existing records.
Per atProto style guide (bluesky-social/atproto#4245): enum sets
cannot be extended without breaking schema evolution rules.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Change sort from closed enum to knownValues
- Add maxLength: 64 per atProto style guide
This enables future sort algorithms without breaking changes:
- "trending" - Recent activity spike detection
- "recommended" - Personalized AI recommendations
- "nearby" - Geo-based sorting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Change visibility from closed enum to knownValues
- Apply to community.profile (record), create, and update endpoints
- Add maxLength: 64 per atProto style guide
This enables future visibility modes without breaking changes:
- "followers-only" - Only subscribers can see
- "instance-only" - Only same-instance users
- "invite-only" - Requires invite code
Files changed:
- community/profile.json (record schema)
- community/create.json (procedure)
- community/update.json (procedure)
Per atProto style guide: closed enums block schema evolution.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Change moderationType from closed enum to knownValues
- Add to required fields (critical before alpha - can't add required later)
- Add default value "moderator" for alpha simplicity
- Add maxLength constraint per atProto style guide
This enables future moderation types without schema migration:
- "sortition" - Community tribunal (Beta Phase 1)
- "instance-labeler" - Instance moderation service
- "third-party-labeler" - External moderation DID
Per atProto style guide: enum sets cannot be extended without breaking
schema evolution. knownValues provides flexible alternative.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements Phase 1 did:web domain verification to prevent domain
impersonation attacks in the Coves federated community system.
This PR addresses all code review feedback across 3 rounds:
Round 1 - Performance & Security:
✅ P0: Multi-part TLD support (fixes .co.uk, .com.au blocking)
✅ HTTP client connection pooling
✅ Bounded LRU cache implementation
✅ Rate limiting for DoS protection
Round 2 - Critical Bug Fixes:
✅ Memory leak (unbounded cache → bounded LRU)
✅ Deadlock (manual locks → thread-safe LRU)
✅ Missing timeout (added 15s overall timeout)
Round 3 - Optimizations:
✅ Cache TTL cleanup (removes expired entries)
✅ Struct field alignment (performance)
✅ All linter issues resolved
Security Impact:
- Prevents malicious instances from claiming communities for domains
they don't control (e.g., evil.com claiming @gaming@nintendo.com)
- Verifies hostedBy domain matches community handle domain
- Optional .well-known/did.json verification for cryptographic proof
- Soft-fail on network errors (resilience)
Test Coverage:
- 13 new security test cases (all passing)
- 42+ total tests (all passing)
- Multi-part TLD support verified (.co.uk, .com.au, .org.uk, .ac.uk)
Code Quality:
✅ All linter checks passing
✅ All code properly formatted
✅ Clean build (no warnings)
✅ Production-ready
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
Adds comprehensive test coverage for hostedBy domain verification,
including multi-part TLD support and security attack scenarios.
Test Coverage:
TestHostedByVerification_DomainMatching:
- ✅ Rejects communities with mismatched hostedBy domains
- ✅ Accepts communities with matching hostedBy domains
- ✅ Rejects non-did:web format hostedBy values
- ✅ Skip verification flag bypasses all checks (dev mode)
TestExtractDomainFromHandle:
- ✅ DNS-style handles with subdomains
- ✅ Simple two-part domains
- ✅ Multi-part subdomains
- ✅ Multi-part TLD: .co.uk (critical fix validation)
- ✅ Multi-part TLD: .com.au (critical fix validation)
- ✅ Multi-part TLD: .org.uk, .ac.uk
- ✅ Correctly rejects incorrect TLD extraction (e.g., did:web:co.uk)
- ✅ Domain mismatch detection
Security Attack Scenarios Tested:
1. Domain impersonation (evil.com claiming nintendo.com) - BLOCKED
2. Non-did:web hostedBy spoofing - BLOCKED
3. Multi-part TLD domain extraction failures - FIXED
All tests passing (9/9 multi-part TLD tests).
Co-Authored-By: Claude <noreply@anthropic.com>
Updates all integration tests to use the new CommunityEventConsumer
constructor signature with instance DID and skip verification flag.
Changes:
- Updated 5 integration test files
- All tests use skipVerification=true to avoid network calls
- Tests use did:web:coves.local as instance DID
- Maintains existing test behavior and coverage
Files Updated:
- community_blocking_test.go
- community_consumer_test.go
- community_e2e_test.go
- community_v2_validation_test.go
- subscription_indexing_test.go
All existing tests continue to pass with no behavior changes.
Co-Authored-By: Claude <noreply@anthropic.com>
Integrates hostedBy verification into the server with environment-based
configuration for development and production use.
Changes:
- Added SKIP_DID_WEB_VERIFICATION env var for dev mode bypass
- Updated consumer initialization with instance DID and skip flag
- Added warning logs when verification is disabled
- Configured .env.dev with skip flag enabled for local development
Server logs will now show:
- "⚠️ WARNING: did:web verification DISABLED (dev mode)" when skipped
- "🚨 SECURITY: Rejecting community" when domain mismatch detected
Production Deployment:
- Set SKIP_DID_WEB_VERIFICATION=false or leave unset
- Ensure .well-known/did.json is properly configured
Co-Authored-By: Claude <noreply@anthropic.com>
Implements hostedBy verification to prevent domain impersonation attacks
where malicious instances claim to host communities for domains they don't
own (e.g., gaming@nintendo.com on non-Nintendo servers).
Core Implementation:
- Added verifyHostedByClaim() to validate hostedBy domain matches handle
- Integrated golang.org/x/net/publicsuffix for proper eTLD+1 extraction
- Supports multi-part TLDs (.co.uk, .com.au, .org.uk, etc.)
- Added verifyDIDDocument() for .well-known/did.json verification
- Bounded LRU cache (max 1000 entries) prevents memory leaks
- Thread-safe operations (no deadlock risk)
- HTTP client connection pooling for performance
- Rate limiting (10 req/sec) prevents DoS attacks
- 15-second timeout prevents consumer blocking
- Cache TTL cleanup removes expired entries
Security Features:
- Hard-fail on domain mismatch (blocks indexing)
- Soft-fail on .well-known errors (network resilience)
- Skip verification flag for development mode
- Optimized struct field alignment for performance
Breaking Changes: None
- Constructor signature updated but all tests migrated
Co-Authored-By: Claude <noreply@anthropic.com>
Implements automatic refresh of community PDS access tokens to prevent
401 errors after 2-hour token expiration. Includes comprehensive security
hardening through multiple review iterations.
## Core Features
- Proactive token refresh (5-minute buffer before expiration)
- Automatic fallback to password re-auth when refresh tokens expire
- Concurrent-safe per-community mutex protection
- Atomic credential updates with retry logic
- Comprehensive structured logging for observability
## Security Hardening (3 Review Rounds)
### Round 1: Initial PR Review Fixes
- Added DB update retry logic (3 attempts, exponential backoff)
- Improved error detection with typed xrpc.Error checking
- Added comprehensive unit tests (8 test cases for NeedsRefresh)
- Enhanced logging for JWT parsing failures
- Memory-bounded mutex cache with warning threshold
### Round 2: Critical Race Condition Fixes
- **CRITICAL:** Eliminated race condition in mutex eviction
- Removed eviction entirely to prevent mutex map corruption
- Added read-lock fast path for performance
- Implemented double-check locking pattern
- **CRITICAL:** Fixed test-production code path mismatch
- Eliminated wrapper function, single exported NeedsRefresh()
- Tests now validate actual production code
### Round 3: Code Quality & Linting
- Fixed struct field alignment (8-byte memory optimization)
- Removed unused functions (splitToken)
- Added proper error handling for deferred Close() calls
- All golangci-lint checks passing
## Implementation Details
**Token Refresh Flow:**
1. Check if access token expires within 5 minutes
2. Acquire per-community mutex (prevent concurrent refresh)
3. Re-fetch from DB (double-check pattern)
4. Attempt refresh using refresh token
5. Fallback to password re-auth if refresh token expired
6. Update DB atomically with retry logic (3 attempts)
7. Return updated community with fresh credentials
**Concurrency Safety:**
- Per-community mutexes (non-blocking for different communities)
- Double-check pattern prevents duplicate refreshes
- Atomic DB updates (access + refresh token together)
- Refresh tokens are single-use (atproto spec compliance)
**Files Changed:**
- internal/core/communities/service.go - Main orchestration
- internal/core/communities/token_refresh.go - Indigo SDK integration
- internal/core/communities/token_utils.go - JWT parsing utilities
- internal/core/communities/interfaces.go - Repository interface
- internal/db/postgres/community_repo.go - UpdateCredentials method
- tests/integration/token_refresh_test.go - Comprehensive tests
- docs/PRD_BACKLOG.md - Documented Alpha blocker resolution
- docs/PRD_COMMUNITIES.md - Updated with token refresh feature
## Testing
- 8 unit tests for token expiration detection (all passing)
- Integration tests for UpdateCredentials (all passing)
- E2E test framework ready for PDS integration
- All linters passing (golangci-lint)
- Build verification successful
## Observability
Structured logging with events:
- token_refresh_started, token_refreshed
- refresh_token_expired, password_fallback_success
- db_update_retry, token_parse_failed
- CRITICAL alerts for lockout conditions
## Risk Mitigation
Before: 🔴 HIGH RISK - Communities lockout after 2 hours
After: 🟢 LOW RISK - Automatic refresh with multiple safety layers
- Race conditions: ELIMINATED (no mutex eviction)
- DB failures: MITIGATED (3-retry with exponential backoff)
- Refresh token expiry: HANDLED (password fallback)
- Test coverage: COMPREHENSIVE (unit + integration)
- Memory leaks: PREVENTED (warning at 10k communities, acceptable at 1M)
## Production Ready
✅ All critical issues resolved
✅ All tests passing
✅ All linters passing
✅ Comprehensive error handling
✅ Security hardened through 3 review rounds
Resolves Alpha blocker: Communities can now be updated indefinitely
without manual token management.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Fix P1 issue: properly bubble up database errors instead of masking as conflict
* Only return ErrBlockAlreadyExists when getErr is ErrBlockNotFound (race condition)
* Real DB errors (outages, connection failures) now propagate to operators
- Remove unused V1 functions flagged by linter:
* createRecordOnPDS, deleteRecordOnPDS, callPDS (replaced by *As versions)
- Apply automatic code formatting via golangci-lint --fix:
* Align struct field tags in CommunityBlock
* Fix comment alignment across test files
* Remove trailing whitespace
- All tests passing, linter clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes four issues identified in PR review:
**BUG 1 - Performance: Remove redundant database query**
- Removed duplicate GetByDID call in BlockCommunity service method
- ResolveCommunityIdentifier already verifies community exists
- Reduces block operations from 2 DB queries to 1
**BUG 2 - Performance: Move regex compilation to package level**
- Moved DID validation regex to package-level variable in block.go
- Prevents recompiling regex on every block/unblock request
- Eliminates unnecessary CPU overhead on hot path
**BUG 3 - DRY: Remove duplicated extractRKeyFromURI**
- Removed duplicate implementations in service.go and tests
- Now uses shared utils.ExtractRKeyFromURI function
- Single source of truth for AT-URI parsing logic
**P1 - Critical: Fix duplicate block race condition**
- Added ErrBlockAlreadyExists error type
- Returns 409 Conflict instead of 500 when PDS has block but AppView hasn't indexed yet
- Handles normal race in eventually-consistent flow gracefully
- Prevents double-click scenarios from appearing as server failures
All tests passing (33.2s runtime, 100% pass rate).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Breaking Change**: XRPC endpoints now strictly enforce lexicon spec.
Changed endpoints to reject handles and accept ONLY DIDs:
- social.coves.community.blockCommunity
- social.coves.community.unblockCommunity
- social.coves.community.subscribe
- social.coves.community.unsubscribe
Rationale:
1. Lexicon defines "subject" field with format: "did" (not "at-identifier")
2. Records are immutable and content-addressed - must use permanent DIDs
3. Handles can change (they're DNS pointers), DIDs cannot
4. Bluesky's app.bsky.graph.block uses same pattern (DID-only)
Previous behavior accepted both DIDs and handles, resolving handles to
DIDs internally. This was convenient but violated the lexicon contract.
Impact:
- Clients must resolve handles to DIDs before calling these endpoints
- Matches standard atProto patterns for block/subscription records
- Ensures federation compatibility
This aligns our implementation with the lexicon specification and
atProto best practices.
Improve validation robustness in block/unblock handlers:
1. DID validation with regex:
- Pattern: ^did:(plc|web):[a-zA-Z0-9._:%-]+$
- Rejects invalid formats like "did:x" or "did:"
- Ensures only supported DID methods (plc, web)
2. Handle validation:
- Verify handle contains @ symbol for domain
- Rejects incomplete handles like "!" or "!name"
- Ensures proper format: !name@domain.tld
Previous validation only checked prefix, allowing invalid values
to pass through to service layer. New validation catches format
errors early with clear error messages.
Addresses: Important review comment #4
Service layer improvements:
1. Add DID verification in ResolveCommunityIdentifier:
- When a DID is provided, verify the community actually exists
in the AppView database
- Prevents accepting non-existent DIDs (e.g., did:plc:fakefake)
- Provides clearer error messages when community doesn't exist
2. Improve duplicate error detection in BlockCommunity:
- Check for HTTP 409 Conflict status code explicitly
- Added "status 409" check in addition to text-based detection
- More robust across different PDS implementations
- Still maintains fallback checks for compatibility
Both changes improve error handling and user experience while
maintaining backward compatibility.
Addresses: Critical review comment #2, Important review comment #3
Database optimization changes:
1. Removed redundant idx_blocks_user_community index:
- UNIQUE constraint on (user_did, community_did) already creates
an index automatically
- Maintaining duplicate index wastes storage and degrades write
performance (every insert updates two identical indexes)
2. Added missing idx_blocks_record_uri index:
- Required for GetBlockByURI() queries used in Jetstream DELETE
operations
- Without this index, DELETE event processing does full table scan
Migration now has optimal indexes without redundancy.
Addresses: Critical review comments #1 and #7
Critical bug fix: The loop variable 'block' was being reused for each
iteration, causing all elements in the returned slice to point to the
same memory location. This resulted in the last row being repeated for
every element when callers read the list.
Fixed by allocating a new block pointer for each iteration:
- Before: var block communities.CommunityBlock (reused)
- After: block := &communities.CommunityBlock{} (new allocation)
Also replaced fmt.Printf with log.Printf for consistency with project
logging standards.
Addresses: P1 review comment - pointer reuse in list operation
Change subscription lexicon subject field format from "at-identifier"
to "did" for consistency and correctness:
Before:
- format: "at-identifier" (accepts DIDs or handles)
- description: "DID or handle of the community"
After:
- format: "did" (only accepts DIDs)
- description: "DID of the community being subscribed to"
Rationale:
1. Matches block.json pattern (which correctly uses "did" format)
2. Aligns with service layer implementation (only supports DIDs)
3. Follows atProto convention: "subject" field references entities by DID
4. Prevents invalid handle values in federated records
This ensures subscription records are properly validated and compatible
with the broader atProto ecosystem.
E2E Tests (3 new test cases):
- Block via XRPC endpoint: Full flow from HTTP → PDS → Jetstream → AppView
- Unblock via XRPC endpoint: Complete unblock flow with DELETE event
- Block fails without authentication: Validates auth requirement (401)
Each E2E test verifies:
✓ XRPC endpoint responds correctly
✓ Record created/deleted on PDS
✓ Jetstream consumer indexes event
✓ AppView database state updated
Unit Test Updates:
- Added 6 mock methods to mockCommunityRepo for blocking operations
- Ensures service layer tests compile and pass
All tests follow existing E2E patterns (subscribe/unsubscribe) for
consistency.
Add 16 integration test cases covering:
1. Jetstream Consumer Indexing (4 tests):
- Block CREATE event indexing
- Block DELETE event indexing
- Idempotent duplicate event handling
- Graceful handling of non-existent block deletion
2. List Operations (3 tests):
- List all blocked communities for user
- Pagination with limit/offset
- Empty list for users with no blocks
3. IsBlocked Queries (3 tests):
- Returns false when not blocked
- Returns true when blocked
- Returns false after unblock
4. GetBlock Operations (3 tests):
- Error when block doesn't exist
- Retrieve block by user DID + community DID
- Retrieve block by AT-URI (for DELETE operations)
All tests verify proper database state, idempotency guarantees,
and Jetstream event processing.
Implement Jetstream consumer support for community block records:
- handleBlock: Routes CREATE/DELETE operations for social.coves.community.block
- createBlock: Indexes block CREATE events from firehose
- Extracts community DID from "subject" field (atProto convention)
- Builds AT-URI: at://user_did/social.coves.community.block/rkey
- Preserves createdAt timestamp for chronological ordering during replays
- Idempotent: handles duplicate events via ON CONFLICT
- deleteBlock: Processes block DELETE events from firehose
- Looks up block by URI (DELETE events don't include record data)
- Removes from AppView index
- Gracefully handles deletion of non-existent blocks
Completes the write-forward flow:
Client → PDS → Jetstream Firehose → Consumer → AppView DB
Register community blocking XRPC endpoints:
- POST /xrpc/social.coves.community.blockCommunity (requires auth)
- POST /xrpc/social.coves.community.unblockCommunity (requires auth)
Both routes use RequireAuth middleware to ensure proper authentication
before allowing block/unblock operations.
Add XRPC handlers for community blocking endpoints:
- HandleBlock: POST /xrpc/social.coves.community.blockCommunity
- HandleUnblock: POST /xrpc/social.coves.community.unblockCommunity
Features:
- Input validation: Community must be DID (did:plc:...) or handle (!name@instance)
- Authentication: Requires user DID and access token from middleware
- Response format: Follows atProto conventions with recordUri/recordCid
- Error handling: Uses shared handleServiceError for consistency
Addresses PR review comment on input validation.
Implement service layer for community blocking following atProto
write-forward architecture:
- BlockCommunity: Creates block record on PDS using user's access token,
handles duplicate errors gracefully by fetching existing block
- UnblockCommunity: Deletes block record from PDS, extracts rkey from URI
- GetBlockedCommunities: Queries AppView with pagination
- IsBlocked: Fast boolean check for block status
Key architectural decisions:
- Write-forward pattern: All mutations go through PDS first
- Race condition fix: Removed preemptive existence check, rely on PDS
duplicate detection + repository ON CONFLICT handling
- User authentication: Uses user's access token (not instance token)
- Identifier resolution: Supports both DIDs and handles via
resolveCommunityIdentifier
Resolves race condition identified in PR review.
Add core domain support for community blocking feature:
- CommunityBlock struct with proper atProto metadata (RecordURI, RecordCID)
- ErrBlockNotFound error constant
- Repository interface methods: BlockCommunity, UnblockCommunity, GetBlock,
GetBlockByURI, ListBlockedCommunities, IsBlocked
- Service interface methods: BlockCommunity, UnblockCommunity,
GetBlockedCommunities, IsBlocked
This establishes the domain layer contracts that will be implemented
in subsequent commits following clean architecture principles.
New Tests:
- TestGetCommunityFeed_BasicRetrieval: Verifies feed endpoint works
- TestGetCommunityFeed_SortOrders: Tests hot/new/top algorithms
- TestGetCommunityFeed_Pagination: Validates cursor-based paging
- TestGetCommunityFeed_EmptyFeed: Handles communities with no posts
- TestGetCommunityFeed_LimitValidation: Ensures limit clamping works
Test Fix:
- community_consumer_test.go: Changed collection from app.bsky.feed.post
to app.bsky.communityFeed.post to better demonstrate non-Coves namespace
filtering (the test verifies we ignore non-social.coves.community.* events)
Test Coverage:
- Feed retrieval with real post data
- Sort algorithm behavior
- Pagination edge cases
- Input validation
- Error handling
All tests use the shared test infrastructure (SetupTestDB, test user helpers)
and follow integration test patterns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Comprehensive documentation for feed systems and future aggregator features:
New Documentation:
- COMMUNITY_FEEDS.md: Complete guide to feed architecture and implementation
- aggregators/PRD_AGGREGATORS.md: Product spec for RSS/aggregator features
- aggregators/PRD_KAGI_NEWS_RSS.md: Kagi News integration design
Updated:
- PRD_POSTS.md: Refined post creation flow and security model
Feed Documentation Coverage:
- Architecture overview (service → repo → postgres)
- Sort algorithms (hot, new, top)
- Query optimization and indexing strategy
- Security considerations
- API examples and usage
Aggregator PRDs:
- RSS feed generation per community
- External content aggregation
- Kagi News integration patterns
- Federation considerations
These docs provide context for current feed implementation and roadmap
for future aggregator features.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extends post types to support feed views and updates lexicon definitions:
Post Types Added:
- PostView: Full post representation with all metadata for feeds
- AuthorView: Author info (DID, handle, displayName, avatar, reputation)
- CommunityRef: Minimal community reference in posts
- PostStats: Aggregated statistics (votes, comments, shares)
- ViewerState: User's relationship with post (votes, saves, tags)
Lexicon Updates:
- social.coves.post.get: Restructured postView definition
- social.coves.feed.getCommunity: Updated query parameters and response
These types align with atProto patterns:
- Follows app.bsky.feed.defs#feedViewPost structure
- Supports viewer-specific state
- Enables efficient feed rendering
- Provides all data clients need in single request
The PostView types are used by feed endpoints to return rich post data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Integrates community feed functionality into the main server:
- Initialize CommunityFeedRepository with database connection
- Initialize CommunityFeedService with dependencies
- Register feed XRPC routes (public endpoints)
- Add startup logging for feed service
Dependency injection flow:
feedRepo ← db
feedService ← feedRepo + communityService
feedHandler ← feedService
Routes registered:
- GET /xrpc/social.coves.feed.getCommunity (public)
The feed service is now live and ready to serve community feeds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements the HTTP handler layer for social.coves.feed.getCommunity:
- GetCommunityHandler: XRPC endpoint handler with proper validation
- Query parameter parsing: community, sort, limit, cursor
- Error handling: Proper XRPC error responses (InvalidRequest, NotFound)
- Route registration: Public endpoint (no auth required for reading)
Security:
- Input validation for all query parameters
- Limit clamping (max 100 posts per request)
- Community existence verification
- No sensitive data exposure in error messages
Handler flow:
1. Parse and validate query parameters
2. Call feed service
3. Transform to XRPC response format
4. Return JSON with proper headers
This implements the read path for community feeds per atProto patterns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements the core service layer and database repository for community feeds:
- CommunityFeedService: Orchestrates feed retrieval with sorting and pagination
- FeedRepository: PostgreSQL queries for post aggregation and filtering
- Feed types: FeedOptions, FeedResponse, SortOrder enums
- Error types: Proper error handling for feed operations
Architecture:
- Service layer handles business logic and coordinates with community service
- Repository performs efficient SQL queries with proper indexing
- Supports multiple sort algorithms (hot, new, top)
- Pagination via cursor-based approach
Security:
- All queries use parameterized statements
- Input validation in service layer
- Proper error wrapping for debugging
This is the foundation for social.coves.feed.getCommunity XRPC endpoint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements Alpha post creation feature with full write-forward to community
PDS and real-time Jetstream indexing.
Features:
- POST /xrpc/social.coves.post.create endpoint
- Write-forward architecture (posts written to community's PDS repository)
- Real-time AppView indexing via Jetstream consumer
- Comprehensive security validation (auth, repository ownership, FK integrity)
- Support for all 4 at-identifier formats (DIDs, canonical, @-prefixed, scoped)
- Database schema with proper indexing (migration 011)
- Full integration test suite (service, repository, handler, E2E with live PDS)
Implementation:
- Domain layer: Post models, service, validation
- Repository layer: PostgreSQL with JSON support
- Handler layer: XRPC endpoint with OAuth auth
- Consumer layer: Jetstream real-time indexing with security checks
- 13 commits, 30+ files, ~2,700 lines
Deferred to Beta:
- Content rules validation
- Post read operations (get, list)
- Post update/delete operations
- Voting system
See docs/PRD_POSTS.md for complete status.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update lexicon validation tests to handle post record types:
- Add social.coves.post.record to test cases
- Verify at-identifier format for community field
- Validate author field (required DID)
Ensures lexicon validation works correctly for new post records.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Move duplicate helper functions from multiple test files to helpers.go:
- authenticateWithPDS() - Used by post e2e tests
- contains() / anySubstring() - String utilities
- Import standardization across test files
Benefits:
- Eliminates code duplication across 6+ test files
- Centralizes test utilities for easier maintenance
- Improves test readability (focus on test logic, not helpers)
All tests continue to pass with consolidated helpers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add SetTestUserDID() function to inject user DID into context for testing.
Purpose:
- Mock authenticated users in integration tests without full OAuth flow
- Used by post handler tests to simulate authenticated requests
- Marked with comment: "ONLY be used in tests"
This enables testing authenticated endpoints (like post creation)
without requiring real PDS authentication in test environment.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update PRD_POSTS.md with implementation status:
- Add "Implementation Status" section showing completed work
- Mark Alpha CREATE features as complete (✅)
- Mark Beta features as deferred (⚠️)
- Update all sections with checkmarks and status
- Add database schema status (migration 011 complete)
- Update success metrics (Alpha checklist complete)
- Reference IMPLEMENTATION_POST_CREATION.md for details
Completed (Alpha):
✅ Post creation endpoint with write-forward to community PDS
✅ Handler with authentication, validation, security checks
✅ Service layer with token refresh and community resolution
✅ PostgreSQL repository with proper indexing
✅ Jetstream consumer for real-time indexing
✅ E2E tests (service, repository, handler, live PDS+Jetstream)
✅ All 4 at-identifier formats supported
Deferred (Beta):
⚠️ Content rules validation
⚠️ Post read operations (get, list)
⚠️ Post update/edit operations
⚠️ Post deletion
⚠️ Voting system
Update other PRDs:
- PRD_BACKLOG: Add post creation to completed items
- PRD_COMMUNITIES: Reference post integration
- PRD_GOVERNANCE: Note content rules deferred to Beta
PRDs now accurately reflect codebase state.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add social.coves.post lexicon definitions:
1. social.coves.post.record
- Post record schema for community repositories
- Fields: community (at-identifier), author (did), title, content
- Rich text support: facets for mentions/links
- Embed support: images, video, external, record
- Content labels: nsfw, spoiler, violence
- Federation fields: originalAuthor, federatedFrom (future)
- Location support (future)
- Author field REQUIRED (added after PR review)
2. social.coves.post.create
- XRPC procedure for post creation
- Input: matches record schema (minus author - server-populated)
- Output: uri (AT-URI), cid (content ID)
- Errors: InvalidRequest, AuthRequired, NotAuthorized, Banned
3. social.coves.post.get
- XRPC query for fetching single post (future)
- Input: uri (AT-URI)
- Output: post view with stats
Update community profile and feed lexicons:
- Reference post record type
- Update descriptions for post integration
All lexicons follow atProto conventions and use at-identifier
format for community references (supports DIDs and handles).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add 4 test files covering full post creation flow:
1. post_creation_test.go - Service layer tests (11 subtests)
- Happy path with DID and handle resolution
- Validation: missing fields, invalid formats, length limits
- Content label validation (nsfw, spoiler, violence)
- Repository tests: create, duplicate URI handling
2. post_e2e_test.go - TRUE end-to-end test
- Part 1: Write-forward to live PDS
- Part 2: Real Jetstream WebSocket consumption
- Verifies complete cycle: HTTP → PDS → Jetstream → AppView DB
- Tests ~1 second indexing latency
- Requires live PDS and Jetstream services
3. post_handler_test.go - Handler security tests (10+ subtests)
- Reject client-provided authorDid (impersonation prevention)
- Require authentication (401 on missing token)
- Request body size limit (1MB DoS prevention)
- Malformed JSON handling
- All 4 at-identifier formats (DIDs, canonical, @-prefixed, scoped)
- Unicode/emoji support
- SQL injection prevention
4. helpers.go - Test utilities
- JWT token generation for test users
All tests passing. Coverage includes security, validation,
business logic, and real-time indexing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Wire up post creation feature in main server:
1. Initialize post service
- Create post repository (PostgreSQL)
- Create post service with community service integration
- Configure with default PDS URL
2. Register XRPC routes
- POST /xrpc/social.coves.post.create
- Requires OAuth authentication via middleware
3. Start Jetstream consumer for posts
- WebSocket URL: ws://localhost:6008/subscribe
- Collection filter: social.coves.post.record
- Runs in background goroutine
- Indexes CREATE operations (UPDATE/DELETE deferred)
Environment variables:
- POST_JETSTREAM_URL: Override default Jetstream endpoint
Initialization order ensures postRepo created before consumer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Export community service methods for post creation:
- EnsureFreshToken() - Auto-refresh PDS tokens before write operations
- GetByDID() - Direct repository access for post service
These methods enable posts service to:
1. Fetch community from AppView by DID
2. Ensure fresh PDS credentials before writing to community repo
3. Use community's access token for PDS write-forward
Changes:
- Made ensureFreshToken() public as EnsureFreshToken()
- Added GetByDID() wrapper for repository access
- No functional changes, just visibility
Supports write-forward architecture where posts are written to
community's PDS repository using community's credentials.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add PostEventConsumer for AppView indexing:
- Listen for social.coves.post.record CREATE events via WebSocket
- Parse post records from Jetstream firehose
- Index posts into AppView database
- Handle UPDATE/DELETE deferred until those features exist
Security validation:
- Verify repository DID matches community DID (prevents fake posts)
- Verify community exists in AppView (foreign key integrity)
- Verify author exists in AppView (foreign key integrity)
- Idempotent indexing (safe for Jetstream replays)
Add PostJetstreamConnector:
- Dedicated WebSocket connector for post events
- Collection filter: social.coves.post.record
- Separate from CommunityJetstreamConnector (different event types)
Posts are indexed with ~1 second latency from PDS write.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add POST /xrpc/social.coves.post.create endpoint:
- Authentication required via OAuth middleware
- Validate request body size (1MB limit for DoS prevention)
- Validate at-identifier format for community field
- Reject client-provided authorDid (security)
- Set authorDid from authenticated JWT
- Error mapping for lexicon-compliant responses
Handler security features:
- Body size limit prevents DoS
- Author impersonation prevented
- All 4 at-identifier formats supported
- Proper error codes (400, 401, 403, 404, 500)
Route registration integrates with auth middleware.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add migration 011: posts table with indexes
- Add foreign keys to users and communities
- Add indexes for common query patterns (community feed, author, score)
- Add PostgreSQL repository implementation
- Add Create() and GetByURI() methods
- Add JSON serialization for facets, embeds, labels
Posts table supports:
- AT-URI, CID, rkey for atProto compliance
- Title, content, facets, embed, labels
- Vote counts and score (denormalized for performance)
- Soft delete with deleted_at timestamp
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add Post domain model with AppView database representation
- Add CreatePostRequest/Response for XRPC endpoint
- Add PostRecord for PDS write-forward
- Add Service and Repository interfaces
- Add error types (ValidationError, ContentRuleViolation)
- Add service implementation with PDS write-forward
- Add validation for content length, labels, at-identifiers
Part of Alpha post creation feature.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Complete implementation of community handle naming convention change
with comprehensive PR review fixes.
This merge includes:
- Critical bug fixes for identifier resolution
- Comprehensive test coverage (31 test cases, all passing)
- Database migration for .communities → .community transition
- Input validation and error handling improvements
- Support for self-hosted instances
All PR review comments addressed:
✅ GetDisplayHandle() bug fixed for multi-part domains
✅ Case sensitivity bug fixed in resolveScopedIdentifier
✅ Comprehensive input validation (DNS labels, domain format)
✅ 100% test coverage for new code
✅ Database migration script with rollback
✅ Improved error messages with identifier context
✅ NewPDSAccountProvisioner argument order corrected
✅ Removed hardcoded coves.social from tests
Test Results:
- 31 identifier resolution tests: PASS
- All integration tests: PASS
- E2E tests with real PDS: PASS
- Regression tests: PASS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update development environment configuration and Go module dependencies.
Changes:
- Update .env.dev with latest environment variables
- Update docker-compose.dev.yml configuration
- Run go mod tidy to clean up dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update Product Requirements Document to document the singular
.community. naming convention.
Changes:
- Update handle format examples
- Document identifier resolution features
- Update API examples with new handle format
This ensures documentation matches the implemented handle format
and provides clear examples for developers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update all integration tests to use singular .community. naming convention
instead of .communities.
Tests updated:
- E2E community creation and XRPC endpoint tests
- HostedBy security validation tests
- Community provisioning tests
- Service integration tests
- V2 validation tests
- Token refresh tests
Changes:
- Update expected handle formats in assertions
- Update test fixtures to use new convention
- Ensure regex patterns match .community. format
All tests passing with new handle format.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update PDS provisioning and Jetstream consumer to generate handles
using singular .community. instead of .communities.
Changes:
- PDSAccountProvisioner: Generate {name}.community.{domain} handles
- JetstreamConsumer: Parse and validate new handle format
- Update handle extraction logic for consistency
Example handle formats:
- gardening.community.coves.social
- gaming.community.coves.social
This maintains consistency with the lexicon schema update and aligns
with AT Protocol naming conventions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update AT Protocol lexicon schema and test data to use singular
.community. instead of .communities. in handle format.
Changes:
- Update profile.json lexicon schema with new handle pattern
- Update test fixtures to use singular convention
- Update lexicon validation tests
This aligns with AT Protocol naming standards where all record types
use singular form (e.g., app.bsky.feed.post, app.bsky.graph.follow).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add production-ready migration script to update existing community handles
from plural (.communities.) to singular (.community.) format.
Migration features:
- Safe UPDATE using REPLACE for targeted changes
- Verification checks to ensure migration completion
- Rollback script for emergency reversion
- Error handling with informative messages
Example transformations:
- gardening.communities.coves.social → gardening.community.coves.social
- gaming.communities.coves.social → gaming.community.coves.social
This migration is required for the .communities → .community naming
convention change to align with AT Protocol lexicon standards
(all record types use singular form).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add 31 test cases covering all identifier resolution code paths:
TestCommunityIdentifierResolution (14 E2E tests):
- DID format resolution (3 tests)
- Canonical handle format (3 tests)
- At-identifier format (2 tests)
- Scoped format !name@instance (5 tests)
- Edge cases (4 tests)
TestResolveScopedIdentifier_InputValidation (9 tests):
- Reject special characters, spaces, invalid DNS labels
- Reject names starting/ending with hyphens
- Reject names exceeding 63 character DNS limit
- Accept valid alphanumeric names with hyphens/numbers
- Validate domain format
TestGetDisplayHandle (5 tests):
- Standard two-part domains
- Multi-part TLDs (e.g., .co.uk)
- Subdomain instances
- Malformed input graceful fallback
TestIdentifierResolution_ErrorContext (3 tests):
- Verify error messages include identifier for debugging
- Verify DID, handle, and scoped errors provide context
Fixes:
- Use environment variables for configuration (no hardcoded coves.social)
- Correct NewPDSAccountProvisioner argument order (instanceDomain, pdsURL)
- Support self-hosted instances via INSTANCE_DOMAIN env var
All tests passing with real PDS integration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Critical fixes:
- Fix GetDisplayHandle() to handle multi-part domains correctly using string.Index
- Add DNS label validation (RFC 1035) with isValidDNSLabel helper
- Add domain format validation with isValidDomain helper
- Normalize instanceDomain to lowercase for case-insensitive lookup
- Improve error messages to include identifier context for debugging
Validation improvements:
- Reject special characters in community names
- Enforce DNS label length limits (1-63 chars)
- Prevent names starting/ending with hyphens
- Validate domain format before lookup
Bug fixes:
- GetDisplayHandle now correctly parses handles with multi-part TLDs (e.g., coves.co.uk)
- resolveScopedIdentifier properly normalizes domain case (!gardening@Coves.social works)
- Error messages now include the identifier that failed resolution
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Comprehensive lexicon extensibility fixes to ensure alpha-readiness
and future-proof schema evolution without requiring V2 migrations.
Key Changes:
- Fixed all closed enums → knownValues (moderationType, visibility, sort)
- Made moderator roles and permissions extensible
- Added authentication documentation to all endpoints
- Removed invalid membership record references
- Documented technical decisions in PRD_GOVERNANCE.md
This locks down the lexicon schemas for alpha while enabling future
beta features (sortition moderation, new visibility modes, moderator
tiers) without breaking changes.
Per atProto style guide (bluesky-social/atproto#4245): enum sets
cannot be extended without breaking schema evolution rules.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Membership tracking is AppView-only data, not atProto records.
Changes:
- Removed membershipUri field from community.get viewerState
- Updated member field description to clarify it's AppView-computed
- Removed membership lexicon file (already deleted)
- Removed membership test data files (already deleted)
Rationale:
- Membership/reputation is indexed from user activity, not explicit records
- No need for AT-URI reference to non-existent record type
- Clarifies that membership status is computed by AppView, not stored in repo
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Per atProto style guide: endpoint descriptions should mention if
authentication is required and whether responses are personalized.
Changes:
- create.json: Added "Requires authentication."
- update.json: Added "Requires authentication and moderator/admin permissions."
- subscribe.json: Added "Requires authentication."
- unsubscribe.json: Added "Requires authentication."
- get.json: Added "Authentication optional; viewer state will be included if authenticated."
- list.json: Added "Authentication optional; viewer state will be included if authenticated."
This improves developer experience by making auth requirements
explicit without requiring documentation lookup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
BREAKING: This is a pre-alpha schema fix. Must be applied before any
moderator records are created.
Changes to social.coves.community.moderator:
- Change role from enum to knownValues (enables future role types)
- Change permissions from enum to knownValues (enables new permissions)
- Add maxLength: 64 to both fields per atProto style guide
Future extensibility examples:
- Roles: "owner", "trainee", "emeritus"
- Permissions: "manage_bots", "manage_flairs", "manage_automoderator"
Documented in PRD_GOVERNANCE.md:
- Technical decision rationale
- atProto style guide reference
- Future beta phase extensibility plan
- Security considerations
This enables Beta Phase 2 (Moderator Tiers & Permissions) without
requiring V2 schema migration or breaking existing records.
Per atProto style guide (bluesky-social/atproto#4245): enum sets
cannot be extended without breaking schema evolution rules.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Change sort from closed enum to knownValues
- Add maxLength: 64 per atProto style guide
This enables future sort algorithms without breaking changes:
- "trending" - Recent activity spike detection
- "recommended" - Personalized AI recommendations
- "nearby" - Geo-based sorting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Change visibility from closed enum to knownValues
- Apply to community.profile (record), create, and update endpoints
- Add maxLength: 64 per atProto style guide
This enables future visibility modes without breaking changes:
- "followers-only" - Only subscribers can see
- "instance-only" - Only same-instance users
- "invite-only" - Requires invite code
Files changed:
- community/profile.json (record schema)
- community/create.json (procedure)
- community/update.json (procedure)
Per atProto style guide: closed enums block schema evolution.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Change moderationType from closed enum to knownValues
- Add to required fields (critical before alpha - can't add required later)
- Add default value "moderator" for alpha simplicity
- Add maxLength constraint per atProto style guide
This enables future moderation types without schema migration:
- "sortition" - Community tribunal (Beta Phase 1)
- "instance-labeler" - Instance moderation service
- "third-party-labeler" - External moderation DID
Per atProto style guide: enum sets cannot be extended without breaking
schema evolution. knownValues provides flexible alternative.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements Phase 1 did:web domain verification to prevent domain
impersonation attacks in the Coves federated community system.
This PR addresses all code review feedback across 3 rounds:
Round 1 - Performance & Security:
✅ P0: Multi-part TLD support (fixes .co.uk, .com.au blocking)
✅ HTTP client connection pooling
✅ Bounded LRU cache implementation
✅ Rate limiting for DoS protection
Round 2 - Critical Bug Fixes:
✅ Memory leak (unbounded cache → bounded LRU)
✅ Deadlock (manual locks → thread-safe LRU)
✅ Missing timeout (added 15s overall timeout)
Round 3 - Optimizations:
✅ Cache TTL cleanup (removes expired entries)
✅ Struct field alignment (performance)
✅ All linter issues resolved
Security Impact:
- Prevents malicious instances from claiming communities for domains
they don't control (e.g., evil.com claiming @gaming@nintendo.com)
- Verifies hostedBy domain matches community handle domain
- Optional .well-known/did.json verification for cryptographic proof
- Soft-fail on network errors (resilience)
Test Coverage:
- 13 new security test cases (all passing)
- 42+ total tests (all passing)
- Multi-part TLD support verified (.co.uk, .com.au, .org.uk, .ac.uk)
Code Quality:
✅ All linter checks passing
✅ All code properly formatted
✅ Clean build (no warnings)
✅ Production-ready
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
Adds comprehensive test coverage for hostedBy domain verification,
including multi-part TLD support and security attack scenarios.
Test Coverage:
TestHostedByVerification_DomainMatching:
- ✅ Rejects communities with mismatched hostedBy domains
- ✅ Accepts communities with matching hostedBy domains
- ✅ Rejects non-did:web format hostedBy values
- ✅ Skip verification flag bypasses all checks (dev mode)
TestExtractDomainFromHandle:
- ✅ DNS-style handles with subdomains
- ✅ Simple two-part domains
- ✅ Multi-part subdomains
- ✅ Multi-part TLD: .co.uk (critical fix validation)
- ✅ Multi-part TLD: .com.au (critical fix validation)
- ✅ Multi-part TLD: .org.uk, .ac.uk
- ✅ Correctly rejects incorrect TLD extraction (e.g., did:web:co.uk)
- ✅ Domain mismatch detection
Security Attack Scenarios Tested:
1. Domain impersonation (evil.com claiming nintendo.com) - BLOCKED
2. Non-did:web hostedBy spoofing - BLOCKED
3. Multi-part TLD domain extraction failures - FIXED
All tests passing (9/9 multi-part TLD tests).
Co-Authored-By: Claude <noreply@anthropic.com>
Updates all integration tests to use the new CommunityEventConsumer
constructor signature with instance DID and skip verification flag.
Changes:
- Updated 5 integration test files
- All tests use skipVerification=true to avoid network calls
- Tests use did:web:coves.local as instance DID
- Maintains existing test behavior and coverage
Files Updated:
- community_blocking_test.go
- community_consumer_test.go
- community_e2e_test.go
- community_v2_validation_test.go
- subscription_indexing_test.go
All existing tests continue to pass with no behavior changes.
Co-Authored-By: Claude <noreply@anthropic.com>
Integrates hostedBy verification into the server with environment-based
configuration for development and production use.
Changes:
- Added SKIP_DID_WEB_VERIFICATION env var for dev mode bypass
- Updated consumer initialization with instance DID and skip flag
- Added warning logs when verification is disabled
- Configured .env.dev with skip flag enabled for local development
Server logs will now show:
- "⚠️ WARNING: did:web verification DISABLED (dev mode)" when skipped
- "🚨 SECURITY: Rejecting community" when domain mismatch detected
Production Deployment:
- Set SKIP_DID_WEB_VERIFICATION=false or leave unset
- Ensure .well-known/did.json is properly configured
Co-Authored-By: Claude <noreply@anthropic.com>
Implements hostedBy verification to prevent domain impersonation attacks
where malicious instances claim to host communities for domains they don't
own (e.g., gaming@nintendo.com on non-Nintendo servers).
Core Implementation:
- Added verifyHostedByClaim() to validate hostedBy domain matches handle
- Integrated golang.org/x/net/publicsuffix for proper eTLD+1 extraction
- Supports multi-part TLDs (.co.uk, .com.au, .org.uk, etc.)
- Added verifyDIDDocument() for .well-known/did.json verification
- Bounded LRU cache (max 1000 entries) prevents memory leaks
- Thread-safe operations (no deadlock risk)
- HTTP client connection pooling for performance
- Rate limiting (10 req/sec) prevents DoS attacks
- 15-second timeout prevents consumer blocking
- Cache TTL cleanup removes expired entries
Security Features:
- Hard-fail on domain mismatch (blocks indexing)
- Soft-fail on .well-known errors (network resilience)
- Skip verification flag for development mode
- Optimized struct field alignment for performance
Breaking Changes: None
- Constructor signature updated but all tests migrated
Co-Authored-By: Claude <noreply@anthropic.com>
Implements automatic refresh of community PDS access tokens to prevent
401 errors after 2-hour token expiration. Includes comprehensive security
hardening through multiple review iterations.
## Core Features
- Proactive token refresh (5-minute buffer before expiration)
- Automatic fallback to password re-auth when refresh tokens expire
- Concurrent-safe per-community mutex protection
- Atomic credential updates with retry logic
- Comprehensive structured logging for observability
## Security Hardening (3 Review Rounds)
### Round 1: Initial PR Review Fixes
- Added DB update retry logic (3 attempts, exponential backoff)
- Improved error detection with typed xrpc.Error checking
- Added comprehensive unit tests (8 test cases for NeedsRefresh)
- Enhanced logging for JWT parsing failures
- Memory-bounded mutex cache with warning threshold
### Round 2: Critical Race Condition Fixes
- **CRITICAL:** Eliminated race condition in mutex eviction
- Removed eviction entirely to prevent mutex map corruption
- Added read-lock fast path for performance
- Implemented double-check locking pattern
- **CRITICAL:** Fixed test-production code path mismatch
- Eliminated wrapper function, single exported NeedsRefresh()
- Tests now validate actual production code
### Round 3: Code Quality & Linting
- Fixed struct field alignment (8-byte memory optimization)
- Removed unused functions (splitToken)
- Added proper error handling for deferred Close() calls
- All golangci-lint checks passing
## Implementation Details
**Token Refresh Flow:**
1. Check if access token expires within 5 minutes
2. Acquire per-community mutex (prevent concurrent refresh)
3. Re-fetch from DB (double-check pattern)
4. Attempt refresh using refresh token
5. Fallback to password re-auth if refresh token expired
6. Update DB atomically with retry logic (3 attempts)
7. Return updated community with fresh credentials
**Concurrency Safety:**
- Per-community mutexes (non-blocking for different communities)
- Double-check pattern prevents duplicate refreshes
- Atomic DB updates (access + refresh token together)
- Refresh tokens are single-use (atproto spec compliance)
**Files Changed:**
- internal/core/communities/service.go - Main orchestration
- internal/core/communities/token_refresh.go - Indigo SDK integration
- internal/core/communities/token_utils.go - JWT parsing utilities
- internal/core/communities/interfaces.go - Repository interface
- internal/db/postgres/community_repo.go - UpdateCredentials method
- tests/integration/token_refresh_test.go - Comprehensive tests
- docs/PRD_BACKLOG.md - Documented Alpha blocker resolution
- docs/PRD_COMMUNITIES.md - Updated with token refresh feature
## Testing
- 8 unit tests for token expiration detection (all passing)
- Integration tests for UpdateCredentials (all passing)
- E2E test framework ready for PDS integration
- All linters passing (golangci-lint)
- Build verification successful
## Observability
Structured logging with events:
- token_refresh_started, token_refreshed
- refresh_token_expired, password_fallback_success
- db_update_retry, token_parse_failed
- CRITICAL alerts for lockout conditions
## Risk Mitigation
Before: 🔴 HIGH RISK - Communities lockout after 2 hours
After: 🟢 LOW RISK - Automatic refresh with multiple safety layers
- Race conditions: ELIMINATED (no mutex eviction)
- DB failures: MITIGATED (3-retry with exponential backoff)
- Refresh token expiry: HANDLED (password fallback)
- Test coverage: COMPREHENSIVE (unit + integration)
- Memory leaks: PREVENTED (warning at 10k communities, acceptable at 1M)
## Production Ready
✅ All critical issues resolved
✅ All tests passing
✅ All linters passing
✅ Comprehensive error handling
✅ Security hardened through 3 review rounds
Resolves Alpha blocker: Communities can now be updated indefinitely
without manual token management.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Fix P1 issue: properly bubble up database errors instead of masking as conflict
* Only return ErrBlockAlreadyExists when getErr is ErrBlockNotFound (race condition)
* Real DB errors (outages, connection failures) now propagate to operators
- Remove unused V1 functions flagged by linter:
* createRecordOnPDS, deleteRecordOnPDS, callPDS (replaced by *As versions)
- Apply automatic code formatting via golangci-lint --fix:
* Align struct field tags in CommunityBlock
* Fix comment alignment across test files
* Remove trailing whitespace
- All tests passing, linter clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes four issues identified in PR review:
**BUG 1 - Performance: Remove redundant database query**
- Removed duplicate GetByDID call in BlockCommunity service method
- ResolveCommunityIdentifier already verifies community exists
- Reduces block operations from 2 DB queries to 1
**BUG 2 - Performance: Move regex compilation to package level**
- Moved DID validation regex to package-level variable in block.go
- Prevents recompiling regex on every block/unblock request
- Eliminates unnecessary CPU overhead on hot path
**BUG 3 - DRY: Remove duplicated extractRKeyFromURI**
- Removed duplicate implementations in service.go and tests
- Now uses shared utils.ExtractRKeyFromURI function
- Single source of truth for AT-URI parsing logic
**P1 - Critical: Fix duplicate block race condition**
- Added ErrBlockAlreadyExists error type
- Returns 409 Conflict instead of 500 when PDS has block but AppView hasn't indexed yet
- Handles normal race in eventually-consistent flow gracefully
- Prevents double-click scenarios from appearing as server failures
All tests passing (33.2s runtime, 100% pass rate).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**Breaking Change**: XRPC endpoints now strictly enforce lexicon spec.
Changed endpoints to reject handles and accept ONLY DIDs:
- social.coves.community.blockCommunity
- social.coves.community.unblockCommunity
- social.coves.community.subscribe
- social.coves.community.unsubscribe
Rationale:
1. Lexicon defines "subject" field with format: "did" (not "at-identifier")
2. Records are immutable and content-addressed - must use permanent DIDs
3. Handles can change (they're DNS pointers), DIDs cannot
4. Bluesky's app.bsky.graph.block uses same pattern (DID-only)
Previous behavior accepted both DIDs and handles, resolving handles to
DIDs internally. This was convenient but violated the lexicon contract.
Impact:
- Clients must resolve handles to DIDs before calling these endpoints
- Matches standard atProto patterns for block/subscription records
- Ensures federation compatibility
This aligns our implementation with the lexicon specification and
atProto best practices.
Improve validation robustness in block/unblock handlers:
1. DID validation with regex:
- Pattern: ^did:(plc|web):[a-zA-Z0-9._:%-]+$
- Rejects invalid formats like "did:x" or "did:"
- Ensures only supported DID methods (plc, web)
2. Handle validation:
- Verify handle contains @ symbol for domain
- Rejects incomplete handles like "!" or "!name"
- Ensures proper format: !name@domain.tld
Previous validation only checked prefix, allowing invalid values
to pass through to service layer. New validation catches format
errors early with clear error messages.
Addresses: Important review comment #4
Service layer improvements:
1. Add DID verification in ResolveCommunityIdentifier:
- When a DID is provided, verify the community actually exists
in the AppView database
- Prevents accepting non-existent DIDs (e.g., did:plc:fakefake)
- Provides clearer error messages when community doesn't exist
2. Improve duplicate error detection in BlockCommunity:
- Check for HTTP 409 Conflict status code explicitly
- Added "status 409" check in addition to text-based detection
- More robust across different PDS implementations
- Still maintains fallback checks for compatibility
Both changes improve error handling and user experience while
maintaining backward compatibility.
Addresses: Critical review comment #2, Important review comment #3
Database optimization changes:
1. Removed redundant idx_blocks_user_community index:
- UNIQUE constraint on (user_did, community_did) already creates
an index automatically
- Maintaining duplicate index wastes storage and degrades write
performance (every insert updates two identical indexes)
2. Added missing idx_blocks_record_uri index:
- Required for GetBlockByURI() queries used in Jetstream DELETE
operations
- Without this index, DELETE event processing does full table scan
Migration now has optimal indexes without redundancy.
Addresses: Critical review comments #1 and #7
Critical bug fix: The loop variable 'block' was being reused for each
iteration, causing all elements in the returned slice to point to the
same memory location. This resulted in the last row being repeated for
every element when callers read the list.
Fixed by allocating a new block pointer for each iteration:
- Before: var block communities.CommunityBlock (reused)
- After: block := &communities.CommunityBlock{} (new allocation)
Also replaced fmt.Printf with log.Printf for consistency with project
logging standards.
Addresses: P1 review comment - pointer reuse in list operation
Change subscription lexicon subject field format from "at-identifier"
to "did" for consistency and correctness:
Before:
- format: "at-identifier" (accepts DIDs or handles)
- description: "DID or handle of the community"
After:
- format: "did" (only accepts DIDs)
- description: "DID of the community being subscribed to"
Rationale:
1. Matches block.json pattern (which correctly uses "did" format)
2. Aligns with service layer implementation (only supports DIDs)
3. Follows atProto convention: "subject" field references entities by DID
4. Prevents invalid handle values in federated records
This ensures subscription records are properly validated and compatible
with the broader atProto ecosystem.
E2E Tests (3 new test cases):
- Block via XRPC endpoint: Full flow from HTTP → PDS → Jetstream → AppView
- Unblock via XRPC endpoint: Complete unblock flow with DELETE event
- Block fails without authentication: Validates auth requirement (401)
Each E2E test verifies:
✓ XRPC endpoint responds correctly
✓ Record created/deleted on PDS
✓ Jetstream consumer indexes event
✓ AppView database state updated
Unit Test Updates:
- Added 6 mock methods to mockCommunityRepo for blocking operations
- Ensures service layer tests compile and pass
All tests follow existing E2E patterns (subscribe/unsubscribe) for
consistency.
Add 16 integration test cases covering:
1. Jetstream Consumer Indexing (4 tests):
- Block CREATE event indexing
- Block DELETE event indexing
- Idempotent duplicate event handling
- Graceful handling of non-existent block deletion
2. List Operations (3 tests):
- List all blocked communities for user
- Pagination with limit/offset
- Empty list for users with no blocks
3. IsBlocked Queries (3 tests):
- Returns false when not blocked
- Returns true when blocked
- Returns false after unblock
4. GetBlock Operations (3 tests):
- Error when block doesn't exist
- Retrieve block by user DID + community DID
- Retrieve block by AT-URI (for DELETE operations)
All tests verify proper database state, idempotency guarantees,
and Jetstream event processing.
Implement Jetstream consumer support for community block records:
- handleBlock: Routes CREATE/DELETE operations for social.coves.community.block
- createBlock: Indexes block CREATE events from firehose
- Extracts community DID from "subject" field (atProto convention)
- Builds AT-URI: at://user_did/social.coves.community.block/rkey
- Preserves createdAt timestamp for chronological ordering during replays
- Idempotent: handles duplicate events via ON CONFLICT
- deleteBlock: Processes block DELETE events from firehose
- Looks up block by URI (DELETE events don't include record data)
- Removes from AppView index
- Gracefully handles deletion of non-existent blocks
Completes the write-forward flow:
Client → PDS → Jetstream Firehose → Consumer → AppView DB
Add XRPC handlers for community blocking endpoints:
- HandleBlock: POST /xrpc/social.coves.community.blockCommunity
- HandleUnblock: POST /xrpc/social.coves.community.unblockCommunity
Features:
- Input validation: Community must be DID (did:plc:...) or handle (!name@instance)
- Authentication: Requires user DID and access token from middleware
- Response format: Follows atProto conventions with recordUri/recordCid
- Error handling: Uses shared handleServiceError for consistency
Addresses PR review comment on input validation.
Implement service layer for community blocking following atProto
write-forward architecture:
- BlockCommunity: Creates block record on PDS using user's access token,
handles duplicate errors gracefully by fetching existing block
- UnblockCommunity: Deletes block record from PDS, extracts rkey from URI
- GetBlockedCommunities: Queries AppView with pagination
- IsBlocked: Fast boolean check for block status
Key architectural decisions:
- Write-forward pattern: All mutations go through PDS first
- Race condition fix: Removed preemptive existence check, rely on PDS
duplicate detection + repository ON CONFLICT handling
- User authentication: Uses user's access token (not instance token)
- Identifier resolution: Supports both DIDs and handles via
resolveCommunityIdentifier
Resolves race condition identified in PR review.
Add core domain support for community blocking feature:
- CommunityBlock struct with proper atProto metadata (RecordURI, RecordCID)
- ErrBlockNotFound error constant
- Repository interface methods: BlockCommunity, UnblockCommunity, GetBlock,
GetBlockByURI, ListBlockedCommunities, IsBlocked
- Service interface methods: BlockCommunity, UnblockCommunity,
GetBlockedCommunities, IsBlocked
This establishes the domain layer contracts that will be implemented
in subsequent commits following clean architecture principles.