fix(audit-r2): resolve all 15 round-2 findings (tx atomicity / privacy / hardening / messaging)#34
Conversation
…, hardening, messaging) 10-agent verification confirmed all 15 REAL (0 false-positive, 0 already-fixed), all code-only (no migration). messaging findings (R2-004..008/013/014) are ship-dark (MESSAGING_ENABLED=false → routes 404 first) = zero launch risk, fixed for correctness anyway. Local typecheck + lint clean. P0 transaction atomicity — raw payload.db.drizzle.execute() runs on an autocommit pool connection, NOT the Payload tx (adapter stashes the tx-bound handle at sessions[id].db). New src/lib/db/tx-handle.ts#txDrizzle resolves it: - R2-001 collab flush: advisory lock + collab_snapshots insert → tx-bound handle. - R2-002 account deletion: the 5 _*_v anonymization updates → deletion's tx. - R2-003 invite claim: atomicClaimInvite takes transactionID, joins user-create tx. Privacy (live): - R2-009 work-page version list gated to publicContentStatuses (no leak of draft/hidden/removed version metadata to anonymous viewers). - R2-010 hot-search HAVING count(*) >= 5 (k-anonymity; low-frequency/sensitive terms never surfaced publicly). Hardening (live): - R2-011 metrics ?token= only honored outside production. - R2-012 shared readJsonWithByteLimit (src/lib/http/read-json.ts) for presign / collab flush (+ yjsState.length cap) / messaging; pre-parse 413/400. - R2-015 account/export + lore/obsidian downloads: positive-int id + try/catch 404. Messaging (ship-dark): - R2-004 drop non-existent chat_room_members.status access condition. - R2-005 document getActiveMembership (existence == active in v1). - R2-006 DM + group room/member creates wrapped in one transaction. - R2-007 sendMessage insert + cursor updates in drizzle.transaction + repair-on-read. - R2-008 markRoomRead clamps upToSeq to room last_seq; route requires safe int >= 0. - R2-013 per-user/IP rate limits on the 4 messaging write routes (+ docs). - R2-014 new tests/int/messaging-service.int.spec.ts (DM atomicity/idempotency, sendMessage atomicity/idempotency, markRoomRead clamp). findings.md updated with the resolution table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94cc7d2b94
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const limited = await assertHTTPRateLimit({ | ||
| group: 'messaging.dm_create', | ||
| limit: 10, | ||
| request, | ||
| severity: 'best-effort', | ||
| windowMs: 60_000, |
There was a problem hiding this comment.
Include the user in messaging rate-limit keys
For authenticated messaging writes, these new limits are described as per-user/IP, but the call does not pass the userID that was just resolved above. assertHTTPRateLimit only adds a user component when userID is provided, so in shared NAT/mobile/corporate networks all users share the same 10/min DM bucket, while an abusive user can also avoid the per-account limit by changing IPs; the other messaging write routes added in this commit have the same omission.
Useful? React with 👍 / 👎.
| and length(trim(properties->>'query')) >= ${MIN_QUERY_LENGTH} | ||
| and length(trim(properties->>'query')) <= ${MAX_QUERY_LENGTH} | ||
| group by trim(properties->>'query') | ||
| having count(*) >= ${MIN_QUERY_OCCURRENCES} |
There was a problem hiding this comment.
Count distinct searchers for the public hot-search floor
This still gates public hot-search exposure on event count, not on distinct users/searchers. A single account can search the same sensitive term five times within the 7-day window and satisfy count(*) >= 5, so the stated k-anonymity/privacy goal is not met; the floor should be based on distinct user/anonymous identities (or otherwise prevent repeated searches from one actor from qualifying).
Useful? React with 👍 / 👎.
… a string)
The 2 failing assertions compared a number (room.lastSeq / result.lastReadSeq,
both Number()-coerced) to sent.seq, which the pg driver returns from the raw
INSERT...RETURNING as a string ('1'), so toBe failed on number-vs-string. Coerce
sent.seq/replay.seq with Number(). The fixes under test (R2-007 tx atomicity,
R2-008 clamp) were correct — only the assertion typing was off. getOrCreateDmRoom
test already passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves all 15 findings in
docs/audit-round-2-2026-06-01/findings.md. A 10-agent verification confirmed every one is REAL (0 false-positive, 0 already-fixed), all code-only (no migration). Localpnpm typecheck+eslintclean.Launch-relevant (live): R2-009 (work-page leaks non-public version metadata → status-gated), R2-010 (hot-search no k-anonymity →
having count(*) >= 5), R2-011 (metrics query-string token → dev-only), R2-012 (JSON body byte caps), R2-015 (download 404 hardening), R2-002/R2-003 (account-delete + invite-claim transaction atomicity).P0 transaction root cause:
payload.db.drizzle.execute()runs on an autocommit pool connection, NOT the Payload transaction (the adapter stashes the tx-bound handle atsessions[id].db). Newsrc/lib/db/tx-handle.ts#txDrizzleresolves it — applied to R2-001/002/003.Messaging (R2-004..008/013/014): ship-DARK (
MESSAGING_ENABLED=false→ every route 404s first), so zero launch risk; fixed for correctness + given a real integration test (tests/int/messaging-service.int.spec.ts) since the prior tests were static-only.See the resolution table in
findings.md. Required checks must pass (incl. the new int test + the unchanged account-delete/invite int paths).🤖 Generated with Claude Code