apiVersion: capsule.dev/v0.1 kind: Capsule name: yingjieli-image-store version: 1.0.0 type: subsystem domain: yingjieli.site maintainers: - name: Quake email: quake0day@gmail.com purpose: summary: | Stores and serves artwork images for yingjieliartist.com. Uploads land in Cloudflare R2 (binding YL_IMAGES); reads go through a Workers Cache layer with long-immutable cache headers and ETag support. owns: - POST /api/upload (multipart; admin-only) - GET /api/img/ (public, cached) - DELETE /api/img/ (admin-only) - "the R2 key format: _.{jpg|png|webp}" - the 8 MB max upload limit and the JPEG/PNG/WebP allow-list - filename sanitization (lowercase, [a-z0-9_], max 60 chars) does_not_own: - image resizing (the client pre-resizes before upload) - mapping images to artwork records (content-store does that) - who is allowed to upload/delete (delegates to yingjieli-admin-auth) interfaces: provides: - kind: http_api name: image-upload entrypoint: src/api/upload.js - kind: http_api name: image-serve entrypoint: src/api/img/[name].js - kind: http_api name: image-delete entrypoint: src/api/img/[name].js requires: - kind: library name: auth-helpers from_capsule: yingjieli-admin-auth - kind: env name: YL_IMAGES description: Cloudflare R2 bucket binding. dependencies: capsules: - name: yingjieli-admin-auth version: ">=1.0.0 <2.0.0" runtime: - node: ">=18" - cloudflare-pages: "*" agent: summary_for_ai: | Images live in R2; their primary URL is /api/img/. Keys are server-generated from sanitized base name + timestamp + 4 random chars so the client cannot dictate the final key. The Workers Cache API is a hot layer in front of R2 — never cache responses behind a session. avoid: - Trusting client-supplied keys; the server always generates them. - Returning R2 objects through paths other than /api/img/. - Allowing path traversal in keys (/, .. are rejected with 400). verification: health_checks: - id: upload-syntax command: node --check src/api/upload.js - id: img-serve-syntax command: node --check "src/api/img/[name].js" invariants: - The server never serves an image whose R2 key was supplied verbatim by the client. - "Path-traversal patterns (`/`, `..`) are rejected with 400, never with 200." - Anonymous DELETE is impossible — auth is checked before R2.delete(). x-reconstruct: install: install.json