feat: enforce mod actions in firehose indexer (ATB-21) (#37)
* feat: add BanEnforcer class for firehose ban enforcement (ATB-21)
* fix: use @atproto/common-web for TID in mod routes
Consistent with all other files in the project. @atproto/common
was listed in package.json but not installed.
* fix: add error logging to BanEnforcer applyBan/liftBan, clarify expired-ban test
- Wrap applyBan and liftBan DB updates in try-catch; log structured error
context (subjectDid, error message) then re-throw so callers know the
operation failed and posts may be in an inconsistent state
- Rename "returns false when only an expired ban exists" test to
"returns false when DB returns no active ban (e.g. no ban or expired
ban filtered by query)" and add a comment explaining that expiry
filtering is a Drizzle SQL concern, not unit-testable with mocks
* feat: skip indexing posts from banned users in firehose (ATB-21)
* feat: soft-delete existing posts when ban is indexed (ATB-21)
* fix: gate applyBan on insert success, not just action type (ATB-21)
* feat: restore posts when ban record is deleted (ATB-21)
Override handleModActionDelete to read the modAction row before deleting
it, then call banEnforcer.liftBan inside the same transaction when the
deleted record was a ban action. All three edge cases are covered by
tests: ban deleted (liftBan called), non-ban deleted (liftBan skipped),
and record already missing (idempotent, liftBan skipped).
* test: add error re-throw coverage for handleModActionDelete (ATB-21)
* test: add race condition coverage for firehose ban enforcement (ATB-21)
* test: strengthen race condition assertion in indexer ban enforcement
Replace `expect(mockDb.transaction).toHaveBeenCalled()` with
`expect(mockDb.insert).toHaveBeenCalled()` — the transaction mock
passes the same insert reference to the callback, so asserting insert
was called proves a record was actually written (not just that a
transaction was opened).
* docs: mark ATB-20 and ATB-21 complete in project plan
* fix: address code review feedback on ATB-21 ban enforcement
- Re-throw programming errors in isBanned (fail closed only for DB errors)
- Remove unused dbOrTx param from isBanned (YAGNI)
- Make ban create atomic: insert + applyBan in one transaction
- Add unban handling to handleModActionCreate (was completely missing)
- Add log + test for ban action with missing subject.did
- Add try/catch to handleModActionCreate (consistent with handleModActionDelete)
- Add error handling to getForumIdByUri/getForumIdByDid (consistent with other helpers)
- Remove duplicate expired-ban test; add applyBan/liftBan re-throw tests
- Add vi.clearAllMocks() to beforeEach; fix not-banned test assertion
- Use structured log objects instead of template literals
* docs: note ATB-25 limitation in liftBan (shared deleted column)
* fix: make unban mod action create atomic (insert + liftBan in one tx)
Mirrors the fix applied to the ban path in the previous commit. Without
this, a liftBan failure after genericCreate committed would store the
unban record but leave posts hidden, with no clean retry path.