Commit Graph
218 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 4.8 61e4b25de7 [feed] Auto-refresh + optimistic insert after publishing a video (#46)
After a successful publish, the new video now appears in the feed instantly
without a manual page reload.

- Add nostrClient.ingestLocalVideoEvent(rawEvent) + a thin nostrService wrapper:
  optimistically insert the just-published event into the shared active-video
  cache. Mirrors live ingestion (convert -> root created_at -> tombstone guard
  -> allEvents/activeMap), so it obeys the same dedupe/active-key rules — a
  later relay echo of the same event won't duplicate it and an older revision
  can't overwrite a newer active entry (#20/#21).
- app.publishVideoNote: inject publishResult.legacy, then refresh via the
  canonical refreshAllVideoGrids({ reason: "video-published", forceMainReload:
  true }) instead of a bare loadVideos() — refreshes whichever feed is active
  (For You / Explore / Recent) plus subscription/channel grids and forces a
  render (bypassing the load cooldown) so the injected video shows.

Tests: tests/optimistic-feed-insert.test.mjs (6).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 22:49:02 -04:00
thePR0M3TH3ANandClaude Opus 4.8 95fe88784a [nostr-core] Per-account NIP-46 session storage for multi-account switching (#48)
Mirror the per-account nsec key store for remote-signer sessions: move from a
single slot (bitvid:nip46:session:v1) to a per-user-pubkey map
(bitvid:nip46:sessions:v2) so several saved NIP-46 accounts each keep their own
reconnectable session and the profile switcher can move between them.
Previously only the last-connected session survived, so switching among saved
NIP-46 accounts reconnected the wrong one (or failed).

- nip46Client.js: new per-pubkey session map keyed by userPubkey.
  readStoredNip46Session takes an optional target pubkey; writeStoredNip46-
  SessionSync writes both the map and the v1 last-connected default;
  clearStoredNip46Session(pubkey) forgets one account (no-arg wipes all). Legacy
  v1 slot migrated into the map on first read. Adds listStoredNip46SessionPubkeys().
- SignerManager: useStoredRemoteSigner({ pubkey }) selects the target account's
  session and, on access-denial, forgets only that account. getStoredNip46-
  Metadata(pubkey) is per-account.
- nip46 provider threads expectPubkey into the reuse call so a switch reconnects
  the requested account, with the expectPubkey guard as a backstop.

nip46Connector.js is dead code; the live path is entirely in SignerManager.
Tests: tests/nip46-multi-account.test.mjs (7).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 22:40:22 -04:00
thePR0M3TH3ANandClaude Opus 4.8 8692dac671 [nostr-core] Per-account nsec key storage for multi-account switching (#48)
Move the encrypted-key store from a single slot to a per-pubkey map so
several saved nsec accounts each keep their own key on the device and the
profile switcher can move between them. Previously bitvid:sessionActor:v1
held one key (last-saved wins), so switching among saved nsec accounts
silently failed.

- sessionActor.js: new bitvid:sessionActors:v2 map keyed by pubkey (follows
  the existing bitvid:<thing>:<npub> convention). readStoredSessionActorEntry,
  persistSessionActor, clearStoredSessionActor take an optional target pubkey;
  legacy v1 slot is kept in sync as the last-saved default and migrated into
  the map on first read. IndexedDB mirror keyed per-pubkey. Adds
  listStoredSessionActorPubkeys().
- client.js: getStoredSessionActorMetadata / unlockStoredSessionActor accept a
  target pubkey; access-denial cleanup forgets only the blocked account.
- nsec provider threads expectPubkey into the unlock so a switch unlocks the
  requested account, with the expectPubkey guard as a backstop.
- authSessionCoordinator: look up the target account's stored-key metadata.

NIP-46 multi-account (single slot bitvid:nip46:session:v1) remains as a
follow-up. Tests: tests/session-actor-multi-account.test.mjs (7).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 22:33:03 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ad83c9f8db [ui] route all native alert()/confirm() through the site notification system
Every browser alert()/confirm() now uses the app's UI instead of native dialogs
(no prompt() existed). Two new pieces:

- js/ui/confirmDialog.js — showConfirm(message, opts) -> Promise<boolean>, a styled
  `bv-modal modal-always-on-top` dialog (Confirm/Cancel/backdrop/Escape; danger variant
  focuses Cancel). Importable anywhere, so future confirmations use it too. It stacks
  above other modals and is typable thanks to the recent stacked-modal focus fix.
- js/ui/appNotify.js — notifyError/notifyStatus/notifySuccess routing to the app's
  NotificationController, for modules without a local showError; re-exports showConfirm.

Migrated: UploadModal (10 alert -> this.showError, 1 confirm), subscriptionHistory
(3 alert -> notify*, 1 confirm), MyVideosController (2), ProfileStorage/Wallet/Relay
controllers, profileModalController (2), settingsRestorePrompt (default confirm now
showConfirm), nostr/client deleteAllVersions. confirmSyncOverwrite helpers made async
(syncConflict already awaits confirmOverwrite).

Tests: tests/confirm-dialog.test.mjs (6 — confirm/cancel/backdrop/Escape resolution,
DOM cleanup, danger focus, no-DOM fallback). Build + lint clean; affected suites green.
(Note: build/lint don't parse JS — caught a multi-line-import split + a wrong import
path via module-import checks.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 16:49:42 -04:00
thePR0M3TH3ANandClaude Opus 4.8 29785783fe nostr: fix legacy-zombie resurrection via canonical getActiveKey
client.js carried a stale duplicate of getActiveKey that lacked the
LEGACY: guard added to nostr/utils.js. The tombstone machinery used the
local copy, so a deleted legacy video keyed its tombstone as
ROOT:LEGACY:... while a bare zombie keyed as <pubkey>:<dTag> — the guard
never matched and the video resurrected. Import the single canonical
getActiveKey and drop the duplicate.

Also convert tests/nostr-delete-flow.test.mjs to node:test (it was
silently excluded from the runner and failing), fix its stale write-relay
config, and rewrite its subscription assertions against the real
VideoEventBuffer. New client-level regression test for the guard.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-23 14:53:11 -04:00
thePR0M3TH3ANandClaude Opus 4.8 afb6200b40 fix(delete): publish tombstones to the write relays so deletes propagate
Root cause of "deleted/tombstoned videos still show in the UI": normal video
publishes fan out to the uncapped WRITE relay set, but both delete paths — the
soft-delete tombstone (revertVideo) and the NIP-09 deletion — published only to
this.relays, the CAPPED read set (<=8). A video published broadly was therefore
tombstoned on just a subset; every relay outside that subset kept serving the
original event, so the video resurrected on the next load (the tombstone's
created_at was already bumped strictly-newer, so replacement worked — it just
never reached the relays that mattered).

Add getDeletePublishRelays() (prefer write set, fall back to read set, then
bundled defaults) and use it for both delete publishes, mirroring publishVideo.

Also removed a stray `const a = 1` debug leftover at end of client.js.

Tests: getDeletePublishRelays prefers the write set / falls back to read; and a
call-site test that revertVideo publishes the tombstone to the full 12-relay
write set rather than the 2-relay capped read subset. Mutation-verified —
restoring `this.relays` at the call sites fails the propagation test.

Build + full lint green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 12:38:51 -04:00
thePR0M3TH3ANandClaude Opus 4.8 3f4299a885 feat(publish): tell the user how many relays accepted their video
Publishing showed a flat "Video shared successfully!" even when most (or all)
relays rejected the event, so users couldn't tell a partial/failed publish from
a clean one. Now publishVideo captures the per-relay accept/reject summary and
stashes a compact {accepted,total} tally on the returned event via a
non-enumerable key (never serialized, never sent to relays). app.js reads it
back and reports the real outcome:
  - all relays accepted  -> "Video published to N relays!"
  - some rejected        -> warning "Video published to N of M relays …"
  - none accepted        -> error (defensive; this path normally throws first)
Falls back to the generic message when no tally is available.

Tests (describePublishOutcome + attach/readRelayPublishSummary): success/partial/
zero/singular/missing-tally messaging, and that the tally round-trips while
staying out of JSON serialization. Mutation-verified — flipping partial→success
and the summary key to enumerable both fail the suite.

Build + full lint green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 12:19:09 -04:00
thePR0M3TH3ANandClaude Opus 4.8 de515462e6 fix(feed): always reserve live default relays in the feed set
The video feed's getDefaultRelays only fell back to bitvid's default relays
when the healthy set was COMPLETELY empty. A user whose own relays are all dead
but not yet probed (so they still pass the liveness filter) would have the feed
subscribe to that fully-broken list and hang forever on "Fetching…" — the
default aggregators were never injected.

Route the healthy set through capReadRelays (via new buildFeedRelaySet), which
always reserves a couple of reliable aggregator slots. Now the feed can reach
live relays regardless of how broken the user's personal list is, while still
prioritizing the user's own relays and bounding REQ fan-out to MAX_SUBSCRIBE_RELAYS.

Tests (buildFeedRelaySet): an all-dead user list still yields a default
aggregator while keeping the user's relays; empty input falls back to defaults;
an empty healthy set uses the configured fallback yet still reserves defaults; a
huge list stays bounded. Mutation-verified — reverting to the old
healthy-or-defaults logic fails 3 of the 4 scenarios.

Build + full lint green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-20 12:14:03 -04:00
thePR0M3TH3ANandClaude Opus 4.8 1b11cb1bc1 [nostr] make video edit/delete reliably replace prior versions
Two replaceable-event (NIP-33/kind 30078) lifecycle fixes found auditing
edit/delete/tombstone:

1. created_at monotonicity. Edits and delete tombstones used bare
   Date.now(), with no guarantee of being newer than the version they replace.
   NIP-01 keeps the highest created_at (ties -> lowest id), so a device clock
   that's behind — or a second edit within the same second — left relays
   serving the OLD version: edits silently didn't apply, and (worse) deletions
   didn't actually remove the video for other clients (the local in-memory
   tombstone masked the failure locally). The edit and revert/delete builders
   now use max(now, baseEvent.created_at + 1). created_at is threaded through
   revertVideo and _softDeleteVersions so the bulk-delete path keeps it too.

2. Legacy tombstone key mismatch. The deletion builder synthesizes
   videoRootId = "LEGACY:<pubkey>:<dTag>" for legacy videos, so its tombstone
   keyed as "ROOT:LEGACY:..." while the original legacy event (no videoRootId)
   keyed as "<pubkey>:<dTag>" — the tombstone never matched the zombie it was
   meant to suppress. getActiveKey now treats a "LEGACY:"-prefixed root as
   non-distinct and falls through to the pubkey:dTag identity, so both match.

Verified sound and unchanged: d-tag + videoRootId preservation on edit,
ownership/version checks, dual delete (NIP-09 kind-5 + per-d-tag soft-delete),
and the tombstone guard wired across all ingestion paths.

Tests: edit/tombstone created_at is bumped above a future-dated base
(clock-skew) and uses wall-clock for a past base; a legacy deletion shares the
original event's key. Both fixes mutation-verified.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 20:32:50 -04:00
thePR0M3TH3ANandClaude Opus 4.8 4a38e27150 [auth] fix nsec login: enforce access control + working remember/unlock
Audit of the nsec (direct private key) login found the client's
registerPrivateKeySigner silently ignored the access-control validator AND
the persist/passphrase options, so:

- Access-control bypass (security): nsec logins skipped the blacklist/whitelist
  gate that NIP-07 and NIP-46 logins enforce. registerPrivateKeySigner now runs
  the validator BEFORE activating the signer and forgets a since-blocked stored
  key on denial.
- "Remember this key" never worked: it persisted nothing (the payload used a
  shape persistSessionActor rejects), and there was no way to detect or unlock a
  saved key — getStoredSessionActorMetadata and unlockStoredSessionActor were
  referenced by the UI/provider but defined nowhere. Implemented both on the
  client: persist now writes the canonical { privateKeyEncrypted, encryption }
  shape (PBKDF2 250k + AES-GCM, already in place), the login modal can detect a
  saved key, and unlock decrypts with the passphrase and re-activates via the
  same proven createNsecAdapter path. The nsec provider now passes the
  access-control validator to the unlock path too.
- Fixed misleading copy: the provider claimed BIP-39 seed support, but only
  hex/nsec are accepted.

Untouched-but-verified: decode/validate (hex64 + nsec1), logout clears the
in-memory key, no key logging.

Tests (tests/nsec-login.test.mjs): a denied validator rejects login and
activates no signer; persist -> unlock round-trips the same pubkey (wrong
passphrase fails); unlock re-checks access control and forgets a blocked key.
The validator-enforcement and persist-shape fixes are both mutation-verified.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 15:40:16 -04:00
thePR0M3TH3ANandClaude Opus 4.8 362016ead6 [watch-history] thread chunkDecryptLimit through resolve + expose deferred count
Service-layer plumbing for bounded watch-history loading (slice B part 2).
manager.resolve() now forwards chunkDecryptLimit to fetch() and records the
resulting deferred-chunk count in a per-actor side-channel, so resolve() keeps
returning a bare items array (its sole caller depends on that) while the
"more older chunks available" signal is still retrievable. Exposed via
getWatchHistoryDeferredChunkCount() on the manager and the NostrClient facade.
clear() resets the side-channel.

Still inert by default (no caller passes a finite limit yet); this is the
substrate for activating fast-first-render. Tested: resolve under
chunkDecryptLimit:1 returns only the newest chunk's items and reports
deferredChunkCount=1, reset to 0 after clear().

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-17 14:49:40 -04:00
thePR0M3TH3ANandClaude Opus 4.8 92d8fabfe5 fix(nip46): implement prepareRemoteSignerHandshake (generate-connect-link login)
NIP-46 login via a generated connect link failed with
"nostrClient.prepareRemoteSignerHandshake is not a function". The auth provider
(authProviders/nip46.js) calls nostrClient.prepareRemoteSignerHandshake() to mint
an ephemeral client keypair + nostrconnect:// URI before connecting, but that
method existed only on the unused Nip46Connector class — neither SignerManager
nor NostrClient exposed it.

Add SignerManager.prepareRemoteSignerHandshake (generates the keypair via
nostr-tools, resolves relays, builds the nostrconnect:// URI with relay/secret/
perms/metadata params) and proxy it on NostrClient next to connectRemoteSigner.

Test: tests/nostr/nip46-prepare-handshake.test.mjs. Full suite green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-17 06:35:11 -04:00
thePR0M3TH3ANandClaude Opus 4.8 7951fd0f09 fix(dm): can't open conversations + "helpers unavailable" on a working signer
After switching to a responsive signer (nos2x), two DM-specific bugs surfaced.

1. Clicking a conversation in the list threw
   "this.controller.handleDmConversationSelect is not a function"
   (ProfileDirectMessageRenderer onSelectConversation). The method exists on
   ProfileDirectMessageActions and the controller delegates ~15 sibling Actions
   methods — but the handleDmConversationSelect delegation was missing. Add it.

2. Loading a conversation's messages threw "DM decryption helpers are
   unavailable" even though the signer decrypts blocks/subscriptions/hashtags
   fine. buildDmDecryptContext gated decryptor registration on (a) the dynamic
   `capabilities` getter (which probes window.nostr and false-negatives on lazy
   injection / nos2x's module shape) and (b) the permission-probe result. The
   list services already learned to register by METHOD EXISTENCE and validate at
   call time; align the DM path to do the same so a present, working signer
   never yields zero decryptors. The permission probe still runs.

Tests: tests/dm-decrypt-context.test.mjs (cheat-resistant — a signer whose
capabilities getter reports false and whose permission probe returns ok:false
must still register nip04+nip44 decryptors). Full unit + dm:unit + dm:integration
suites green. KNOWN_BUGS #0.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 14:52:11 -04:00
thePR0M3TH3ANandClaude Opus 4.8 55bd181867 fix(nostr): bound read/subscribe relay set to stop cold-login REQ storm
On cold login the app opened WebSockets to a user's full NIP-65 read set
(in prod often ~20 relays, most dead). The flood of failing connections
churned the main thread and starved the nip-07 extension's postMessage
round-trips, so encrypted lists (blocks, subscriptions, hashtags) were
fetched but timed out during decryption ("message channel closed",
"Decryption timed out").

Bound every read/subscribe path to MAX_SUBSCRIBE_RELAYS=8 via a shared,
test-aware capReadRelays helper, applied at the chokepoints:
- client.js applyRelayPreferences (this.relays / this.readRelays)
- relayBatchFetcher.js fetchListIncrementally (blocks, subs, hashtags)
Writes stay uncapped (publish reach preserved).

capReadRelays is user-relays-first (a user's own relays are where their
data authoritatively lives, and small fast-path limits would otherwise
crowd them out — the regression that reverted the first attempt) while
reserving RESERVED_DEFAULT_RELAY_SLOTS=2 for reliable default aggregators
so the "~20 personal relays, all dead" case still reaches a live relay.
Under test the reserved core resolves to the test-relay override, keeping
harnesses isolated.

Tests: new relay-subscribe-cap coverage (bounded + reserved-defaults +
writes-uncapped); spec-corrected user-blocks, subscriptions-manager and
nostr/client fetch tests to bounded (not exact) relay fan-out while
keeping all behavioral/security assertions (per-relay coverage, correct
decrypt, block hydration, dedup) intact. See TEST_INTEGRITY.md and
KNOWN_BUGS #0/#4. Full unit + dm:unit suites green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 22:51:36 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ee9c2700e7 refactor(watch-history): route relay fetches through SubscriptionManager (P4)
watchHistory.fetch now reads via the L1 SubscriptionManager (deduped +
health-gated) instead of pool.list directly: a new getSubscriptionManager dep
is threaded from the client, and both fetches (index + chunks) go through a
listEvents() helper. Raw pool.list is retained only as a fallback for
environments without a manager (tests/mocks), so the file stays grandfathered.

Verified: watch-history unit test passes; wh-repro cold resolve returns 10
items with 2 decrypts (routing intact end-to-end).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 16:01:29 -04:00
thePR0M3TH3ANandClaude Opus 4.8 2509c4d950 refactor(feed): route the video feed through the SubscriptionManager (P1 pilot)
subscribeVideos now goes through the L1 SubscriptionManager instead of calling
pool.sub directly: health-gated relay resolution (healthy-or-default fallback
preserved), centralized dedup, and registration for reconnect re-issue. The
buffer + .unsub() contract are unchanged. The client's manager uses its own
RelaySubscriptionService instance (isolated from the global singleton).

Verified: subscribeVideos relay-fallback + client + nostr-service suites pass;
ux-harness renders the grid (first card ~1.1s); steady-state REQ/ext idle.
(Pre-existing nostr-delete-flow failure confirmed unrelated — fails at the
pre-session baseline too.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 10:19:36 -04:00
thePR0M3TH3ANandClaude Opus 4.8 70aab38470 perf(relay): cap the subscribe/read relay set (interim RelayHealth)
A user's NIP-65 list can carry ~20 relays, most dead. Subscribing on all of
them is the dominant REQ-storm amplifier: every subsystem fans every query
across all relays and re-fires on each flaky-relay reconnect. Cap the
read/subscribe set to a bounded core (6), prioritizing the bundled known-good
defaults; WRITES still fan out to the full set so publish reach is unaffected.
Interim until RelayHealth ranks by liveness (docs/architecture-refactor.md P1).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 10:03:00 -04:00
thePR0M3TH3ANandClaude Opus 4.8 a3b205bfa1 fix(login-perf): kill relay REQ storms and feedback loops
Real-environment console logs revealed nip-07 login pegging the CPU and
starving encrypted-list decryption via relay REQ storms (~1000 REQ/s):

- moderationService.rebuildTrustedContacts emitted "contacts"
  unconditionally, forming an emit->fetch->emit loop (~1600 kind-3 REQ/s).
  Now emits only on actual change. Regression test added.
- Per-video live report (kind 1984) and view-count (kind 30079)
  subscriptions fan out O(videos x relays) and re-fire on every flaky-relay
  reconnect. Circuit-broken behind flags until batched (see refactor plan).
- onVideosShouldRefresh now coalesces forced reloads (2s leading-edge
  throttle) so list-change signals can't drive a reload loop.
- nip-07 queue reserves one foreground slot; DM decryption demoted to LOW
  so background DM backfill can't starve feed-driving lists. Test added.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-14 09:52:33 -04:00
thePR0M3TH3ANandClaude Opus 4.8 39aa81fb23 perf(dm): coalesce concurrent decrypts of the same DM event
Multiple subscribers (history list + notifications) decrypt the same DM event
at once; before the in-memory cache fills, each hit the serialized nip-07
extension, multiplying round-trips per message. Cache the in-flight decrypt
promise by event id so concurrent callers share one decrypt.

Harness: cold-load extension decrypt calls drop from ~50-60 to exactly 20 for 20
DMs (1 per message); warm reload stays 0 via the persisted cache. Combined with
the earlier ordering/sequential and persist-cache fixes, legacy DM decryption
went from ~241 extension calls (20 DMs) to 20 cold / 0 warm.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 19:56:36 -04:00
thePR0M3TH3ANandClaude Opus 4.8 8171fcb347 perf(dm): persist decrypted DM results so reloads skip the extension
Consolidates the persisted-plaintext-decrypt-cache pattern (already used by watch
history) into a shared factory, persistedPlaintextCache.js, and adds a DM cache
on top of it. watchHistoryDecryptCache now delegates to the factory (same public
API; tests unchanged).

decryptDirectMessageEvent now also consults/writes a persisted cache keyed by the
immutable DM event id (JSON of the decrypt result; flushed once per
listDirectMessages batch; cleared on logout alongside the in-memory cache). DM
events never change, so a reload re-decrypts only messages it has not seen and
skips the (serialized, slow) nip-07 extension for the rest.

Harness (dm-repro, now cold/warm): 20 DMs -> COLD nip04.decrypt 50, WARM 0 —
reloads cost zero extension decrypt calls. Also excludes the actor's own key from
NIP-04 remote candidates (decrypting against self is always wasted). DM unit +
integration and watch-history cache tests green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 19:55:07 -04:00
thePR0M3TH3ANandClaude Opus 4.8 874897b933 perf(nostr): verify event signatures off the main thread
Profiling the live app showed the single biggest CPU cost was the relay pool
verifying every incoming event's schnorr signature synchronously on the main
thread: ~1.2s of secp256k1 work ([email protected] 855ms) during feed load and tab
switches, dropping to ~16ms when skipped. The pool was constructed as
`new SimplePool()` with no options, so nostr-tools used its default verifying
verifyEvent; the `verifyEvent: () => true` that looked like it disabled this
only existed on a mock tools object and was never wired to the real pool.

Move verification off the main thread instead of dropping it:
- Construct the pool with `verifyEvent: () => true` so it never blocks.
- Add signatureVerifyWorker (+ client) that verifies event batches in a Web
  Worker and returns the valid ids; falls back to main-thread verification only
  when Workers are unavailable (never silently trusts).
- VideoEventBuffer verifies each batch via the worker before committing, and
  drops events that fail (fails closed). The verifier is injectable so tests can
  virtualize the worker; subscribeVideos passes it through and returns the flush
  promise from EOSE so commits are awaitable.

Tests: new signatureVerifyWorkerClient.test.mjs (worker protocol + drop-invalid
+ empty/no-id handling); videoEventBuffer.test.mjs and client.test.mjs adapted
to the async, verified commit path with a passthrough verifier.

Note: tests/nostr-delete-flow.test.mjs has a PRE-EXISTING failure on
`should not trigger subscription callbacks` (fails on clean unstable, and is a
standalone script not run by the unit-test runner); its edits here only adapt it
to the now-async flush.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 11:16:25 -04:00
thePR0M3TH3ANandClaude Opus 4.8 818fe16254 fix(feed): fall back to default relays in subscribeVideos on cold start
subscribeVideos called pool.sub(getHealthyRelays(this.relays)) with no
empty-relay fallback, unlike the fetch paths in the same file. On a cold first
load the 5s relay connect probe can time out, marking every relay unreachable;
init() only warns and schedules a background reconnect (it does not throw), so
boot proceeds and the feed subscribes to ZERO relays. The background reconnect
later succeeds but nothing re-issues the subscription, so the feed stays empty
until the user refreshes (when the SWR cache renders) — the reported
"notes don't load on first visit / need a refresh" symptom.

Fall back to DEFAULT_RELAY_URLS when no relays are healthy, mirroring the
established pattern. Because the fallback bypasses the unreachable cooldown, the
subscription retries those relays directly and self-heals instead of staying
dead until refresh.

Adds tests/nostr/subscribeVideosRelayFallback.test.mjs: empty healthy set =>
default relays (never zero), populated healthy set => uses those relays.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 09:41:17 -04:00
thePR0M3TH3AN b7eac53185 update 2026-02-21 19:30:02 -05:00
google-labs-jules[bot]andPR0M3TH3AN 47ac8e81b5 perf: parallelize revertVideo in _softDeleteVersions
Refactors `_softDeleteVersions` to execute `revertVideo` calls in parallel using `Promise.all`. This allows network publishing operations (which are the bottleneck) to overlap, while `SignRequestQueue` ensures signing remains sequential and safe.

Measured improvement for 20 versions: ~2200ms -> ~300ms.

Verification:
- `tests/nostr-delete-flow.test.mjs` passed.
- `npm run lint` checks passed.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-17 04:48:37 +00:00
google-labs-jules[bot]andPR0M3TH3AN 066355e47e [constants] extract STANDARD_TIMEOUT_MS (10000) -> js/constants.js
Refactors duplicated `10000` ms numeric literals to use the canonical `STANDARD_TIMEOUT_MS` constant.

Affects:
- `js/testHarness.js`
- `js/nostr/relayBatchFetcher.js`
- `js/nostr/client.js`

Verification:
- `npm run lint` passed.
- `npm run test:unit` passed.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-16 04:55:18 +00:00
google-labs-jules[bot]andPR0M3TH3AN e6a73687dd fix: resolve NostrClient initialization race condition
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-14 13:22:43 +00:00
google-labs-jules[bot]andPR0M3TH3AN a997120f42 chore(ai): doc js/nostr/client.js and videoEventBuffer.js
- Add JSDoc to exported methods in `js/nostr/client.js`
- Add JSDoc to `js/nostr/videoEventBuffer.js`
- Add in-code comments for complex algorithms (history hydration, soft/hard delete, buffering)
- Update `docs/nostr-client-overview.md` with invariants and pitfalls
- No functional changes

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-12 15:28:54 +00:00
google-labs-jules[bot]andPR0M3TH3AN d3fa684e45 perf: bound relay background concurrency to 3
Replaces unbounded `Promise.all(relays.map(...))` calls with `pMap` using a concurrency limit of 3 for background operations like history hydration, metadata fetching, and DM sending.
This prevents network saturation and potential main-thread jank when many relays are configured.
Includes benchmark verification script `benchmarks/relay_concurrency_repro.mjs`.
Audited upload docs and confirmed accuracy.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-11 21:31:53 +00:00
thePR0M3TH3AN 244f5f80a8 Merge pull request #2367 from PR0M3TH3AN/claude/fix-sync-and-dm-issues-NMShC
Fix NIP-07 adapter support and improve signer resolution
2026-02-11 15:01:35 -05:00
Claude 53b8a2609c fix: recognize NIP-07 signer type for extension permission pre-granting
The NIP-07 adapter uses type "nip07" but all permission pre-granting
checks only matched type "extension", causing NIP-07 browser extension
users to fail decryption for block/mute lists, subscriptions, DMs, and
watch history. Hashtag preferences appeared to work because their read
path had a separate fix, masking the systemic issue.

Changes:
- shouldRequestExtensionPermissions() now matches both "extension" and
  "nip07" signer types (central fix affecting DM signals, comments,
  publish helpers, and watch history fetch)
- buildDmDecryptContext() pre-grants extension permissions for NIP-07
- userBlocks.js: remove capabilities-gated decryptor check that failed
  with lazy NIP-07 module injection (same fix already in subscriptions.js);
  always resolve signer regardless of allowPermissionPrompt; increase
  fast-path relay timeout from 6s to 10s
- watchHistoryService: recognize NIP-07 type and always resolve signer
- All publish paths (blocks, subscriptions, hashtags, relays, uploads,
  sharing, moderation) now pre-grant permissions for NIP-07 signers

https://claude.ai/code/session_01PipsdaTQmw7wBTJ1LmJjkk
2026-02-11 19:41:53 +00:00
google-labs-jules[bot]andPR0M3TH3AN b28e62963f perf: Batch video timestamp resolution to reduce N+1 network requests
- Implemented `hydrateVideoHistoryBatch` in `NostrClient` to fetch missing root events and sparse histories in a single `pool.list` request.
- Refactored `hydrateVideoHistory` to use shared helper methods `getLocalVideoHistory` and `_processAndCacheEvent`.
- Added `resolveVideoPostedAtBatch` to `PlaybackCoordinator` which delegates to the batched client method.
- Updated `VideoListView` to collect videos needing timestamp resolution and call the batch method, falling back to individual calls if necessary.
- Added regression test `tests/performance/resolvePostedAt.test.mjs` to verify reduced network calls.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-11 12:01:39 +00:00
google-labs-jules[bot]andPR0M3TH3AN 8027ec9b2b Refactor sendDirectMessage to use strategy pattern for NIP-04/17
Extracted NIP-17 and NIP-04 logic into separate private methods `_sendNip17DirectMessage` and `_sendNip04DirectMessage` to reduce complexity and improve readability of `sendDirectMessage`.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-11 10:46:17 +00:00
thePR0M3TH3AN 96f1938004 Merge pull request #2337 from PR0M3TH3AN/fix/security-remove-unsafe-logging-997674150393261615
🔒 Remove vulnerable unused logging function
2026-02-11 05:27:52 -05:00
google-labs-jules[bot]andPR0M3TH3AN 58fc143c66 Refactor editVideo into validation, prep, and publish steps
Split the long `editVideo` method in `js/nostr/client.js` into three
distinct parts:
1. `_validateEditPrerequisites`: Handles input validation and state checks.
2. `prepareVideoEditPayload`: Existing helper for payload construction.
3. `_signAndPublishEdit`: Handles signing and network operations.

This improves maintainability and readability by separating concerns.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-11 04:13:50 +00:00
google-labs-jules[bot]andPR0M3TH3AN 99fbe96f5c security: Remove unused vulnerable logging function in NostrClient
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-11 03:57:24 +00:00
thePR0M3TH3AN f1c7c5755d Merge pull request #2313 from PR0M3TH3AN/ai/doc-client-js-20250212-11570413463835939766
chore(ai): document js/nostr/client.js
2026-02-10 10:16:09 -05:00
google-labs-jules[bot]andPR0M3TH3AN 8df741f525 chore(ai): doc js/nostr/client.js (agent)
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 15:10:49 +00:00
google-labs-jules[bot]andPR0M3TH3AN 0b35e2639b Refactor subscribeVideos buffering logic to VideoEventBuffer class
Separated the complex event buffering and flushing logic from `NostrClient.subscribeVideos` into a dedicated `VideoEventBuffer` utility class. This improves readability and maintainability of the `client.js` file.

- Created `js/nostr/videoEventBuffer.js`
- Updated `js/nostr/client.js` to use `VideoEventBuffer`
- Added unit tests in `tests/nostr/videoEventBuffer.test.mjs`

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 14:37:02 +00:00
google-labs-jules[bot]andPR0M3TH3AN 4f193e28a4 fix(ci): prevent automated-pr-review from failing the workflow
The `automated-pr-review` script was throwing an error when lint or test failures were found, causing the GitHub Action job to fail.
However, the purpose of this script is to provide feedback via a PR comment, not to block the pipeline (other jobs handle blocking).
This change updates the script to log the failure but exit successfully (0), ensuring the review comment is always delivered.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 06:59:04 +00:00
google-labs-jules[bot]andPR0M3TH3AN 3ef17d1122 fix(ci): systematic fixes for E2E tests and CI environment
- `js/nostr/client.js`: Exposed `derivePrivateKeyFromSecret` to `NostrClient` (delegating to `SignerManager`) to support programmatic login in E2E tests.
- `tests/e2e/helpers/bitvidTestFixture.ts`: Refactored to use dynamic relay ports based on `workerIndex` to fix parallel execution collisions.
- `scripts/agent/simple-relay.mjs`: Updated `close()` to force terminate connected clients, preventing test teardown hangs.
- `scripts/check-sw-compat.mjs`: Added try-catch around git commands to gracefully handle shallow clones in CI environments.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 06:45:11 +00:00
google-labs-jules[bot]andPR0M3TH3AN c3972af1e3 Refactor editVideo to use signAndPublishEvent and NIP-94/71 handlers
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 04:20:09 +00:00
Claude 804b3323c4 Add code health lint guards, consolidate relay constants, document agent prompts
Three CI-enforced lint scripts to prevent common codebase growth problems:

- lint:file-size — enforces 1000-line max for new JS files, grandfathers
  existing oversized files and prevents them from growing further
- lint:innerhtml — tracks innerHTML usage per-file with a baseline count,
  blocks new innerHTML additions (guides agents toward safe DOM APIs)
- Relay constants consolidated from duplicate definitions in client.js and
  ConnectionManager.js into shared js/nostr/relayConstants.js
- AGENTS.md Section 13: documents all three rules with examples
- docs/scheduled-agent-prompts.md: copy-paste prompts for recurring
  file decomposition, innerHTML migration, and constant dedup tasks

https://claude.ai/code/session_01LzErpH9FNw4hAPsjoYb1v9
2026-02-10 03:25:18 +00:00
google-labs-jules[bot]andPR0M3TH3AN 4e27744287 Refactor publishVideo to use context object for NIP-94/71 handling
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 03:04:42 +00:00
google-labs-jules[bot]andPR0M3TH3AN 79407b8c8a Refactor editVideo to use payload builder
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 02:39:33 +00:00
google-labs-jules[bot]andPR0M3TH3AN 74a44ebb36 Refactor publishVideo: Extract NIP-94/71 logic to videoPublisher.js
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 02:16:58 +00:00
google-labs-jules[bot]andPR0M3TH3AN f529a414f5 Refactor publishVideo in NostrClient to extract helper methods
Extracted NIP-94 and NIP-71 publishing logic into private helper methods
`_handlePublishNip94` and `_handlePublishNip71`. This improves readability
and maintainability of the `publishVideo` method in `js/nostr/client.js`.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 01:59:10 +00:00
google-labs-jules[bot]andPR0M3TH3AN ac9eab9a10 Refactor deleteAllVersions in NostrClient and fix delete flow test
- Extract logic from `deleteAllVersions` into helper methods: `_hydrateHistoryForDelete`, `_getEventsToDelete`, `_softDeleteVersions`, and `_hardDeleteVersions`.
- Update `_hardDeleteVersions` to use `{ waitForAll: true }` when publishing Kind 5 events, ensuring relay failures are correctly reported.
- Fix `tests/nostr-delete-flow.test.mjs` by explicitly registering the active signer in the test environment.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 01:25:41 +00:00
google-labs-jules[bot]andPR0M3TH3AN 6a043c2946 Fix CI test failures and clean up NostrClient
- Fixed duplicate imports and initializations in `js/nostr/client.js`.
- Fixed `registerPrivateKeySigner` to populate `sessionActor`, ensuring fallback encryption works in tests (fixing `tests/nostr-send-direct-message.test.mjs`).
- Removed duplicate/unused methods (`resolveEventDTag`, `extensionPermissionCache` getter).
- Installed missing dev dependencies (`jsdom`, `fake-indexeddb`).

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-10 01:07:50 +00:00
thePR0M3TH3AN 1124707ab9 Merge branch 'unstable' into refactor-publishVideo-method-129817906368864748 2026-02-09 19:49:45 -05:00
google-labs-jules[bot]andPR0M3TH3AN ab8c46d54d Refactor publishVideo logic and fix signer tests
- Extracted prepareVideoPublishPayload and prepareVideoMirrorOptions to js/nostr/videoPayloadBuilder.js
- Created js/utils/cryptoUtils.js and js/utils/mime.js
- Updated js/nostr/client.js to use new builder and helpers
- Fixed SignerManager permission hydration and session actor logic
- Fixed nostrClientRegistry update logic
- Fixed unit tests: nostr-private-key-signer, integration-remote-flow, dmSignalEvents

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-09 21:32:29 +00:00