mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
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]>
155 lines
6.0 KiB
JavaScript
155 lines
6.0 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert";
|
|
import {
|
|
encryptSessionPrivateKey,
|
|
decryptSessionPrivateKey,
|
|
persistSessionActor,
|
|
readStoredSessionActorEntry,
|
|
clearStoredSessionActor,
|
|
SESSION_ACTOR_STORAGE_KEY,
|
|
__testExports
|
|
} from "../../js/nostr/sessionActor.js";
|
|
|
|
const { generateRandomBytes, isSubtleCryptoAvailable } = __testExports;
|
|
|
|
// Ensure localStorage is mocked
|
|
import "../../tests/test-helpers/setup-localstorage.mjs";
|
|
|
|
test("js/nostr/sessionActor.js", async (t) => {
|
|
if (!isSubtleCryptoAvailable()) {
|
|
t.skip("WebCrypto not available in this environment");
|
|
return;
|
|
}
|
|
|
|
t.beforeEach(async () => {
|
|
localStorage.clear();
|
|
await clearStoredSessionActor();
|
|
});
|
|
|
|
await t.test("Encryption and Decryption Roundtrip", async () => {
|
|
const privateKey = "nsec1testkey..."; // In reality this would be hex or bech32, but for encryption it is just a string payload
|
|
const passphrase = "secure-password";
|
|
|
|
const encrypted = await encryptSessionPrivateKey(privateKey, passphrase);
|
|
|
|
assert.ok(encrypted.ciphertext);
|
|
assert.ok(encrypted.salt);
|
|
assert.ok(encrypted.iv);
|
|
assert.strictEqual(encrypted.algorithm, "AES-GCM");
|
|
|
|
const payload = {
|
|
privateKeyEncrypted: encrypted.ciphertext,
|
|
encryption: encrypted,
|
|
};
|
|
|
|
const decrypted = await decryptSessionPrivateKey(payload, passphrase);
|
|
assert.strictEqual(decrypted, privateKey);
|
|
});
|
|
|
|
await t.test("Decryption fails with wrong passphrase", async () => {
|
|
const privateKey = "secret";
|
|
const passphrase = "correct";
|
|
const wrongPassphrase = "wrong";
|
|
|
|
const encrypted = await encryptSessionPrivateKey(privateKey, passphrase);
|
|
const payload = {
|
|
privateKeyEncrypted: encrypted.ciphertext,
|
|
encryption: encrypted,
|
|
};
|
|
|
|
await assert.rejects(
|
|
async () => {
|
|
await decryptSessionPrivateKey(payload, wrongPassphrase);
|
|
},
|
|
(err) => err.code === "decrypt-failed" || err.message.includes("decrypt-failed")
|
|
);
|
|
});
|
|
|
|
await t.test("Persistence and Retrieval", async () => {
|
|
const pubkey = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
const privateKey = "my-secret-key";
|
|
const passphrase = "my-password";
|
|
|
|
const encrypted = await encryptSessionPrivateKey(privateKey, passphrase);
|
|
|
|
const actor = {
|
|
pubkey,
|
|
privateKeyEncrypted: encrypted.ciphertext,
|
|
encryption: encrypted,
|
|
createdAt: Date.now(),
|
|
};
|
|
|
|
persistSessionActor(actor);
|
|
|
|
// Verify localStorage
|
|
const storedRaw = localStorage.getItem(SESSION_ACTOR_STORAGE_KEY);
|
|
assert.ok(storedRaw);
|
|
|
|
const stored = readStoredSessionActorEntry();
|
|
assert.ok(stored);
|
|
assert.strictEqual(stored.pubkey, pubkey);
|
|
assert.strictEqual(stored.privateKeyEncrypted, encrypted.ciphertext);
|
|
|
|
// Verify we can decrypt what we retrieved
|
|
const decrypted = await decryptSessionPrivateKey(stored, passphrase);
|
|
assert.strictEqual(decrypted, privateKey);
|
|
});
|
|
|
|
// spec_correction: no-arg clearStoredSessionActor() used to wipe the WHOLE
|
|
// per-account map — the data-loss bug where logging out of one account
|
|
// silently destroyed every other saved account's remembered nsec key. The
|
|
// deliberate fix (2026-07-02, same class as the NIP-46 session-map wipe)
|
|
// made no-arg clear ONLY the legacy v1 "last saved" slot; forgetting a
|
|
// specific account is clearStoredSessionActor(pubkey). This test encoded the
|
|
// dangerous pre-fix behavior; it now asserts the fixed semantics, including
|
|
// the multi-account survival property the fix exists for.
|
|
//
|
|
// test_integrity_note:
|
|
// change_type: ["spec_correction"]
|
|
// scenarios:
|
|
// - id: SCN-session-actor-targeted-clear
|
|
// given: "two accounts with persisted encrypted keys"
|
|
// when: "no-arg clear runs (logout-ish) and then a targeted clear"
|
|
// then: "no-arg leaves BOTH accounts retrievable by pubkey; targeted clear removes exactly one"
|
|
// observable_outcomes:
|
|
// - "clearStoredSessionActor() never deletes other accounts' keys"
|
|
// - "clearStoredSessionActor(pubkey) removes that account's entry"
|
|
// determinism_controls:
|
|
// - "in-memory localStorage; fixed pubkeys"
|
|
// anti_cheat_rationale:
|
|
// prevents: ["snapshot rubber-stamping"]
|
|
// relaxation:
|
|
// did_relax_any_assertion: false
|
|
// if_true_explain_spec_basis: "replaced with the fixed (stronger, multi-account) semantics"
|
|
await t.test("Clear semantics: targeted forget, no-arg never nukes other accounts", async () => {
|
|
const pubkeyA = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
const pubkeyB = "0000000000000000000000000000000000000000000000000000000000000002";
|
|
const encryption = {
|
|
salt: "fake-salt",
|
|
iv: "fake-iv",
|
|
iterations: 1000,
|
|
hash: "SHA-256",
|
|
algorithm: "AES-GCM",
|
|
version: 1
|
|
};
|
|
persistSessionActor({ pubkey: pubkeyA, privateKeyEncrypted: "cipher-a", encryption, createdAt: Date.now() });
|
|
persistSessionActor({ pubkey: pubkeyB, privateKeyEncrypted: "cipher-b", encryption, createdAt: Date.now() });
|
|
|
|
// No-arg clear (the logout-path call) drops only the "last saved" default…
|
|
clearStoredSessionActor();
|
|
assert.strictEqual(localStorage.getItem(SESSION_ACTOR_STORAGE_KEY), null);
|
|
// …while BOTH accounts' keys survive and stay switchable.
|
|
assert.ok(readStoredSessionActorEntry(pubkeyA), "account A must survive a no-arg clear");
|
|
assert.ok(readStoredSessionActorEntry(pubkeyB), "account B must survive a no-arg clear");
|
|
|
|
// Targeted clear forgets exactly that account.
|
|
clearStoredSessionActor(pubkeyA);
|
|
assert.strictEqual(readStoredSessionActorEntry(pubkeyA), null, "targeted clear removes the account");
|
|
assert.ok(readStoredSessionActorEntry(pubkeyB), "the other account is untouched");
|
|
|
|
clearStoredSessionActor(pubkeyB);
|
|
assert.strictEqual(readStoredSessionActorEntry(pubkeyB), null);
|
|
assert.strictEqual(readStoredSessionActorEntry(), null, "nothing left after every account is forgotten");
|
|
});
|
|
});
|