feat: add image support (upload, serve, manage) - #4
Conversation
- PUT /api/image: upload images as base64 JSON, stored in KV with img: prefix - GET /s/<slug>/img/<filename>: serve images with correct Content-Type and immutable cache - DELETE /api/image/<slug>/<filename>: delete individual images - GET /api/images/<slug>: list images for a note - Markdown image rewriting: img/<file> references auto-resolve to /s/<slug>/img/<file> - Cascade delete: unpublishing a note also deletes all associated images - Image metadata tracked in KV (img-meta:<slug>) for efficient listing - 10MB max image size, allowlisted content types (jpeg, png, gif, webp, svg)
gisk0
left a comment
There was a problem hiding this comment.
PR Review: #4 — feat: add image support (upload, serve, manage)
Verdict: Approve with suggestions
Summary: Clean, well-structured addition of image support using the existing KV namespace. Auth, input validation, and cascade delete are all handled correctly. One security concern with SVG serving, and a couple of robustness items worth addressing.
🔴 Blockers (must fix before merge)
- [src/index.ts:531] SVG XSS via
image/svg+xml— SVGs can contain<script>tags and event handlers. Serving user-uploaded SVGs withContent-Type: image/svg+xmlfrom the same origin enables XSS attacks. Either (a) removeimage/svg+xmlfromALLOWED_CONTENT_TYPES, (b) serve SVGs withContent-Disposition: attachment, or (c) serve images from a separate cookieless domain. Option (a) is simplest since this is for Obsidian note images where SVG is rarely needed.
🟡 Warnings (should fix, not blocking)
-
[src/index.ts:569-580] Race condition on image metadata —
getImageMetas→ modify →putImageMetasis a read-modify-write without atomicity. Concurrent uploads to the same slug can lose metadata entries. Low risk for single-user, but worth noting. KV doesn't offer CAS — if this ever becomes multi-user, consider a different data model. -
[src/index.ts:714]
rewriteImageUrlsregex is narrow — Only matches. Won't matchwhich Obsidian also generates. Consider:/!\[([^\]]*)\]\(\.?\/?\/?img\/([^)]+)\)/g -
[src/index.ts:555] No note-existence check on image upload — Images can be uploaded to slugs that don't have a published note, creating orphans. Minor, since cascade delete only fires on unpublish — but orphaned images from typo'd slugs will persist in KV indefinitely.
💡 Suggestions (optional but worth considering)
-
[src/index.ts:598-600]
handleServeImage— addContent-Lengthheader — Browsers and CDNs benefit from knowing the response size upfront. Easy:"Content-Length": String(result.value.byteLength). -
[src/index.ts:625-626]
handleDeleteImage— no 404 on missing image — Delete returns{ ok: true }even if the image didn't exist. Fine for idempotency, but a204vs200distinction (or afound: true/falsefield) could be useful for scripting. -
JSON body for image upload — Base64 encoding inflates payload ~33%. For a 10MB limit that means the JSON body can be ~13.3MB. Not a problem now but if you ever need larger images, consider
multipart/form-data. Current approach is fine for the use case.
✅ Good Stuff
- Auth token check on all management endpoints — consistent security posture
FILENAME_REvalidation prevents path traversal via filenames- Cascade delete on unpublish is the right call — no orphaned data
- Immutable cache headers on served images — excellent for performance
- Clean separation of helpers, route handlers, and routing logic
- Image metadata tracking via separate KV key is smart — avoids KV list operations
Test Coverage: No automated tests in this PR (none in repo). Manual testing documented in PR description — acceptable for a personal tool, but worth adding integration tests as the API surface grows.
Security: SVG XSS needs addressing (see blocker). Everything else is solid.
Ready to merge? After SVG blocker is resolved.
- Remove SVG from allowed content types (XSS risk) - Add race condition comment on image metadata read-modify-write - Fix image URL regex to match ./img/ relative paths - Verify note exists before accepting image uploads (prevents orphans) - Add Content-Length header on served images - Return 404 on delete when image doesn't exist - Add comment about base64 payload size limitation
Summary
Adds full image support to the obs.gisk0.dev worker:
New API Endpoints
PUT /api/image— Upload images (base64 JSON body)GET /s/<slug>/img/<filename>— Serve images publicly with immutable cachingDELETE /api/image/<slug>/<filename>— Delete individual imagesGET /api/images/<slug>— List all images for a noteArchitecture
PUBLISHED_NOTESKV namespace withimg:<slug>/<filename>keysimg-meta:<slug>keys for efficient listingFeatures
auto-resolves to/s/<slug>/img/file.jpgat render timemax-age=31536000)Helper Scripts (in workspace)
scripts/upload-image.sh <slug> <filepath>— standalone image uploadscripts/publish-note.sh— updated to scan forimg/references and auto-uploadTested
tofu apply