A community based topic aggregation platform built on atproto

feat(posts): support multiple trusted aggregator DIDs

Replace single KAGI_AGGREGATOR_DID with comma-separated
TRUSTED_AGGREGATOR_DIDS env var. Allows multiple aggregators
to bypass community authorization checks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+33 -16
+7 -4
.env.dev
··· 38 38 PDS_ADMIN_PASSWORD=admin 39 39 40 40 # Handle domains (users will get handles like alice.local.coves.dev) 41 - # Communities will use .community.coves.social (singular per atProto conventions) 42 - PDS_SERVICE_HANDLE_DOMAINS=.local.coves.dev,.community.coves.social 41 + # Communities will use c-{name}.coves.social (3-level format with c- prefix) 42 + PDS_SERVICE_HANDLE_DOMAINS=.local.coves.dev,.coves.social 43 43 44 44 # PLC Rotation Key (k256 private key in hex format - for local dev only) 45 45 # This is a randomly generated key for testing - DO NOT use in production ··· 133 133 PDS_INSTANCE_HANDLE=testuser123.local.coves.dev 134 134 PDS_INSTANCE_PASSWORD=test-password-123 135 135 136 - # Kagi News Aggregator DID (for trusted thumbnail URLs) 137 - KAGI_AGGREGATOR_DID=did:plc:yyf34padpfjknejyutxtionr 136 + # Trusted Aggregator DIDs (bypasses community authorization check) 137 + # Comma-separated list of DIDs 138 + # - did:plc:yyf34padpfjknejyutxtionr = kagi-news.coves.social (production) 139 + # - did:plc:igjbg5cex7poojsniebvmafb = test-aggregator.local.coves.dev (dev) 140 + TRUSTED_AGGREGATOR_DIDS=did:plc:yyf34padpfjknejyutxtionr,did:plc:igjbg5cex7poojsniebvmafb 138 141 139 142 # ============================================================================= 140 143 # Development Settings
+1 -1
.env.dev.example
··· 46 46 PDS_DID_PLC_URL=http://plc-directory:3000 47 47 PDS_JWT_SECRET=local-dev-jwt-secret-change-in-production 48 48 PDS_ADMIN_PASSWORD=admin 49 - PDS_SERVICE_HANDLE_DOMAINS=.local.coves.dev,.community.coves.social 49 + PDS_SERVICE_HANDLE_DOMAINS=.local.coves.dev,.coves.social 50 50 PDS_PLC_ROTATION_KEY=<generate-a-random-hex-key> 51 51 52 52 # =============================================================================
+25 -11
internal/core/posts/service.go
··· 10 10 "log" 11 11 "net/http" 12 12 "os" 13 + "strings" 13 14 "time" 14 15 15 16 "Coves/internal/api/middleware" ··· 83 84 return nil, fmt.Errorf("authenticated DID does not match author DID") 84 85 } 85 86 86 - // 3. Determine actor type: Kagi aggregator, other aggregator, or regular user 87 - kagiAggregatorDID := os.Getenv("KAGI_AGGREGATOR_DID") 88 - isTrustedKagi := kagiAggregatorDID != "" && req.AuthorDID == kagiAggregatorDID 87 + // 3. Determine actor type: trusted aggregator, other aggregator, or regular user 88 + // Check against comma-separated list of trusted aggregator DIDs 89 + trustedDIDs := os.Getenv("TRUSTED_AGGREGATOR_DIDS") 90 + if trustedDIDs == "" { 91 + // Fallback to legacy single DID env var 92 + trustedDIDs = os.Getenv("KAGI_AGGREGATOR_DID") 93 + } 94 + isTrustedAggregator := false 95 + if trustedDIDs != "" { 96 + for _, did := range strings.Split(trustedDIDs, ",") { 97 + if strings.TrimSpace(did) == req.AuthorDID { 98 + isTrustedAggregator = true 99 + break 100 + } 101 + } 102 + } 89 103 90 - // Check if this is a non-Kagi aggregator (requires database lookup) 104 + // Check if this is a non-trusted aggregator (requires database lookup) 91 105 var isOtherAggregator bool 92 106 var err error 93 - if !isTrustedKagi && s.aggregatorService != nil { 107 + if !isTrustedAggregator && s.aggregatorService != nil { 94 108 isOtherAggregator, err = s.aggregatorService.IsAggregator(ctx, req.AuthorDID) 95 109 if err != nil { 96 110 log.Printf("[POST-CREATE] Warning: failed to check if DID is aggregator: %v", err) ··· 138 152 } 139 153 140 154 // 7. Apply validation based on actor type (aggregator vs user) 141 - if isTrustedKagi { 155 + if isTrustedAggregator { 142 156 // TRUSTED AGGREGATOR VALIDATION FLOW 143 - // Kagi aggregator is authorized via KAGI_AGGREGATOR_DID env var (temporary) 157 + // Trusted aggregators are authorized via TRUSTED_AGGREGATOR_DIDS env var (temporary) 144 158 // TODO: Replace with proper XRPC aggregator authorization endpoint 145 - log.Printf("[POST-CREATE] Trusted Kagi aggregator detected: %s posting to community: %s", req.AuthorDID, communityDID) 159 + log.Printf("[POST-CREATE] Trusted aggregator detected: %s posting to community: %s", req.AuthorDID, communityDID) 146 160 // Aggregators skip membership checks and visibility restrictions 147 161 // They are authorized services, not community members 148 162 } else if isOtherAggregator { ··· 219 233 220 234 // TRUSTED AGGREGATOR: Allow Kagi aggregator to provide thumbnail URLs directly 221 235 // This bypasses unfurl for more accurate RSS-sourced thumbnails 222 - if req.ThumbnailURL != nil && *req.ThumbnailURL != "" && isTrustedKagi { 236 + if req.ThumbnailURL != nil && *req.ThumbnailURL != "" && isTrustedAggregator { 223 237 log.Printf("[AGGREGATOR-THUMB] Trusted aggregator provided thumbnail: %s", *req.ThumbnailURL) 224 238 225 239 if s.blobService != nil { ··· 239 253 240 254 // Unfurl enhancement (optional, only if URL is supported) 241 255 // Skip unfurl for trusted aggregators - they provide their own metadata 242 - if !isTrustedKagi { 256 + if !isTrustedAggregator { 243 257 if uri, ok := external["uri"].(string); ok && uri != "" { 244 258 // Check if we support unfurling this URL 245 259 if s.unfurlService != nil && s.unfurlService.IsSupported(uri) { ··· 313 327 314 328 // 13. Return response (AppView will index via Jetstream consumer) 315 329 log.Printf("[POST-CREATE] Author: %s (trustedKagi=%v, otherAggregator=%v), Community: %s, URI: %s", 316 - req.AuthorDID, isTrustedKagi, isOtherAggregator, communityDID, uri) 330 + req.AuthorDID, isTrustedAggregator, isOtherAggregator, communityDID, uri) 317 331 318 332 return &CreatePostResponse{ 319 333 URI: uri,