# stapel-cdn 0.10.0

Media management: image/video/file/audio upload, processing (thumbnail/preview generation via libvips) and CDN ref tracking, with a media-submodule opt-in (images core, video/recordings behind ENABLED_SUBMODULES + ffmpeg system checks), a watermark seam, upload speed-limiting, and cdn.media_exists / cdn.refs_sync comm Functions for other modules to check/sync media references without importing this package.

Contract: axes 3 · surface 5 · extension points 4.
Generated from docs/capabilities.json by `stapel-llms-txt` — do not edit; drift-gated by `make contract-check`.

## Configuration axes — what a product switches on
Settings keys; `default` is what you get by saying nothing. Turning an axis off unmounts the operations it gates.
- ASSET_TYPES [list, default ["avatar"]] — Which image categories this deployment accepts
  Image.type choices — the same STAPEL_CDN key the client-side CdnImageField reads, so server validation and client typing agree on what's legal. Default is the zero-infrastructure 'avatar' only; marketplace-specific types (product/chat/review) are added explicitly by the host (conf.py, MODULE.md Extension points table).
- ENABLED_SUBMODULES [list, default ["images"]] — Enabled media types (images / video / audio recordings)
  Which of images/video/recordings this deployment turns on. images needs no opt-in; adding video/recordings activates the ffmpeg system-check probe for that submodule (conf.py, MODULE.md 'Media submodules').
- WATERMARK [enum, default ""] — Watermark preview images
  Dotted path to (or direct callable) a watermark engine applied to preview variants; empty disables watermarking (conf.py, MODULE.md 'Storage / processing backends').

## Usage surface — call these before writing your own
This is the answer to "does Stapel already have something for X?". `instead of` names the outside symbol this one displaces.
### gate_function
- detect_image_extension — stapel_cdn.fetch.detect_image_extension
  instead of: trusting the URL or Content-Type extension
  Magic-byte content check for fetched or uploaded image bytes: reads the DETECTED format from the file signature — never the URL suffix or the Content-Type header — and confirms it with a real libvips decode, raising ImageImportError otherwise. Pair with fetch_image_bytes for any external-image intake: trusting a URL's own extension is how a '.jpg' that is actually an HTML/script payload reaches storage.
- enforce_rate_limit — stapel_cdn.fetch.enforce_rate_limit
  instead of: hand-rolled cache-based counters (race-prone get+set)
  Per-caller fixed-window quota (STAPEL_CDN['IMPORT_FROM_URL_RATE']) guarding cdn's import-from-url egress path — call it before any DNS/network work on a caller-supplied URL, which is what stops the fetch from being turned into an open proxy/amplifier. Scoped to this one feature's bucket namespace; for a NEW feature's own rate limiting reach for stapel_core.gateway.ratelimit.CacheRateLimiter directly instead of repurposing this wrapper.
- fetch_image_bytes — stapel_cdn.fetch.fetch_image_bytes
  instead of: requests.get(url), urllib.request.urlopen(url)
  SSRF-hardened fetch of a remote image over HTTPS: resolves and allowlists every hop's IP (rejects private/loopback/link-local/CGNAT/cloud-metadata ranges, including IPv4-mapped/6to4/NAT64 IPv6 forms), pins the TCP connection to the validated address to defeat DNS-rebinding, and caps size/timeout/redirect hops. Reach for this — not a bare HTTP client — anywhere a deployment fetches an attacker-influenced URL (an OAuth-provider avatar, a webhook-supplied image link); this is the only outbound-HTTP path stapel-cdn ships and it exists because that shape of fetch is a classic SSRF sink.
- validate_image_file — stapel_cdn.validators.validate_image_file
  instead of: PIL.Image.open() ad-hoc checks, hand-rolled image validation, pyvips.Image.new_from_file() ad-hoc checks
  Validate an uploaded image before it touches hashing or storage: extension allowlist + the decompression-bomb pixel cap (STAPEL_CDN['MAX_IMAGE_PIXELS']) + a real decode with libvips — the SAME decoder stapel_cdn.services uses to process the file, so this gate can never refuse input the pipeline behind it would have handled. Reach for this on ANY image-intake path in the deployment, not only cdn's own upload views. Catch stapel_cdn.decoders.ImageDecoderUnavailable separately from ValidationError: it is a ValidationError subclass meaning 'this deployment has no decoder for that format', an operator's problem, and answering it as bad input is the defect that produced this note — a valid HEIC was refused as 'Invalid image file' because the guard asked Pillow while the pipeline read HEIC natively.
### factory
- text_watermark — stapel_cdn.watermarks.text_watermark
  instead of: hand-rolled pyvips text/logo compositing
  The built-in WATERMARK engine (`(pyvips.Image) -> pyvips.Image`) the preview-generation pipeline applies when STAPEL_CDN['WATERMARK'] points at it. Point the setting here directly for a plain bottom-right text label (STAPEL_CDN['WATERMARK_TEXT']), or call it from your own callable to layer a logo/opacity effect on top — reach for this before writing a pyvips text/compositing routine from scratch.

## Extension points — what a product replaces, fork-free
- ASSET_TYPES [value_list_override]
  Read through the callable models.get_image_type_choices, so adding types never produces a model/migration change (MODULE.md Extension points table).
- WATERMARK [dotted_path]
  The only dotted-path key in the namespace: a callable (pyvips.Image) -> pyvips.Image; built-in reference stapel_cdn.watermarks.text_watermark renders WATERMARK_TEXT (MODULE.md 'Storage / processing backends').
- media_processed signal [django_signal]
  stapel_core.signals.media_processed sent after ImageProcessingService.process_image() generates all variants — in-process hook for cache warm-up/denormalization (MODULE.md 'Signals').
- serializer_seams [class_override]
  Every view in stapel_cdn.views mixes in SerializerSeamMixin (request_serializer_class/response_serializer_class + get_* getters); subclass and re-route the URL to swap (views.py:62-79, MODULE.md 'Serializer seams').

## Fits with — fleet dependencies
- stapel-core (required) — comm bus (cdn.media_exists/cdn.refs_sync Functions, user.deleted/user.deletion_initiated consume for GDPR), AppSettings, error registry (pyproject.toml dependency)
