capsule AI-native Unix-like composition layer

capsule.yaml

2,612 bytes · 83 lines · capsule://quake0day/[email protected] raw on github

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