mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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]>
72 lines
2.6 KiB
JavaScript
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);
|
|
});
|