mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
feat: refine trust controls and harden BitLogin
This commit is contained in:
1 parent
f25c2ac242
commit
18e08bf2cb
32 files changed
+1207
-740
No files matched your search
@@ -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"), "");
|
||||
});
|
||||
@@ -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 () => {} };
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
});
|
||||
@@ -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/,
|
||||
);
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user