+1
-2
internal/api/handlers/aggregator/errors.go
+1
-2
internal/api/handlers/aggregator/errors.go
+1
-2
internal/api/handlers/aggregator/get_services.go
+1
-2
internal/api/handlers/aggregator/get_services.go
+1
-2
internal/api/handlers/aggregator/list_for_community.go
+1
-2
internal/api/handlers/aggregator/list_for_community.go
+1
-1
internal/api/handlers/aggregator/register.go
+1
-1
internal/api/handlers/aggregator/register.go
+1
-2
internal/api/handlers/comments/errors.go
+1
-2
internal/api/handlers/comments/errors.go
+2
-3
internal/api/handlers/comments/get_comments.go
+2
-3
internal/api/handlers/comments/get_comments.go
···
+1
-2
internal/api/handlers/comments/middleware.go
+1
-2
internal/api/handlers/comments/middleware.go
+1
-2
internal/api/handlers/comments/service_adapter.go
+1
-2
internal/api/handlers/comments/service_adapter.go
+2
-3
internal/api/handlers/community/block.go
+2
-3
internal/api/handlers/community/block.go
···
+2
-3
internal/api/handlers/community/create.go
+2
-3
internal/api/handlers/community/create.go
+6
-7
internal/api/handlers/community/create_test.go
+6
-7
internal/api/handlers/community/create_test.go
·········-func (m *mockCommunityService) ListCommunities(ctx context.Context, req communities.ListCommunitiesRequest) ([]*communities.Community, int, error) {+func (m *mockCommunityService) ListCommunities(ctx context.Context, req communities.ListCommunitiesRequest) ([]*communities.Community, error) {func (m *mockCommunityService) SearchCommunities(ctx context.Context, req communities.SearchCommunitiesRequest) ([]*communities.Community, int, error) {···
+1
-2
internal/api/handlers/community/errors.go
+1
-2
internal/api/handlers/community/errors.go
+1
-2
internal/api/handlers/community/get.go
+1
-2
internal/api/handlers/community/get.go
+1
-2
internal/api/handlers/community/list.go
+1
-2
internal/api/handlers/community/list.go
+1
-2
internal/api/handlers/community/search.go
+1
-2
internal/api/handlers/community/search.go
+2
-3
internal/api/handlers/community/subscribe.go
+2
-3
internal/api/handlers/community/subscribe.go
···
+2
-3
internal/api/handlers/community/update.go
+2
-3
internal/api/handlers/community/update.go
+1
-2
internal/api/handlers/communityFeed/errors.go
+1
-2
internal/api/handlers/communityFeed/errors.go
+2
-3
internal/api/handlers/communityFeed/get_community.go
+2
-3
internal/api/handlers/communityFeed/get_community.go
···
+1
-2
internal/api/handlers/discover/errors.go
+1
-2
internal/api/handlers/discover/errors.go
+2
-3
internal/api/handlers/discover/get_discover.go
+2
-3
internal/api/handlers/discover/get_discover.go
···
+2
-3
internal/api/handlers/post/create.go
+2
-3
internal/api/handlers/post/create.go
···
+2
-3
internal/api/handlers/post/errors.go
+2
-3
internal/api/handlers/post/errors.go
+1
-2
internal/api/handlers/timeline/errors.go
+1
-2
internal/api/handlers/timeline/errors.go
+3
-4
internal/api/handlers/timeline/get_timeline.go
+3
-4
internal/api/handlers/timeline/get_timeline.go
···
+1
-2
internal/atproto/jetstream/aggregator_consumer.go
+1
-2
internal/atproto/jetstream/aggregator_consumer.go
+2
-3
internal/atproto/jetstream/comment_consumer.go
+2
-3
internal/atproto/jetstream/comment_consumer.go
······
+3
-4
internal/atproto/jetstream/community_consumer.go
+3
-4
internal/atproto/jetstream/community_consumer.go
······
+3
-4
internal/atproto/jetstream/post_consumer.go
+3
-4
internal/atproto/jetstream/post_consumer.go
······
+2
-3
internal/atproto/jetstream/user_consumer.go
+2
-3
internal/atproto/jetstream/user_consumer.go
······
+3
-4
internal/atproto/jetstream/vote_consumer.go
+3
-4
internal/atproto/jetstream/vote_consumer.go
···
+1
-2
internal/core/aggregators/service.go
+1
-2
internal/core/aggregators/service.go
+1
-2
internal/core/blobs/service.go
+1
-2
internal/core/blobs/service.go
+3
-4
internal/core/comments/comment_service.go
+3
-4
internal/core/comments/comment_service.go
······
+5
-6
internal/core/comments/comment_service_test.go
+5
-6
internal/core/comments/comment_service_test.go
······-func (m *mockCommunityRepo) List(ctx context.Context, req communities.ListCommunitiesRequest) ([]*communities.Community, int, error) {+func (m *mockCommunityRepo) List(ctx context.Context, req communities.ListCommunitiesRequest) ([]*communities.Community, error) {
+1
-2
internal/core/communities/service.go
+1
-2
internal/core/communities/service.go
······
+1
-2
internal/core/communityFeeds/service.go
+1
-2
internal/core/communityFeeds/service.go
+1
-2
internal/core/communityFeeds/types.go
+1
-2
internal/core/communityFeeds/types.go
+1
-2
internal/core/discover/types.go
+1
-2
internal/core/discover/types.go
+5
-6
internal/core/posts/service.go
+5
-6
internal/core/posts/service.go
······
+1
-2
internal/core/timeline/types.go
+1
-2
internal/core/timeline/types.go
+1
-2
internal/core/users/service.go
+1
-2
internal/core/users/service.go
······
+1
-2
internal/db/postgres/aggregator_repo.go
+1
-2
internal/db/postgres/aggregator_repo.go
+1
-2
internal/db/postgres/comment_repo.go
+1
-2
internal/db/postgres/comment_repo.go
+2
-4
internal/db/postgres/community_repo.go
+2
-4
internal/db/postgres/community_repo.go
······
+1
-2
internal/db/postgres/community_repo_blocks.go
+1
-2
internal/db/postgres/community_repo_blocks.go
+1
-2
internal/db/postgres/community_repo_memberships.go
+1
-2
internal/db/postgres/community_repo_memberships.go
+1
-2
internal/db/postgres/community_repo_subscriptions.go
+1
-2
internal/db/postgres/community_repo_subscriptions.go
+1
-2
internal/db/postgres/discover_repo.go
+1
-2
internal/db/postgres/discover_repo.go
+1
-2
internal/db/postgres/feed_repo.go
+1
-2
internal/db/postgres/feed_repo.go
+1
-2
internal/db/postgres/feed_repo_base.go
+1
-2
internal/db/postgres/feed_repo_base.go
······
+1
-2
internal/db/postgres/post_repo.go
+1
-2
internal/db/postgres/post_repo.go
+1
-2
internal/db/postgres/timeline_repo.go
+1
-2
internal/db/postgres/timeline_repo.go
+1
-2
internal/db/postgres/user_repo.go
+1
-2
internal/db/postgres/user_repo.go
+1
-2
internal/db/postgres/vote_repo.go
+1
-2
internal/db/postgres/vote_repo.go
+1
-2
internal/db/postgres/vote_repo_test.go
+1
-2
internal/db/postgres/vote_repo_test.go
+7
-8
tests/e2e/error_recovery_test.go
+7
-8
tests/e2e/error_recovery_test.go
············+_, _ = w.Write([]byte(`{"error":"ServiceUnavailable","message":"PDS temporarily unavailable"}`))
+1
-2
tests/e2e/ratelimit_e2e_test.go
+1
-2
tests/e2e/ratelimit_e2e_test.go
+4
-5
tests/e2e/user_signup_test.go
+4
-5
tests/e2e/user_signup_test.go
······
+10
-11
tests/integration/aggregator_e2e_test.go
+10
-11
tests/integration/aggregator_e2e_test.go
······
+14
-14
tests/integration/aggregator_registration_test.go
+14
-14
tests/integration/aggregator_registration_test.go
······wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {···wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {·········wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {···wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {·········wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {·········wellKnownServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+3
-4
tests/integration/aggregator_test.go
+3
-4
tests/integration/aggregator_test.go
···
+13
-14
tests/integration/blob_upload_e2e_test.go
+13
-14
tests/integration/blob_upload_e2e_test.go
···············
+14
-14
tests/integration/block_handle_resolution_test.go
+14
-14
tests/integration/block_handle_resolution_test.go
·······································
+3
-4
tests/integration/comment_consumer_test.go
+3
-4
tests/integration/comment_consumer_test.go
···
+3
-4
tests/integration/comment_query_test.go
+3
-4
tests/integration/comment_query_test.go
······
+4
-5
tests/integration/comment_vote_test.go
+4
-5
tests/integration/comment_vote_test.go
···
+2
-3
tests/integration/community_blocking_test.go
+2
-3
tests/integration/community_blocking_test.go
···
+4
-5
tests/integration/community_consumer_test.go
+4
-5
tests/integration/community_consumer_test.go
···
+2
-3
tests/integration/community_credentials_test.go
+2
-3
tests/integration/community_credentials_test.go
···
+13
-14
tests/integration/community_e2e_test.go
+13
-14
tests/integration/community_e2e_test.go
·····················
+4
-5
tests/integration/community_hostedby_security_test.go
+4
-5
tests/integration/community_hostedby_security_test.go
······// TestHostedByVerification_DomainMatching tests that hostedBy domain must match handle domain······
+2
-3
tests/integration/community_identifier_resolution_test.go
+2
-3
tests/integration/community_identifier_resolution_test.go
······
+2
-3
tests/integration/community_provisioning_test.go
+2
-3
tests/integration/community_provisioning_test.go
···
+2
-3
tests/integration/community_repo_test.go
+2
-3
tests/integration/community_repo_test.go
···
+2
-3
tests/integration/community_service_integration_test.go
+2
-3
tests/integration/community_service_integration_test.go
······
+3
-4
tests/integration/community_v2_validation_test.go
+3
-4
tests/integration/community_v2_validation_test.go
···
+6
-7
tests/integration/concurrent_scenarios_test.go
+6
-7
tests/integration/concurrent_scenarios_test.go
······
+2
-3
tests/integration/discover_test.go
+2
-3
tests/integration/discover_test.go
······
+4
-5
tests/integration/feed_test.go
+4
-5
tests/integration/feed_test.go
······
+2
-3
tests/integration/helpers.go
+2
-3
tests/integration/helpers.go
······
+1
-2
tests/integration/identity_resolution_test.go
+1
-2
tests/integration/identity_resolution_test.go
+3
-4
tests/integration/jetstream_consumer_test.go
+3
-4
tests/integration/jetstream_consumer_test.go
···
+3
-4
tests/integration/jwt_verification_test.go
+3
-4
tests/integration/jwt_verification_test.go
·········
+3
-4
tests/integration/post_consumer_test.go
+3
-4
tests/integration/post_consumer_test.go
···
+4
-5
tests/integration/post_creation_test.go
+4
-5
tests/integration/post_creation_test.go
···
+8
-9
tests/integration/post_e2e_test.go
+8
-9
tests/integration/post_e2e_test.go
······
+5
-6
tests/integration/post_handler_test.go
+5
-6
tests/integration/post_handler_test.go
······
+5
-6
tests/integration/post_thumb_validation_test.go
+5
-6
tests/integration/post_thumb_validation_test.go
······
+5
-6
tests/integration/post_unfurl_test.go
+5
-6
tests/integration/post_unfurl_test.go
······
+2
-3
tests/integration/subscription_indexing_test.go
+2
-3
tests/integration/subscription_indexing_test.go
···
+3
-4
tests/integration/timeline_test.go
+3
-4
tests/integration/timeline_test.go
······
+2
-3
tests/integration/token_refresh_test.go
+2
-3
tests/integration/token_refresh_test.go
···
+11
-11
tests/integration/user_journey_e2e_test.go
+11
-11
tests/integration/user_journey_e2e_test.go
···············
+1
-2
tests/unit/community_service_test.go
+1
-2
tests/unit/community_service_test.go
+5
-3
Caddyfile
+5
-3
Caddyfile
······
+23
static/.well-known/did.json
+23
static/.well-known/did.json
···
+1
-1
docs/E2E_TESTING.md
+1
-1
docs/E2E_TESTING.md
+52
internal/atproto/auth/combined_key_fetcher.go
+52
internal/atproto/auth/combined_key_fetcher.go
···+func NewCombinedKeyFetcher(directory indigoIdentity.Directory, jwksFetcher JWKSFetcher) *CombinedKeyFetcher {+func (f *CombinedKeyFetcher) FetchPublicKey(ctx context.Context, issuer, token string) (interface{}, error) {
+116
internal/atproto/auth/did_key_fetcher.go
+116
internal/atproto/auth/did_key_fetcher.go
···+func (f *DIDKeyFetcher) FetchPublicKey(ctx context.Context, issuer, token string) (interface{}, error) {
+5
.env.dev
+5
.env.dev
···
+28
.env.prod.example
+28
.env.prod.example
···
+484
internal/atproto/auth/dpop.go
+484
internal/atproto/auth/dpop.go
···+func (v *DPoPVerifier) VerifyDPoPProof(dpopProof, httpMethod, httpURI string) (*DPoPProof, error) {+verifiedToken, err := jwt.ParseWithClaims(dpopProof, &DPoPClaims{}, func(token *jwt.Token) (interface{}, error) {+func (v *DPoPVerifier) validateDPoPClaims(claims *DPoPClaims, expectedMethod, expectedURI string) error {+return fmt.Errorf("DPoP proof htm mismatch: expected %s, got %s", expectedMethod, claims.HTTPMethod)+return fmt.Errorf("DPoP proof htu mismatch: expected %s, got %s", expectedURIBase, claimURIBase)+return fmt.Errorf("DPoP proof is too old (issued %v ago, max %v)", now.Sub(iat), v.MaxProofAge)+func (v *DPoPVerifier) VerifyTokenBinding(proof *DPoPProof, expectedThumbprint string) error {+// Serialize to JSON (Go's json.Marshal produces lexicographically ordered keys for map[string]string)
+921
internal/atproto/auth/dpop_test.go
+921
internal/atproto/auth/dpop_test.go
···+func createDPoPProof(t *testing.T, key *testECKey, method, uri string, iat time.Time, jti string) string {
+416
internal/api/middleware/auth_test.go
+416
internal/api/middleware/auth_test.go
·········+// TestGetDPoPProof_NotAuthenticated tests that GetDPoPProof returns nil when no DPoP was verified+handler := middleware.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {+handler := middleware.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {+middleware := NewAtProtoAuthMiddleware(fetcher, false) // skipVerify=false - REAL verification+handler := middleware.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {+t.Errorf("SECURITY VULNERABILITY: Expected 401, got %d. Token was not properly verified!", w.Code)+handler := middleware.OptionalAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {+handler := middleware.OptionalAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+134
-2
internal/atproto/auth/README.md
+134
-2
internal/atproto/auth/README.md
···+DPoP (Demonstrating Proof-of-Possession) binds access tokens to client-controlled cryptographic keys, preventing token theft and replay attacks.+DPoP is an OAuth extension (RFC 9449) that adds proof-of-possession semantics to bearer tokens. When a PDS issues a DPoP-bound access token:+> โ ๏ธ **DPoP is an ADDITIONAL security layer, NOT a replacement for token signature verification.**+1. **ALWAYS verify the access token signature first** (via JWKS, HS256 shared secret, or DID resolution)+**Why This Matters**: An attacker could create a fake token with `sub: "did:plc:victim"` and their own `cnf.jkt`, then present a valid DPoP proof signed with their key. If we accept DPoP as a fallback, the attacker can impersonate any user.+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ>โ+โ<โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ+DPoP proofs include a unique `jti` (JWT ID) claim. The server tracks seen `jti` values to prevent replay attacks:+// The verifier automatically rejects reused jti values within the proof validity window (5 minutes)······
+4
-1
.gitignore
+4
-1
.gitignore
+5
-6
go.mod
+5
-6
go.mod
·········
+6
-8
go.sum
+6
-8
go.sum
···-github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe h1:VBhaqE5ewQgXbY5SfSWFZC/AwHFo7cHxZKFYi2ce9Yo=-github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck=-github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=+github.com/bluesky-social/indigo v0.0.0-20251127021457-6f2658724b36 h1:Vc+l4sltxQfBT8qC3dm87PRYInmxlGyF1dmpjaW0WkU=+github.com/bluesky-social/indigo v0.0.0-20251127021457-6f2658724b36/go.mod h1:Pm2I1+iDXn/hLbF7XCg/DsZi6uDCiOo7hZGWprSM7k0=github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=···github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=+github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg=+github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw=······github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=