A community based topic aggregation platform built on atproto

chore(deps): update indigo to v0.0.0-20260202 and simplify OAuth scopes

Update bluesky-social/indigo dependency to the latest version and adapt
to its API changes. Also simplify OAuth configuration by removing the
deprecated transition:generic scope.

Changes:
- Update Go version from 1.24.0 to 1.25
- Update indigo to v0.0.0-20260202181658-ea3d39eec464
- Fix DID parsing calls to use value type instead of pointer (indigo API change)
- Remove transition:generic from OAuth scopes (now using atproto only)
- Fix test isolation in CleanupExpiredSessions test

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

+24 -19
+1 -1
cmd/server/main.go
··· 204 oauthConfig := &oauth.OAuthConfig{ 205 PublicURL: os.Getenv("APPVIEW_PUBLIC_URL"), 206 SealSecret: oauthSealSecret, 207 - Scopes: []string{"atproto", "transition:generic"}, 208 DevMode: isDevMode, 209 AllowPrivateIPs: isDevMode, // Allow private IPs only in dev mode 210 PLCURL: plcURL,
··· 204 oauthConfig := &oauth.OAuthConfig{ 205 PublicURL: os.Getenv("APPVIEW_PUBLIC_URL"), 206 SealSecret: oauthSealSecret, 207 + Scopes: []string{"atproto"}, 208 DevMode: isDevMode, 209 AllowPrivateIPs: isDevMode, // Allow private IPs only in dev mode 210 PLCURL: plcURL,
+2 -2
go.mod
··· 1 module Coves 2 3 - go 1.24.0 4 5 toolchain go1.25.1 6 7 require ( 8 - github.com/bluesky-social/indigo v0.0.0-20251010013709-8f2296eee90f 9 github.com/disintegration/imaging v1.6.2 10 github.com/go-chi/chi/v5 v5.2.1 11 github.com/go-chi/cors v1.2.2
··· 1 module Coves 2 3 + go 1.25 4 5 toolchain go1.25.1 6 7 require ( 8 + github.com/bluesky-social/indigo v0.0.0-20260202181658-ea3d39eec464 9 github.com/disintegration/imaging v1.6.2 10 github.com/go-chi/chi/v5 v5.2.1 11 github.com/go-chi/cors v1.2.2
+2 -2
go.sum
··· 2 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 - github.com/bluesky-social/indigo v0.0.0-20251010013709-8f2296eee90f h1:95XLfnirbj3yKv5zA2gYbUDrnZObG/1Irwmaw4ukLec= 6 - github.com/bluesky-social/indigo v0.0.0-20251010013709-8f2296eee90f/go.mod h1:GuGAU33qKulpZCZNPcUeIQ4RW6KzNvOy7s8MSUXbAng= 7 github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 8 github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 9 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
··· 2 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 + github.com/bluesky-social/indigo v0.0.0-20260202181658-ea3d39eec464 h1:jL6cPOk1CZ8H06sEn+WFGWufHmqkawsGyDRl+BJhQjs= 6 + github.com/bluesky-social/indigo v0.0.0-20260202181658-ea3d39eec464/go.mod h1:VG/LeqLGNI3Ew7lsYixajnZGFfWPv144qbUddh+Oyag= 7 github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 8 github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 9 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+1 -1
internal/atproto/identity/base_resolver.go
··· 51 } 52 53 // Resolve using Indigo's directory 54 - ident, err := r.directory.Lookup(ctx, *atID) 55 if err != nil { 56 // Check if it's a "not found" error 57 errStr := err.Error()
··· 51 } 52 53 // Resolve using Indigo's directory 54 + ident, err := r.directory.Lookup(ctx, atID) 55 if err != nil { 56 // Check if it's a "not found" error 57 errStr := err.Error()
+2 -2
internal/atproto/oauth/dev_auth_resolver.go
··· 190 if err != nil { 191 return "", fmt.Errorf("not a valid DID (%s): %w", identifier, err) 192 } 193 - ident, err := dir.Lookup(ctx, *atid) 194 if err != nil { 195 return "", fmt.Errorf("failed to resolve DID (%s): %w", identifier, err) 196 } ··· 225 if err != nil { 226 return "", fmt.Errorf("not a valid DID (%s): %w", did, err) 227 } 228 - ident, err := dir.Lookup(ctx, *atid) 229 if err != nil { 230 return "", fmt.Errorf("failed to resolve DID document (%s): %w", did, err) 231 }
··· 190 if err != nil { 191 return "", fmt.Errorf("not a valid DID (%s): %w", identifier, err) 192 } 193 + ident, err := dir.Lookup(ctx, atid) 194 if err != nil { 195 return "", fmt.Errorf("failed to resolve DID (%s): %w", identifier, err) 196 } ··· 225 if err != nil { 226 return "", fmt.Errorf("not a valid DID (%s): %w", did, err) 227 } 228 + ident, err := dir.Lookup(ctx, atid) 229 if err != nil { 230 return "", fmt.Errorf("failed to resolve DID document (%s): %w", did, err) 231 }
+10 -5
internal/atproto/oauth/store_test.go
··· 120 HostURL: "https://pds2.example.com", 121 AuthServerURL: "https://auth2.example.com", 122 AuthServerTokenEndpoint: "https://auth2.example.com/oauth/token", 123 - Scopes: []string{"atproto", "transition:generic"}, 124 AccessToken: "new_access_token", 125 RefreshToken: "new_refresh_token", 126 DPoPPrivateKeyMultibase: "z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktX", ··· 136 assert.Equal(t, "new_access_token", retrieved.AccessToken) 137 assert.Equal(t, "new_refresh_token", retrieved.RefreshToken) 138 assert.Equal(t, "https://pds2.example.com", retrieved.HostURL) 139 - assert.Equal(t, []string{"atproto", "transition:generic"}, retrieved.Scopes) 140 } 141 142 func TestPostgresOAuthStore_GetSession_NotFound(t *testing.T) { ··· 335 func TestPostgresOAuthStore_CleanupExpiredSessions(t *testing.T) { 336 db := setupTestDB(t) 337 defer func() { _ = db.Close() }() 338 - // Clean up before AND after to ensure test isolation 339 - cleanupOAuth(t, db) 340 defer cleanupOAuth(t, db) 341 342 storeInterface := NewPostgresOAuthStore(db, 0) // Use default TTL ··· 344 require.True(t, ok, "store should be *PostgresOAuthStore") 345 ctx := context.Background() 346 347 did1, err := syntax.ParseDID("did:plc:testexpired1") 348 require.NoError(t, err) 349 - did2, err := syntax.ParseDID("did:plc:testexpired2") 350 require.NoError(t, err) 351 352 // Create an expired session (manually insert with past expiration)
··· 120 HostURL: "https://pds2.example.com", 121 AuthServerURL: "https://auth2.example.com", 122 AuthServerTokenEndpoint: "https://auth2.example.com/oauth/token", 123 + Scopes: []string{"atproto"}, 124 AccessToken: "new_access_token", 125 RefreshToken: "new_refresh_token", 126 DPoPPrivateKeyMultibase: "z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktX", ··· 136 assert.Equal(t, "new_access_token", retrieved.AccessToken) 137 assert.Equal(t, "new_refresh_token", retrieved.RefreshToken) 138 assert.Equal(t, "https://pds2.example.com", retrieved.HostURL) 139 + assert.Equal(t, []string{"atproto"}, retrieved.Scopes) 140 } 141 142 func TestPostgresOAuthStore_GetSession_NotFound(t *testing.T) { ··· 335 func TestPostgresOAuthStore_CleanupExpiredSessions(t *testing.T) { 336 db := setupTestDB(t) 337 defer func() { _ = db.Close() }() 338 defer cleanupOAuth(t, db) 339 340 storeInterface := NewPostgresOAuthStore(db, 0) // Use default TTL ··· 342 require.True(t, ok, "store should be *PostgresOAuthStore") 343 ctx := context.Background() 344 345 + // Pre-cleanup any expired sessions (from any source) to ensure the count 346 + // assertion below returns exactly 1. cleanupOAuth only handles did:plc:test% 347 + // records, but CleanupExpiredSessions operates on all expired sessions. 348 + _, err := store.CleanupExpiredSessions(ctx) 349 + require.NoError(t, err, "Failed to cleanup pre-existing expired sessions") 350 + 351 did1, err := syntax.ParseDID("did:plc:testexpired1") 352 require.NoError(t, err) 353 + var did2 syntax.DID 354 + did2, err = syntax.ParseDID("did:plc:testexpired2") 355 require.NoError(t, err) 356 357 // Create an expired session (manually insert with past expiration)
+1 -1
static/client-metadata.json
··· 9 "https://coves.social/oauth/callback", 10 "social.coves:/oauth/callback" 11 ], 12 - "scope": "atproto transition:generic", 13 "grant_types": ["authorization_code", "refresh_token"], 14 "response_types": ["code"], 15 "application_type": "native",
··· 9 "https://coves.social/oauth/callback", 10 "social.coves:/oauth/callback" 11 ], 12 + "scope": "atproto", 13 "grant_types": ["authorization_code", "refresh_token"], 14 "response_types": ["code"], 15 "application_type": "native",
+3 -3
tests/integration/oauth_e2e_test.go
··· 94 SessionID: fmt.Sprintf("localhost-test-%d", time.Now().UnixNano()), 95 HostURL: "http://localhost:3001", 96 AccessToken: "mocked-access-token", 97 - Scopes: []string{"atproto", "transition:generic"}, 98 } 99 100 err = store.SaveSession(ctx, testSession) ··· 435 RequestURI: "http://localhost:3001/authorize", 436 AuthServerTokenEndpoint: "http://localhost:3001/oauth/token", 437 AuthServerRevocationEndpoint: "http://localhost:3001/oauth/revoke", 438 - Scopes: []string{"atproto", "transition:generic"}, 439 } 440 441 // Save auth request ··· 549 RefreshToken: "initial-refresh-token", 550 DPoPPrivateKeyMultibase: "test-dpop-key", 551 DPoPAuthServerNonce: "test-nonce", 552 - Scopes: []string{"atproto", "transition:generic"}, 553 } 554 555 // Save the session
··· 94 SessionID: fmt.Sprintf("localhost-test-%d", time.Now().UnixNano()), 95 HostURL: "http://localhost:3001", 96 AccessToken: "mocked-access-token", 97 + Scopes: []string{"atproto"}, 98 } 99 100 err = store.SaveSession(ctx, testSession) ··· 435 RequestURI: "http://localhost:3001/authorize", 436 AuthServerTokenEndpoint: "http://localhost:3001/oauth/token", 437 AuthServerRevocationEndpoint: "http://localhost:3001/oauth/revoke", 438 + Scopes: []string{"atproto"}, 439 } 440 441 // Save auth request ··· 549 RefreshToken: "initial-refresh-token", 550 DPoPPrivateKeyMultibase: "test-dpop-key", 551 DPoPAuthServerNonce: "test-nonce", 552 + Scopes: []string{"atproto"}, 553 } 554 555 // Save the session
+2 -2
tests/integration/oauth_helpers.go
··· 70 config = &oauth.OAuthConfig{ 71 PublicURL: "http://localhost:3000", // Test server callback URL 72 SealSecret: sealSecretB64, // For sealing mobile tokens 73 - Scopes: []string{"atproto", "transition:generic"}, 74 DevMode: false, // Production mode for HTTPS PDS 75 AllowPrivateIPs: false, // No private IPs in production mode 76 PLCURL: "", // Use default PLC directory (plc.directory) ··· 81 config = &oauth.OAuthConfig{ 82 PublicURL: "http://localhost:3000", // Match the callback URL expected by PDS 83 SealSecret: sealSecretB64, // For sealing mobile tokens 84 - Scopes: []string{"atproto", "transition:generic"}, 85 DevMode: true, // Enable dev mode for localhost testing 86 AllowPrivateIPs: true, // Allow private IPs for local testing 87 PLCURL: getTestPLCURL(), // Use local PLC directory for DID resolution
··· 70 config = &oauth.OAuthConfig{ 71 PublicURL: "http://localhost:3000", // Test server callback URL 72 SealSecret: sealSecretB64, // For sealing mobile tokens 73 + Scopes: []string{"atproto"}, 74 DevMode: false, // Production mode for HTTPS PDS 75 AllowPrivateIPs: false, // No private IPs in production mode 76 PLCURL: "", // Use default PLC directory (plc.directory) ··· 81 config = &oauth.OAuthConfig{ 82 PublicURL: "http://localhost:3000", // Match the callback URL expected by PDS 83 SealSecret: sealSecretB64, // For sealing mobile tokens 84 + Scopes: []string{"atproto"}, 85 DevMode: true, // Enable dev mode for localhost testing 86 AllowPrivateIPs: true, // Allow private IPs for local testing 87 PLCURL: getTestPLCURL(), // Use local PLC directory for DID resolution