The per-attachment GetAttachmentByID pre-check loop was a check-then-link
TOCTOU (the same race pattern this branch fixes elsewhere), an N+1 on the
hot send path, and a hard ErrForbidden for legit retries naming an
already-linked attachment. Ownership now lives in the one UPDATE that
links: `AND message_id IS NULL AND (uploader_id = ? OR uploader_id IS
NULL)` — a foreign attachment can never be claimed, legacy NULL-uploader
rows stay claimable, and skipped rows (foreign/linked/missing) are logged
but never fail the send, so retries can't hard-fail. Subsumes W2-4; the
MemStore (nil,nil) GetAttachmentByID contortion is replaced by a real
map-backed attachment store so the guard is testable (W3-5).
Companion commit updates test callsites and adds ownership coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Critical fixes:
- Move svc creation in router.go above MountInviteRoutes/MountChannelRoutes
(was used before definition — compile error)
- Restore hasChannelPermREST in channel_handler.go for upload_handler.go
(was removed but still referenced — compile error)
Permission cache invalidation:
- Add PermissionInvalidator interface to admin package
- Wire through NewHandler → NewAdminAPI → handlePatchUser
- Call InvalidateUser(userID) after role changes in admin panel
- Update all admin test files to pass nil as new parameter
Also clarifies WithTx documentation for SQLite single-writer semantics.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
Define the Store interface composing domain-specific sub-interfaces
(MessageStore, ChannelStore, UserStore, etc.) that decouple services
from the concrete database. SQLiteStore wraps *db.DB, delegating all
operations to existing query methods.
https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8