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
@@ -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 = {};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user