From 36944dc7d0b386a53774c9f571f882f1d227d8f2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:03:03 -0500 Subject: [PATCH] Add R2 service documentation --- docs/storage-r2.md | 104 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docs/storage-r2.md diff --git a/docs/storage-r2.md b/docs/storage-r2.md new file mode 100644 index 00000000..69db5436 --- /dev/null +++ b/docs/storage-r2.md @@ -0,0 +1,104 @@ +# Cloudflare R2 integration (r2Service) + +## Purpose and scope +`js/services/r2Service.js` is the client-side coordinator for Cloudflare R2 uploads. It owns: + +- Local persistence of Cloudflare credentials and bucket metadata used by the Upload modal. +- Ensuring the target bucket exists, has CORS rules, and has a public domain (custom or managed). +- Uploading the selected video/HLS asset via multipart S3 uploads and publishing the resulting + public URL into the video note payload. + +The service is used from the Upload modal UI (via `js/ui/components/UploadModal.js`) and is not a +backend integration. + +## Assets stored in R2 +R2 is used as the URL-first playback source. The upload path supports a single file per upload, but +file types include: + +- Video files (MP4/WebM/MOV/MKV/MPG/MPEG). +- HLS/DASH assets (manifest `.m3u8`/`.mpd` and segment files like `.ts`/`.m4s`). +- Related static media if the operator chooses to upload them separately (images, subtitles such as + `.vtt`/`.srt`), which benefit from cache-friendly headers set in the upload flow. + +The object key format is generated by `buildR2Key()` in `js/r2.js` and looks like: +`u////.`, which keeps uploads scoped by user and month. + +## Upload lifecycle +1. **Settings saved locally** + - The Upload modal calls `r2Service.saveSettings()` to persist Cloudflare credentials and + optional domain configuration. Settings are stored in IndexedDB (`bitvidSettings` → `kv`) + with a localStorage fallback (`bitvid:r2Settings`). + +2. **Bucket and domain preparation** (`ensureBucketConfigForNpub`) + - If an API token is provided, the service: + - Creates the bucket if it does not exist. + - Applies CORS rules for the current origin. + - Attempts to attach a custom domain using the configured `zoneId` and `baseDomain`. + - If the custom domain cannot be activated or already exists, it falls back to the managed + `*.r2.dev` domain. + - If no API token is available, it: + - Uses the last-known bucket/domain (or builds a managed `*.r2.dev` URL), + - Tries to set CORS via S3 keys when possible. + +3. **Upload and publish** + - The service builds the R2 object key and public URL. + - `multipartUpload()` uploads the file using S3 multipart uploads. + - On success, `publishVideoNote()` is called with the hosted URL, so playback can be URL-first + with magnet fallback. + +4. **UI feedback** + - The Upload modal receives progress, status, and managed-domain fallback warnings via + `r2Service` events. + +## Download / playback flow +There is no direct “download” API for R2 in `r2Service`. Instead, R2 provides the public URL that +gets saved into the video note payload. Playback uses that URL as the primary source and only falls +back to magnet/WebTorrent when needed, preserving the URL-first playback promise. + +## Configuration keys and where they live +**Stored locally (IndexedDB / localStorage):** +- `accountId`: Cloudflare account ID. +- `accessKeyId`: R2 S3 access key ID. +- `secretAccessKey`: R2 S3 secret access key. +- `apiToken`: Cloudflare API token used for bucket/domain management. +- `zoneId`: Cloudflare zone ID (required for custom domain attachment). +- `baseDomain`: Base domain used to derive per-user subdomains (e.g., `videos.example.com`). +- `buckets`: Cached map of `npub` → `{ bucket, publicBaseUrl, domainType, lastUpdated }`. + +**Environment / global flags:** +- `globalThis.__BITVID_DISABLE_NETWORK_IMPORTS__`: disables the AWS SDK network import used by the + R2 S3 client loader (`js/storage/r2-s3.js`). When true, uploads will fail with a load error until + the SDK is bundled or otherwise provided. + +**Config files:** +- There are no R2-specific keys in `config/` today; all R2 settings are entered through the UI and + stored locally in the browser. + +## Retry and fallback behavior +- **Bucket/domain setup:** + - Custom domain attachment is attempted when `apiToken`, `zoneId`, and `baseDomain` are present. + - If custom domain attachment fails or is unavailable, the service falls back to the managed + `*.r2.dev` domain and surfaces a warning to the user. +- **CORS:** + - CORS setup is attempted via the API token or (secondarily) via S3 credentials. Failures are + logged and do not block the upload, but missing CORS may prevent playback from browsers. +- **Multipart uploads:** + - There is no custom retry loop inside `multipartUpload()`. Failures propagate to the UI; the + caller can retry by re-initiating the upload. + +## Security considerations +- **Credential storage:** R2 credentials and API tokens are stored in browser storage. Treat them as + sensitive and avoid sharing browser profiles. Clear settings via the Upload modal when done. +- **Public exposure:** The generated URLs are public; uploaded assets should be safe for public + access. Use signed URLs only if you add a separate access layer. +- **CORS and origins:** CORS rules are scoped to the current origin to avoid overly broad exposure. +- **Domain ownership:** Custom domain attachment requires `zoneId` and a Cloudflare API token with + the necessary R2 and DNS permissions. Keep tokens scoped to least privilege. + +## Impact on uploads and playback +- **Uploads:** Successful R2 uploads produce a hosted URL stored in the video note payload, enabling + URL-first playback. Metadata such as MIME type and the public URL are also stitched into the NIP-71 + metadata block during publish. +- **Playback:** The hosted URL is the primary stream source. If it fails, playback can still fall + back to the magnet/WebTorrent path, so the R2 integration improves reliability without removing + P2P fallback.