Files
bitvid/docs/blossom-plan.md
T
thePR0M3TH3ANandClaude Opus 4.8 b7c6be830f docs(blossom): lock decisions D1–D10 to recommendations
Status → DECISIONS LOCKED. Locks: vendor blossom-client-sdk v5; multi-server +
mirror; kind-10063 read (v1) / publish (v1.5); /upload for video (/media for
thumbnails later); reuse bitvid's signer; keep torrent parity; management in v2;
keyless multi-server config form; FEATURE_BLOSSOM_STORAGE (off = no trace);
public-blobs-only privacy posture.

Local commit (with the unpushed Bitcoin Connect batch); not pushed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 15:31:22 -04:00

13 KiB
Raw Blame History

Blossom storage — first-class alongside R2 / S3 — Dev Plan

Status: DECISIONS LOCKED (D1–D10, per recommendations, 2026-07-09) — ready to build. Adds Blossom (nostr-native blob storage) as a first-class upload provider next to Cloudflare R2 / generic S3 / Backblaze B2, using the maintained blossom-client-sdk. Feature-flagged (off = no trace). Tracks TODO #30. Research: docs sources at the bottom; spec is github.com/hzrd149/blossom (BUDs). Sibling of the audio / Bitcoin Connect plans.

Executive summary

bitvid's uploads already flow through one seam: mediaUploader selects a service by provider and calls service.uploadFile({ … }) → { url, key, … } (the url is what lands in the kind-30078 video event). Both r2Service and s3UploadService implement that uploadFile contract; routing is a one-liner (isR2Provider(provider) ? r2Service : s3Service, mediaUploader.js:38).

Blossom = a new blossomService implementing the same uploadFile contract, plus an isBlossomProvider branch. Blossom's upload is simpler than S3 (no SigV4, no multipart, no bucket creation, no client-side CORS provisioning) and its auth is a signed nostr event (kind 24242) — so it reuses bitvid's existing signer with zero new crypto. The one real cost is the Storage-pane config UI, which today hard-assumes S3 credential fields and needs a parallel keyless, multi-server form.

The payoff: for anyone who picks Blossom, the whole "store an upload secret in the browser + encrypted-sync it" surface disappears (there is no secret — only server URLs + the user's signer), and files gain resilience via mirroring to multiple servers, which is Blossom's actual advantage over a single S3 bucket.

Landscape — the Blossom spec today (researched July 2026)

The spec has modularized since older write-ups; the auth and management endpoints moved into their own BUDs:

BUD Covers Tier
BUD-01 server requirements + blob retrieval (GET/HEAD /<sha256>) core
BUD-02 PUT /upload (returns a blob descriptor) core
BUD-11 Nostr Authorization — kind-24242 auth event core
BUD-12 GET /list/<pubkey> + DELETE /<sha256> (moved out of 02) opt
BUD-03 server discovery via kind-10063 user server list recommended
BUD-04 cross-server mirroring (PUT /mirror) recommended
BUD-05 /media endpoint (metadata strip / re-encode) optional
BUD-06 upload negotiation (preflight before sending bytes) recommended
BUD-07 / 09 / 10 payment-gated / reporting / URI scheme optional

Core minimum a client should implement today: BUD-01 + BUD-02 + BUD-11. Add BUD-03 (discovery) + BUD-06 (preflight) + BUD-04 (mirror) for a good client.

  • Client SDK: blossom-client-sdk v5 (hzrd149, the spec author). ESM; only required dep is @noble/hashes — which bitvid already vendors (crypto- helpers). Optional peer deps (cashu, hls.js) are skipped. Small bundle.
  • Auth (BUD-11): a kind-24242 event with t (get/upload/list/delete/ media), a required NIP-40 expiration, optional server (scope to a domain) and x (scope to a sha256), sent as Authorization: Nostr <base64url event>. The SDK's createUploadAuth(signer, sha256, { type }) builds it; the signer it wants is exactly bitvid's shape (async (eventTemplate) → signed event).
  • Blob descriptor (PUT /upload response): { url, sha256, size, type, uploaded, … } — and MAY include magnet / infohash / ipfs, a direct tie-in to bitvid's torrent story. sha256 maps onto bitvid's existing fileSha256/x.

Decisions needed

DECISION 1 — Client library. ✅ LOCKED (= recommendation below).

  • A — Vendor blossom-client-sdk v5 (esbuild bundle like bitcoin-connect / floating-ui; its @noble/hashes dep is already vendored). Tracks the spec's ongoing BUD refactors; gives upload/mirror/discovery/negotiation for free.
  • B — Hand-roll the ~3 HTTP calls + the kind-24242 auth event. Recommendation: A — the spec is actively modularizing (auth→BUD-11, list/delete→BUD-12); the maintained SDK absorbs that churn, and it's small.

DECISION 2 — Single server vs multi-server + mirror. ✅ LOCKED (= recommendation below).

  • A — Multi-server list (multiServerUpload): upload to N servers (HEAD preflight, /mirror to register on ones that already have the blob).
  • B — Single server URL only. Recommendation: A — multiple live copies (resilience/censorship-resistance) is the entire point of Blossom over one S3 bucket. Config becomes a server list.

DECISION 3 — kind-10063 server discovery/publish (BUD-03). ✅ LOCKED (= recommendation below). Read the user's published server list to pre-fill their servers, and publish/ update it so other clients can find bitvid-hosted media. Recommendation: read in v1 (getServersFromServerListEvent), publish in v1.5 (a "publish my server list" action). Spec says clients SHOULD do both.

DECISION 4 — /upload vs /media (BUD-05). ✅ LOCKED (= recommendation below). /media strips metadata + may re-encode; /upload stores exact bytes. Recommendation: /upload for videos (keeps the exact file so the torrent infohash/seed stays valid), optional /media for thumbnails/avatars later (privacy/EXIF strip).

DECISION 5 — Signer wiring. ✅ LOCKED (= recommendation below). Recommendation: reuse bitvid's active signer adapter as the SDK signer (NIP-07 / NIP-46 / nsec all already produce async (tpl) → signed event). No new crypto. Respect the signer circuit-breaker; reuse one auth across a mirror set (the SDK supports this) so a multi-server upload isn't N prompts.

DECISION 6 — Torrent parity. ✅ LOCKED (= recommendation below). S3 uploads also build a magnet + seed via WebTorrent. Keep that for Blossom? Recommendation: Yes — keep parity (Blossom URL as the hosted source + magnet as the P2P fallback). If the server's blob descriptor already returns magnet/infohash, prefer/verify it; else compute as today. sha256 = the shared content id.

DECISION 7 — Management (list/delete/orphan-GC). ✅ LOCKED (= recommendation below). BUD-12 GET /list/<pubkey> + DELETE /<sha256> for the storage-management UI (My Videos orphan tools #8/#13). Recommendation: v2 — v1 ships upload; wire list/delete + orphan parity after.

DECISION 8 — Storage-pane config type. ✅ LOCKED (= recommendation below). Recommendation: add a "Blossom" option to #storageProvider that swaps the S3 fields (endpoint/region/access-key/secret/bucket + CORS helper) for a keyless multi-server list (add/remove server URLs, mark default, "test", optional "import from my kind-10063 list" / "publish my list"). New connection-payload variant { servers: [...] } in the per-provider slot model.

DECISION 9 — Feature flag. ✅ LOCKED (= recommendation below). Recommendation: FEATURE_BLOSSOM_STORAGE in config/instance-config.js (default off = no trace), wired like FEATURE_AUDIO_INGEST / FEATURE_BITCOIN_CONNECT. Off ⇒ no Blossom option, SDK never imported.

DECISION 10 — Privacy posture. ✅ LOCKED (= recommendation below). Blossom blobs are public + content-addressed (anyone with the URL/sha256 can fetch), same as a public S3 bucket. There are no private blobs in the base spec. Recommendation: treat Blossom as public hosting (bitvid videos are already public); use /media to strip EXIF on images; do NOT route anything sensitive (e.g. private DMs) through public Blossom without BUD-07-style gating — keep DM attachments on the existing path until a private story exists.

Config flag (off = no trace)

  • FEATURE_BLOSSOM_STORAGE (default false) — gates the "Blossom" storage provider option, the blossomService, and the lazy SDK import. Off ⇒ Storage pane shows only R2 / S3 / B2 exactly as today.

The uploadFile contract (the seam Blossom implements)

mediaUploader calls, for both thumbnail and video (4 call sites):

service.uploadFile({ file, key?, contentType?, onProgress, credentials, … })
  → { url, key, storagePointer?, infoHash?, magnet?, torrentUrl?, … }

blossomService.uploadFile maps to:

  1. sha256 = @noble/hashes of the file (SDK helper) → the content id + key.
  2. createUploadAuth(signer, sha256, { type }) → kind-24242 auth (scoped x, short expiration).
  3. multiServerUpload(servers, file, { onAuth }) (BUD-06 preflight + BUD-04 mirror) → blob descriptors; pick the primary url (default server), keep the mirror URLs as fallbacks (map into the video's imeta sources, #17/#20).
  4. Return { url, key: sha256, storagePointer, magnet?, infoHash? } so the rest of the publish pipeline is untouched.

Architecture

  • js/services/blossomService.js — implements uploadFile (+ later listObjects/deleteObject for BUD-12). Wraps the vendored SDK; takes the app's active signer. No credentials object — just the server list + signer.
  • mediaUploader.js — add isBlossomProvider; route to blossomService.
  • Storage pane (components/profile-modal.html, ProfileStorageController, storageConnections) — Blossom provider type: keyless multi-server form + payload variant { servers }; hide S3 fields + CORS helper for Blossom.
  • Discovery — getServersFromServerListEvent() + USER_BLOSSOM_SERVER_LIST_KIND (kind 10063) to read; a publish action to write (BUD-03).
  • Flag — config/instance-config.js → js/config.js → js/constants.js.
  • Vendor — scripts/build-blossom-sdk.mjs → vendor/blossom-client-sdk.bundle.min.js (lazy-imported only when a Blossom upload runs).

Security

  • No stored secret for Blossom — only public server URLs + the user's signer. This removes the encrypted-cred-sync surface (#15) for Blossom users; nothing to leak, sync, or lock.
  • Auth is short-lived + scoped — kind-24242 events carry a required NIP-40 expiration and MUST be scoped (t, x to the blob's sha256, optional server to the domain). Build fresh per action; never a long-lived token.
  • Signer discipline — uploads sign per action; reuse one auth across a mirror set (SDK) so a multi-server upload isn't N signer prompts, and respect the NIP-07 circuit-breaker / decrypt-budget invariants. Never log the signed auth event's contents beyond what devLogger already gates.
  • Integrity — verify the returned sha256 matches the uploaded bytes; on download, clients SHOULD verify the hash (BUD-01).
  • Public data only — Blossom blobs are world-readable; keep private content off it (D10).

Phases (each flag-gated from day one)

  • Phase 0 — Flag + vendor SDK + service skeleton. FEATURE_BLOSSOM_STORAGE, scripts/build-blossom-sdk.mjs, a blossomService stub. No UX.
  • Phase 1 — Upload (headline). blossomService.uploadFile via multiServerUpload + signer auth + BUD-06 preflight; isBlossomProvider routing; Storage-pane keyless multi-server form + "test"; torrent parity (D6). Video + thumbnail upload land in the same publish pipeline.
  • Phase 1.5 — Discovery. Read kind-10063 to pre-fill servers; a "publish my server list" action (BUD-03).
  • Phase 2 — Management. BUD-12 list/delete, orphan-GC parity (#8/#13), mirror controls (BUD-04), optional /media for thumbnails (BUD-05).
  • Phase 3 — Extras. DM attachments (only with a private story), BUD-09 reporting, richer multi-server health/status.

Risks / watch-items

  • Server availability/churn → mitigated by multi-server + mirror (D2); surface per-server status.
  • Spec still evolving (BUD-11/12 refactor is recent) → the SDK tracks it; pin the exact version (like bitcoin-connect) and wrap it behind our thin service.
  • Large videos = one PUT (no S3 multipart); some servers cap size → BUD-06 preflight + fall over to another server; surface clear size errors.
  • Server CORS is the operator's job, not the client's — a misconfigured server blocks browser upload; detect + suggest known-good servers.
  • Signer prompts on multi-server → reuse one auth across the mirror set.
  • Bundle — lazy-import the SDK only when a Blossom upload actually runs.

Sources

  • Spec + BUDs: github.com/hzrd149/blossom (BUD-01/02/03/04/05/06/11/12).
  • github.com/hzrd149/blossom-client-sdk (v5): createUploadAuth, uploadBlob, mirrorBlob, multiServerUpload, multiServerMediaUpload, listBlobs, getServersFromServerListEvent, USER_BLOSSOM_SERVER_LIST_KIND, encodeAuthorizationHeader.
  • NIP-B7 (Blossom media in notes), NIP-40 (expiration), NIP-98 (HTTP auth lineage).
  • bitvid: js/ui/components/mediaUploader.js (uploadFile seam), js/services/{r2Service,s3UploadService}.js, js/ui/profileModal/{ProfileStorageController,storageConnections}.js, components/profile-modal.html (Storage pane), docs/bitcoin-connect-plan.md (flag + vendor pattern), TODO #30.