Files
last30days-skill/docs/plans/2026-03-09-fix-bluesky-auth-opt-in-plan.md
T
Matt Van Horn b6fd5ff406 docs: v2.9.5 README update, fix plugin.json hooks (#62)
- Fix plugin.json hooks field from invalid array to empty object
- Add Claude Code plugin install above ClawHub badge
- Version bump to v2.9.5 with new features block (Bluesky, comparative
  mode, ScrapeCreators X, per-project env, expanded tests)
- Add Bluesky to all source list references
- Document BSKY_HANDLE/BSKY_APP_PASSWORD env vars in install + optional sections

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:40:07 -07:00

4.7 KiB

title, type, status, date
title type status date
fix: Make Bluesky opt-in with auth (searchPosts now requires authentication) fix completed 2026-03-09

fix: Make Bluesky opt-in with auth (searchPosts now requires authentication)

Problem

Bluesky's app.bsky.feed.searchPosts endpoint now returns 403 Forbidden for unauthenticated requests. This broke today (2026-03-09), likely related to the CEO transition. The searchActors endpoint still works without auth, but searchPosts does not.

We built Bluesky as an always-on source (like HN and Polymarket) because the AT Protocol public API was documented as free. That's no longer true for post search.

Current behavior: Bluesky silently returns 0 results (403 caught, empty list returned). No stats line appears, no error shown. Users don't know it exists.

Desired behavior: Bluesky is opt-in via env vars. When not configured, completely invisible (no error, no stats line, no mention). When configured with auth, it works as before.

Approach

Follow the same pattern as X/Twitter auth: env vars for credentials, availability check gates dispatch.

AT Protocol auth flow:

  1. User creates an App Password at bsky.app/settings/app-passwords
  2. User sets BSKY_HANDLE and BSKY_APP_PASSWORD env vars
  3. bluesky.py calls com.atproto.server.createSession to get a bearer token
  4. Subsequent searchPosts calls include Authorization: Bearer {token}

Files to Change

File Change Lines (est.)
scripts/lib/bluesky.py Add _create_session() auth, pass bearer token to search ~25
scripts/lib/env.py Update is_bluesky_available() to check env vars; add env var loading ~10
scripts/last30days.py Gate do_bluesky on is_bluesky_available(config); update diag dicts ~8
tests/test_bluesky.py Add auth session tests, update existing tests ~15
SKILL.md Document BSKY_HANDLE / BSKY_APP_PASSWORD env vars in setup section ~5

Implementation Steps

1. scripts/lib/env.py

  • Add BSKY_HANDLE and BSKY_APP_PASSWORD to the env var loading in get_config()
  • Update is_bluesky_available():
    def is_bluesky_available(config: Dict[str, Any]) -> bool:
        return bool(config.get('BSKY_HANDLE') and config.get('BSKY_APP_PASSWORD'))
    

2. scripts/lib/bluesky.py

  • Add _create_session(handle, app_password) that POSTs to https://bsky.social/xrpc/com.atproto.server.createSession
  • Cache the access token for the lifetime of the search (module-level or passed through)
  • Update search_bluesky() to accept config dict, extract creds, create session, add Authorization: Bearer {token} header
  • On auth failure, return {"posts": [], "error": "Bluesky auth failed"} (don't raise)

3. scripts/last30days.py

  • Change do_bluesky default from True to checking is_bluesky_available(config):
    has_bluesky = env.is_bluesky_available(config)
    
  • Gate the bluesky_future submit on has_bluesky (already gated on do_bluesky, just wire it)
  • Update both diagnostic dicts: "bluesky": True -> "bluesky": has_bluesky
  • Pass config to _search_bluesky() so it can extract auth creds

4. tests/test_bluesky.py

  • Test _create_session with mocked HTTP response
  • Test search_bluesky with auth header injection
  • Existing parse/normalize tests don't change (they don't hit the network)

5. SKILL.md

  • Add Bluesky to the optional env vars section (near AUTH_TOKEN/CT0):
    BSKY_HANDLE=your-handle.bsky.social
    BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
    
  • Keep Bluesky in source lists but note "(requires app password)" in setup

What NOT to Change

  • render.py - already handles empty bluesky gracefully (hides stats line when 0 items)
  • schema.py, normalize.py, score.py, dedupe.py - no changes needed, they work on items already
  • No error display when unconfigured - completely silent, same as how TikTok/Instagram behave when SCRAPECREATORS_API_KEY is missing

Acceptance Criteria

  • is_bluesky_available() returns False when env vars not set
  • No Bluesky stats line, no error, no mention when unconfigured
  • With valid BSKY_HANDLE + BSKY_APP_PASSWORD, posts are returned
  • Auth failure returns empty results gracefully (no crash)
  • SKILL.md documents the env vars
  • All existing Bluesky tests still pass
  • New auth tests added
  • bash scripts/sync.sh deploys successfully

Sources

  • AT Protocol auth: POST https://bsky.social/xrpc/com.atproto.server.createSession with {identifier, password}
  • App passwords: bsky.app/settings/app-passwords
  • Existing pattern: X auth with AUTH_TOKEN/CT0 in env.py:256-257
  • Existing pattern: TikTok/Instagram opt-in via is_tiktok_available() in env.py:499