Files
bitvid/tests/upload-modal-nsec-unlock.test.mjs
thePR0M3TH3ANandClaude Opus 4.8 f00d2b5f49 [ui] upload modal: prompt for the nsec passphrase instead of dead-ending after reload
With a persisted ("remember this key") nsec login, uploading a video after a page
reload reported "storage is locked" and then "No signer available to unlock storage."
The reload restores the logged-in pubkey but NOT the in-memory signer (the key is
passphrase-encrypted), and UploadModal.handleUnlock dead-ended on the alert — it never
asked for the passphrase.

Fix (mirrors the storage pane's #36 behavior): handleUnlock now calls
promptStoredNsecUnlock(pubkey) when there's no usable signer. If the active account has
a locked saved nsec key (getStoredSessionActorMetadata), it shows an actionable message
and opens the login modal's existing unlock-saved-key (passphrase) flow via a new
onRequestUnlock hook (wired in initUploadModal -> app.loginModalController.openModal),
instead of the dead-end alert. After the user re-enters their passphrase the signer is
active and the upload proceeds.

Tests: tests/upload-modal-nsec-unlock.test.mjs (5 — detects locked nsec for the active
account + opens unlock; ignores missing/non-nsec/other-account; falls through with no
hook). Build + lint clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 15:24:29 -04:00

72 lines
2.6 KiB
JavaScript

// Bug: with a persisted nsec login, uploading after a page reload said "storage is
// locked" and then "No signer available to unlock storage." — the persisted nsec
// signer isn't in memory after reload (the key is passphrase-encrypted), and the
// upload modal dead-ended instead of prompting for the passphrase.
//
// promptStoredNsecUnlock() detects that exact case (a saved nsec key for the active
// account) and opens the re-unlock flow, returning true so handleUnlock stops before
// the dead-end alert. Tested via the prototype to avoid constructing the full modal.
import test from "node:test";
import { strict as assert } from "node:assert";
import { UploadModal } from "../js/ui/components/UploadModal.js";
const PUBKEY = "a".repeat(64);
const OTHER = "b".repeat(64);
const prompt = UploadModal.prototype.promptStoredNsecUnlock;
function makeCtx({ meta, withUnlock = true } = {}) {
const calls = { unlock: 0, errors: [] };
return {
ctx: {
authService: {
nostrClient: { getStoredSessionActorMetadata: () => meta },
},
onRequestUnlock: withUnlock
? () => {
calls.unlock += 1;
return true;
}
: null,
showError: (m) => calls.errors.push(m),
},
calls,
};
}
const lockedNsec = { hasEncryptedKey: true, pubkey: PUBKEY, source: "nsec" };
test("detects a locked saved nsec key for the active account and opens the unlock flow", () => {
const { ctx, calls } = makeCtx({ meta: lockedNsec });
const handled = prompt.call(ctx, PUBKEY);
assert.equal(handled, true, "returns true so handleUnlock stops before the dead-end");
assert.equal(calls.unlock, 1, "opened the re-unlock (passphrase) flow");
assert.equal(calls.errors.length, 1);
assert.match(calls.errors[0], /passphrase/i);
});
test("returns false when there is no saved session (real missing-signer case)", () => {
const { ctx } = makeCtx({ meta: null });
assert.equal(prompt.call(ctx, PUBKEY), false);
});
test("returns false for a non-nsec stored session", () => {
const { ctx } = makeCtx({
meta: { hasEncryptedKey: true, pubkey: PUBKEY, source: "extension" },
});
assert.equal(prompt.call(ctx, PUBKEY), false);
});
test("does not hijack when the saved key is for a DIFFERENT account", () => {
const { ctx, calls } = makeCtx({
meta: { hasEncryptedKey: true, pubkey: OTHER, source: "nsec" },
});
assert.equal(prompt.call(ctx, PUBKEY), false);
assert.equal(calls.unlock, 0);
});
test("returns false (falls through to the alert) when no unlock hook is wired", () => {
const { ctx } = makeCtx({ meta: lockedNsec, withUnlock: false });
assert.equal(prompt.call(ctx, PUBKEY), false);
});