mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
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]>
93 lines
3.6 KiB
JavaScript
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;
|
|
}
|
|
});
|