feat: enforce mod actions in read/write-path API responses (ATB-20) (#36)
* feat(appview): add getActiveBans helper for filtering banned users
- Query active ban status for multiple users in one query
- Returns Set of currently banned DIDs
- Handles ban/unban reversals correctly
- Includes comprehensive test coverage (4 tests)
* fix(appview): add database index and error handling to getActiveBans
- Add index on mod_actions.subject_did for query performance
- Add error handling with fail-open strategy for read-path
- Re-throw programming errors, log and return empty set for DB errors
- Add test for database error handling
* feat(appview): add getTopicModStatus helper for lock/pin status (ATB-20)
- Add database index on mod_actions.subject_post_uri for performance
- Query lock/pin status for a topic by ID
- Handles lock/unlock and pin/unpin reversals
- Returns current state based on most recent action
- Fail-open error handling (returns unlocked/unpinned on DB error)
- Includes comprehensive test coverage (5 tests)
Technical details:
- Most recent action wins for state determination
- Known limitation: Cannot be both locked AND pinned simultaneously
(most recent action overwrites previous state)
- Index improves query performance for subjectPostUri lookups
Related: Task 2 of ATB-20 implementation plan
* feat(appview): add getHiddenPosts helper for filtering deleted posts
- Query hidden status for multiple posts in one query
- Returns Set of post IDs with active delete actions
- Handles delete/undelete reversals correctly
- Includes comprehensive test coverage (5 tests: 4 functional + 1 error handling)
- Follows fail-open pattern (returns empty Set on DB error)
- Re-throws programming errors (TypeError, ReferenceError, SyntaxError)
- Uses existing mod_actions_subject_post_uri_idx index for performance
* fix(appview): add defensive query limits to getHiddenPosts (ATB-20)
- Add .limit(1000) to post URI lookup query
- Add .limit(10000) to mod actions query
- Verify console.error called in error handling test
- Prevents memory exhaustion per CLAUDE.md standards
* feat(appview): filter banned users and hidden posts in GET /api/topics/:id
- Query active bans for all users in topic thread
- Query hidden status for all replies
- Filter replies to exclude banned users and hidden posts
- Includes tests for ban enforcement and unban reversal
Task 4 of ATB-20: Enforce moderation in topic GET endpoint
* feat(appview): add locked and pinned flags to GET /api/topics/:id
- Query topic lock/pin status from mod actions
- Include locked and pinned boolean flags in response
- Defaults to false when no mod actions exist
- Includes tests for locked, pinned, and normal topics
* fix(appview): allow topics to be both locked and pinned (ATB-20)
- Fix getTopicModStatus to check lock/pin independently
- Previously only the most recent action was checked
- Now checks most recent lock action AND most recent pin action separately
- Add test verifying topic can be both locked and pinned
- Fixes critical bug where lock would clear pin status (and vice versa)
* feat(appview): block banned users from creating topics (ATB-20)
- Add ban check to POST /api/topics handler
- Return 403 Forbidden if user is banned
- Add 3 tests for ban enforcement (success, blocked, error)
- Ban check happens before PDS write to prevent wasted work
- Fail closed on error (deny access if ban check fails)
* fix(appview): classify ban check errors correctly (ATB-20)
- Distinguish database errors (503) from unexpected errors (500)
- Add test for database error → 503 response
- Update existing error test to verify 500 for unexpected errors
- Users get actionable feedback: retry (503) vs report (500)
* fix(appview): re-throw programming errors in ban check (ATB-20)
- Add isProgrammingError check before error classification in ban check catch block
- Programming errors (TypeError, ReferenceError, SyntaxError) are logged with CRITICAL prefix and re-thrown
- Prevents hiding bugs by catching them as 500 errors
- Add test verifying TypeError triggers CRITICAL log and is re-thrown (not swallowed as 500)
- Aligns with CLAUDE.md error handling standards and matches the main try-catch block pattern
* feat(appview): block banned users and locked topics in POST /api/posts (ATB-20)
- Add ban check before request processing (403 Forbidden if banned)
- Add lock check after root post lookup (423 Locked if topic locked)
- Full error classification: programming errors re-throw, DB errors → 503, unexpected → 500
- Add 8 tests: 5 for ban enforcement, 3 for lock enforcement
* fix(appview): make helpers fail-closed and fix error classification (ATB-20)
- Change getActiveBans, getTopicModStatus, getHiddenPosts to re-throw DB errors
(helpers now propagate errors; callers control fail policy)
- Add isDatabaseError classification to POST /api/posts main catch block
- Update helper tests: verify throws instead of safe-default returns
- Update Bruno Create Reply docs with 403 (banned) and 423 (locked) responses
* fix: address PR review feedback for moderation enforcement
Critical fixes:
- Fix action string mismatch: helpers used hash notation
(space.atbb.modAction.action#ban) but mod.ts writes dot notation
(space.atbb.modAction.ban) - feature was a no-op in production
- Scope each mod query to relevant action type pairs (ban/unban,
lock/unlock/pin/unpin, delete/undelete) to prevent cross-type
contamination breaking "most recent action wins" logic
- Add .limit() to all three mod helper queries (defensive limits)
- Extract lock check to its own try/catch block in posts.ts
(previously shared catch with PDS write, hiding errors)
- Fix GET /api/topics/:id to be fail-open: individual try/catch
per helper, safe fallback on error (empty set / unlocked)
Status code fixes:
- Change 423 → 403 for locked topics (423 is WebDAV-specific)
- Update Create Reply.bru to document 403 for locked topics
Error classification fixes:
- Remove econnrefused/connection/timeout from isDatabaseError
(these are network-level errors, not database errors)
Test fixes:
- Update all action strings in test data from hash to dot notation
- Update mock chain for getActiveBans to end with .limit()
- Update posts.test.ts: 423 → 403 assertion
- Add integration test for hidden post filtering in GET /api/topics/:id
- Add fail-open error handling test for GET /api/topics/:id
- Update Bruno docs: locked/pinned in Get Topic assertions and response,
403 error code in Create Topic, 403 (not 423) in Create Reply
Cleanup:
- Delete test-output.txt (stray committed artifact)
- Add test-output.txt to .gitignore