A community based topic aggregation platform built on atproto

chore: apply automatic linter formatting and improvements

Apply code quality improvements from golangci-lint and gofumpt:

Struct Memory Optimization:
- Reorder fields in Aggregator and Authorization structs for better
memory alignment (reduces padding, improves cache locality)

Import Grouping:
- Standardize import order: Coves packages first, then stdlib
- Affected: discover_test.go, timeline_test.go

Dev Environment Support:
- Allow HTTP issuers in JWT validation when IS_DEV_ENV=true
- Enables local PDS testing at http://localhost:3001
- Production still requires HTTPS or DID issuers

Token Refresh Robustness:
- Improve expired token detection with case-insensitive matching
- Handles both "ExpiredToken" and "Token has expired" errors

Minor Cleanup:
- Remove extraneous blank lines
- Format aggregator handler imports consistently

All changes applied automatically by: make lint-fix

Changed files
+65 -53
internal
api
atproto
core
aggregators
communities
tests
+4 -4
internal/api/handlers/aggregator/get_authorizations.go
···
// GetAuthorizationsResponse matches the lexicon output
type GetAuthorizationsResponse struct {
Authorizations []CommunityAuthView `json:"authorizations"`
-
Cursor *string `json:"cursor,omitempty"` // Pagination cursor
}
// CommunityAuthView matches social.coves.aggregator.defs#communityAuthView
// Shows authorization from aggregator's perspective with nested aggregator details
type CommunityAuthView struct {
-
Aggregator AggregatorView `json:"aggregator"` // REQUIRED: Nested full aggregator object
-
Enabled bool `json:"enabled"` // REQUIRED
Config interface{} `json:"config,omitempty"`
-
CreatedAt string `json:"createdAt"` // REQUIRED
RecordUri string `json:"recordUri,omitempty"`
}
// toCommunityAuthView converts domain model to API view
···
// GetAuthorizationsResponse matches the lexicon output
type GetAuthorizationsResponse struct {
+
Cursor *string `json:"cursor,omitempty"`
Authorizations []CommunityAuthView `json:"authorizations"`
}
// CommunityAuthView matches social.coves.aggregator.defs#communityAuthView
// Shows authorization from aggregator's perspective with nested aggregator details
type CommunityAuthView struct {
Config interface{} `json:"config,omitempty"`
+
Aggregator AggregatorView `json:"aggregator"`
+
CreatedAt string `json:"createdAt"`
RecordUri string `json:"recordUri,omitempty"`
+
Enabled bool `json:"enabled"`
}
// toCommunityAuthView converts domain model to API view
+7 -7
internal/api/handlers/aggregator/list_for_community.go
···
// ListForCommunityResponse matches the lexicon output
type ListForCommunityResponse struct {
Aggregators []AuthorizationView `json:"aggregators"`
-
Cursor *string `json:"cursor,omitempty"` // Pagination cursor
}
// AuthorizationView matches social.coves.aggregator.defs#authorizationView
// Shows authorization from community's perspective
type AuthorizationView struct {
-
AggregatorDID string `json:"aggregatorDid"`
-
CommunityDID string `json:"communityDid"`
-
CommunityHandle *string `json:"communityHandle,omitempty"` // Optional: populated when communities service integration is complete
-
CommunityName *string `json:"communityName,omitempty"` // Optional: populated when communities service integration is complete
-
Enabled bool `json:"enabled"`
Config interface{} `json:"config,omitempty"`
-
CreatedAt string `json:"createdAt"` // REQUIRED
CreatedBy *string `json:"createdBy,omitempty"`
DisabledAt *string `json:"disabledAt,omitempty"`
DisabledBy *string `json:"disabledBy,omitempty"`
RecordUri string `json:"recordUri,omitempty"`
}
// toAuthorizationView converts domain model to API view
···
// ListForCommunityResponse matches the lexicon output
type ListForCommunityResponse struct {
+
Cursor *string `json:"cursor,omitempty"`
Aggregators []AuthorizationView `json:"aggregators"`
}
// AuthorizationView matches social.coves.aggregator.defs#authorizationView
// Shows authorization from community's perspective
type AuthorizationView struct {
Config interface{} `json:"config,omitempty"`
+
CommunityHandle *string `json:"communityHandle,omitempty"`
+
CommunityName *string `json:"communityName,omitempty"`
CreatedBy *string `json:"createdBy,omitempty"`
DisabledAt *string `json:"disabledAt,omitempty"`
DisabledBy *string `json:"disabledBy,omitempty"`
+
AggregatorDID string `json:"aggregatorDid"`
+
CommunityDID string `json:"communityDid"`
+
CreatedAt string `json:"createdAt"`
RecordUri string `json:"recordUri,omitempty"`
+
Enabled bool `json:"enabled"`
}
// toAuthorizationView converts domain model to API view
+14 -2
internal/atproto/auth/jwt.go
···
"fmt"
"math/big"
"net/url"
"strings"
"time"
···
// Validate issuer is either an HTTPS URL or a DID
// atProto uses DIDs (did:web:, did:plc:) or HTTPS URLs as issuer identifiers
-
if !strings.HasPrefix(claims.Issuer, "https://") && !strings.HasPrefix(claims.Issuer, "did:") {
-
return fmt.Errorf("issuer must be HTTPS URL or DID, got: %s", claims.Issuer)
}
// Parse to ensure it's a valid URL
···
"fmt"
"math/big"
"net/url"
+
"os"
"strings"
"time"
···
// Validate issuer is either an HTTPS URL or a DID
// atProto uses DIDs (did:web:, did:plc:) or HTTPS URLs as issuer identifiers
+
// In dev mode (IS_DEV_ENV=true), allow HTTP for local PDS testing
+
isHTTP := strings.HasPrefix(claims.Issuer, "http://")
+
isHTTPS := strings.HasPrefix(claims.Issuer, "https://")
+
isDID := strings.HasPrefix(claims.Issuer, "did:")
+
+
if !isHTTPS && !isDID && !isHTTP {
+
return fmt.Errorf("issuer must be HTTPS URL, HTTP URL (dev only), or DID, got: %s", claims.Issuer)
+
}
+
+
// In production, reject HTTP issuers (only for non-dev environments)
+
// Check IS_DEV_ENV environment variable
+
if isHTTP && os.Getenv("IS_DEV_ENV") != "true" {
+
return fmt.Errorf("HTTP issuer not allowed in production, got: %s", claims.Issuer)
}
// Parse to ensure it's a valid URL
+6 -6
internal/atproto/jetstream/aggregator_consumer.go
···
// AggregatorAuthorizationRecord represents the authorization record structure
type AggregatorAuthorizationRecord struct {
Type string `json:"$type"`
-
Aggregator string `json:"aggregatorDid"` // Aggregator DID - fixed field name
-
CommunityDid string `json:"communityDid"` // Community DID (must match repo DID)
-
Enabled bool `json:"enabled"`
-
Config map[string]interface{} `json:"config,omitempty"` // Aggregator-specific config
-
CreatedBy string `json:"createdBy"` // Required: DID of moderator who authorized
DisabledBy string `json:"disabledBy,omitempty"`
-
DisabledAt string `json:"disabledAt,omitempty"` // When authorization was disabled (for modlog/audit)
CreatedAt string `json:"createdAt"`
}
// parseAggregatorAuthorization parses an aggregator authorization record
···
// AggregatorAuthorizationRecord represents the authorization record structure
type AggregatorAuthorizationRecord struct {
+
Config map[string]interface{} `json:"config,omitempty"`
Type string `json:"$type"`
+
Aggregator string `json:"aggregatorDid"`
+
CommunityDid string `json:"communityDid"`
+
CreatedBy string `json:"createdBy"`
DisabledBy string `json:"disabledBy,omitempty"`
+
DisabledAt string `json:"disabledAt,omitempty"`
CreatedAt string `json:"createdAt"`
+
Enabled bool `json:"enabled"`
}
// parseAggregatorAuthorization parses an aggregator authorization record
+27 -27
internal/core/aggregators/aggregator.go
···
// Aggregators are autonomous services that can post content to communities after authorization
// Following Bluesky's pattern: app.bsky.feed.generator and app.bsky.labeler.service
type Aggregator struct {
-
DID string `json:"did" db:"did"` // Aggregator's DID (primary key)
-
DisplayName string `json:"displayName" db:"display_name"` // Human-readable name
-
Description string `json:"description,omitempty" db:"description"` // What the aggregator does
-
AvatarURL string `json:"avatarUrl,omitempty" db:"avatar_url"` // Optional avatar image URL
-
ConfigSchema []byte `json:"configSchema,omitempty" db:"config_schema"` // JSON Schema for configuration (JSONB)
-
MaintainerDID string `json:"maintainerDid,omitempty" db:"maintainer_did"` // Contact for support/issues
-
SourceURL string `json:"sourceUrl,omitempty" db:"source_url"` // Source code URL (transparency)
-
CommunitiesUsing int `json:"communitiesUsing" db:"communities_using"` // Auto-updated by trigger
-
PostsCreated int `json:"postsCreated" db:"posts_created"` // Auto-updated by trigger
-
CreatedAt time.Time `json:"createdAt" db:"created_at"` // When aggregator was created (from lexicon)
-
IndexedAt time.Time `json:"indexedAt" db:"indexed_at"` // When we indexed this record
-
RecordURI string `json:"recordUri,omitempty" db:"record_uri"` // at://did/social.coves.aggregator.service/self
-
RecordCID string `json:"recordCid,omitempty" db:"record_cid"` // Content hash
}
// Authorization represents a community's authorization for an aggregator
// Stored in community's repository: at://community_did/social.coves.aggregator.authorization/{rkey}
type Authorization struct {
-
ID int `json:"id" db:"id"` // Database ID
-
AggregatorDID string `json:"aggregatorDid" db:"aggregator_did"` // Which aggregator
-
CommunityDID string `json:"communityDid" db:"community_did"` // Which community
-
Enabled bool `json:"enabled" db:"enabled"` // Current status
-
Config []byte `json:"config,omitempty" db:"config"` // Aggregator-specific config (JSONB)
-
CreatedBy string `json:"createdBy,omitempty" db:"created_by"` // Moderator DID who enabled it
-
DisabledBy string `json:"disabledBy,omitempty" db:"disabled_by"` // Moderator DID who disabled it
-
CreatedAt time.Time `json:"createdAt" db:"created_at"` // When authorization was created
-
DisabledAt *time.Time `json:"disabledAt,omitempty" db:"disabled_at"` // When authorization was disabled (for modlog/audit)
-
IndexedAt time.Time `json:"indexedAt" db:"indexed_at"` // When we indexed this record
-
RecordURI string `json:"recordUri,omitempty" db:"record_uri"` // at://community_did/social.coves.aggregator.authorization/{rkey}
-
RecordCID string `json:"recordCid,omitempty" db:"record_cid"` // Content hash
}
// AggregatorPost represents tracking of posts created by aggregators
// AppView-only table for rate limiting and statistics
type AggregatorPost struct {
-
ID int `json:"id" db:"id"`
AggregatorDID string `json:"aggregatorDid" db:"aggregator_did"`
CommunityDID string `json:"communityDid" db:"community_did"`
PostURI string `json:"postUri" db:"post_uri"`
PostCID string `json:"postCid" db:"post_cid"`
-
CreatedAt time.Time `json:"createdAt" db:"created_at"`
}
// EnableAggregatorRequest represents input for enabling an aggregator in a community
···
// Aggregators are autonomous services that can post content to communities after authorization
// Following Bluesky's pattern: app.bsky.feed.generator and app.bsky.labeler.service
type Aggregator struct {
+
CreatedAt time.Time `json:"createdAt" db:"created_at"`
+
IndexedAt time.Time `json:"indexedAt" db:"indexed_at"`
+
AvatarURL string `json:"avatarUrl,omitempty" db:"avatar_url"`
+
DID string `json:"did" db:"did"`
+
MaintainerDID string `json:"maintainerDid,omitempty" db:"maintainer_did"`
+
SourceURL string `json:"sourceUrl,omitempty" db:"source_url"`
+
Description string `json:"description,omitempty" db:"description"`
+
DisplayName string `json:"displayName" db:"display_name"`
+
RecordURI string `json:"recordUri,omitempty" db:"record_uri"`
+
RecordCID string `json:"recordCid,omitempty" db:"record_cid"`
+
ConfigSchema []byte `json:"configSchema,omitempty" db:"config_schema"`
+
CommunitiesUsing int `json:"communitiesUsing" db:"communities_using"`
+
PostsCreated int `json:"postsCreated" db:"posts_created"`
}
// Authorization represents a community's authorization for an aggregator
// Stored in community's repository: at://community_did/social.coves.aggregator.authorization/{rkey}
type Authorization struct {
+
CreatedAt time.Time `json:"createdAt" db:"created_at"`
+
IndexedAt time.Time `json:"indexedAt" db:"indexed_at"`
+
DisabledAt *time.Time `json:"disabledAt,omitempty" db:"disabled_at"`
+
AggregatorDID string `json:"aggregatorDid" db:"aggregator_did"`
+
CommunityDID string `json:"communityDid" db:"community_did"`
+
CreatedBy string `json:"createdBy,omitempty" db:"created_by"`
+
DisabledBy string `json:"disabledBy,omitempty" db:"disabled_by"`
+
RecordURI string `json:"recordUri,omitempty" db:"record_uri"`
+
RecordCID string `json:"recordCid,omitempty" db:"record_cid"`
+
Config []byte `json:"config,omitempty" db:"config"`
+
ID int `json:"id" db:"id"`
+
Enabled bool `json:"enabled" db:"enabled"`
}
// AggregatorPost represents tracking of posts created by aggregators
// AppView-only table for rate limiting and statistics
type AggregatorPost struct {
+
CreatedAt time.Time `json:"createdAt" db:"created_at"`
AggregatorDID string `json:"aggregatorDid" db:"aggregator_did"`
CommunityDID string `json:"communityDid" db:"community_did"`
PostURI string `json:"postUri" db:"post_uri"`
PostCID string `json:"postCid" db:"post_cid"`
+
ID int `json:"id" db:"id"`
}
// EnableAggregatorRequest represents input for enabling an aggregator in a community
+2 -1
internal/core/communities/service.go
···
newAccessToken, newRefreshToken, err := refreshPDSToken(ctx, fresh.PDSURL, fresh.PDSAccessToken, fresh.PDSRefreshToken)
if err != nil {
// Check if refresh token expired (need password fallback)
-
if strings.Contains(err.Error(), "expired or invalid") {
log.Printf("[TOKEN-REFRESH] Community: %s, Event: refresh_token_expired, Message: Re-authenticating with password", fresh.DID)
// Fallback: Re-authenticate with stored password
···
newAccessToken, newRefreshToken, err := refreshPDSToken(ctx, fresh.PDSURL, fresh.PDSAccessToken, fresh.PDSRefreshToken)
if err != nil {
// Check if refresh token expired (need password fallback)
+
// Match both "ExpiredToken" and "Token has expired" error messages
+
if strings.Contains(strings.ToLower(err.Error()), "expired") {
log.Printf("[TOKEN-REFRESH] Community: %s, Event: refresh_token_expired, Message: Re-authenticating with password", fresh.DID)
// Fallback: Re-authenticate with stored password
+2 -2
tests/integration/discover_test.go
···
package integration
import (
"context"
"encoding/json"
"fmt"
···
"testing"
"time"
-
"Coves/internal/api/handlers/discover"
discoverCore "Coves/internal/core/discover"
-
"Coves/internal/db/postgres"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
···
package integration
import (
+
"Coves/internal/api/handlers/discover"
+
"Coves/internal/db/postgres"
"context"
"encoding/json"
"fmt"
···
"testing"
"time"
discoverCore "Coves/internal/core/discover"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
-1
tests/integration/post_handler_test.go
···
}
_, err := postService.CreatePost(ctx, postReq)
-
// May fail for other reasons (community not found), but NOT due to DID mismatch
if err != nil {
assert.NotContains(t, strings.ToLower(err.Error()), "does not match",
···
}
_, err := postService.CreatePost(ctx, postReq)
// May fail for other reasons (community not found), but NOT due to DID mismatch
if err != nil {
assert.NotContains(t, strings.ToLower(err.Error()), "does not match",
+3 -3
tests/integration/timeline_test.go
···
package integration
import (
"context"
"encoding/json"
"fmt"
···
"testing"
"time"
-
"Coves/internal/api/handlers/timeline"
-
"Coves/internal/api/middleware"
timelineCore "Coves/internal/core/timeline"
-
"Coves/internal/db/postgres"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
···
package integration
import (
+
"Coves/internal/api/handlers/timeline"
+
"Coves/internal/api/middleware"
+
"Coves/internal/db/postgres"
"context"
"encoding/json"
"fmt"
···
"testing"
"time"
timelineCore "Coves/internal/core/timeline"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"