feat: refine trust controls and harden BitLogin

This commit is contained in:
thePR0M3TH3AN committed 2026-07-25 22:25:50 -04:00
1 parent f25c2ac242
commit 18e08bf2cb
32 files changed
+1207 -740

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
import test from "node:test";
import assert from "node:assert/strict";
import { resolveVerifiedBitloginPubkey } from "../js/ui/bitloginModalIntegration.js";
const PUBKEY = "a".repeat(64);
test("BitLogin accepts a login identity only when the event and widget worker agree", async () => {
const widget = { getPublicKey: async () => PUBKEY.toUpperCase() };
assert.equal(await resolveVerifiedBitloginPubkey(widget, PUBKEY), PUBKEY);
});
test("BitLogin rejects a forged event pubkey even when its claimed account has access", async () => {
const widget = { getPublicKey: async () => "b".repeat(64) };
assert.equal(await resolveVerifiedBitloginPubkey(widget, PUBKEY), "");
});
test("BitLogin rejects malformed claims and widgets without a worker-backed public key", async () => {
assert.equal(await resolveVerifiedBitloginPubkey({}, PUBKEY), "");
assert.equal(await resolveVerifiedBitloginPubkey({ getPublicKey: async () => PUBKEY }, "npub1nope"), "");
});
+8
View File
@@ -163,6 +163,14 @@ function createController(options = {}) {
});
}
test('login modal keeps Nostr sign-in options visible beside BitLogin', () => {
const nostrSection = container.querySelector('[aria-labelledby="nostrLoginOptionsTitle"]');
assert.ok(nostrSection, 'Nostr login options should be visible without expanding a disclosure');
assert.ok(nostrSection.querySelector('[data-login-providers]'));
assert.equal(container.querySelector('details'), null);
assert.match(container.querySelector('#bitloginMount').textContent, /BitLogin/);
});
test('LoginModalController shows custom error for empty nsec input', async (t) => {
const providers = [{ id: 'nsec', label: 'Secret Key', login: async () => {} }];
const authService = { requestLogin: async () => {} };
+10 -4
View File
@@ -6,7 +6,7 @@ import {
DEFAULT_BLUR_THRESHOLD,
} from "../js/constants.js";
test("moderation stage enforces admin lists without whitelist bypass", async () => {
test("moderation stage keeps web-of-trust actions separate from creator access", async () => {
const whitelistedHex = "1".repeat(64);
const blockedHex = "2".repeat(64);
const blacklistedHex = "3".repeat(64);
@@ -496,17 +496,22 @@ test("moderation stage propagates whitelist, muters, and threshold updates", asy
assert.equal(updatedMuted.video.moderation.blurReason, undefined);
});
test("moderation stage blurs viewer-muted authors", async () => {
test("moderation stage preserves a viewer mute for a whitelisted creator", async () => {
const mutedHex = "f".repeat(64);
const service = {
async refreshViewerFromClient() {},
async setActiveEventIds() {},
getAdminListSnapshot() {
return { whitelist: new Set(), whitelistHex: new Set(), blacklist: new Set(), blacklistHex: new Set() };
return {
whitelist: new Set([`npub${mutedHex}`]),
whitelistHex: new Set([mutedHex]),
blacklist: new Set(),
blacklistHex: new Set(),
};
},
getAccessControlStatus(identifier) {
return { hex: identifier, whitelisted: false, blacklisted: false };
return { hex: identifier, whitelisted: identifier === mutedHex, blacklisted: false };
},
getTrustedReportSummary() {
return null;
@@ -551,6 +556,7 @@ test("moderation stage blurs viewer-muted authors", async () => {
assert.equal(entry.video.moderation.viewerMuted, true);
assert.equal(entry.metadata.moderation.viewerMuted, true);
assert.equal(entry.video.moderation.adminWhitelistBypass, false);
assert.equal(entry.video.moderation.blockAutoplay, true);
assert.equal(entry.video.moderation.blurThumbnail, true);
assert.equal(entry.video.moderation.blurReason, "viewer-mute");
+1 -2
View File
@@ -329,7 +329,7 @@ test("moderation stage bypasses hard hides on home feed", async () => {
assert.equal(hideWhy.hideBypass, "feed-policy");
});
test("moderation stage hides admin-whitelisted videos once thresholds fire", async () => {
test("moderation stage hides admin-whitelisted videos when web-of-trust thresholds fire", async () => {
const whitelistedHex = hex("f");
const videoId = hex("4");
@@ -400,4 +400,3 @@ test("moderation stage hides admin-whitelisted videos once thresholds fire", asy
assert.equal(hideWhy.adminWhitelist, true);
assert.equal(hideWhy.trustedReportCount, 6);
});
+3 -18
View File
@@ -224,22 +224,7 @@ test("bootstrap seeds track editor roster and ignore whitelist-only changes", as
"initial editor roster should seed trust"
);
const whitelistListener = Array.from(whitelistListeners)[0];
if (typeof whitelistListener === "function") {
whitelistListener(["npub1whitelistentrywhitelistentrywhitelistentry"]);
}
assert.equal(
setTrustedSeedsCalls.length,
2,
"whitelist change should retrigger trusted seed derivation"
);
const [, whitelistSeeds] = setTrustedSeedsCalls;
assert.deepEqual(
new Set(whitelistSeeds),
new Set(initialSeeds),
"whitelist updates should not introduce new trusted seeds"
);
assert.equal(whitelistListeners.size, 0, "creator whitelist changes must not affect trust seeds");
currentEditors = [
"npub1newmoderatorexamplemoderatorexamplemoderator",
@@ -251,10 +236,10 @@ test("bootstrap seeds track editor roster and ignore whitelist-only changes", as
assert.equal(
setTrustedSeedsCalls.length,
3,
2,
"editor change should recompute trusted seed roster"
);
const latestSeeds = setTrustedSeedsCalls[2];
const latestSeeds = setTrustedSeedsCalls[1];
assert.deepEqual(
new Set(latestSeeds),
new Set([ADMIN_SUPER_NPUB, ...currentEditors]),
@@ -167,7 +167,7 @@ test("blocking and unblocking reporters recomputes trusted summaries", async (t)
assert.deepEqual(setActiveCalls, [[eventId], [eventId]]);
});
test("trustedReportCount only counts eligible F1 reporters and admin whitelist", (t) => {
test("trustedReportCount only counts eligible F1 reporters, not creator whitelist entries", (t) => {
withMockedNostrTools(t);
const eventId = "c".repeat(64);
@@ -217,14 +217,14 @@ test("trustedReportCount only counts eligible F1 reporters and admin whitelist",
}
const summary = service.getTrustedReportSummary(eventId);
assert.equal(summary.totalTrusted, 2);
assert.equal(summary.totalTrusted, 1);
assert.equal(summary.types.nudity.total, 3);
assert.equal(summary.types.nudity.trusted, 2);
assert.equal(service.trustedReportCount(eventId, "nudity"), 2);
assert.equal(summary.types.nudity.trusted, 1);
assert.equal(service.trustedReportCount(eventId, "nudity"), 1);
const reporters = service.getTrustedReporters(eventId, "nudity");
assert.deepEqual(
reporters.map((entry) => entry.pubkey).sort(),
[adminWhitelisted, f1Reporter].sort(),
[f1Reporter],
);
});
+80
View File
@@ -0,0 +1,80 @@
import assert from "node:assert/strict";
import test from "node:test";
import { JSDOM } from "jsdom";
import {
SITE_UPDATE_DISMISSED_KEY,
SITE_UPDATE_LAST_SEEN_KEY,
initializeSiteUpdateNotice,
} from "../js/ui/siteUpdateNotice.js";
function createDocument(version = "deadbeef") {
return new JSDOM(
`<!doctype html><html><body><div data-site-version="${version}" data-site-version-date="2026-07-26"></div></body></html>`,
{ url: "https://bitvid.example" },
);
}
test("site update notice records a first visit without interrupting it", () => {
const dom = createDocument();
const result = initializeSiteUpdateNotice({
doc: dom.window.document,
storage: dom.window.localStorage,
});
assert.deepEqual(result, { shown: false, reason: "first-visit" });
assert.equal(dom.window.localStorage.getItem(SITE_UPDATE_LAST_SEEN_KEY), "deadbeef");
assert.equal(dom.window.document.querySelector(".site-update-notice"), null);
});
test("site update notice shows a dismissible build summary after an update", async () => {
const dom = createDocument("cafebabe");
dom.window.localStorage.setItem(SITE_UPDATE_LAST_SEEN_KEY, "deadbeef");
const result = initializeSiteUpdateNotice({
doc: dom.window.document,
storage: dom.window.localStorage,
fetchImpl: async () => ({
ok: true,
json: async () => [
{ sha: "1234567890abcdef", commit: { message: "Make Nostr login obvious\n\nMore detail" } },
],
}),
});
assert.equal(result.shown, true);
const button = dom.window.document.querySelector(".site-update-notice__button");
const panel = dom.window.document.querySelector(".site-update-notice__panel");
assert.ok(button);
assert.ok(panel.classList.contains("hidden"));
button.click();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(button.getAttribute("aria-expanded"), "true");
assert.equal(panel.classList.contains("hidden"), false);
assert.match(panel.textContent, /cafebabe/);
assert.match(panel.textContent, /12345678 — Make Nostr login obvious/);
dom.window.document.querySelector("[aria-label='Dismiss this update']").click();
assert.equal(dom.window.document.querySelector(".site-update-notice"), null);
assert.equal(dom.window.localStorage.getItem(SITE_UPDATE_LAST_SEEN_KEY), "cafebabe");
assert.equal(dom.window.localStorage.getItem(SITE_UPDATE_DISMISSED_KEY), "cafebabe");
});
test("site update notice keeps its curated summary when GitHub is unavailable", async () => {
const dom = createDocument("cafebabe");
dom.window.localStorage.setItem(SITE_UPDATE_LAST_SEEN_KEY, "deadbeef");
initializeSiteUpdateNotice({
doc: dom.window.document,
storage: dom.window.localStorage,
updateItems: ["Fallback update copy."],
fetchImpl: async () => ({ ok: false }),
});
dom.window.document.querySelector(".site-update-notice__button").click();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.match(
dom.window.document.querySelector(".site-update-notice__panel").textContent,
/Fallback update copy/,
);
});
+11
View File
@@ -1,6 +1,11 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
DEFAULT_BLUR_THRESHOLD,
DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
} from "../config/instance-config.js";
import { validateInstanceConfig } from "../config/validate-config.js";
// Common valid overrides to use as a base
@@ -10,6 +15,12 @@ const VALID_OVERRIDES = {
PLATFORM_LUD16_OVERRIDE: "[email protected]",
};
test("instance moderation defaults require multiple trusted reports", () => {
assert.equal(DEFAULT_BLUR_THRESHOLD, 3);
assert.equal(DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, 3);
assert.equal(DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, 5);
});
test("validateInstanceConfig succeeds with valid configuration", () => {
assert.doesNotThrow(() => validateInstanceConfig(VALID_OVERRIDES));
});
+26
View File
@@ -0,0 +1,26 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
const playerStyles = await readFile(
new URL("../css/tailwind.source.css", import.meta.url),
"utf8"
);
test("mobile player modal uses the dynamic viewport and keeps the comment action above browser chrome", () => {
assert.match(
playerStyles,
/#playerModal\s*\{[^}]*height:\s*100dvh;[^}]*min-height:\s*100dvh;[^}]*overflow:\s*hidden;/s,
"the player root should use the dynamic viewport instead of scrolling beneath browser chrome"
);
assert.match(
playerStyles,
/#playerModal\s*>\s*\.bv-modal__panel\s*\{[^}]*overflow-y:\s*auto;/s,
"the modal panel should remain the single scroll region used by sticky navigation"
);
assert.match(
playerStyles,
/#playerModal\s+\.comment-composer\s*\{[^}]*padding-bottom:\s*max\(var\(--space-lg\),\s*env\(safe-area-inset-bottom\)\);/s,
"the comment action row should clear the mobile safe area"
);
});