transactional post writes + safe cache keys
Created by: jedmund
Summary
Architecture audit A-6 and A-7 — two isolated integrity/security fixes.
A-6 · Transactional post writes
POST /api/posts, PUT /api/posts/[id], and DELETE /api/posts/[id] all did multi-step writes (post row + media-usage bookkeeping) without a transaction, with the media-usage block wrapped in a silent try/catch that warned and continued. The failure mode was exactly what A-6 flags: post updated, media usage left stale — orphaned MediaUsage rows.
Now wrapped in prisma.$transaction:
-
trackMediaUsage()/removeMediaUsage()accept an optionalPrisma.TransactionClient - Post create/update/delete handlers pass the tx through so all writes commit atomically
- Silent catch removed — media-usage failures now roll back the post write (this is the intended behavior; the previous catch was the bug)
A-7 · Redis cache key escaping
User-supplied query params were concatenated directly into Redis keys (e.g. `tags:list:${search}`). A search term containing `:` could collide with a structurally different key, poisoning the cache or breaking wildcard invalidation (`redis.keys('tags:list:*')`).
- New `src/lib/server/cache-keys.ts` with `safeKey()` — base64url-encodes any user-supplied segment
- Applied to the public `/api/og-metadata` (highest attack surface — unauthenticated, URL param), `/api/tags` search, `/api/tags/suggest` query, and all 6 `/api/admin/garden/search/*` routes
- Key prefixes/structure preserved so wildcard patterns keep working
Test plan
-
`pnpm check` — baseline 116 errors / 9 warnings, no new ones -
Edit a post with attachments → verify DB: post row updated AND `MediaUsage` rows match the new attachment set -
Force a media-usage failure (e.g. temporarily break the upsert call) → post row should NOT update -
Delete a post → `MediaUsage` rows gone in same transaction -
Search tags with `?search=foo:bar:baz` → no Redis key collision (inspect `redis-cli KEYS 'tags:list:*'` — final segment is base64url) -
`/api/og-metadata?url=https://example.com\` still caches and returns consistently