+125
internal/atproto/jetstream/post_jetstream_connector.go
+125
internal/atproto/jetstream/post_jetstream_connector.go
···+func NewPostJetstreamConnector(consumer *PostEventConsumer, wsURL string) *PostJetstreamConnector {+if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
+38
internal/core/communityFeeds/errors.go
+38
internal/core/communityFeeds/errors.go
···
+25
internal/core/communityFeeds/interfaces.go
+25
internal/core/communityFeeds/interfaces.go
···+// GetAuthorFeed(ctx context.Context, authorDID string, limit int, cursor *string) (*FeedResponse, error)+GetCommunityFeed(ctx context.Context, req GetCommunityFeedRequest) ([]*FeedViewPost, *string, error)+// GetTimeline(ctx context.Context, userDID string, limit int, cursor *string) ([]*FeedViewPost, *string, error)+// GetAuthorFeed(ctx context.Context, authorDID string, limit int, cursor *string) ([]*FeedViewPost, *string, error)
+23
internal/api/routes/communityFeed.go
+23
internal/api/routes/communityFeed.go
···
+214
internal/db/migrations/012_create_aggregators_tables.sql
+214
internal/db/migrations/012_create_aggregators_tables.sql
···+-- Source: Aggregator's own repository (at://aggregator_did/social.coves.aggregator.service/self)+created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When the aggregator service was created (from lexicon createdAt field)+communities_using INTEGER NOT NULL DEFAULT 0, -- Count of communities with enabled authorizations+COMMENT ON TABLE aggregators IS 'Aggregator service declarations indexed from social.coves.aggregator.service records';+COMMENT ON COLUMN aggregators.config_schema IS 'JSON Schema defining what config options communities can set';+COMMENT ON COLUMN aggregators.created_at IS 'When the aggregator service was created (from lexicon record createdAt field)';+COMMENT ON COLUMN aggregators.communities_using IS 'Cached count of communities with enabled=true authorizations';+-- Source: Community's repository (at://community_did/social.coves.aggregator.authorization/rkey)+CONSTRAINT fk_aggregator FOREIGN KEY (aggregator_did) REFERENCES aggregators(did) ON DELETE CASCADE,+CONSTRAINT fk_community FOREIGN KEY (community_did) REFERENCES communities(did) ON DELETE CASCADE+CREATE INDEX idx_aggregator_auth_agg_enabled ON aggregator_authorizations(aggregator_did, enabled) WHERE enabled = true;+CREATE INDEX idx_aggregator_auth_comm_enabled ON aggregator_authorizations(community_did, enabled) WHERE enabled = true;+CREATE INDEX idx_aggregator_auth_lookup ON aggregator_authorizations(aggregator_did, community_did, enabled);+CREATE INDEX idx_aggregator_auth_agg_did ON aggregator_authorizations(aggregator_did, created_at DESC);+CREATE INDEX idx_aggregator_auth_comm_did ON aggregator_authorizations(community_did, created_at DESC);+COMMENT ON TABLE aggregator_authorizations IS 'Community authorizations for aggregators indexed from social.coves.aggregator.authorization records';+COMMENT ON COLUMN aggregator_authorizations.config IS 'Community-specific config, validated against aggregators.config_schema';+COMMENT ON INDEX idx_aggregator_auth_lookup IS 'CRITICAL: Fast lookup for post creation authorization checks';+CONSTRAINT fk_aggregator_posts_agg FOREIGN KEY (aggregator_did) REFERENCES aggregators(did) ON DELETE CASCADE,+CONSTRAINT fk_aggregator_posts_comm FOREIGN KEY (community_did) REFERENCES communities(did) ON DELETE CASCADE+CREATE INDEX idx_aggregator_posts_rate_limit ON aggregator_posts(aggregator_did, community_did, created_at DESC);+CREATE INDEX idx_aggregator_posts_agg_did ON aggregator_posts(aggregator_did, created_at DESC);+CREATE INDEX idx_aggregator_posts_comm_did ON aggregator_posts(community_did, created_at DESC);+COMMENT ON TABLE aggregator_posts IS 'AppView-only tracking of posts created by aggregators for rate limiting and stats';+COMMENT ON INDEX idx_aggregator_posts_rate_limit IS 'CRITICAL: Fast rate limit checks (posts in last hour per community)';+COMMENT ON FUNCTION update_aggregator_communities_count IS 'Maintains aggregators.communities_using count when authorizations change';+COMMENT ON FUNCTION update_aggregator_posts_count IS 'Maintains aggregators.posts_created count when posts are tracked';+DROP TRIGGER IF EXISTS trigger_update_aggregator_communities_count ON aggregator_authorizations;
+3
internal/core/posts/errors.go
+3
internal/core/posts/errors.go
···
+136
internal/atproto/jetstream/aggregator_jetstream_connector.go
+136
internal/atproto/jetstream/aggregator_jetstream_connector.go
···+// AggregatorJetstreamConnector handles WebSocket connection to Jetstream for aggregator events+// NewAggregatorJetstreamConnector creates a new Jetstream WebSocket connector for aggregator events+func NewAggregatorJetstreamConnector(consumer *AggregatorEventConsumer, wsURL string) *AggregatorJetstreamConnector {
+41
aggregators/kagi-news/.gitignore
+41
aggregators/kagi-news/.gitignore
···
+3
aggregators/kagi-news/src/__init__.py
+3
aggregators/kagi-news/src/__init__.py
+165
aggregators/kagi-news/src/config.py
+165
aggregators/kagi-news/src/config.py
···+logger.info(f"Loaded configuration with {len(feeds)} feeds ({sum(1 for f in feeds if f.enabled)} enabled)")
+71
aggregators/kagi-news/src/rss_fetcher.py
+71
aggregators/kagi-news/src/rss_fetcher.py
···
+213
aggregators/kagi-news/src/state_manager.py
+213
aggregators/kagi-news/src/state_manager.py
···
+1
aggregators/kagi-news/tests/__init__.py
+1
aggregators/kagi-news/tests/__init__.py
···
+12
aggregators/kagi-news/tests/fixtures/sample_rss_item.xml
+12
aggregators/kagi-news/tests/fixtures/sample_rss_item.xml
···+<description><p>The White House confirmed President Trump will hold a bilateral meeting with Chinese President Xi Jinping in South Korea on October 30, at the end of an Asia trip that includes Malaysia and Japan . The administration said the meeting will take place Thursday morning local time, and Mr Trump indicated his first question to Xi would concern fentanyl and other bilateral issues . The talks come amid heightened trade tensions after Beijing expanded export curbs on rare-earth minerals and following Mr Trump's recent threat of additional tariffs on Chinese goods, making the meeting a focal point for discussions on trade, technology supply chains and energy .</p><img src='https://kagiproxy.com/img/Q2SRXQtwTYBIiQeI0FG-X6taF_wHSJaXDiFUzju2kbCWGuOYIFUX--8L0BqE4VKxpbOJY3ylFPJkDpfSnyQYZ1qdOLXbphHTnsOK4jb7gqC4KCn5nf3ANbWCuaFD5ZUSijiK0k7wOLP2fyX6tynu2mPtXlCbotLo2lTrEswZl4-No2AI4mI4lkResfnRdp-YjpoEfCOHkNfbN1-0cNcHt9T2dmgBSXrQ2w' alt='News image associated with coverage of President Trump&#x27;s Asia trip and planned meeting with President Xi' /><br /><h3>Highlights:</h3><ul><li>Itinerary details: The Asia swing begins in Malaysia, continues to Japan and ends with the bilateral meeting in South Korea on Thursday morning local time, White House press secretary Karoline Leavitt said at a briefing .</li><li>APEC context: US officials indicated the leaders will meet on the sidelines of the Asia-Pacific Economic Cooperation gathering, shaping expectations for short, high-level talks rather than a lengthy summit .</li><li>Tariff escalation: President Trump recently threatened an additional 100% tariff on Chinese goods starting in November, a step he has described as unsustainable but that has heightened urgency for talks .</li><li>Rare-earth impact: Beijing's expanded curbs on rare-earth exports have exposed supply vulnerabilities because US high-tech firms rely heavily on those materials, raising strategic and economic stakes for the meeting .</li></ul><blockquote>Work out a lot of our doubts and questions - President Trump</blockquote><h3>Perspectives:</h3><ul><li>President Trump: He said his first question to President Xi would be about fentanyl and indicated he hoped to resolve bilateral doubts and questions in the talks. (<a href='https://www.straitstimes.com/world/united-states/trump-to-meet-xi-in-south-korea-on-oct-30-as-part-of-asia-swing'>The Straits Times</a>)</li><li>White House (press secretary): Karoline Leavitt confirmed the bilateral meeting will occur Thursday morning local time during a White House briefing. (<a href='https://www.scmp.com/news/us/diplomacy/article/3330131/donald-trump-meet-chinas-xi-jinping-next-thursday-south-korea-crunch-talks'>South China Morning Post</a>)</li><li>Beijing/Chinese authorities: Officials have defended tighter export controls on rare-earths, a move described in reporting as not explicitly targeting the US though it has raised tensions. (<a href='https://www.rt.com/news/626890-white-house-announces-trump-xi-meeting/'>RT</a>)</li></ul><h3>Sources:</h3><ul><li><a href='https://www.straitstimes.com/world/united-states/trump-to-meet-xi-in-south-korea-on-oct-30-as-part-of-asia-swing'>Trump to meet Xi in South Korea on Oct 30 as part of Asia swing</a> - straitstimes.com</li><li><a href='https://www.scmp.com/news/us/diplomacy/article/3330131/donald-trump-meet-chinas-xi-jinping-next-thursday-south-korea-crunch-talks'>Trump to meet Xi in South Korea next Thursday as part of key Asia trip</a> - scmp.com</li><li><a href='https://www.rt.com/news/626890-white-house-announces-trump-xi-meeting/'>White House announces Trump-Xi meeting</a> - rt.com</li><li><a href='https://www.thehindu.com/news/international/trump-to-meet-xi-in-south-korea-as-part-of-asia-swing/article70195667.ece'>Trump to meet Xi in South Korea as part of Asia swing</a> - thehindu.com</li><li><a href='https://www.aljazeera.com/news/2025/10/24/white-house-confirms-trump-to-meet-xi-in-south-korea-as-part-of-asia-tour'>White House confirms Trump to meet Xi in South Korea as part of Asia tour</a> - aljazeera.com</li></ul></description>+<guid isPermaLink="true">https://kite.kagi.com/96cf948f-8a1b-4281-9ba4-8a9e1ad7b3c6/world/10</guid>
+246
aggregators/kagi-news/tests/test_config.py
+246
aggregators/kagi-news/tests/test_config.py
···
+122
aggregators/kagi-news/tests/test_html_parser.py
+122
aggregators/kagi-news/tests/test_html_parser.py
···+html_content = """<p>The White House confirmed President Trump will hold a bilateral meeting with Chinese President Xi Jinping in South Korea on October 30, at the end of an Asia trip that includes Malaysia and Japan . The administration said the meeting will take place Thursday morning local time, and Mr Trump indicated his first question to Xi would concern fentanyl and other bilateral issues . The talks come amid heightened trade tensions after Beijing expanded export curbs on rare-earth minerals and following Mr Trump's recent threat of additional tariffs on Chinese goods, making the meeting a focal point for discussions on trade, technology supply chains and energy .</p><img src='https://kagiproxy.com/img/Q2SRXQtwTYBIiQeI0FG-X6taF_wHSJaXDiFUzju2kbCWGuOYIFUX--8L0BqE4VKxpbOJY3ylFPJkDpfSnyQYZ1qdOLXbphHTnsOK4jb7gqC4KCn5nf3ANbWCuaFD5ZUSijiK0k7wOLP2fyX6tynu2mPtXlCbotLo2lTrEswZl4-No2AI4mI4lkResfnRdp-YjpoEfCOHkNfbN1-0cNcHt9T2dmgBSXrQ2w' alt='News image associated with coverage of President Trump's Asia trip and planned meeting with President Xi' /><br /><h3>Highlights:</h3><ul><li>Itinerary details: The Asia swing begins in Malaysia, continues to Japan and ends with the bilateral meeting in South Korea on Thursday morning local time, White House press secretary Karoline Leavitt said at a briefing .</li><li>APEC context: US officials indicated the leaders will meet on the sidelines of the Asia-Pacific Economic Cooperation gathering, shaping expectations for short, high-level talks rather than a lengthy summit .</li></ul><blockquote>Work out a lot of our doubts and questions - President Trump</blockquote><h3>Perspectives:</h3><ul><li>President Trump: He said his first question to President Xi would be about fentanyl and indicated he hoped to resolve bilateral doubts and questions in the talks. (<a href='https://www.straitstimes.com/world/united-states/trump-to-meet-xi-in-south-korea-on-oct-30-as-part-of-asia-swing'>The Straits Times</a>)</li><li>White House (press secretary): Karoline Leavitt confirmed the bilateral meeting will occur Thursday morning local time during a White House briefing. (<a href='https://www.scmp.com/news/us/diplomacy/article/3330131/donald-trump-meet-chinas-xi-jinping-next-thursday-south-korea-crunch-talks'>South China Morning Post</a>)</li></ul><h3>Sources:</h3><ul><li><a href='https://www.straitstimes.com/world/united-states/trump-to-meet-xi-in-south-korea-on-oct-30-as-part-of-asia-swing'>Trump to meet Xi in South Korea on Oct 30 as part of Asia swing</a> - straitstimes.com</li><li><a href='https://www.scmp.com/news/us/diplomacy/article/3330131/donald-trump-meet-chinas-xi-jinping-next-thursday-south-korea-crunch-talks'>Trump to meet Xi in South Korea next Thursday as part of key Asia trip</a> - scmp.com</li></ul>"""+assert result['perspectives'][0]['source_url'] == "https://www.straitstimes.com/world/united-states/trump-to-meet-xi-in-south-korea-on-oct-30-as-part-of-asia-swing"+assert result['sources'][0]['title'] == "Trump to meet Xi in South Korea on Oct 30 as part of Asia swing"
+299
aggregators/kagi-news/tests/test_richtext_formatter.py
+299
aggregators/kagi-news/tests/test_richtext_formatter.py
···+summary="The White House confirmed President Trump will hold a bilateral meeting with Chinese President Xi Jinping in South Korea on October 30.",
+91
aggregators/kagi-news/tests/test_rss_fetcher.py
+91
aggregators/kagi-news/tests/test_rss_fetcher.py
···
+227
aggregators/kagi-news/tests/test_state_manager.py
+227
aggregators/kagi-news/tests/test_state_manager.py
···
+6
aggregators/kagi-news/.env.example
+6
aggregators/kagi-news/.env.example
+29
aggregators/kagi-news/config.example.yaml
+29
aggregators/kagi-news/config.example.yaml
···
+5
aggregators/kagi-news/crontab
+5
aggregators/kagi-news/crontab
+12
aggregators/kagi-news/pytest.ini
+12
aggregators/kagi-news/pytest.ini
+17
aggregators/kagi-news/requirements.txt
+17
aggregators/kagi-news/requirements.txt
···
+30
internal/api/routes/discover.go
+30
internal/api/routes/discover.go
···
+23
internal/api/routes/timeline.go
+23
internal/api/routes/timeline.go
···+r.With(authMiddleware.RequireAuth).Get("/xrpc/social.coves.feed.getTimeline", getTimelineHandler.HandleGetTimeline)
+71
internal/core/discover/service.go
+71
internal/core/discover/service.go
···+func (s *discoverService) GetDiscover(ctx context.Context, req GetDiscoverRequest) (*DiscoverResponse, error) {+return NewValidationError("timeframe", "timeframe must be one of: hour, day, week, month, year, all")
+76
internal/core/timeline/service.go
+76
internal/core/timeline/service.go
···+func (s *timelineService) GetTimeline(ctx context.Context, req GetTimelineRequest) (*TimelineResponse, error) {+return NewValidationError("timeframe", "timeframe must be one of: hour, day, week, month, year, all")
+19
internal/api/handlers/errors.go
+19
internal/api/handlers/errors.go
···
+125
internal/atproto/jetstream/vote_jetstream_connector.go
+125
internal/atproto/jetstream/vote_jetstream_connector.go
···+func NewVoteJetstreamConnector(consumer *VoteEventConsumer, wsURL string) *VoteJetstreamConnector {+if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
+26
internal/core/votes/errors.go
+26
internal/core/votes/errors.go
···
+22
internal/db/migrations/014_remove_votes_voter_fk.sql
+22
internal/db/migrations/014_remove_votes_voter_fk.sql
···
+14
scripts/dev-run.sh
+14
scripts/dev-run.sh
···
+68
scripts/setup-mobile-ports.sh
+68
scripts/setup-mobile-ports.sh
···+echo "Install Android SDK Platform Tools: https://developer.android.com/studio/releases/platform-tools"+echo -e "${CYAN}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"+echo -e "${CYAN}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"+echo -e "${GREEN}PDS (3000):${NC} localhost:3001 โ device:3000 ${YELLOW}(DID document port)${NC}"+echo -e "${CYAN}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"+echo -e "${YELLOW}๐ก Note: Port forwarding persists until device disconnects or you run:${NC}"
+116
scripts/start-ngrok.sh
+116
scripts/start-ngrok.sh
···+echo -e "${CYAN}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"+echo -e "${CYAN}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"+PDS_URL=$(echo "$TUNNELS" | jq -r '.tunnels[] | select(.config.addr | contains("3001")) | select(.proto=="https") | .public_url' 2>/dev/null | head -1)+PLC_URL=$(echo "$TUNNELS" | jq -r '.tunnels[] | select(.config.addr | contains("3002")) | select(.proto=="https") | .public_url' 2>/dev/null | head -1)+APPVIEW_URL=$(echo "$TUNNELS" | jq -r '.tunnels[] | select(.config.addr | contains("8081")) | select(.proto=="https") | .public_url' 2>/dev/null | head -1)+URLS=($(echo "$TUNNELS" | jq -r '.tunnels[] | select(.proto=="https") | .public_url' 2>/dev/null))+echo -e "${CYAN}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"+if [[ "$PDS_URL" == "ERROR" ]] || [[ "$PLC_URL" == "ERROR" ]] || [[ "$APPVIEW_URL" == "ERROR" ]]; then
+26
scripts/stop-ngrok.sh
+26
scripts/stop-ngrok.sh
···
-67
internal/atproto/lexicon/social/coves/interaction/createVote.json
-67
internal/atproto/lexicon/social/coves/interaction/createVote.json
···
-37
internal/atproto/lexicon/social/coves/interaction/deleteVote.json
-37
internal/atproto/lexicon/social/coves/interaction/deleteVote.json
···
+27
-27
internal/core/aggregators/aggregator.go
+27
-27
internal/core/aggregators/aggregator.go
···-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-CreatedAt time.Time `json:"createdAt" db:"created_at"` // When aggregator was created (from lexicon)-RecordURI string `json:"recordUri,omitempty" db:"record_uri"` // at://did/social.coves.aggregator.service/self// Stored in community's repository: at://community_did/social.coves.aggregator.authorization/{rkey}-DisabledBy string `json:"disabledBy,omitempty" db:"disabled_by"` // Moderator DID who disabled it-DisabledAt *time.Time `json:"disabledAt,omitempty" db:"disabled_at"` // When authorization was disabled (for modlog/audit)-RecordURI string `json:"recordUri,omitempty" db:"record_uri"` // at://community_did/social.coves.aggregator.authorization/{rkey}
+47
internal/db/migrations/015_alter_content_labels_to_jsonb.sql
+47
internal/db/migrations/015_alter_content_labels_to_jsonb.sql
···+-- Change content_labels from TEXT[] to JSONB to preserve full com.atproto.label.defs#selfLabels structure+COMMENT ON COLUMN posts.content_labels IS 'Self-applied labels per com.atproto.label.defs#selfLabels (JSONB: {"values":[{"val":"nsfw","neg":false}]})';
+1
-1
tests/lexicon-test-data/moderation/tribunal-vote-invalid-decision.json
+1
-1
tests/lexicon-test-data/moderation/tribunal-vote-invalid-decision.json
···
+5
-6
tests/lexicon-test-data/post/post-invalid-missing-community.json
+5
-6
tests/lexicon-test-data/post/post-invalid-missing-community.json
···
+6
-7
tests/lexicon-test-data/post/post-valid-text.json
+6
-7
tests/lexicon-test-data/post/post-valid-text.json
···-"text": "I've been working with Go for a while now and wanted to share some thoughts on error handling patterns...",+"content": "I've been working with Go for a while now and wanted to share some thoughts on error handling patterns...",···
+17
-17
docs/PRD_POSTS.md
+17
-17
docs/PRD_POSTS.md
·········-- [x] **Handler:** `POST /xrpc/social.coves.post.create` โ (Alpha - see IMPLEMENTATION_POST_CREATION.md)+- [x] **Handler:** `POST /xrpc/social.coves.community.post.create` โ (Alpha - see IMPLEMENTATION_POST_CREATION.md)···- [x] **E2E Test:** Create text post โ Write to **community's PDS** โ Index via Jetstream โ Verify in AppView โ·········- โ ๏ธ **Derive post characteristics:** DEFERRED (embed_type, text_length, has_title, has_embed for content rules filtering)···············- Post "type" is derived from structure (has embed? what embed type? has title? text length?)
+3
-3
docs/aggregators/PRD_KAGI_NEWS_RSS.md
+3
-3
docs/aggregators/PRD_KAGI_NEWS_RSS.md
···โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ···
-86
internal/atproto/lexicon/social/coves/interaction/comment.json
-86
internal/atproto/lexicon/social/coves/interaction/comment.json
···
-75
internal/atproto/lexicon/social/coves/interaction/createComment.json
-75
internal/atproto/lexicon/social/coves/interaction/createComment.json
···
-41
internal/atproto/lexicon/social/coves/interaction/deleteComment.json
-41
internal/atproto/lexicon/social/coves/interaction/deleteComment.json
···
-39
internal/atproto/lexicon/social/coves/post/crosspost.json
-39
internal/atproto/lexicon/social/coves/post/crosspost.json
···
-41
internal/atproto/lexicon/social/coves/post/delete.json
-41
internal/atproto/lexicon/social/coves/post/delete.json
···
-294
internal/atproto/lexicon/social/coves/post/get.json
-294
internal/atproto/lexicon/social/coves/post/get.json
···-"description": "Get posts by AT-URI. Supports batch fetching for feed hydration. Returns posts in same order as input URIs.",-"description": "Post is blocked due to viewer blocking author/community, or community moderation",-"description": "What caused the block: viewer blocked author, viewer blocked community, or post was removed by moderators"
-99
internal/atproto/lexicon/social/coves/post/getCrosspostChain.json
-99
internal/atproto/lexicon/social/coves/post/getCrosspostChain.json
···
-80
internal/atproto/lexicon/social/coves/post/search.json
-80
internal/atproto/lexicon/social/coves/post/search.json
···
-104
internal/atproto/lexicon/social/coves/post/update.json
-104
internal/atproto/lexicon/social/coves/post/update.json
···
+156
internal/atproto/lexicon/com/atproto/label/defs.json
+156
internal/atproto/lexicon/com/atproto/label/defs.json
···+"description": "AT URI of the record, repository (account), or other resource that this label applies to."+"description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."+"description": "Metadata tags on an atproto record, published by the author within the record.",+"description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",+"description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",+"description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",+"description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",+"description": "Does the user need to have adult content enabled in order to configure this label?"+"description": "Strings which describe the label in the UI, localized into a specific language.",
+15
internal/atproto/lexicon/com/atproto/repo/strongRef.json
+15
internal/atproto/lexicon/com/atproto/repo/strongRef.json
···
+3
-3
internal/validation/lexicon_test.go
+3
-3
internal/validation/lexicon_test.go
······
+9
tests/lexicon-test-data/feed/vote-valid.json
+9
tests/lexicon-test-data/feed/vote-valid.json
-5
tests/lexicon-test-data/interaction/vote-valid.json
-5
tests/lexicon-test-data/interaction/vote-valid.json
-33
internal/atproto/lexicon/social/coves/actor/block.json
-33
internal/atproto/lexicon/social/coves/actor/block.json
···
-59
internal/atproto/lexicon/social/coves/actor/blockUser.json
-59
internal/atproto/lexicon/social/coves/actor/blockUser.json
···
-85
internal/atproto/lexicon/social/coves/actor/getSaved.json
-85
internal/atproto/lexicon/social/coves/actor/getSaved.json
···
-198
internal/atproto/lexicon/social/coves/actor/preferences.json
-198
internal/atproto/lexicon/social/coves/actor/preferences.json
···
-63
internal/atproto/lexicon/social/coves/actor/saveItem.json
-63
internal/atproto/lexicon/social/coves/actor/saveItem.json
···
-37
internal/atproto/lexicon/social/coves/actor/saved.json
-37
internal/atproto/lexicon/social/coves/actor/saved.json
···
-39
internal/atproto/lexicon/social/coves/actor/subscription.json
-39
internal/atproto/lexicon/social/coves/actor/subscription.json
···
-37
internal/atproto/lexicon/social/coves/actor/unblockUser.json
-37
internal/atproto/lexicon/social/coves/actor/unblockUser.json
···
-37
internal/atproto/lexicon/social/coves/actor/unsaveItem.json
-37
internal/atproto/lexicon/social/coves/actor/unsaveItem.json
···
-5
tests/lexicon-test-data/actor/block-invalid-did.json
-5
tests/lexicon-test-data/actor/block-invalid-did.json
-6
tests/lexicon-test-data/actor/block-valid.json
-6
tests/lexicon-test-data/actor/block-valid.json
-7
tests/lexicon-test-data/actor/preferences-invalid-enum.json
-7
tests/lexicon-test-data/actor/preferences-invalid-enum.json
-40
tests/lexicon-test-data/actor/preferences-valid.json
-40
tests/lexicon-test-data/actor/preferences-valid.json
···
-6
tests/lexicon-test-data/actor/profile-invalid-handle-format.json
-6
tests/lexicon-test-data/actor/profile-invalid-handle-format.json
-4
tests/lexicon-test-data/actor/profile-invalid-missing-handle.json
-4
tests/lexicon-test-data/actor/profile-invalid-missing-handle.json
-1
tests/lexicon-test-data/actor/profile-valid.json
-1
tests/lexicon-test-data/actor/profile-valid.json
-32
internal/atproto/lexicon/social/coves/federation/post.json
-32
internal/atproto/lexicon/social/coves/federation/post.json
···
-31
internal/atproto/lexicon/social/coves/interaction/share.json
-31
internal/atproto/lexicon/social/coves/interaction/share.json
···
-33
internal/atproto/lexicon/social/coves/interaction/tag.json
-33
internal/atproto/lexicon/social/coves/interaction/tag.json
···
-9
tests/lexicon-test-data/community/moderator-invalid-permissions.json
-9
tests/lexicon-test-data/community/moderator-invalid-permissions.json
···
-5
tests/lexicon-test-data/interaction/share-valid-no-community.json
-5
tests/lexicon-test-data/interaction/share-valid-no-community.json
-6
tests/lexicon-test-data/interaction/share-valid.json
-6
tests/lexicon-test-data/interaction/share-valid.json
-6
tests/lexicon-test-data/interaction/tag-invalid-empty.json
-6
tests/lexicon-test-data/interaction/tag-invalid-empty.json
-6
tests/lexicon-test-data/interaction/tag-valid-custom.json
-6
tests/lexicon-test-data/interaction/tag-valid-custom.json
-6
tests/lexicon-test-data/interaction/tag-valid-known.json
-6
tests/lexicon-test-data/interaction/tag-valid-known.json
+13
scripts/validate-schemas.sh
+13
scripts/validate-schemas.sh
+63
internal/db/migrations/016_create_comments_table.sql
+63
internal/db/migrations/016_create_comments_table.sql
···+CREATE INDEX idx_comments_root ON comments(root_uri, created_at DESC) WHERE deleted_at IS NULL;+CREATE INDEX idx_comments_parent ON comments(parent_uri, created_at DESC) WHERE deleted_at IS NULL;+CREATE INDEX idx_comments_parent_score ON comments(parent_uri, score DESC, created_at DESC) WHERE deleted_at IS NULL;+COMMENT ON TABLE comments IS 'Comments indexed from user repositories via Jetstream firehose consumer';+COMMENT ON COLUMN comments.uri IS 'AT-URI in format: at://commenter_did/social.coves.feed.comment/rkey';+COMMENT ON COLUMN comments.root_uri IS 'Strong reference to the original post that started the thread';+COMMENT ON COLUMN comments.parent_uri IS 'Strong reference to immediate parent (post or comment)';+COMMENT ON COLUMN comments.score IS 'Computed as upvote_count - downvote_count for ranking replies';+COMMENT ON COLUMN comments.content_labels IS 'Self-applied labels per com.atproto.label.defs#selfLabels (JSONB: {"values":[{"val":"nsfw","neg":false}]})';
+125
internal/atproto/jetstream/comment_jetstream_connector.go
+125
internal/atproto/jetstream/comment_jetstream_connector.go
···+func NewCommentJetstreamConnector(consumer *CommentEventConsumer, wsURL string) *CommentJetstreamConnector {+if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
+11
internal/core/comments/interfaces.go
+11
internal/core/comments/interfaces.go
···GetVoteStateForComments(ctx context.Context, viewerDID string, commentURIs []string) (map[string]interface{}, error)
+200
internal/core/unfurl/circuit_breaker.go
+200
internal/core/unfurl/circuit_breaker.go
···+"[UNFURL-CIRCUIT] Opening circuit for provider '%s' after %d consecutive failures. Last error: %v",
+175
internal/core/unfurl/circuit_breaker_test.go
+175
internal/core/unfurl/circuit_breaker_test.go
···
+202
internal/core/unfurl/kagi_test.go
+202
internal/core/unfurl/kagi_test.go
···
+269
internal/core/unfurl/opengraph_test.go
+269
internal/core/unfurl/opengraph_test.go
···+assert.Equal(t, "Meta description fallback", og.Description, "Should fall back to meta description")
+170
internal/core/unfurl/service.go
+170
internal/core/unfurl/service.go
···
+27
internal/core/unfurl/types.go
+27
internal/core/unfurl/types.go
···
+14
internal/core/unfurl/errors.go
+14
internal/core/unfurl/errors.go
···
+19
internal/core/unfurl/interfaces.go
+19
internal/core/unfurl/interfaces.go
···
+117
internal/core/unfurl/repository.go
+117
internal/core/unfurl/repository.go
···+func (r *postgresUnfurlRepo) Set(ctx context.Context, url string, result *UnfurlResult, ttl time.Duration) error {+_, err = r.db.ExecContext(ctx, query, url, result.Provider, metadataJSON, thumbnailURL, intervalStr)
+23
internal/db/migrations/017_create_unfurl_cache.sql
+23
internal/db/migrations/017_create_unfurl_cache.sql
···+COMMENT ON TABLE unfurl_cache IS 'Cache for oEmbed/URL unfurl results to reduce external API calls';+COMMENT ON COLUMN unfurl_cache.provider IS 'Provider name (streamable, youtube, reddit, etc.)';+COMMENT ON COLUMN unfurl_cache.metadata IS 'Full unfurl result as JSON (title, description, type, etc.)';+COMMENT ON COLUMN unfurl_cache.expires_at IS 'When this cache entry should be refetched (TTL-based)';
+9
internal/core/blobs/types.go
+9
internal/core/blobs/types.go
+7
-1
internal/core/posts/interfaces.go
+7
-1
internal/core/posts/interfaces.go
···+// When unfurlService is provided, external embeds will be automatically enriched with metadata.
+2
internal/core/posts/post.go
+2
internal/core/posts/post.go
······
+81
internal/core/posts/blob_transform.go
+81
internal/core/posts/blob_transform.go
···
+312
internal/core/posts/blob_transform_test.go
+312
internal/core/posts/blob_transform_test.go
···+"http://localhost:3001/xrpc/com.atproto.sync.getBlob?did=did:plc:testcommunity&cid=bafyreib6tbnql2ux3whnfysbzabthaj2vvck53nimhbi5g5a7jgvgr5eqm",+expectedURL := "http://localhost:3001/xrpc/com.atproto.sync.getBlob?did=did:plc:test&cid=bafytest"+"http://localhost:3001/xrpc/com.atproto.sync.getBlob?did=did:plc:test&cid=bafyreib6tbnql2ux3whnfysbzabthaj2vvck53nimhbi5g5a7jgvgr5eqm",+expectedURL := "http://localhost:3001/xrpc/com.atproto.sync.getBlob?did=did:plc:test&cid=bafytest"
+4
-3
aggregators/kagi-news/src/main.py
+4
-3
aggregators/kagi-news/src/main.py
···
+134
scripts/post_streamable.py
+134
scripts/post_streamable.py
···+STREAMABLE_TITLE = "NBACentral - \"Your son don't wanna be here, we know it's your last weekend. Enjoy ..."+REDDIT_URL = "https://www.reddit.com/r/nba/comments/1orfsgm/highlight_giannis_antetokounmpo_41_pts_15_reb_9/"+REDDIT_TITLE = "[Highlight] Giannis Antetokounmpo (41 PTS, 15 REB, 9 AST) tallies his 56th career regular season game of 40+ points, passing Kareem Abdul-Jabbar for the most such games in franchise history. Milwaukee defeats Chicago 126-110 to win their NBA Cup opener."
+34
internal/db/migrations/018_migrate_comment_namespace.sql
+34
internal/db/migrations/018_migrate_comment_namespace.sql
···+-- Migration: Update comment URIs from social.coves.feed.comment to social.coves.community.comment+SET root_uri = REPLACE(root_uri, '/social.coves.feed.comment/', '/social.coves.community.comment/')+SET parent_uri = REPLACE(parent_uri, '/social.coves.feed.comment/', '/social.coves.community.comment/')+-- Rollback: Revert comment URIs from social.coves.community.comment to social.coves.feed.comment+SET root_uri = REPLACE(root_uri, '/social.coves.community.comment/', '/social.coves.feed.comment/')+SET parent_uri = REPLACE(parent_uri, '/social.coves.community.comment/', '/social.coves.feed.comment/')
+2
-2
internal/core/comments/view_models.go
+2
-2
internal/core/comments/view_models.go
······
+1
-1
internal/validation/lexicon.go
+1
-1
internal/validation/lexicon.go
···
+18
-15
docs/COMMENT_SYSTEM_IMPLEMENTATION.md
+18
-15
docs/COMMENT_SYSTEM_IMPLEMENTATION.md
···············-commentJetstreamURL = "ws://localhost:6008/subscribe?wantedCollections=social.coves.feed.comment"+commentJetstreamURL = "ws://localhost:6008/subscribe?wantedCollections=social.coves.community.comment"·········+**Note:** Since we're pre-production, no historical data migration was needed. Migration script updates URIs in comments table (uri, root_uri, parent_uri columns).···-export COMMENT_JETSTREAM_URL="ws://localhost:6008/subscribe?wantedCollections=social.coves.feed.comment"+export COMMENT_JETSTREAM_URL="ws://localhost:6008/subscribe?wantedCollections=social.coves.community.comment"
+3
-2
internal/core/unfurl/providers.go
+3
-2
internal/core/unfurl/providers.go
···
+785
docs/federation-prd.md
+785
docs/federation-prd.md
···+Enable Lemmy-style federation where users on any Coves instance can post to communities hosted on other instances, while maintaining community ownership and moderation control.+1. **Enable cross-instance posting** - Users can post to any community on any federated instance+โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ+โ @coves.soc โโโโโโโโโโถโ AppView โโโโโโโโโโถโ .com PDS โ+โโโโโโโโโโโโโโโ (1) โโโโโโโโโโโโโโโโโโโโ (2) โโโโโโโโโโโโโโโ+โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ+โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ+func (s *postService) CreatePost(ctx context.Context, req CreatePostRequest) (*CreatePostResponse, error) {+func (c *serviceAuthClient) RequestServiceAuth(ctx context.Context, opts ServiceAuthOptions) (*ServiceAuthToken, error) {+func (s *postService) createFederatedPost(ctx context.Context, community *communities.Community, req CreatePostRequest) (*CreatePostResponse, error) {+uri, cid, err := s.createPostOnRemotePDS(ctx, community.PDSURL, community.DID, postRecord, token.Token)+writeError(w, http.StatusForbidden, "FederationDisabled", "Community doesn't accept federated posts")+-- federation_mode: 'open' (any instance), 'allowlist' (trusted only), 'local' (no federation)+**No data loss:** Posts are written to PDS, indexed via firehose regardless of federation method.+4. **Federation Symmetry:** If instance-a trusts instance-b, does instance-b auto-trust instance-a?+- [atProto Service Auth Spec](https://atproto.com/specs/service-auth) (hypothetical - check actual docs)+| Instance not trusted | 403 | `UntrustedInstance` | "This community doesn't accept posts from your instance" | No retry |+| Rate limit exceeded | 429 | `RateLimited` | "Too many posts. Try again in X minutes" | Exponential backoff |+| PDS unreachable | 503 | `ServiceUnavailable` | "Community temporarily unavailable" | Retry 3x with backoff |+| Invalid token | 401 | `InvalidToken` | "Session expired. Please try again" | Refresh token & retry |+| Service auth failed | 500 | `FederationFailed` | "Unable to connect. Try again later" | Retry 2x |
+130
-28
docs/PRD_ALPHA_GO_LIVE.md
+130
-28
docs/PRD_ALPHA_GO_LIVE.md
······**Test Quality**: Enhanced with comprehensive database record verification to catch race conditions+- **Jetstream**: Connects to Bluesky's production firehose (wss://jetstream2.us-east.bsky.network)This document tracks the remaining work required to launch Coves alpha with real users. Focus is on critical functionality, security, and operational readiness.···+**Important**: Jetstream connects to Bluesky's production firehose, which automatically includes events from all production PDS instances (including coves.me once it's live)+- **Location**: [internal/atproto/jetstream/community_consumer.go](../internal/atproto/jetstream/community_consumer.go)······+**Total**: 16-23 hours (added 4-6 hours for PDS deployment, reduced from original due to did:web completion)···**Total**: ~~20-25 hours~~ โ **13 hours actual** (E2E tests) + 7-12 hours remaining (load testing, polish)-**Grand Total: ~~65-80 hours~~ โ 50-65 hours remaining (approximately 1.5-2 weeks full-time)**-*(Originally 70-85 hours. Reduced by completed items: handle resolution, comment count reconciliation, and ALL E2E tests)*+**Grand Total: ~~65-80 hours~~ โ 51-68 hours remaining (approximately 1.5-2 weeks full-time)**+*(Originally 70-85 hours. Adjusted for: +4-6 hours PDS deployment, -3 hours did:web completion, -13 hours E2E tests completion, -4 hours handle resolution and comment reconciliation)***โ Progress Update**: E2E testing section COMPLETE ahead of schedule - saved ~7-12 hours through parallel agent implementation······-**๐ Major Milestone**: All E2E tests complete! Test coverage now includes full user journey, blob uploads, concurrent operations, rate limiting, and error recovery.+- All E2E tests complete! Test coverage now includes full user journey, blob uploads, concurrent operations, rate limiting, and error recovery.+- Bidirectional DID verification complete! Bluesky-compatible security model with alsoKnownAs validation, 24h cache TTL, and comprehensive test coverage.
+19
-16
docs/PRD_BACKLOG.md
+19
-16
docs/PRD_BACKLOG.md
···-**Added:** 2025-10-11 | **Updated:** 2025-10-16 | **Effort:** 2-3 days | **Priority:** ALPHA BLOCKER+**Added:** 2025-10-11 | **Updated:** 2025-11-16 | **Completed:** 2025-11-16 | **Status:** โ DONE1. **Domain Impersonation**: Self-hosters can set `INSTANCE_DID=did:web:nintendo.com` without owning the domain, enabling attacks where communities appear hosted by trusted domains···-1. **Basic Validation (Phase 1)**: Verify `did:web:` domain matches configured `instanceDomain`-2. **Cryptographic Verification (Phase 2)**: Fetch `https://domain/.well-known/did.json` and verify:-3. **Auto-populate hostedByDID**: Remove from client API, derive from instance configuration in service layer+3. โ **Auto-populate hostedByDID**: Removed from client API, derived from instance configuration in service layer+- **Security Model**: Matches Bluesky's approach - relies on DNS/HTTPS authority, not cryptographic proof+- **Enforcement**: MANDATORY hard-fail in production (rejects communities with verification failures)+- **Dev Mode**: Set `SKIP_DID_WEB_VERIFICATION=true` to bypass verification for local development+- **Bidirectional Check**: Prevents impersonation by requiring DID document to claim the handle+- **Location**: [internal/atproto/jetstream/community_consumer.go](../internal/atproto/jetstream/community_consumer.go)
+591
docs/aggregators/SETUP_GUIDE.md
+591
docs/aggregators/SETUP_GUIDE.md
···+**Aggregators** are automated services that post content to Coves communities. They are similar to Bluesky's feed generators and labelers - self-managed external services that integrate with the platform.+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ+See [scripts/aggregator-setup/README.md](../../scripts/aggregator-setup/README.md) for detailed script documentation.+Your aggregator needs its own atProto identity (DID). The easiest way is to create an account on an existing PDS.+**Save these credentials securely!** You'll need the DID and access token for all subsequent operations.+To register with Coves, you must prove you own a domain by serving your DID at `https://yourdomain.com/.well-known/atproto-did`.+"message": "Aggregator registered successfully. Next step: create a service declaration record at at://did:plc:abc123.../social.coves.aggregator.service/self"+Write a `social.coves.aggregator.service` record to your repository. This contains metadata about your aggregator and gets indexed by Coves' Jetstream consumer.+**Wait 5-10 seconds** for Jetstream to index your service declaration into the `aggregators` table.+2. **Authorization record**: Moderator writes `social.coves.aggregator.authorization` to community's repo+curl "https://api.coves.social/xrpc/social.coves.aggregator.getAuthorizations?aggregatorDid=did:plc:abc123...&enabledOnly=true"+3. Verify record was created: Check PDS at `at://your-did/social.coves.aggregator.service/self`
+95
scripts/aggregator-setup/1-create-pds-account.sh
+95
scripts/aggregator-setup/1-create-pds-account.sh
···
+93
scripts/aggregator-setup/2-setup-wellknown.sh
+93
scripts/aggregator-setup/2-setup-wellknown.sh
···
+103
scripts/aggregator-setup/3-register-with-coves.sh
+103
scripts/aggregator-setup/3-register-with-coves.sh
···
+125
scripts/aggregator-setup/4-create-service-declaration.sh
+125
scripts/aggregator-setup/4-create-service-declaration.sh
···
+252
scripts/aggregator-setup/README.md
+252
scripts/aggregator-setup/README.md
···+This directory contains scripts to help you set up and register your aggregator with Coves instances.+Aggregators are automated services that post content to Coves communities. They are similar to Bluesky's feed generators and labelers. To use aggregators with Coves, you need to:+- **Domain ownership**: You must own a domain where you can host the `.well-known/atproto-did` file+For a reference implementation of automated setup, see the Kagi News aggregator at [aggregators/kagi-news/scripts/setup.sh](../../aggregators/kagi-news/scripts/setup.sh).+The Kagi script shows how to automate all 4 steps (with the manual .well-known upload step in between).+1. Creates a `social.coves.aggregator.service` record at `at://your-did/social.coves.aggregator.service/self`+Authorizations are created by community moderators, not by aggregators. The moderator writes a `social.coves.aggregator.authorization` record to their community's repository.+- Verify `.well-known/atproto-did` is accessible: `curl https://yourdomain.com/.well-known/atproto-did`+- Verify the record was created: Check your PDS at `at://your-did/social.coves.aggregator.service/self`+For a complete reference implementation, see the Kagi News aggregator at `aggregators/kagi-news/`.+The Kagi aggregator includes an automated setup script at [aggregators/kagi-news/scripts/setup.sh](../../aggregators/kagi-news/scripts/setup.sh) that demonstrates how to:+This shows how you can package scripts 1-4 into a single automated flow for your specific aggregator.
+188
-1
aggregators/kagi-news/README.md
+188
-1
aggregators/kagi-news/README.md
······+Before running the aggregator, you must register it with a Coves instance. This creates a DID for your aggregator and registers it with Coves.+**Manual step required:** During the process, you'll need to upload the `.well-known/atproto-did` file to your domain so it's accessible at `https://yourdomain.com/.well-known/atproto-did`.+See [scripts/aggregator-setup/README.md](../../scripts/aggregator-setup/README.md) for detailed documentation on each step.···+The easiest way to deploy the Kagi aggregator is using Docker. The cron job runs inside the container automatically.+- **`COVES_API_URL`** (optional): Override Coves API endpoint (defaults to `https://api.coves.social`)+- **`RUN_ON_STARTUP`** (optional): Set to `true` to run immediately on container start (useful for testing)
+195
aggregators/kagi-news/scripts/setup.sh
+195
aggregators/kagi-news/scripts/setup.sh
···
+55
aggregators/kagi-news/.dockerignore
+55
aggregators/kagi-news/.dockerignore
···
+53
aggregators/kagi-news/Dockerfile
+53
aggregators/kagi-news/Dockerfile
···
+48
aggregators/kagi-news/docker-compose.yml
+48
aggregators/kagi-news/docker-compose.yml
···
+41
aggregators/kagi-news/docker-entrypoint.sh
+41
aggregators/kagi-news/docker-entrypoint.sh
···
+20
.beads/.gitignore
+20
.beads/.gitignore
···
+56
.beads/config.yaml
+56
.beads/config.yaml
···+# - sync.branch - Git branch for beads commits (use BEADS_SYNC_BRANCH env var or bd config set)
+4
.beads/metadata.json
+4
.beads/metadata.json
+3
.gitattributes
+3
.gitattributes
+131
AGENTS.md
+131
AGENTS.md
···+**IMPORTANT**: This project uses **bd (beads)** for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods.+6. **Commit together**: Always commit the `.beads/issues.jsonl` file together with the code changes so issue state stays in sync with code state
+57
scripts/backup.sh
+57
scripts/backup.sh
···+log "To restore: gunzip -c $BACKUP_FILE | docker compose -f docker-compose.prod.yml exec -T postgres psql -U $POSTGRES_USER -d $POSTGRES_DB"
+133
scripts/deploy.sh
+133
scripts/deploy.sh
···+if docker compose -f "$COMPOSE_FILE" exec -T postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" > /dev/null 2>&1; then+if docker compose -f "$COMPOSE_FILE" exec -T postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" > /dev/null 2>&1; then+if docker compose -f "$COMPOSE_FILE" exec -T appview /app/coves-server migrate 2>/dev/null; then+warn "โ ๏ธ Migration command not available or failed - AppView will run migrations on startup"+if docker compose -f "$COMPOSE_FILE" exec -T appview wget --spider -q http://localhost:8080/xrpc/_health 2>/dev/null; then+warn "โ ๏ธ AppView health check failed - check logs with: docker compose -f docker-compose.prod.yml logs appview"+if docker compose -f "$COMPOSE_FILE" exec -T pds wget --spider -q http://localhost:3000/xrpc/_health 2>/dev/null; then+warn "โ ๏ธ PDS health check failed - check logs with: docker compose -f docker-compose.prod.yml logs pds"+log " Rollback: docker compose -f docker-compose.prod.yml down && git checkout HEAD~1 && ./scripts/deploy.sh"
+149
scripts/generate-did-keys.sh
+149
scripts/generate-did-keys.sh
···+PUBLIC_KEY_HEX=$(openssl ec -in "$PRIVATE_KEY_PEM" -pubout -conv_form compressed -outform DER 2>/dev/null | \
+106
scripts/setup-production.sh
+106
scripts/setup-production.sh
···+until docker compose -f docker-compose.prod.yml exec -T postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" > /dev/null 2>&1; do
+19
static/.well-known/did.json.template
+19
static/.well-known/did.json.template
···
+18
static/client-metadata.json
+18
static/client-metadata.json
···
+97
static/oauth/callback.html
+97
static/oauth/callback.html
···+<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'">
+2
-1
Dockerfile
+2
-1
Dockerfile
···
+187
scripts/derive-did-from-key.sh
+187
scripts/derive-did-from-key.sh
···+PUBLIC_KEY_HEX=$(openssl ec -in "$TEMP_DIR/private.pem" -pubout -conv_form compressed -outform DER 2>/dev/null | \
+3
-2
internal/api/routes/community.go
+3
-2
internal/api/routes/community.go
···-func RegisterCommunityRoutes(r chi.Router, service communities.Service, authMiddleware *middleware.AtProtoAuthMiddleware) {+func RegisterCommunityRoutes(r chi.Router, service communities.Service, authMiddleware *middleware.AtProtoAuthMiddleware, allowedCommunityCreators []string) {
+1
-2
internal/api/handlers/aggregator/errors.go
+1
-2
internal/api/handlers/aggregator/errors.go
+1
-2
internal/api/handlers/comments/errors.go
+1
-2
internal/api/handlers/comments/errors.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
···
+1
-2
internal/api/handlers/community/list.go
+1
-2
internal/api/handlers/community/list.go
+1
-2
internal/api/handlers/communityFeed/errors.go
+1
-2
internal/api/handlers/communityFeed/errors.go
+1
-2
internal/api/handlers/discover/errors.go
+1
-2
internal/api/handlers/discover/errors.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
+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
······
+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/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
+1
-2
internal/core/timeline/types.go
+1
-2
internal/core/timeline/types.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
+1
-2
internal/db/postgres/discover_repo.go
+1
-2
internal/db/postgres/discover_repo.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
+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"}`))
+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
···
+4
-5
tests/integration/community_consumer_test.go
+4
-5
tests/integration/community_consumer_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_repo_test.go
+2
-3
tests/integration/community_repo_test.go
···
+6
-7
tests/integration/concurrent_scenarios_test.go
+6
-7
tests/integration/concurrent_scenarios_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
······
+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
···
+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
······
+11
-11
tests/integration/user_journey_e2e_test.go
+11
-11
tests/integration/user_journey_e2e_test.go
···············
+23
static/.well-known/did.json
+23
static/.well-known/did.json
···
+1
-1
docs/E2E_TESTING.md
+1
-1
docs/E2E_TESTING.md
+3
-3
internal/api/routes/user.go
+3
-3
internal/api/routes/user.go
···
+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
···
+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 {
+148
-6
internal/api/middleware/auth.go
+148
-6
internal/api/middleware/auth.go
······func NewAtProtoAuthMiddleware(jwksFetcher auth.JWKSFetcher, skipVerify bool) *AtProtoAuthMiddleware {······+log.Printf("[AUTH_FAILURE] type=missing_dpop ip=%s method=%s path=%s error=token has cnf.jkt but no DPoP header",+log.Printf("[AUTH_WARNING] type=unexpected_dpop ip=%s method=%s path=%s warning=DPoP header present but token has no cnf.jkt",······+log.Printf("[AUTH_WARNING] Optional auth: token has cnf.jkt but no DPoP header - treating as unauthenticated (potential token theft)")+log.Printf("[AUTH_WARNING] Optional auth: DPoP verification failed - treating as unauthenticated: %v", err)···+func (m *AtProtoAuthMiddleware) verifyDPoPBinding(r *http.Request, claims *auth.Claims, dpopProofHeader string) (*auth.DPoPProof, error) {
+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) {
+4
-1
internal/atproto/auth/jwt.go
+4
-1
internal/atproto/auth/jwt.go
···
+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=