/**
* Optional first-party media storage — **public bucket only**. Not
* part of the required v0.1.0 provisioning path: starters must keep
* working without an implementation.
*
* # Multi-variant by default (#272)
*
* Every committed `MediaAsset` carries one or more `variants` — at
* minimum the format `` falls back to (`role: "primary"`), and
* typically additional formats (`webp`, `avif`) the renderer prefers
* via ``. Variant *bytes* are produced agent-side by the
* MCP client or operator tooling (sharp / libvips); the Worker only
* receives already-processed uploads and enforces policy. workerd
* has no usable image-processing stack — pushing optimization onto
* the agent sidesteps that entirely.
*
* # Scope: public bucket only
*
* `getPublicUrl()` returns an unconditional public URL. The native R2
* adapter serves it directly through a public bucket; hosts with only
* an R2 binding may serve it through a cacheable same-origin Worker
* route. Each variant's
* `publicUrl` is frozen at commit time. The asset is persisted to the
* `media_assets` table by the commit use case; entries reference it
* by `MediaAsset.id` (`x-mantle-ref: media_assets`) and the renderer
* calls `runtime.media.resolve(id)` to materialise the full variants
* set at render time.
*
* **Private content (subscription-gated, fan-club, signed-GET, etc.)
* is a separate port + separate R2 bucket in v0.2** — see ADR-0011
* § "Public vs private media — two buckets, two ports". The private
* port will live alongside this one (`PrivateMediaStorage`); current
* callers stay untouched. Don't bolt a `visibility` flag onto this
* port to retrofit private semantics — that's not the seam.
*
* Method-arg shapes use the `*Args` suffix (matching `EntryRepository`)
* to leave the `*Request` / `*Response` namespace for use-case DTOs in
* `usecase/dto/media/`.
*/
export interface MediaStorage {
/** Issue short-lived PUT capabilities for every declared variant of
* one logical asset. The adapter mints storage keys (typically under
* a shared `/` prefix). `uploadUrl` may be a signed
* R2 URL or an authenticated same-origin Worker route. */
createUpload(args: CreateUploadArgs): Promise;
/** Commit a previously-PUT variant bundle. The adapter HEADs every
* storageKey, verifies actual mime + bytes, and returns a fully
* populated `MediaAsset` — the use case then persists it to the
* `media_assets` table via `MediaAssetRepository.save`. All-or-
* nothing: any variant failing verification rejects the whole
* commit. */
commitUpload(args: CommitUploadArgs): Promise;
/** Resolve the stable public URL for a single stored object by
* storageKey. Used by adapter internals and the orphan sweeper
* (#254); render paths read URLs straight off `MediaAsset.variants`
* rather than calling this per request. */
getPublicUrl(args: GetPublicUrlArgs): Promise;
/** Delete a single stored object. The use-case-level deletion path
* orchestrates "lookup by asset id → delete every variant object →
* delete media_assets row" by calling this once per variant. */
deleteObject(args: DeleteObjectArgs): Promise;
}
/** Variant role within a logical asset.
*
* - `primary` — the `` / fallback rendering. Every asset has
* exactly one. Conventionally jpeg / png (universal browser support).
* - `alternate` — additional format candidates the renderer prefers
* via ``. Modern formats (avif, webp).
* - `fallback` — reserved for future use (e.g. very-small thumbnail
* for above-the-fold inlining). Not currently emitted by the MCP
* media upload flow.
*/
export type MediaVariantRole = "primary" | "alternate" | "fallback";
export interface CreateUploadArgs {
/** Logical asset id — also the future `MediaAsset.id`. Minted by
* the use case via `IdGenerator`; the adapter uses it as the
* storage-key prefix so every variant of one asset lives under a
* common path. */
readonly uploadGroupId: string;
readonly purpose: string;
/** Original filename — stamped into each variant's customMetadata
* at commit time. Storage keys remain server-generated; the
* filename is purely for operator visibility in R2 / S3 dashboards
* and for renderers that want to recover a download name. */
readonly filename: string;
readonly variants: ReadonlyArray;
readonly now: number;
readonly expiresAt: number;
/** Optional opaque metadata the adapter may stamp onto the stored
* objects (e.g. customer tag, deployment env). Not interpreted. */
readonly metadata?: Readonly>;
}
export interface CreateUploadVariantSpec {
readonly mimeType: string;
/** Caller-declared payload size. The use case already verified it
* satisfies `maxBytes`; passed through so the adapter can forward
* it as a Content-Length hint where the backend supports one. */
readonly byteSize: number;
/** Per-mime cap, sourced from `siteDefaults.media.purposes[name].maxBytes`. */
readonly maxBytes: number;
readonly role: MediaVariantRole;
}
export interface CreateUploadResult {
readonly uploadGroupId: string;
readonly capabilities: ReadonlyArray;
readonly expiresAt: number;
}
export interface UploadCapability {
readonly mimeType: string;
readonly role: MediaVariantRole;
readonly method: "PUT";
readonly uploadUrl: string;
readonly storageKey: string;
readonly publicUrl: string;
readonly requiredHeaders?: Readonly>;
}
/**
* Adapter contract: for each variant, before populating the returned
* `MediaAsset`, verify the stored object's actual content-type
* matches `mimeType` and actual byte size ≤ `maxBytes`. Any failure
* rejects the whole bundle with `DiagnosticError(MEDIA_MIME_REJECTED)`
* or `MEDIA_VARIANT_SIZE_EXCEEDED`. Without adapter-side verification,
* a caller can declare `image/png` and PUT a PDF.
*/
export interface CommitUploadArgs {
readonly uploadGroupId: string;
/** Original filename forwarded from the create-time call; adapter
* stamps it into each variant's customMetadata so operator
* dashboards see the human-meaningful name alongside the
* server-generated storage key. */
readonly filename: string;
readonly variants: ReadonlyArray;
readonly alt?: string;
readonly caption?: string;
readonly now: number;
}
export interface CommitUploadVariantSpec {
readonly mimeType: string;
readonly role: MediaVariantRole;
readonly storageKey: string;
readonly maxBytes: number;
}
export interface GetPublicUrlArgs {
readonly storageKey: string;
}
export interface DeleteObjectArgs {
readonly storageKey: string;
}
/**
* Committed asset — what `commitUpload` returns and what the
* `media_assets` table persists. Renderers consume `variants`
* directly; `` emits one `` per non-primary
* variant with `` falling back to the primary one. There is
* no top-level `publicUrl` — that single-URL world is what
* #272 replaces.
*/
export interface MediaAsset {
readonly id: string;
readonly variants: ReadonlyArray;
readonly alt?: string;
readonly caption?: string;
readonly createdAt: number;
readonly metadata?: Readonly>;
}
export interface MediaVariant {
readonly mimeType: string;
readonly publicUrl: string;
readonly storageKey: string;
readonly byteSize: number;
readonly role: MediaVariantRole;
}
/** Pick the primary variant (the one `` falls back to). Helper
* for renderers / SSR that want one URL when they don't care about
* the full `` form. Throws if the asset has no primary —
* the use case rejects commits in that shape, so a runtime-reachable
* asset always has one. */
export declare function pickPrimaryVariant(asset: MediaAsset): MediaVariant;
//# sourceMappingURL=MediaStorage.d.ts.map