apiVersion: capsule.dev/v0.1
kind: Capsule
name: yingjieli-image-store
version: 1.0.0
type: subsystem
domain: yingjieli.site
maintainers:
- name: Quake
email: [email protected]
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/<key> (public, cached)
- DELETE /api/img/<key> (admin-only)
- "the R2 key format: <slug>_<timestamp36><rand>.{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/<key>. 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/<key>.
- 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