Files
bitvid/tests/nsec-login.test.mjs
T
thePR0M3TH3ANandClaude Fable 5 99d1b1dd36 tests: spec-correct four stale tests hidden by piped exit codes
A per-file sweep (local suite runs were reading the PIPE's exit code, not
npm's) surfaced four tests failing on HEAD:

- hash-change-handler: logged-out default is deliberately TRENDING since
  04621efc; test still expected most-recent-videos
- session-actor / sessionActor / nsec-login: encoded the pre-fix "no-arg
  clearStoredSessionActor wipes EVERY account" behavior that 2026-07-02
  deliberately removed (one account's logout must not destroy other saved
  accounts' keys). Replaced with stronger assertions: multi-account survival
  on no-arg clear, targeted forget, and the blocked-key security property
  asserted on the blocked account specifically.

Integrity notes in each file; TODO #11b addendum documents the pipe lesson.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 21:01:45 -04:00

166 lines
5.4 KiB
JavaScript

// Audit follow-ups for nsec (direct private key) login:
// - access control must be enforced before activating the signer;
// - "remember this key" must persist an encrypted key that can be unlocked;
// - unlocking must re-check access control and forget a blocked key.
import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import { describe, it, before, after } from "node:test";
import { ensureNostrTools } from "../js/nostr/toolkit.js";
let nostrClient;
let getActiveSigner;
let clearActiveSigner;
let clearStoredSessionActor;
let listStoredSessionActorPubkeys;
let readStoredSessionActorEntry;
let tools;
let bytesToHex;
// No-arg clearStoredSessionActor() deliberately clears only the legacy
// "last saved" slot (per-account keys must survive another account's logout —
// fixed 2026-07-02). Tests that need a pristine store wipe every account.
function wipeStoredActors() {
for (const pk of listStoredSessionActorPubkeys?.() || []) {
clearStoredSessionActor(pk);
}
clearStoredSessionActor();
}
before(async () => {
tools = await ensureNostrTools();
const canonical = { ...tools };
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = canonical;
globalThis.NostrTools = canonical;
globalThis.nostrToolsReady = Promise.resolve({ ok: true, value: canonical });
({ nostrClient } = await import("../js/nostrClientFacade.js"));
({ getActiveSigner, clearActiveSigner } = await import("../js/nostr/client.js"));
({
clearStoredSessionActor,
listStoredSessionActorPubkeys,
readStoredSessionActorEntry,
} = await import("../js/nostr/sessionActor.js"));
bytesToHex = (bytes) => {
if (tools.bytesToHex) return tools.bytesToHex(bytes);
if (tools.utils?.bytesToHex) return tools.utils.bytesToHex(bytes);
return Buffer.from(bytes).toString("hex");
};
});
after(() => {
try {
wipeStoredActors();
} catch (error) {
// ignore
}
setTimeout(() => process.exit(0), 50);
});
function makeKey() {
const sk = tools.generateSecretKey();
return { hex: bytesToHex(sk), pubkey: tools.getPublicKey(sk) };
}
describe("nsec login", () => {
it("enforces the access-control validator before activating the signer", async () => {
clearActiveSigner();
wipeStoredActors();
const { hex } = makeKey();
let seenPubkey = null;
const validator = (pubkey) => {
seenPubkey = pubkey;
throw new Error("Your account has been blocked on this platform.");
};
await assert.rejects(
() => nostrClient.registerPrivateKeySigner({ privateKey: hex, validator }),
/blocked/i,
"a denied validator must reject the login",
);
assert.ok(seenPubkey, "validator was actually consulted");
assert.equal(
getActiveSigner(),
null,
"no signer may be activated when access is denied",
);
});
it("persists an encrypted key and unlocks it with the passphrase", async () => {
clearActiveSigner();
wipeStoredActors();
const { hex, pubkey } = makeKey();
const passphrase = "correct horse battery staple";
await nostrClient.registerPrivateKeySigner({
privateKey: hex,
pubkey,
persist: true,
passphrase,
});
const meta = nostrClient.getStoredSessionActorMetadata();
assert.ok(meta, "metadata should be available after persisting");
assert.equal(meta.hasEncryptedKey, true, "an encrypted key is stored");
assert.equal(meta.pubkey, pubkey, "metadata carries the pubkey");
clearActiveSigner();
assert.equal(getActiveSigner(), null, "signer cleared before unlock");
const result = await nostrClient.unlockStoredSessionActor(passphrase);
assert.equal(result.pubkey, pubkey, "unlock restores the same pubkey");
const signer = getActiveSigner();
assert.ok(signer, "a signer is active after unlocking");
assert.equal(signer.pubkey, pubkey, "the unlocked signer matches");
// Wrong passphrase must fail.
clearActiveSigner();
await assert.rejects(
() => nostrClient.unlockStoredSessionActor("the wrong passphrase"),
/unlock|passphrase|decrypt/i,
"a wrong passphrase must not unlock the key",
);
});
it("unlock re-checks access control and forgets a blocked key", async () => {
clearActiveSigner();
wipeStoredActors();
const { hex, pubkey } = makeKey();
const passphrase = "another secret passphrase";
await nostrClient.registerPrivateKeySigner({
privateKey: hex,
pubkey,
persist: true,
passphrase,
});
clearActiveSigner();
const validator = () => {
throw new Error("Your account has been blocked on this platform.");
};
await assert.rejects(
() => nostrClient.unlockStoredSessionActor(passphrase, { validator }),
/blocked/i,
"a blocked key must not unlock",
);
assert.equal(getActiveSigner(), null, "no signer activated for a blocked key");
// The security property, asserted on the blocked account SPECIFICALLY (a
// no-arg metadata read can legitimately surface a different saved account
// via the single-entry fallback — that is not a leak of THIS key).
assert.equal(
readStoredSessionActorEntry(pubkey),
null,
"the blocked account's encrypted key is forgotten (cleared from storage)",
);
assert.equal(
nostrClient.getStoredSessionActorMetadata(),
null,
"with no other saved accounts, no metadata survives the blocked-key cleanup",
);
});
});