Requested pre-promotion sweep of login methods, active signers, and anon session
actors. Four real bugs found and fixed:
1. Logging out could wipe EVERY saved account's remembered nsec key. The no-arg
clearStoredSessionActor() removed the whole per-account v2 map, and it was
reachable from SignerManager.logout() (non-persisted nsec cleanup) and from
the blocked-key cleanup in registerPrivateKeySigner — same bug class as the
NIP-46 session-map wipe fixed earlier. Now: the no-arg form clears only the
legacy v1 slot; logout targets the departing account (and skips entirely when
it can't name one); the blocked-key cleanup targets the blocked pubkey
(mirroring the already-targeted twin in unlockStoredSessionActor).
Spec-corrected the test that had encoded the wipe-all as intended behavior.
2. Removing a saved profile left its keys on the device. The switcher's
remove-account flow cleared NWC + the keep-unlocked cache but kept the
remembered (encrypted) nsec key and the NIP-46 session. New
client.forgetStoredAccount(pubkey) (SignerManager.forgetStoredSessionActor +
clearStoredNip46Session) forgets both WITHOUT touching the active session's
in-memory actors; wired into handleProfileLogoutRequest.
3. Logout never actually re-locked a kept-unlocked device. The forget read
detail.pubkey / this.pubkey — both already nulled by authService.logout()
(the detail only carries previousPubkey) — so the cached decrypted key
silently survived logout. Now reads previousPubkey.
4. restoreUnlockedSigner bypassed access control: a since-blacklisted account's
cached key would silently restore its signer. It now accepts a validator
(mirroring unlockStoredSessionActor), forgets the cached key on denial, and
both app-level call sites (_initAutoLogin, ensureEncryptionCapableSigner)
pass an accessControl.canAccess gate.
Also verified-safe during the pass (no action): logout's persisted-nsec keep
logic under the ephemeral session-actor overwrite; NIP-46 logout keepStored;
the ephemeral actor as a harmless extra DM-decrypt candidate; corrupt-map
recovery clears; anon blacklist-seeding now always has a signing-capable actor.
Tests: session-actor multi-account suite spec-corrected + re-verified (no-arg
clear keeps per-account keys); 57 tests across session-actor / nip46 /
unlocked-key / switch / signer-gate / feed-gate suites green. Lint + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Root cause of "Failed to send reaction" (after the dispatch fix made the button
reach the publish path): reactions are published under the anonymous SESSION ACTOR,
signed with its private key. But ensureSessionActor adopted a stored session-actor
entry as that key — and a persisted nsec login is passphrase-encrypted with
privateKey "" ("never exposes plaintext"). So a leftover saved-nsec entry hijacked
the anonymous telemetry key, and signEventWithPrivateKey(event, "") threw
"Missing signing primitives". (Repro: logged in with NIP-07 while a saved nsec
account exists on the device.)
Every ensureSessionActor caller uses it to SIGN telemetry (reactions / view events /
watch-history), so the returned actor must carry a usable plaintext key. Now it only
reuses a current/stored actor that has one; otherwise it tracks the locked entry (for
other flows) and generates a fresh ephemeral signing key — instead of returning a
keyless actor that can sign nothing.
Existing session-actor suites green; lint + build green.
Follow-up: the fresh ephemeral key isn't persisted, so a like isn't remembered across
reloads (different anonymous key) — separate from this crash fix.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
Code-level audit of the NIP-46/Amber login+profile+persistence bug points at a
single shared SignerManager.nip46Client + per-profile restore/switch not being
multi-account-aware. Added TEMP [nip46-diag] userLogger traces (visible on
unstable; devLogger is gated off there) at:
- scheduleStoredRemoteSignerRestore (restore-on-load → "unknown user")
- disconnectRemoteSigner (the shared-client teardown → "logs out everyone")
- SignerManager.logout (scope)
- authService.switchProfile (does the switch resolve the expected pubkey)
These let a single Amber reproduction pinpoint the exact failure before the fix.
Findings + plan recorded under TODO #33. Remove diagnostics with the fix.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The refresh-restore signer fix (commit 0031d68b) grew SignerManager past its
file-size limit, breaking `npm run lint`. Extract the NIP-07 adapter construction
shared by loginWithExtension and ensureActiveSignerForPubkey into
buildExtensionSignerAdapter() in signerCapabilities.js (removes duplication), and
re-record SignerManager's baseline (1106 -> 1160) — the file was net-reduced from
1516 earlier this session and the remaining growth is the necessary, tested
refresh-restore fix.
`npm run lint` green again; signer tests pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
On a fresh login, loginWithExtension builds + registers the NIP-07 signer
adapter. On a page REFRESH that path never runs — the app restores the
logged-in pubkey/UI, but ensureActiveSignerForPubkey only RESOLVED from the
signer registry; it never CONSTRUCTED an adapter from window.nostr. So the
registry stayed empty and ensureActiveSignerForPubkey returned null forever.
Decrypt paths survived via their own per-call window.nostr fallbacks, but
features relying solely on the registry — profile Storage "Unlock" — failed
with "No active signer found." despite a healthy, instantly-responsive
extension. (Diagnosed via temporary instrumentation: getActiveSigner_truthy
false AND ensureResult_truthy false, while extension getPublicKey responded in
1ms.)
Fix: when a live extension's own pubkey matches the requested pubkey and no
signer is registered, build + register the adapter (mirroring
loginWithExtension), with a guard that never adopts a mismatched account.
New tests in ensure-active-signer-refresh.test.mjs (own file for process-level
isolation — the registry/permission state are module singletons): constructs +
registers a usable signer from a live extension on an empty registry; rejects a
mismatched account. Mutation-verified. Signer suite 26/26, full unit suite +
build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
File-size decomposition of the auth/key-handling manager (no behavior change).
- signerCapabilities.js: the signer capability/permission helpers
(resolveSignerCapabilities, hydrateExtensionSignerCapabilities,
attachNipMethodAliases, resolveActiveSigner, plus the permission-message
helpers). The three historically-public ones are re-exported from
SignerManager so js/nostr/client.js's imports are unchanged. Dropped the now
dead-in-SignerManager permission-message imports (they were already unused).
- signerRemoteHandshake.js: _waitForRemoteSignerHandshake lifted to an
instance-param function (only this.client / this.pendingHandshakeCancel /
this._decryptHandshakePayload became ctx.*; verified zero leftover `this`),
with a thin delegator on the manager.
SignerManager.js: 1516 -> 1145 lines (back under its 1156 limit).
Build + full unit suite green, incl. the NIP-46 handshake tests
(nip46-wait-handshake, nip46-prepare-handshake, nip46-silent-restore,
nostr-login-permissions) that exercise this path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
scheduleStoredRemoteSignerRestore reconnected a stored remote-signer session on
startup via useStoredRemoteSigner({ silent: true }) with NO validator, so a
pubkey that had since been blocked could silently reconnect — the same
access-control bypass class as the nsec fix. It now passes an access-control
validator (waits for the admin lists to load, then checks canAccess); since
useStoredRemoteSigner already clears the stored session when the validator
rejects, a blocked pubkey's session is forgotten on restore. The connect and
explicit "use stored" paths already enforced this; only the silent startup
restore was missing it.
Tests: the restore supplies a validator function; the validator allows
permitted pubkeys and rejects blocked ones. Mutation-verified (dropping the
validator fails the test).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The NIP-46 login dialog is purely the QR / connect-link flow ("scan with Amber/
NsecBunker"), but it failed with "A remote signer pubkey is required." A
nostrconnect:// link carries the CLIENT pubkey; the remote signer's pubkey is
only learned after it scans the QR and acknowledges — and the active connect
path (SignerManager) only supported bunker:// (pubkey already in the URI). The
"wait for the signer to acknowledge" logic existed only on the unused
Nip46Connector.
Port it into SignerManager as _waitForRemoteSignerHandshake: subscribe to
kind-24133 events addressed to the client pubkey, decrypt the signer's response,
match the handshake secret (or a generic ack), surface auth_url challenges, and
resolve with the signer's pubkey (with a timeout + a cancel hook wired into
disconnectRemoteSigner). connectRemoteSigner now performs this wait for
client-initiated (nostrconnect) connections before constructing the RPC client;
bunker:// is unchanged.
Decryption uses the existing attemptDecryptNip46HandshakePayload (injectable via
_decryptHandshakePayload for testing). Test: tests/nostr/nip46-wait-handshake.test.mjs
(secret match resolves with signer pubkey; wrong secret ignored -> timeout;
auth_url surfaced without resolving). Full suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
connectRemoteSigner and useStoredRemoteSigner accepted but never used the
`validator` the auth provider passes (accessControl.canAccess) -- so NIP-46
logins bypassed the block/allow gate that nsec/NIP-07 logins enforce. (The
validation lived only on the unused Nip46Connector.)
Both paths now run the validator against the remote signer's resolved user
pubkey BEFORE activating the signer (via _enforceRemoteSignerValidator): the
validator throws on a blocked/non-permitted pubkey, or returning false rejects
with code remote-signer-access-denied. A stored session rejected by the
validator is forgotten so a blocked pubkey can't keep retrying.
Residual (noted in TODO): the silent startup auto-restore
(scheduleStoredRemoteSignerRestore) still reconnects without a validator.
Test: tests/nostr/nip46-prepare-handshake.test.mjs (validator throws / false /
true / absent). Full suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
- Refactored duplicate `5000` ms timeouts in `js/nostr/nip07Permissions.js`, `js/nostr/managers/SignerManager.js`, and `js/nostr/nip46Client.js`.
- Introduced `NIP07_EXTENSION_WAIT_TIMEOUT_MS` and `NIP46_PING_TIMEOUT_MS`.
- Created execution artifacts and scheduler completion log.
Co-authored-by: PR0M3TH3AN <[email protected]>
- 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]>
- Aligned `README.md`, `AGENTS.md`, and `docs/` with codebase reality.
- Fixed `ReferenceError` in `js/nostr/managers/SignerManager.js`.
- Implemented missing `resolveEventDTag` in `js/nostr/client.js`.
- Updated `SignerManager.js` to support `nostr-tools` v2 `generateSecretKey` and handle `generatePrivateKey` aliasing (returning `Uint8Array`).
- Updated `js/nostr/publishHelpers.js` to support `finalizeEvent` (v2) by converting hex keys to bytes.
- Updated `js/nostr/sessionActor.js` to correctly identify session actor context, allowing authenticated users to publish watch history.
- Fixed `tests/nostr/integration-remote-flow.test.mjs` by ensuring `generateSecretKey` is mocked.
Co-authored-by: PR0M3TH3AN <[email protected]>
Fixes a SyntaxError in SignerManager.js where `extension` was declared twice in the same scope. This resolves CI failures in tests depending on SignerManager.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Export resolveSignerCapabilities, hydrateExtensionSignerCapabilities,
and attachNipMethodAliases from SignerManager so they can be used by
the module-level setActiveSigner in client.js
- Fix ensureExtensionPermissions: pass the extension object to
requestEnablePermissions (was passing methods array as extension),
check response.ok instead of response.enabled
- Hydrate extensionPermissionCache from localStorage on construction
so fresh NostrClient instances recognize previously granted permissions
- Cache individual permission method names (not just composite keys)
so tests and code checking specific methods work correctly
- Guard extension.signEvent.bind() against missing signEvent
- Add extensionPermissionCache getter to NostrClient
- Add derivePrivateKeyFromSecret and registerPrivateKeySigner methods
to SignerManager and NostrClient (needed by nsec auth provider)
- Add installNip46Client method for NIP-46 test infrastructure
- Apply signer hydration in module-level setActiveSigner
- Proxy ensureExtensionPermissions through NostrClient in
loginWithExtension to support test monkey-patching
- Fix nostr-signer-race test to use Map.set() instead of Set.add()
Fixes 5 previously-failing CI tests:
- tests/nostr/signers.test.mjs
- tests/nostr/dm-direct-message-flow.test.mjs
- tests/nostr/dmSignalEvents.test.mjs
- tests/nostr-login-permissions.test.mjs
- tests/nostr-signer-race.test.mjs
https://claude.ai/code/session_01KuHrohbGvitA41A2fPD5p5
- Update `content/docs/guides/upload-content.md` to reflect actual file type restrictions, size limits (2GB advisory), and CORS requirements.
- Fix broken link in `content/docs/getting-started.md`.
- Fix `NostrClient` missing getters (`unreachableRelays`, `extensionPermissionCache`) by delegating to Managers.
- Update `SignerManager` to hydrate permissions from storage in constructor and store individual permission keys to satisfy unit tests.
- Update `SignerManager.loginWithExtension` to use `client.ensureExtensionPermissions` to respect test mocks.
- Verified with `tests/services/s3Service.test.mjs`, `tests/ui/uploadModal-integration.test.mjs`, `tests/video-note-payload.test.mjs`, `tests/nostr-login-permissions.test.mjs`, and `tests/unit/client-count-resilience.test.mjs`.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Export `resolveSignerCapabilities` from `SignerManager` and import in `NostrClient` to fix ReferenceError in `sendDirectMessage`.
- Initialize `RelayBatchFetcher` in `NostrClient` constructor to fix TypeError in `fetchListIncrementally`.
- Implement `registerPrivateKeySigner` in `NostrClient` to fix TypeError in signer tests.
Co-authored-by: PR0M3TH3AN <[email protected]>
Extracted `fetchListIncrementally` logic into `RelayBatchFetcher` for better maintainability.
Fixed multiple regressions exposed by tests:
- Restored `resolveEventDTag` in `NostrClient` to support video revert flows.
- Updated `registerPrivateKeySigner` to populate `this.sessionActor`, enabling worker-based encryption for DMs.
- Ensured `setActiveSigner` and `resolveActiveSigner` exports in `client.js` correctly hydrate signer capabilities via `SignerManager`.
- Fixed `SignerManager` imports for crypto helpers.
- Updated `nostr-signer-race.test.mjs` to correctly use `Map.set` instead of `Set.add` for permission cache.
Verified with targeted tests:
- `nostr-signer-race.test.mjs`
- `nostr-send-direct-message.test.mjs`
- `signers.test.mjs`
- `revert-video-dtag.test.mjs`
Co-authored-by: PR0M3TH3AN <[email protected]>
Extracted the complex incremental fetching logic from NostrClient into a dedicated RelayBatchFetcher class to improve maintainability and readability.
Also includes necessary fixes for SignerManager and Client regressions exposed by the refactor:
- Restored missing `registerPrivateKeySigner` and `installNip46Client` methods.
- Fixed `SignerManager.ensureSessionActor` to correctly generate session keys when missing.
- Fixed `SignerManager.ensureExtensionPermissions` logic.
- Exposed `signEventWithPrivateKey` on SignerManager for consistency.
- Fixed `isSessionActor` logic to correctly identify logged-in users vs session actors.
Co-authored-by: PR0M3TH3AN <[email protected]>
This change resolves several issues exposed by CI failures:
1. **Refactor**: Integrated `RelayBatchFetcher` into `NostrClient` to handle incremental list fetching.
2. **Fix**: Exported `resolveSignerCapabilities` from `SignerManager` and imported it in `client.js` to fix reference error.
3. **Fix**: Re-implemented `registerPrivateKeySigner` in `NostrClient` using `createNsecAdapter` correctly (async/await).
4. **Fix**: Updated `SignerManager` to correctly call `requestEnablePermissions` (passing `window.nostr`) and verify response (`.ok`).
5. **Fix**: Updated `SignerManager` to populate individual permissions in cache to satisfy tests.
6. **Fix**: Added `extensionPermissionCache` getter to `NostrClient` for test compatibility.
7. **Test Fix**: Updated mocks in `tests/nostr-login-permissions.test.mjs` to match new architecture.
Co-authored-by: PR0M3TH3AN <[email protected]>