Commit Graph
6777 Commits
Author SHA1 Message Date
thePR0M3TH3AN 04c8511a50 Merge pull request #2787 from PR0M3TH3AN/codex/enable-sound-by-default-in-player-2ks9wb
Whitelist-scoped Most Zapped feed, autoplay-fallback mute handling, BitLogin DOM hardening, and auth-thread reload
site-20260726-04c8511
2026-07-26 09:23:35 -04:00
thePR0M3TH3AN ce14004725 Preserve autoplay preference on fallback; reload open comment thread after login
### Motivation

- Prevent a browser-policy muted-autoplay retry from overwriting a viewer's explicit mute/unmute preference and ensure fallback mutes are transient.
- Ensure that when a user logs in while a video comment modal is open, the open thread is reloaded (not just refreshed) so comments continue to display correctly after relay-backed services are reconfigured.

### Description

- Add `js/utils/autoplayPreference.js` with `muteForAutoplayFallback` and `consumeAutoplayFallbackMute` helpers to mark and consume transient fallback mutes.
- Update `js/services/playbackService.js` to treat sound as default unless the viewer explicitly chose mute, persist explicit choices via `localStorage`, ignore fallback-generated volume changes using `consumeAutoplayFallbackMute`, and expose `muteForAutoplayFallback` delegating to the new util.
- Replace direct `videoElement.muted = true` calls with `playbackService.muteForAutoplayFallback` in `js/app/playbackCoordinator.js` and `js/webtorrent.js` so fallback mutes are marked as transient.
- Change `js/app/authSessionCoordinator.js` to reload the currently open video comment thread with `commentController.load(activeModalVideo)` after login if a modal video is active, instead of only calling `refreshAuthState`.
- Update tests in `tests/services/playbackService.test.mjs` and add a test in `tests/app/login-defers-dms.test.mjs` to cover the login behavior that reloads an open comment thread.

### Testing

- Ran the `PlaybackService` unit tests (`tests/services/playbackService.test.mjs`) which assert `prepareVideoElement` behavior, explicit mute persistence, and the transient nature of fallback mute, and they passed.
- Ran the auth login unit tests including `tests/app/login-defers-dms.test.mjs` which verifies DM deferral and reload of an open comment thread, and they passed.
2026-07-26 09:23:05 -04:00
thePR0M3TH3AN 4458b269be Merge pull request #2786 from PR0M3TH3AN/codex/enable-sound-by-default-in-player
Preserve autoplay preference on fallback; reload open comment thread after login
site-20260726-4458b26
2026-07-26 08:59:35 -04:00
thePR0M3TH3AN f373bb9ce7 fix: keep autoplay fallback from saving mute 2026-07-26 08:58:58 -04:00
thePR0M3TH3AN 98c623c85a Promote beta to main: trust controls and hardened BitLogin site-20260726-98c623c 2026-07-25 22:48:54 -04:00
thePR0M3TH3AN 0513c7fcd8 test(moderation): align channel blur fixture with threshold 2026-07-25 22:48:46 -04:00
thePR0M3TH3AN 855022e911 Promote unstable to beta: trust controls and hardened BitLogin 2026-07-25 22:43:27 -04:00
thePR0M3TH3AN 589bda8cb0 feat(auth): enable BitLogin on unstable 2026-07-25 22:33:32 -04:00
thePR0M3TH3AN 18e08bf2cb feat: refine trust controls and harden BitLogin 2026-07-25 22:25:50 -04:00
thePR0M3TH3AN 126a613527 docs: link BitUnlock permission layer plan site-20260724-126a613 2026-07-23 21:32:59 -04:00
thePR0M3TH3AN 4eaa78cf59 docs: add BitUnlock permission layer TODO 2026-07-23 21:32:49 -04:00
thePR0M3TH3AN f49080558b docs: add BitUnlock permission layer plan site-20260724-f490805 2026-07-23 21:31:57 -04:00
thePR0M3TH3ANandClaude Sonnet 5 f25c2ac242 Re-vendor bitlogin widget: local session persistence
Pulls in bitlogin's session-persistence feature -- after a successful
sign-in, the everyday private key is cached in IndexedDB for this
origin, so a page reload restores the session instead of asking for
the login name + password again. No changes needed in
bitloginModalIntegration.js: the existing "bitlogin-login" listener
fires the same way whether the sign-in was just typed or silently
restored. See bitlogin's CHANGELOG.md (2026-07-22 entry).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-22 07:50:06 -04:00
thePR0M3TH3ANandClaude Sonnet 5 4d1116530b Surface BitLogin sign-in rejections instead of silently swallowing them
devLogger.warn is a no-op outside dev mode (IS_DEV_MODE = false on
this deployment), so when AuthService.requestLogin() rejected a
BitLogin sign-in -- e.g. the invite-only access-control check
("Access restricted to admins and moderators users only.") -- nothing
ever told the user why. The widget itself doesn't know bitvid rejected
it, so it kept showing its own "Signed in" screen, and the login modal
never closed, looking exactly like a hung/broken close button.

On rejection: route the error through app.handleLoginModalError (the
same visible-banner path every other sign-in method already uses) and
reset the widget via widget.logout() so it doesn't keep claiming a
session bitvid never actually granted.

Also added a WeakSet guard so wireBitloginLogin() can never attach two
"bitlogin-login" listeners to the same element, regardless of how
attachWidget() ends up getting invoked -- cheap insurance, no observed
bug currently triggers it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-22 00:19:43 -04:00
thePR0M3TH3ANandClaude Sonnet 5 f2d390ff18 Wire up NIP-04 in the BitLogin adapter, flip capabilities.nip04 to true
bitlogin's widget now implements NIP-04 (nip04Encrypt/nip04Decrypt),
closing the one gap left relative to a real NIP-07 extension. bitvid's
own nip07Adapter/nsecAdapter/nip46Adapter all already wire this up and
report it via signerCapabilities.nip04 -- BitLogin's adapter was the
one exception, hardcoded to nip04: false because the widget genuinely
didn't support it before. js/nostr/client.js's DM encrypt/decrypt path
only offers NIP-04 to a caller when that capability flag is true, so a
BitLogin-signed-in user previously had no working legacy-DM path at
all. Re-vendored the widget bundle (bitlogin's CHANGELOG.md, 2026-07-21
entry, third one) and updated the one test that hardcoded the old
(now-incorrect) capabilities shape.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 23:32:27 -04:00
thePR0M3TH3ANandClaude Sonnet 5 ca7b711b8a Widen pinned BitLogin relay set for redundancy against a single dead relay
relay.nostr.band (shared by both relay roles pinned here) is currently
unreachable, and BitLogin's publishAndVerify() enforces a fixed
ack/readback floor of 2 regardless of relay count -- so pinning to
only 3 vault relays left zero margin the moment one went down,
surfacing as a registration/login quorum failure.

Widen the <bitlogin-auth> vault-relays/discovery-relays attributes in
components/login-modal.html to match bitlogin's own updated built-in
defaults, and re-vendor the widget bundle (see bitlogin's CHANGELOG.md
2026-07-21 entry). No CSP change needed -- this branch's connect-src
already allows wss: broadly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DY9UMTuF43Ekc2U4E1eDwX
2026-07-21 21:44:40 -04:00
thePR0M3TH3AN 88c906fd34 Add BitLogin as the primary sign-in method (gated, unstable)
The login modal now shows <bitlogin-auth> -- a portable Nostr identity
behind a login name and password -- as the default sign-in path,
styled with bitvid's own brand tokens (accent-strong, surfaces, radii).
The existing NIP-07 extension, nsec, and NIP-46 remote-signer options
still work exactly as before, just collapsed under a new "Other login
options" fold instead of a flat "Available methods" grid; nip07 no
longer carries the stale "Recommended" eyebrow now that BitLogin has
that role.

BitLogin isn't a single button-click-then-await-a-promise flow like
the other providers -- it's a whole multi-screen sign-in/create/recover
UI -- so it gets a permanent, statically-declared widget in
components/login-modal.html (js/ui/bitloginModalIntegration.js relocates
it, never destroys and recreates it, since removal tears down its crypto
Web Worker) instead of a grid button. It still goes through the exact
same session-establishment path as every other signer: a new "bitlogin"
provider (js/services/authProviders/bitlogin.js) lets AuthService
resolve and activate it via the normal requestLogin -> handleAuthLogin
path, while providersForModal excludes it from the grid so it isn't
rendered twice.

Two things this needed beyond the wiring itself:
- js/app.js is one line from its lint/file-size ceiling, so the only
  change there is renaming an import binding (providers -> providersForModal),
  a net-zero line change. The actual widget/event wiring lives in a new,
  unconstrained module instead.
- The login modal's HTML (including the permanent widget element) loads
  asynchronously after bootstrap runs, so wireBitloginLogin() waits via
  MutationObserver rather than assuming a specific call order.

Vendored via scripts/build-bitlogin-widget.mjs (mirrors the existing
Bitcoin Connect/Blossom vendor steps): BitLogin isn't published to npm,
so the widget build is copied verbatim from the sibling ../bitlogin
checkout into committed vendor/bitlogin/, then lazy-imported (like
Bitcoin Connect) so its ~350KB never weighs on visitors who never open
the login modal.

Gated behind FEATURE_BITLOGIN (config/instance-config.js), default on
for this unstable-branch experiment per AGENTS.md §1 -- reset to false
before this reaches main until verified end-to-end against production
relays.

Verified: npm run lint (clean), the full auth/login test suite (43/43,
including new coverage for the provider + adapter), and a manual
Chrome pass against the built dist/ -- widget renders themed correctly,
"Other login options" still shows the original four methods unchanged.
2026-07-21 20:56:01 -04:00
thePR0M3TH3AN 61f7d3f15e Promote beta → main: upload-artifact→v7, download-artifact→v8 (Node 24, clears remaining deprecation) site-20260714-61f7d3f 2026-07-14 10:11:48 -04:00
thePR0M3TH3AN 1d14a1f392 Promote unstable → beta: upload-artifact→v7, download-artifact→v8 (Node 24, clears remaining deprecation) 2026-07-14 10:11:46 -04:00
thePR0M3TH3ANandClaude Opus 4.8 676844edfc ci: bump upload-artifact→v7 and download-artifact→v8 for Node 24
The v5 line of these two actions still targets Node.js 20 (unlike
checkout/setup-node@v5, which are already on Node 24), so the deprecation
warning persisted. upload-artifact reaches Node 24 at v6+ and
download-artifact at v7+; move to the current latest (v7 / v8), which
share the same v4-generation artifact backend and pair correctly.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DwbE3uTiFaKRNjx5vP2muV
2026-07-14 10:11:44 -04:00
thePR0M3TH3AN 1340ad666b Promote beta → main: bump GitHub Actions (checkout/setup-node/upload+download-artifact) v4 → v5 to clear Node 20 deprecation site-20260714-1340ad6 2026-07-14 10:09:56 -04:00
thePR0M3TH3AN ada954b99c Promote unstable → beta: bump GitHub Actions (checkout/setup-node/upload+download-artifact) v4 → v5 to clear Node 20 deprecation 2026-07-14 10:09:48 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c35b60c539 ci: bump checkout/setup-node/upload+download-artifact v4 → v5
The v4 actions target Node.js 20, which GitHub now force-runs on Node 24
and flags as deprecated. Bump all four across every workflow to v5 (their
native Node-24 line) to clear the warning. Mechanical version-only change;
upload/download-artifact stay on matching majors as required.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DwbE3uTiFaKRNjx5vP2muV
2026-07-14 10:09:42 -04:00
thePR0M3TH3AN feee092a4d Promote beta → main: nsite site-archive automation — on-push downloadable artifact (all branches) + GitHub Release with bitvid-site.zip on main (stable /releases/latest/download URL); manual-publish plan doc site-20260714-feee092 2026-07-14 09:36:43 -04:00
thePR0M3TH3AN 7cc6aab20d Promote unstable → beta: nsite site-archive automation — on-push downloadable artifact (all branches) + GitHub Release with bitvid-site.zip on main (stable /releases/latest/download URL); manual-publish plan doc 2026-07-14 09:36:35 -04:00
thePR0M3TH3ANandClaude Opus 4.8 bff716948d ci(nsite): cut a GitHub Release with the site archive on main pushes
Add a `release` job (main only, contents: write) that builds the archive
and publishes it as an immutable GitHub Release tagged
site-<date>-<shortsha>, marked --latest, with bitvid-site.zip attached.
This gives a stable download URL for the current production archive:
  https://github.com/PR0M3TH3AN/bitvid/releases/latest/download/bitvid-site.zip

Re-runs of the same commit refresh the asset instead of failing. The
Release holds only the public compiled dist/ (same as Vercel serves) and
no Nostr signing key — publishing to nsite stays a manual, client-signed
step. Per-branch artifact upload is unchanged. Docs updated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DwbE3uTiFaKRNjx5vP2muV
2026-07-14 09:35:50 -04:00
thePR0M3TH3ANandClaude Opus 4.8 10b93efa02 ci(nsite): build a downloadable site archive on every push
Add .github/workflows/nsite-archive.yml (push to any branch +
workflow_dispatch): npm ci + npm run build, then upload dist/ as an
artifact (bitvid-site-<branch>-<shortsha>, 30d retention). GitHub serves
it as a .zip with index.html at the root, ready for a manual nsite
publish. The workflow holds no signing key and publishes nothing.

Also add an `archive` npm script for the same output locally
(bitvid-site.zip, now gitignored), and land docs/nsite-mirror-plan.md
revised to the manual-publish + auto-archive model: sign client-side via
NIP-07 or local nsyte, never an nsec/nbunksec in CI. The prior CI-signing
workflow is retained as a labeled Phase-3 reference option.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DwbE3uTiFaKRNjx5vP2muV
2026-07-14 09:29:41 -04:00
thePR0M3TH3AN 0dd7072b12 merge: promote beta → main — Blossom companion-event P2P, blob management, storage fixes (#30) 2026-07-12 22:07:11 -04:00
thePR0M3TH3AN 5313675896 merge: promote unstable → beta — Blossom companion-event P2P, blob management (BUD-03/BUD-12), storage sync + selector fixes (#30) 2026-07-12 22:07:02 -04:00
thePR0M3TH3ANandClaude Opus 4.8 e81e65ff40 feat(blossom): Phase 2 management — blob delete-on-orphan + kind-10063 server list (#30)
- deleteByUrl maps a Blossom URL back to its content-addressed blob (blobSha256FromUrl) and deletes it via BUD-12, defaulting to the URL's own origin. Wired into the edit-modal orphan cleanup so a superseded Blossom video/thumbnail is reclaimed (best-effort, never blocks the edit).

- BUD-03 kind-10063 server-list discovery: read (serversFromServerListEvent) + build (buildBlossomServerListEvent) + an extracted, testable import/publish helper (blossomServerList.js), plus 'Import from my server list' / 'Publish my server list' controls in the Storage pane's Blossom section.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 10:32:22 -04:00
thePR0M3TH3ANandClaude Opus 4.8 51072e4871 fix(storage): let a Blossom-only (keyless) account sync across devices
importAccountRecord hard-required encryptedMasterKey, but a Blossom-only account is keyless (no encrypted payloads) and legitimately has none — so its synced record failed to import. Require the envelope ONLY when the record carries an encrypted connection that actually needs it to unlock.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 10:22:22 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ef2e48912d fix(playback): P2P peer discovery for Blossom videos + forced-torrent URL fallback (#30)
Two torrent-path robustness fixes surfaced while testing WebTorrent peering:

- streamVideo now sets announce: [...WSS_TRACKERS]. A .torrent-buffer add (Blossom companion piece-map) has an EMPTY embedded announce list, so without this it reached no tracker, found no WebRTC peers, and streamed webseed-only (looked like a stuck '1 peer'). WebTorrent merges these with any trackers already on the source.

- A forced-torrent playback now falls back to the hosted URL when the torrent DEFINITIVELY fails (e.g. a transient companion-lookup failure under relay congestion, or a dead swarm) instead of hard-erroring 'No playable source found'. Only triggers on real failure, not a slow swarm. Rollback noted inline.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 19:04:14 -04:00
thePR0M3TH3ANandClaude Opus 4.8 4906745bff feat(storage): live-refresh the open upload modal when connections sync from Nostr
storageService now emits onConnectionsChanged (from importAccountRecord, the sync-import path); the upload modal subscribes and, when open + active user, re-derives its storage destination in place (re-attempt auto-unlock, recompute unlocked state, reload, repaint) — so credentials synced from Nostr are reflected without reopening. Bumped UploadModal's file-size baseline for the growth.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 19:04:06 -04:00
thePR0M3TH3ANandClaude Opus 4.8 d23cfa6697 feat(blossom): P2P for Blossom videos via a WebTorrent-metadata companion event (#30)
Blossom servers reject a .torrent (415), so a Blossom video's magnet is webseed-only and can't bootstrap WebTorrent. This restores P2P without hosting a .torrent:

- Upload hardening: correct Content-Type (overrides empty/generic application/octet-stream), HTTP status surfaced in errors, and best-effort .torrent sidecar so a rejected sidecar no longer sinks the upload (URL-first fallback).

- Publish: when the .torrent can't be hosted, publish the piece-map as an infohash-keyed companion event (kind 30078, d=bitvid:torrent:<infohash>) and ship a ws= magnet. Gated by FEATURE_BLOSSOM_TORRENT_METADATA (default off = no trace); size-capped.

- Playback: torrentMetadataService lazily fetches the companion, VERIFIES the reconstructed infohash === the magnet's btih (untrusted content), sources the webseed from the video event, and hands the .torrent buffer to WebTorrent so it streams with zero peers. Relay reads go through the subscription manager.

Inherited by any upload-based media (shorts, audio-publish); excludes ingest/live. See docs/blossom-torrent-metadata-plan.md. Includes a Prettier reflow in css/tailwind.source.css.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 14:08:53 -04:00
thePR0M3TH3ANandClaude Opus 4.8 56aff1ada0 fix(storage): default the storage-tab provider selector to the active default connection
fillStorageForm read the provider only from conn.provider (top-level), but getConnection returns { ...payload, meta } with the provider in meta (and no top-level provider for keyless Blossom) — so the selector always fell back to cloudflare_r2. Mirror connectionProvider()'s conn.provider || conn.meta?.provider fallback so the pane reflects the loaded default connection.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-11 14:08:41 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c22fa23353 fix(blossom): push the encrypted synced copy after saving Blossom servers [LOCAL] (#30)
The keyless Blossom save returned early without triggering storageSyncService.push
(the S3 path does it after saving), so changing Blossom servers never updated the
synced copy — it looked like the settings didn't sync. Blossom servers already
live in the account record's plaintext meta (which exportAccountRecord ships), so
the same push the S3 branch uses now runs after a keyless save: the record is
re-encrypted to self and published, and it roams like S3.

NOTE: sync export/import needs the account's encryptedMasterKey, which exists once
any secret-bearing (S3) connection has been unlocked/saved. A Blossom-ONLY account
(no S3 ever) has no master key envelope yet — encrypted sync for that case is a
follow-up.

Lint + build clean. Local only.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 22:00:21 -04:00
thePR0M3TH3ANandClaude Opus 4.8 18ea5428eb fix(blossom): upload directly per-server instead of the failing /mirror flow [LOCAL] (#30)
Real-device failure: multiServerUpload uploads to one server then has the OTHERS
fetch it server-to-server via PUT /mirror — those /mirror requests fail from a
browser (net::ERR_FAILED / CORS), so the whole upload returned no blob URL
("Blossom upload did not return a usable blob URL").

Switch blossomService.uploadFile to upload DIRECTLY to each server (BUD-02
PUT /upload) in parallel via uploadBlob, resilient: first success wins, per-server
failures are collected, and if ALL fail the error names each server + the likely
cause (CORS / size / payment) and suggests blossom.band. A fresh signed upload
auth (BUD-11) is created per request.

Tests: blossom-service 11/11 (added partial-failure fallback + all-fail error).
Lint + build clean. Local only.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 21:49:50 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ba62b35ada fix(storage): make Blossom actually usable in the upload modal [LOCAL] (#30)
Two root causes for "Blossom set as default but not showing / not usable":

1. SAVE required unlock. saveConnection throws "Storage is locked" without a
   master key — but Blossom has no secret. Added
   storageService.saveKeylessConnection() (writes plaintext meta, no encryption,
   no unlock) and the Blossom save branch now uses it. So a Blossom connection
   saves even if the user never unlocked storage.

2. UploadModal treated Blossom like a locked S3 connection: it only set
   activeCredentials when isStorageUnlocked, showed "Locked 🔒", blocked the
   file-pick behind an unlock prompt, and mislabeled it "S3 Compatible". Now:
   getProviderLabel → "Blossom"; loadFromStorage resolves servers from the
   plaintext meta (no unlock); the summary shows "N servers / Nostr-native"; the
   two upload gates and updateLockUi bypass unlock for Blossom
   (activeProviderIsBlossom). Uploads authorize with the Nostr signer instead.

Bumped UploadModal size baseline (+gating code). Lint + build clean. Local only.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 21:40:18 -04:00
thePR0M3TH3ANandClaude Opus 4.8 5935702e75 feat(storage): Blossom Phase 2 — BUD-12 delete/list primitives [LOCAL] (#30)
The management primitives, self-contained + unit-tested. Not yet wired into the
S3-shaped orphan-GC / delete-flow (that reconcile system assumes bucket/key
semantics — its own browser-tested increment, same pattern as the upload).

- Vendor entry: export createDeleteAuth (+ rebuilt bundle, still ~27KB).
- blossomService.deleteFile({ server, sha256, signer }) — BUD-12 DELETE /<sha256>
  with a signed kind-24242 delete auth (BUD-11).
- blossomService.listFiles({ server, pubkey, signer }) — BUD-12 GET /list/<pubkey>,
  optional list auth.

Tests: blossom-service 9/9 (added delete/list guards + happy paths). Lint + build
clean.

REMAINING Phase 2: wire deleteFile into video-delete cleanup (resolve
server+sha256 from the blossom storagePointer), orphan-GC parity, and the
kind-10063 discovery UI (import/publish server list — needs relay fetch/publish
plumbing).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 16:36:31 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c5e8c7f9f1 feat(storage): Blossom Phase 1b — keyless multi-server config UI [LOCAL, flag ON] (#30)
Makes Blossom configurable + uploadable end-to-end (local testing).

- Storage pane: a "Blossom (Nostr-native)" provider option (revealed only when the
  flag is on) that swaps the S3 field group (#storageS3Fields) for a keyless
  #storageBlossomSection — a multi-server textarea (one URL per line). No
  access-key/secret/bucket/CORS.
- ProfileStorageController: parseBlossomServers(); save branch persists the server
  list in the PLAINTEXT connection meta ({servers}, no encrypted secret) via the
  per-provider slot; provider-change loads servers from meta WITHOUT an unlock;
  Test does a best-effort reachability ping; visibility branch hides S3 fields +
  CORS helper for Blossom.
- mediaUploader.resolveActiveConnection: Blossom connections resolve from plaintext
  meta with no unlock (uploads authorize via the Nostr signer, not a stored key),
  so credentials = { provider, servers, meta } flow straight into the Phase-1a
  engine.
- instance-config: FEATURE_BLOSSOM_STORAGE = true for LOCAL testing (reset to false
  before pushing).

Tests: blossom-service 7/7 (isAvailable now flag-relative). Lint + build clean.
Blossom UI present in dist.

VERIFY locally: Storage → Provider → Blossom → add a server → Save → upload a
video → it plays from <server>/<sha256>. Watch the UploadModal's own storage
gating (may need a tweak for the no-secret/no-PIN Blossom case).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 16:31:34 -04:00
thePR0M3TH3ANandClaude Opus 4.8 0eafd38ec4 feat(storage): Blossom Phase 1a — upload engine [LOCAL] (#30)
The Blossom upload orchestration, isolated + unit-tested. Only routes when a
connection's provider is "blossom", so the S3/R2 path is completely unaffected
(that branch can't trigger until the Phase-1b config UI can create a Blossom
connection).

- blossomService now owns the orchestration (WebTorrent-free, so it's Node-
  testable): uploadFile (multiServerUpload + BUD-11 auth) and uploadVideo (blob →
  injected torrent generator → upload .torrent as a 2nd blob → magnet with ws=the
  Blossom video url + xs=the Blossom .torrent url → blossom storagePointer). Same
  result shape as the S3 path, so publish is untouched. Exports isBlossomProvider
  + resolveBlossomServers.
- mediaUploader: thin Blossom branch at the top of uploadThumbnail/uploadVideo
  (delegates to blossomService, injecting generateTorrentMetadata so the service
  never imports WebTorrent); new getSigner dep. UploadModal + EditModal pass
  getSigner: () => getActiveSigner().

Tests: tests/blossom-service.test.mjs (7 — guards, mirror, torrent/magnet
orchestration, storagePointer). Lint + build clean. Flag still off.

NEXT (Phase 1b): the keyless multi-server Storage-pane config UI + connection
model (servers in plaintext connection meta — no secret) + resolveActiveConnection
Blossom branch, so a server can be configured and uploads run end-to-end.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 15:56:05 -04:00
thePR0M3TH3ANandClaude Opus 4.8 02e60f1842 feat(storage): Blossom Phase 0 — flag + vendored SDK + service skeleton (#30)
Phase 0 of docs/blossom-plan.md. No UX; off-by-default ("off = no trace").

- Vendored: pinned [email protected] bundled by scripts/build-blossom-sdk.mjs
  → vendor/blossom-client-sdk.bundle.min.js (esbuild, browser ESM, ~27KB). The
  optional @cashu/cashu-ts peer dep (BUD-07 payments, only reached on a lazy 402
  branch bitvid never hits) is aliased to a throwing stub. `npm run build:blossom-sdk`.
- Flag: FEATURE_BLOSSOM_STORAGE wired like FEATURE_AUDIO_INGEST / _BITCOIN_CONNECT
  (instance-config → config → constants), default FALSE.
- Service: js/services/blossomService.js implements the same
  uploadFile({…}) → {url,key} contract as s3UploadService (so a later
  isBlossomProvider branch drops into mediaUploader). Lazy-loads the SDK; core
  path = createUploadAuth(signer, sha256) + multiServerUpload (BUD-11 auth via
  bitvid's existing signer, BUD-04 mirror). Torrent parity + result-shape mapping +
  mediaUploader wiring + Storage-pane config land in Phase 1.

Tests: tests/blossom-service.test.mjs (5 — availability gate + input guards).
Lint + build clean. Nothing wired into the app yet.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 15:42:19 -04:00
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
thePR0M3TH3ANandClaude Opus 4.8 16aa7557ab docs: add Blossom storage dev plan (#30) — SDK-based, multi-server, flag-gated
Writes docs/blossom-plan.md (house format) from the July 2026 research: use the
maintained blossom-client-sdk v5 (only dep @noble/hashes, already vendored); the
spec's current modular BUD map (BUD-01 retrieval, 02 /upload, 11 auth kind-24242,
12 list/delete, 03 kind-10063 discovery, 04 mirror, 05 /media, 06 preflight);
the existing service.uploadFile seam as the integration point; and 10 decisions
(SDK vs hand-roll, multi-server+mirror, discovery, /upload vs /media, signer
reuse, torrent parity, management, keyless config UI, FEATURE_BLOSSOM_STORAGE,
public-only privacy). Notes Blossom removes the encrypted-cred-sync surface
(no secret — server URLs + signer) and the lift is medium (config UI is the main
cost). Expands TODO #30 with the plan pointer + decisions.

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

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 15:23:42 -04:00
thePR0M3TH3ANandClaude Opus 4.8 1add0bebec feat(wallet): tuck manual URI under an "Advanced" disclosure (Bitcoin Connect D5)
When FEATURE_BITCOIN_CONNECT is on, "Connect wallet" is the primary path and the
raw Wallet Connect URI field collapses under an "Advanced — paste a connection
manually" <details> disclosure (backup, de-emphasized). When the flag is off, the
controller forces the disclosure open and hides its summary, so the URI field
shows plainly exactly as before (no behavior change). Default-zap field stays
always visible. Adds a small token-based rotating-caret style for the summary.

Still LOCAL (flag ON for testing); not pushed, not deployed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 14:56:00 -04:00
thePR0M3TH3ANandClaude Opus 4.8 69e87c6e15 feat(wallet): Bitcoin Connect NWC "Connect wallet" flow [LOCAL — flag ON, do not push as-is]
Phase 0+1 of docs/bitcoin-connect-plan.md (#61). Adds Alby's Bitcoin Connect as a
one-tap NWC connect front-end that feeds bitvid's EXISTING wallet pipeline; manual
URI entry stays as the backup.

- Vendored: pinned @getalby/[email protected].3 bundled by
  scripts/build-bitcoin-connect.mjs → vendor/bitcoin-connect.bundle.min.js (esbuild,
  browser ESM). crypto-shim aliases the L402-only `import('crypto')` bitvid never
  calls. `npm run build:bitcoin-connect` rebuilds it.
- Flag: FEATURE_BITCOIN_CONNECT wired like FEATURE_AUDIO_INGEST
  (instance-config → config → constants).
- UI: a "Connect wallet" button in the Wallet Connect pane (hidden unless the flag
  is on); the manual URI field stays below as the backup.
- Controller: ProfileWalletController.handleBitcoinConnect() lazy-imports the bundle
  (only on click — the ~700KB never touches the main bundle), inits NWC-only with
  least-privilege requestMethods and persistConnection:false, then on onConnected
  extracts provider.client.nostrWalletConnectUrl → the SAME validate→save path as
  the manual field → disconnect() so bitvid solely holds the spending secret. The
  URI is never logged.

NOTE: config/instance-config.js FEATURE_BITCOIN_CONNECT is TRUE for local testing
only. Reset to false (shipping default) before pushing/deploying. Local commit;
not pushed, not deployed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 14:49:37 -04:00
thePR0M3TH3ANandClaude Opus 4.8 6dd40ce330 docs: add Bitcoin Connect (NWC connect UX) integration plan + TODO #61
Requested: integrate getAlby/bitcoin-connect as a smooth NWC "connect wallet"
front-end, keeping manual URI entry as backup. Plan (docs/bitcoin-connect-plan.md)
grounds it in bitvid's existing NWC stack: BC's NWC connector exposes the raw
pairing URI (provider.client.nostrWalletConnectUrl) which feeds the existing
validateWalletUri → per-npub store → zap-split/pay_invoice pipeline unchanged.
BC is a connect adapter, not a payment layer. Covers delivery (vendor+lazy import
vs esm.sh; CSP already allows esm.sh), persistence ownership, NWC-only connector
scope, least-privilege requestMethods, UI placement, FEATURE_BITCOIN_CONNECT flag,
and the security constraints (bearer spending secret, Alpha lib pin/vendor).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 14:19:17 -04:00
thePR0M3TH3ANandClaude Opus 4.8 d8d1ccb1f6 Promote unstable → beta: share popover copy buttons fix
Wire the video-modal share popover's Copy URL / Copy Magnet / Copy CDN / Share
on Nostr actions (were dead — no click delegation).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 12:47:17 -04:00
thePR0M3TH3ANandClaude Opus 4.8 793d609d6a fix(video-modal): wire the share popover's Copy URL / Magnet / CDN buttons
The share popover's menu items (Copy URL, Copy Magnet, Copy CDN Link, Share on
Nostr) only set `data-action` — like the ⋯ menu — but unlike the ⋯ menu (which
moreMenuController delegates) nothing wired their clicks, so they did nothing.
Only the bottom "forced-source" test links self-wire. Same dead-button class as
the old embed/creator-link bugs.

Add a delegated click handler on the share panel (bindShareMenuActions) that
routes each action to the event videoModalController already handles:
copy-magnet → video:copy-magnet, copy-cdn → video:copy-cdn, share-nostr →
video:share-nostr, and share (Copy URL) → a new video:copy-url (buildShareUrl
makes the plain `?v=<nevent>` deep link; controller copies it with a toast).
Forced-source links are skipped (they self-wire).

Tests: tests/video-modal-controllers.test.mjs (+5 routing scenarios, 24/24).
Lint + build clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 12:46:37 -04:00
thePR0M3TH3ANandClaude Opus 4.8 19b7d8215d Promote unstable → beta: beacon spinner-on-load fix
.ds-overlay-backdrop[hidden] guard so the beacon processing overlay no longer
covers the page on load (#28 follow-up).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 12:30:01 -04:00