Files
bitvid/tests/ensure-active-signer-refresh.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 0031d68b67 fix(signer): register NIP-07 adapter on refresh restore (storage unlock)
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]>
2026-06-20 07:39:05 -04:00

93 lines
3.6 KiB
JavaScript

// Regression: on a page refresh the logged-in pubkey/UI is restored but no
// fresh loginWithExtension runs, so the signer registry starts empty.
// ensureActiveSignerForPubkey only RESOLVED from the registry — it never
// CONSTRUCTED a NIP-07 adapter from window.nostr — so it returned null forever.
// Decrypt paths limped along via per-call window.nostr fallbacks, but features
// that rely solely on the registry (e.g. profile Storage "Unlock") failed with
// "No active signer found." despite a healthy, responsive extension.
//
// Fix: when a live extension's own pubkey matches the requested pubkey and no
// signer is registered, build + register the adapter (mirroring
// loginWithExtension) so getActiveSigner()/sign/decrypt work app-wide.
//
// In its own file for process-level isolation: the signer registry and NIP-07
// permission/circuit-breaker state are module singletons, so sharing a file
// with other signer tests bleeds state.
import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import test from "node:test";
import {
NostrClient,
getActiveSigner,
clearActiveSigner,
} from "../js/nostr/client.js";
const PK = "a".repeat(64);
function makeExtension(getPubkey) {
return {
getPublicKey: async () => getPubkey,
signEvent: async () => ({ id: "ext-signed" }),
nip04: { encrypt: async () => "enc04", decrypt: async () => "dec04" },
nip44: { encrypt: async () => "enc44", decrypt: async () => "dec44" },
};
}
test("constructs + registers a usable signer from the live extension when the registry is empty", async () => {
const originalWindow = global.window;
global.window = { nostr: makeExtension(PK) };
try {
clearActiveSigner();
const client = new NostrClient();
client.pubkey = PK;
assert.equal(getActiveSigner(), null, "precondition: empty registry (post-refresh)");
const resolved = await client.ensureActiveSignerForPubkey(PK);
assert.ok(resolved, "must resolve a signer instead of returning null");
assert.equal(typeof resolved.signEvent, "function", "resolved signer can sign");
assert.equal(
typeof resolved.nip44Decrypt,
"function",
"resolved signer exposes a hydrated nip44 decrypt alias",
);
assert.equal(
typeof resolved.nip04Decrypt,
"function",
"resolved signer exposes a hydrated nip04 decrypt alias",
);
// The whole point: it is now the registered active signer app-wide.
const active = getActiveSigner();
assert.ok(active, "getActiveSigner() now returns the registered signer");
assert.equal(typeof active.nip44Decrypt, "function", "active signer can decrypt");
} finally {
if (originalWindow) global.window = originalWindow;
else delete global.window;
}
});
test("does NOT adopt a mismatched extension account", async () => {
const originalWindow = global.window;
// Distinct, never-registered pubkeys — the registry's registeredSigners map is
// a module singleton that clearActiveSigner() does not purge, so reusing the
// previous test's pubkey would resolve its leftover entry.
const WANT = "c".repeat(64);
const OTHER = "b".repeat(64);
global.window = { nostr: makeExtension(OTHER) }; // extension is a different account
try {
clearActiveSigner();
const client = new NostrClient();
client.pubkey = WANT;
const resolved = await client.ensureActiveSignerForPubkey(WANT);
assert.equal(resolved, null, "must not adopt a mismatched account");
assert.equal(getActiveSigner(), null, "registry stays empty on mismatch");
} finally {
if (originalWindow) global.window = originalWindow;
else delete global.window;
}
});