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

+3 -2
View File
@@ -1135,13 +1135,14 @@ export function createModerationStage({
trustedMuteCategory,
);
const adminWhitelist = adminStatus?.whitelisted === true;
const adminWhitelistBypass = false;
const blockAutoplay =
trustedCount >= normalizedAutoplayThreshold || trustedMuted || viewerMuted;
const blurFromReports = trustedCount >= normalizedBlurThreshold;
let blurThumbnail = blurFromReports;
let blurReason = blurThumbnail ? "trusted-report" : "";
const adminWhitelist = adminStatus?.whitelisted === true;
const adminWhitelistBypass = false;
if (!item.metadata || typeof item.metadata !== "object") {
item.metadata = {};
+2
View File
@@ -54,6 +54,7 @@ import { userBlocks } from "./userBlocks.js";
import { relayManager } from "./relayManager.js";
import createApplication from "./bootstrap.js";
import SearchFilterModal from "./ui/components/SearchFilterModal.js";
import { initializeSiteUpdateNotice } from "./ui/siteUpdateNotice.js";
import { installTestHarness } from "./testHarness.js";
validateInstanceConfig();
@@ -1320,6 +1321,7 @@ async function bootstrapInterface() {
}
async function initializeInterface() {
initializeSiteUpdateNotice();
startApplication();
try {
+3 -3
View File
@@ -1910,7 +1910,7 @@ export class ModerationService {
*
* Algorithm:
* 1. Iterate over all reports for the event.
* 2. Check if the reporter is in the `trustedContacts` set (or Admin Whitelist).
* 2. Check if the reporter is in the `trustedContacts` set.
* 3. If trusted, increment the count for that report type (e.g., "nudity").
* 4. Update the summary state and emit change events if thresholds are crossed.
*
@@ -1966,7 +1966,7 @@ export class ModerationService {
continue;
}
const isTrustedReporter = status.whitelisted || this.trustedContacts.has(reporterHex);
const isTrustedReporter = this.trustedContacts.has(reporterHex);
for (const [type, detail] of typeMap.entries()) {
let stats = typeStats.get(type);
@@ -2092,7 +2092,7 @@ export class ModerationService {
continue;
}
const isTrustedReporter = status.whitelisted || this.trustedContacts.has(reporterHex);
const isTrustedReporter = this.trustedContacts.has(reporterHex);
if (!isTrustedReporter) {
continue;
}
+1 -5
View File
@@ -70,7 +70,7 @@ async function waitForAccessControl({ timeoutMs = TRUST_SEED_READY_TIMEOUT_MS }
// Cached admin lists (hydrated synchronously from localStorage at
// construction) are enough to seed trust — don't stall boot behind the relay
// refresh. If the refresh later changes editors/whitelist/blacklist, the
// refresh. If the refresh later changes editors/blacklist, the
// change listeners registered in bootstrapTrustedSeeds re-apply the seeds.
if (
typeof accessControl.isHydrated === "function" &&
@@ -189,10 +189,6 @@ export async function bootstrapTrustedSeeds() {
applySeeds();
};
if (accessControl && typeof accessControl.onWhitelistChange === "function") {
accessControl.onWhitelistChange(applyOnChange);
}
if (accessControl && typeof accessControl.onEditorsChange === "function") {
accessControl.onEditorsChange(applyOnChange);
}
+27 -1
View File
@@ -25,6 +25,7 @@ import { FEATURE_BITLOGIN } from "../constants.js";
// side effect, which retroactively upgrades the already-present <bitlogin-auth>
// tag in the modal's static HTML once it resolves.
const BITLOGIN_WIDGET_BUNDLE_URL = "../../vendor/bitlogin/bitlogin.js";
const PUBKEY_PATTERN = /^[0-9a-f]{64}$/i;
let widgetLoadPromise = null;
@@ -52,6 +53,29 @@ function closeLoginModal() {
// and each independently call requestLogin() for one real sign-in.
const wiredWidgets = new WeakSet();
/**
* A CustomEvent's detail is not an authentication boundary: any same-origin
* script can dispatch one. Re-read the key from the widget's crypto worker and
* require it to match before BitVid adopts the identity or its signer.
*/
export async function resolveVerifiedBitloginPubkey(widget, claimedPubkey) {
const claimed = typeof claimedPubkey === "string" ? claimedPubkey.trim() : "";
if (!PUBKEY_PATTERN.test(claimed) || typeof widget?.getPublicKey !== "function") {
return "";
}
try {
const actual = await widget.getPublicKey();
const normalizedActual = typeof actual === "string" ? actual.trim() : "";
if (!PUBKEY_PATTERN.test(normalizedActual) || normalizedActual.toLowerCase() !== claimed.toLowerCase()) {
return "";
}
return normalizedActual.toLowerCase();
} catch (error) {
devLogger.warn("[BitLogin] Unable to verify the widget signer:", error);
return "";
}
}
function attachWidget(app, widget) {
const mount = document.getElementById("bitloginMount");
if (!FEATURE_BITLOGIN) {
@@ -77,11 +101,13 @@ function attachWidget(app, widget) {
});
widget.addEventListener("bitlogin-login", async (event) => {
const pubkey =
const claimedPubkey =
event?.detail && typeof event.detail.publicKey === "string"
? event.detail.publicKey.trim()
: "";
const pubkey = await resolveVerifiedBitloginPubkey(widget, claimedPubkey);
if (!pubkey) {
userLogger.warn("[BitLogin] Ignored an unverifiable login event.");
return;
}
@@ -857,7 +857,7 @@ export class ProfileModerationController {
if (isBlocked(status.hex)) {
return false;
}
return Boolean(status.whitelisted || trustedContacts.has(status.hex));
return trustedContacts.has(status.hex);
};
if (service.trustedMuteLists instanceof Map) {
+179
View File
@@ -0,0 +1,179 @@
export const SITE_UPDATE_LAST_SEEN_KEY = "bitvid:site-update:last-seen";
export const SITE_UPDATE_DISMISSED_KEY = "bitvid:site-update:dismissed";
export const GITHUB_COMMITS_ENDPOINT =
"https://api.github.com/repos/PR0M3TH3AN/bitvid/commits?sha=unstable&per_page=5";
export const LATEST_UPDATE_ITEMS = Object.freeze([
"Clearer Nostr sign-in choices alongside BitLogin.",
"Creator approval is now separate from web-of-trust moderation.",
"Trusted-report thresholds now require broader agreement before action.",
]);
function readStorage(storage, key) {
try {
return storage?.getItem?.(key) || "";
} catch {
return "";
}
}
function writeStorage(storage, key, value) {
try {
storage?.setItem?.(key, value);
} catch {
// Privacy mode or quota errors should never block the application.
}
}
function createElement(doc, tagName, className, text = "") {
const element = doc.createElement(tagName);
if (className) {
element.className = className;
}
if (text) {
element.textContent = text;
}
return element;
}
function renderUpdateItems(doc, list, items) {
list.replaceChildren();
for (const item of Array.isArray(items) ? items : []) {
if (typeof item === "string" && item.trim()) {
list.appendChild(createElement(doc, "li", "", item.trim()));
}
}
}
function normalizeCommitItems(payload) {
if (!Array.isArray(payload)) {
return [];
}
return payload
.map((entry) => {
const sha = typeof entry?.sha === "string" ? entry.sha.slice(0, 8) : "";
const message =
typeof entry?.commit?.message === "string"
? entry.commit.message.split("\n")[0].trim()
: "";
return sha && message ? `${sha} — ${message}` : "";
})
.filter(Boolean);
}
async function loadGitHubCommitItems({ doc, list, fetchImpl, fallbackItems }) {
if (typeof fetchImpl !== "function") {
return;
}
try {
const response = await fetchImpl(GITHUB_COMMITS_ENDPOINT, {
headers: { Accept: "application/vnd.github+json" },
});
if (!response?.ok) {
throw new Error("github-commits-unavailable");
}
const commits = normalizeCommitItems(await response.json());
if (!commits.length) {
throw new Error("github-commits-empty");
}
renderUpdateItems(doc, list, commits);
} catch {
renderUpdateItems(doc, list, fallbackItems);
}
}
export function initializeSiteUpdateNotice({
doc = typeof document === "undefined" ? null : document,
storage = typeof window === "undefined" ? null : window.localStorage,
updateItems = LATEST_UPDATE_ITEMS,
fetchImpl = typeof fetch === "function" ? fetch : null,
} = {}) {
if (!doc?.body) {
return { shown: false, reason: "document-unavailable" };
}
const versionMarker = doc.querySelector("[data-site-version]");
const version = versionMarker?.dataset?.siteVersion?.trim() || "";
const date = versionMarker?.dataset?.siteVersionDate?.trim() || "";
if (!version) {
return { shown: false, reason: "version-unavailable" };
}
const lastSeen = readStorage(storage, SITE_UPDATE_LAST_SEEN_KEY);
const dismissed = readStorage(storage, SITE_UPDATE_DISMISSED_KEY);
if (!lastSeen) {
writeStorage(storage, SITE_UPDATE_LAST_SEEN_KEY, version);
return { shown: false, reason: "first-visit" };
}
if (lastSeen === version || dismissed === version) {
return { shown: false, reason: "already-seen" };
}
const root = createElement(doc, "aside", "site-update-notice");
root.setAttribute("aria-label", "Latest site update");
const panel = createElement(doc, "section", "site-update-notice__panel hidden");
panel.id = "siteUpdateNoticePanel";
panel.setAttribute("role", "status");
panel.setAttribute("aria-live", "polite");
const panelHeader = createElement(doc, "div", "flex items-start justify-between gap-3");
const title = createElement(doc, "h2", "text-base font-semibold", "Latest update");
const closeButton = createElement(doc, "button", "site-update-notice__close", "×");
closeButton.type = "button";
closeButton.setAttribute("aria-label", "Dismiss this update");
panelHeader.append(title, closeButton);
const intro = createElement(
doc,
"p",
"mt-2 text-sm leading-relaxed text-text-muted",
"BitVid has been updated. Here is what changed:",
);
const list = createElement(doc, "ul", "mt-3 list-disc space-y-1 pl-5 text-sm text-text-muted");
renderUpdateItems(doc, list, updateItems);
const build = createElement(
doc,
"p",
"mt-3 text-xs font-mono text-text-muted",
`Build ${version}${date ? ` • ${date}` : ""}`,
);
panel.append(panelHeader, intro, list, build);
const toggleButton = createElement(doc, "button", "site-update-notice__button", "Latest update");
toggleButton.type = "button";
toggleButton.setAttribute("aria-expanded", "false");
toggleButton.setAttribute("aria-controls", panel.id);
const setPanelOpen = (open) => {
panel.classList.toggle("hidden", !open);
toggleButton.setAttribute("aria-expanded", String(open));
};
let commitLoadStarted = false;
toggleButton.addEventListener("click", () => {
const nextOpen = panel.classList.contains("hidden");
setPanelOpen(nextOpen);
if (nextOpen && !commitLoadStarted) {
commitLoadStarted = true;
void loadGitHubCommitItems({
doc,
list,
fetchImpl,
fallbackItems: updateItems,
});
}
});
closeButton.addEventListener("click", () => {
writeStorage(storage, SITE_UPDATE_LAST_SEEN_KEY, version);
writeStorage(storage, SITE_UPDATE_DISMISSED_KEY, version);
root.remove();
});
root.append(panel, toggleButton);
doc.body.appendChild(root);
return { shown: true, version, root };
}