import { AwsClient } from "aws4fetch"; import { type CommitUploadArgs, type CreateUploadArgs, type CreateUploadResult, type DeleteObjectArgs, type GetPublicUrlArgs, type IdGenerator, type MediaAsset, type MediaStorage } from "@aotter/mantle-runtime"; /** * `R2MediaStorage` — `MediaStorage` adapter backed by Cloudflare R2, * **public bucket only**. * * The bound R2 bucket is expected to have public access enabled and * (typically) a custom domain or `pub-.r2.dev` URL pointing at * it. Reads bypass the Worker entirely. CORS on the bucket should be * scoped to the admin SPA origin so browser direct PUTs work without * exposing other origins. * * Two access modes — both required: * - `bucket` (`R2Bucket` binding) — server-side `get` / `put` / * `delete`. Used at commit-time to read uploaded-object metadata * and rewrite it with `committedAt`, and to clean up failed * uploads. * - `s3` (`AwsClient` from `aws4fetch`) — SigV4 presigned PUT URL * generation. The R2 binding cannot issue presigned URLs. * * Public URL resolution is fully consumer-supplied via `publicBase`. * The hash in `pub-.r2.dev` is opaque (assigned only after the * user enables public access on the bucket); custom domains are * configured separately. Adapters never derive the public URL from * account / bucket — the consumer must wire it through the `cmsConfig` * env (typically `MEDIA_PUBLIC_URL_BASE`). * * # Multi-variant (#272) * * `createUpload` produces one presigned PUT URL per declared variant; * `commitUpload` verifies and streams every object through a metadata rewrite. Storage keys are scoped * under a shared `/` prefix so operators can eyeball * the variant set in R2 dashboards, and the orphan sweeper (#254) * can identify partially-committed groups by listing the prefix. * * Optimization runs agent-side in the MCP client's runtime. The * Worker never decodes / re-encodes bytes — it only verifies content- * type + size from `bucket.get` before streaming the metadata-rewrite PUT. * * # Future: private bucket adapter * * A v0.2 `R2PrivateMediaStorage` will live alongside this class and * implement a separate `PrivateMediaStorage` port. It binds a * different R2 bucket — public access disabled — and routes every * read through a Worker policy gate (staff / subscription check) * before streaming via `bucket.get()` or 302-ing to a short-lived * signed GET. **Two buckets, two ports**, by design — see ADR-0011 * § "Public vs private media — two buckets, two ports". Don't bolt * a `visibility` flag onto this class to handle private content. */ export declare class R2MediaStorage implements MediaStorage { private readonly bucket; private readonly s3; /** S3 endpoint for THIS bucket — must include the bucket as the * subdomain, e.g. `https://..r2.cloudflarestorage.com`. * Used as the host for presigned PUT URLs. */ private readonly s3Endpoint; /** ID source for fallback storage-key randomness when an upstream * caller lands without a usable upload-group prefix (defensive). * Defaults to `RandomUuidGenerator`; tests inject a deterministic * fake to assert exact key strings. **Production must use a * CSPRNG-backed generator** — `uploadGroupId` is bearer-token- * equivalent and a predictable storageKey leaks pre-commit * object locations. See `IdGenerator`'s "Security invariant". */ private readonly idgen; constructor(bucket: R2Bucket, s3: AwsClient, /** S3 endpoint for THIS bucket — must include the bucket as the * subdomain, e.g. `https://..r2.cloudflarestorage.com`. * Used as the host for presigned PUT URLs. */ s3Endpoint: string, /** Public read-base URL — `https://media.example.com` (custom * domain) or `https://pub-.r2.dev` (R2 public dev domain). * Trailing slash is normalised away. */ publicBase: string, /** ID source for fallback storage-key randomness when an upstream * caller lands without a usable upload-group prefix (defensive). * Defaults to `RandomUuidGenerator`; tests inject a deterministic * fake to assert exact key strings. **Production must use a * CSPRNG-backed generator** — `uploadGroupId` is bearer-token- * equivalent and a predictable storageKey leaks pre-commit * object locations. See `IdGenerator`'s "Security invariant". */ idgen?: IdGenerator); private readonly publicBase; createUpload(args: CreateUploadArgs): Promise; commitUpload(args: CommitUploadArgs): Promise; getPublicUrl(args: GetPublicUrlArgs): Promise; deleteObject(args: DeleteObjectArgs): Promise; private verifyAndCommitVariant; /** Object keys are server-generated. Layout: * * //. * * Purpose is prefixed (when it matches a permissive slug shape) so * operators can eyeball "post-cover/abc123/primary.jpg" in R2 * dashboards. The `uploadGroupId/` directory groups every variant * of one logical asset — orphan sweep (#254) lists a prefix to * find partially-committed bundles. */ private buildVariantStorageKey; } //# sourceMappingURL=R2MediaStorage.d.ts.map