Files
bitvid/tests/nip46-silent-restore-access-control.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 ff5c641951 [auth] enforce access control on silent NIP-46 session restore
scheduleStoredRemoteSignerRestore reconnected a stored remote-signer session on
startup via useStoredRemoteSigner({ silent: true }) with NO validator, so a
pubkey that had since been blocked could silently reconnect — the same
access-control bypass class as the nsec fix. It now passes an access-control
validator (waits for the admin lists to load, then checks canAccess); since
useStoredRemoteSigner already clears the stored session when the validator
rejects, a blocked pubkey's session is forgotten on restore. The connect and
explicit "use stored" paths already enforced this; only the silent startup
restore was missing it.

Tests: the restore supplies a validator function; the validator allows
permitted pubkeys and rejects blocked ones. Mutation-verified (dropping the
validator fails the test).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 15:53:56 -04:00

84 lines
2.6 KiB
JavaScript

// The silent NIP-46 session restore on startup must enforce access control,
// so a since-blocked pubkey's stored remote-signer session can't reconnect
// unchecked.
import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import { test } from "node:test";
import { SignerManager } from "../js/nostr/managers/SignerManager.js";
import { accessControl } from "../js/accessControl.js";
const NIP46_SESSION_KEY = "bitvid:nip46:session:v1";
function seedStoredSession() {
localStorage.setItem(
NIP46_SESSION_KEY,
JSON.stringify({
version: 1,
remotePubkey: "a".repeat(64),
clientPrivateKey: "b".repeat(64),
relays: ["wss://relay.example"],
}),
);
}
test("scheduleStoredRemoteSignerRestore passes an access-control validator", async () => {
seedStoredSession();
const manager = new SignerManager({ relays: ["wss://relay.example"] });
let capturedOptions = null;
manager.useStoredRemoteSigner = async (options) => {
capturedOptions = options;
return { ok: true, pubkey: "a".repeat(64) };
};
await manager.scheduleStoredRemoteSignerRestore();
// schedule fires the restore without awaiting it; let the microtask run.
await new Promise((resolve) => setTimeout(resolve, 0));
assert.ok(capturedOptions, "the stored signer restore was attempted");
assert.equal(
capturedOptions.silent,
true,
"startup restore stays silent",
);
assert.equal(
typeof capturedOptions.validator,
"function",
"a validator must be supplied so access control is enforced on restore",
);
localStorage.removeItem(NIP46_SESSION_KEY);
});
test("the restore validator allows permitted pubkeys and rejects blocked ones", async () => {
const manager = new SignerManager({ relays: [] });
const validator = manager.buildAccessControlValidator();
const origWaitForReady = accessControl.waitForReady;
const origCanAccess = accessControl.canAccess;
const origIsBlacklisted = accessControl.isBlacklisted;
accessControl.waitForReady = async () => {};
try {
accessControl.canAccess = () => true;
accessControl.isBlacklisted = () => false;
await assert.doesNotReject(
() => validator("a".repeat(64)),
"an allowed pubkey passes the validator",
);
accessControl.canAccess = () => false;
accessControl.isBlacklisted = () => true;
await assert.rejects(
() => validator("c".repeat(64)),
/blocked/i,
"a blocked pubkey is rejected so the stored session is forgotten",
);
} finally {
accessControl.waitForReady = origWaitForReady;
accessControl.canAccess = origCanAccess;
accessControl.isBlacklisted = origIsBlacklisted;
}
});