A community based topic aggregation platform built on atproto

fix(bluesky): use production PLC for resolving Bluesky handles

Server was using local PLC directory (localhost:3002) for resolving real
Bluesky handles like "bretton.dev", which failed with 404 errors. Now uses
a dedicated production PLC resolver (https://plc.directory) that is READ-ONLY
for looking up existing Bluesky identities.

- Add productionPLCResolver in main.go for Bluesky handle resolution
- Update tests to use production PLC helper function
- Clean up debug logging in post service

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

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

+49 -24
+21 -1
cmd/server/main.go
··· 429 429 log.Println("✅ Unfurl and blob services initialized") 430 430 431 431 // Initialize Bluesky post cache repository and service 432 + // 433 + // Production PLC Read-Only Resolver 434 + // ================================== 435 + // This resolver is used ONLY for resolving real Bluesky handles (e.g., "bretton.dev") 436 + // that exist on the production AT Protocol network. 437 + // 438 + // READ-ONLY GUARANTEE: The identity.Resolver interface only supports read operations: 439 + // - Resolve(), ResolveHandle(), ResolveDID() - HTTP GET lookups only 440 + // - Purge() - clears local cache, does NOT write to PLC 441 + // 442 + // DO NOT use this resolver for: 443 + // - Integration tests (use local PLC at localhost:3002 via identityResolver) 444 + // - Creating/registering new DIDs (handled by separate PLC client) 445 + // 446 + // Safe in dev/test: only performs HTTP GET to resolve existing Bluesky identities. 447 + productionPLCConfig := identity.DefaultConfig() 448 + productionPLCConfig.PLCURL = "https://plc.directory" // Production PLC - READ ONLY 449 + productionPLCResolver := identity.NewResolver(db, productionPLCConfig) 450 + log.Println("✅ Production PLC resolver initialized (READ-ONLY for Bluesky handle resolution)") 451 + 432 452 blueskyRepo := blueskypost.NewRepository(db) 433 453 blueskyService := blueskypost.NewService( 434 454 blueskyRepo, 435 - identityResolver, 455 + productionPLCResolver, // READ-ONLY: resolves real Bluesky handles like "bretton.dev" 436 456 blueskypost.WithTimeout(10*time.Second), 437 457 blueskypost.WithCacheTTL(1*time.Hour), // 1 hour cache (shorter than unfurl) 438 458 )
+4 -9
internal/core/posts/service.go
··· 182 182 183 183 // 10. Validate and enhance external embeds 184 184 if postRecord.Embed != nil { 185 - if embedType, ok := postRecord.Embed["$type"].(string); ok && embedType == "social.coves.embed.external" { 186 - if external, ok := postRecord.Embed["external"].(map[string]interface{}); ok { 185 + embedType, typeOk := postRecord.Embed["$type"].(string) 186 + if typeOk && embedType == "social.coves.embed.external" { 187 + if external, extOk := postRecord.Embed["external"].(map[string]interface{}); extOk { 187 188 // Check if this is a Bluesky post URL and convert to post embed 188 189 if !s.tryConvertBlueskyURLToPostEmbed(ctx, external, &postRecord) { 189 190 // Not a Bluesky URL or conversion failed - continue with normal external embed processing ··· 481 482 482 483 // 2. Extract and validate URL 483 484 url, ok := external["uri"].(string) 484 - if !ok { 485 - if external["uri"] != nil { 486 - log.Printf("[POST-CREATE] DEBUG: External embed URI is not a string (type: %T)", external["uri"]) 487 - } 488 - return false 489 - } 490 - if url == "" { 485 + if !ok || url == "" { 491 486 return false 492 487 } 493 488
+24 -14
tests/integration/bluesky_post_test.go
··· 13 13 "github.com/stretchr/testify/require" 14 14 ) 15 15 16 + // productionPLCIdentityResolver creates an identity resolver that uses the production 17 + // PLC directory (https://plc.directory) for resolving real Bluesky handles. 18 + // 19 + // READ-ONLY: This resolver only performs HTTP GET requests to look up existing identities. 20 + // It does NOT write to the production PLC directory. 21 + // 22 + // Use this for tests that need to resolve real Bluesky handles like "ianboudreau.com". 23 + // Do NOT use for tests involving local Coves identities (use local PLC instead). 24 + func productionPLCIdentityResolver() identity.Resolver { 25 + config := identity.DefaultConfig() 26 + config.PLCURL = "https://plc.directory" // Production PLC - READ ONLY 27 + return identity.NewResolver(nil, config) 28 + } 29 + 16 30 // TestBlueskyPostCrossPosting_URLParsing tests URL detection and parsing 17 31 func TestBlueskyPostCrossPosting_URLParsing(t *testing.T) { 18 32 if testing.Short() { ··· 22 36 db := setupTestDB(t) 23 37 defer func() { _ = db.Close() }() 24 38 25 - // Setup identity resolver for handle resolution 26 - identityConfig := identity.DefaultConfig() 27 - identityResolver := identity.NewResolver(db, identityConfig) 39 + // Use production PLC resolver for real Bluesky handles (READ-ONLY) 40 + identityResolver := productionPLCIdentityResolver() 28 41 29 42 // Setup Bluesky post service 30 43 repo := blueskypost.NewRepository(db) ··· 105 118 // Cleanup cache from previous runs 106 119 _, _ = db.Exec("DELETE FROM bluesky_post_cache") 107 120 108 - // Setup services 109 - identityConfig := identity.DefaultConfig() 110 - identityResolver := identity.NewResolver(db, identityConfig) 121 + // Use production PLC resolver for real Bluesky handles (READ-ONLY) 122 + identityResolver := productionPLCIdentityResolver() 111 123 112 124 repo := blueskypost.NewRepository(db) 113 125 service := blueskypost.NewService(repo, identityResolver, ··· 372 384 db := setupTestDB(t) 373 385 defer func() { _ = db.Close() }() 374 386 375 - identityConfig := identity.DefaultConfig() 376 - identityResolver := identity.NewResolver(db, identityConfig) 387 + // Use production PLC resolver for real Bluesky handles (READ-ONLY) 388 + identityResolver := productionPLCIdentityResolver() 377 389 378 390 repo := blueskypost.NewRepository(db) 379 391 service := blueskypost.NewService(repo, identityResolver, ··· 529 541 // Cleanup cache 530 542 _, _ = db.Exec("DELETE FROM bluesky_post_cache WHERE at_uri LIKE 'at://did:plc:%'") 531 543 532 - // Setup services 533 - identityConfig := identity.DefaultConfig() 534 - identityResolver := identity.NewResolver(db, identityConfig) 544 + // Use production PLC resolver for real Bluesky handles (READ-ONLY) 545 + identityResolver := productionPLCIdentityResolver() 535 546 536 547 repo := blueskypost.NewRepository(db) 537 548 service := blueskypost.NewService(repo, identityResolver, ··· 599 610 // Cleanup cache from previous runs 600 611 _, _ = db.Exec("DELETE FROM bluesky_post_cache") 601 612 602 - // Setup identity resolver for handle resolution 603 - identityConfig := identity.DefaultConfig() 604 - identityResolver := identity.NewResolver(db, identityConfig) 613 + // Use production PLC resolver for real Bluesky handles (READ-ONLY) 614 + identityResolver := productionPLCIdentityResolver() 605 615 606 616 // Setup Bluesky post service 607 617 repo := blueskypost.NewRepository(db)