mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 18:32:27 +00:00
Merge pull request #931 from PR0M3TH3AN/codex/update-access-control-for-hex-pubkeys
Handle hex pubkeys in access control
This commit is contained in:
2 files changed
+334
-6
No files matched your search
+205
-6
@@ -13,12 +13,45 @@ import {
|
||||
persistAdminState,
|
||||
readCachedAdminState,
|
||||
} from "./adminListStore.js";
|
||||
import { ensureNostrTools } from "./nostr/toolkit.js";
|
||||
import { userLogger } from "./utils/logger.js";
|
||||
|
||||
const HEX_KEY_PATTERN = /^[0-9a-f]{64}$/i;
|
||||
|
||||
function normalizeNpub(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function normalizeHexKey(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
return HEX_KEY_PATTERN.test(trimmed) ? trimmed : "";
|
||||
}
|
||||
|
||||
function bytesToHex(bytes) {
|
||||
if (!bytes || typeof bytes !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeof bytes.length !== "number") {
|
||||
return "";
|
||||
}
|
||||
|
||||
let hex = "";
|
||||
for (const byte of Array.from(bytes)) {
|
||||
const normalized = Number(byte);
|
||||
if (!Number.isInteger(normalized) || normalized < 0 || normalized > 255) {
|
||||
return "";
|
||||
}
|
||||
hex += normalized.toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
return normalizeHexKey(hex);
|
||||
}
|
||||
|
||||
function dedupeNpubs(values) {
|
||||
const normalized = Array.isArray(values) ? values.map(normalizeNpub) : [];
|
||||
return Array.from(
|
||||
@@ -73,7 +106,9 @@ class AccessControl {
|
||||
constructor() {
|
||||
this.editors = new Set();
|
||||
this.whitelist = new Set();
|
||||
this.whitelistPubkeys = new Set();
|
||||
this.blacklist = new Set();
|
||||
this.blacklistPubkeys = new Set();
|
||||
this.whitelistEnabled = getWhitelistMode();
|
||||
this.hasLoaded = false;
|
||||
this.lastError = null;
|
||||
@@ -112,6 +147,85 @@ class AccessControl {
|
||||
}
|
||||
}
|
||||
|
||||
_rebuildHexSets(
|
||||
whitelistValues = [],
|
||||
blacklistValues = [],
|
||||
toolkitCandidate = null
|
||||
) {
|
||||
const whitelistHex = new Set();
|
||||
const blacklistHex = new Set();
|
||||
|
||||
const decodeSources = [];
|
||||
const addDecodeSource = (candidate) => {
|
||||
if (
|
||||
candidate &&
|
||||
typeof candidate === "object" &&
|
||||
candidate.nip19 &&
|
||||
typeof candidate.nip19.decode === "function" &&
|
||||
!decodeSources.includes(candidate.nip19)
|
||||
) {
|
||||
decodeSources.push(candidate.nip19);
|
||||
}
|
||||
};
|
||||
|
||||
addDecodeSource(toolkitCandidate);
|
||||
|
||||
const scopeTools =
|
||||
(typeof window !== "undefined" ? window?.NostrTools : null) ||
|
||||
(typeof globalThis !== "undefined" ? globalThis?.NostrTools : null) ||
|
||||
null;
|
||||
addDecodeSource(scopeTools);
|
||||
|
||||
const decodeNpubToHex = (npub) => {
|
||||
const normalized = normalizeNpub(npub);
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (const nip19 of decodeSources) {
|
||||
try {
|
||||
const decoded = nip19.decode(normalized);
|
||||
if (!decoded || decoded.type !== "npub") {
|
||||
continue;
|
||||
}
|
||||
const data = decoded.data;
|
||||
if (typeof data === "string") {
|
||||
const normalizedHex = normalizeHexKey(data);
|
||||
if (normalizedHex) {
|
||||
return normalizedHex;
|
||||
}
|
||||
} else {
|
||||
const converted = bytesToHex(data);
|
||||
if (converted) {
|
||||
return converted;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
for (const entry of whitelistValues) {
|
||||
const decodedHex = decodeNpubToHex(entry);
|
||||
if (decodedHex) {
|
||||
whitelistHex.add(decodedHex);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of blacklistValues) {
|
||||
const decodedHex = decodeNpubToHex(entry);
|
||||
if (decodedHex) {
|
||||
blacklistHex.add(decodedHex);
|
||||
}
|
||||
}
|
||||
|
||||
this.whitelistPubkeys = whitelistHex;
|
||||
this.blacklistPubkeys = blacklistHex;
|
||||
}
|
||||
|
||||
_applyState(state, options = {}) {
|
||||
const markLoaded = options.markLoaded !== false;
|
||||
const editors = Array.isArray(state?.editors) ? state.editors : [];
|
||||
@@ -152,6 +266,37 @@ class AccessControl {
|
||||
});
|
||||
this.blacklist = new Set(sanitizedBlacklist);
|
||||
|
||||
const toolkitCandidate =
|
||||
(typeof window !== "undefined" ? window?.NostrTools : null) ||
|
||||
(typeof globalThis !== "undefined" ? globalThis?.NostrTools : null);
|
||||
this._rebuildHexSets(normalizedWhitelist, sanitizedBlacklist, toolkitCandidate);
|
||||
|
||||
try {
|
||||
const ensured = ensureNostrTools();
|
||||
if (ensured && typeof ensured.then === "function") {
|
||||
ensured
|
||||
.then((tools) => {
|
||||
if (!tools) {
|
||||
return;
|
||||
}
|
||||
this._rebuildHexSets(
|
||||
Array.from(this.whitelist),
|
||||
Array.from(this.blacklist),
|
||||
tools
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
} else if (ensured) {
|
||||
this._rebuildHexSets(
|
||||
Array.from(this.whitelist),
|
||||
Array.from(this.blacklist),
|
||||
ensured
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore toolkit errors; we'll attempt to hydrate again on the next refresh
|
||||
}
|
||||
|
||||
this.whitelistEnabled = getWhitelistMode();
|
||||
this.lastError = null;
|
||||
|
||||
@@ -220,7 +365,9 @@ class AccessControl {
|
||||
if (!this.hasLoaded && !this._hydratedFromCache) {
|
||||
this.editors.clear();
|
||||
this.whitelist.clear();
|
||||
this.whitelistPubkeys.clear();
|
||||
this.blacklist.clear();
|
||||
this.blacklistPubkeys.clear();
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -497,29 +644,72 @@ class AccessControl {
|
||||
if (this.whitelist.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedHexCandidate = normalizeHexKey(normalized);
|
||||
if (normalizedHexCandidate) {
|
||||
if (this.whitelistPubkeys.has(normalizedHexCandidate)) {
|
||||
return false;
|
||||
}
|
||||
return this.blacklistPubkeys.has(normalizedHexCandidate);
|
||||
}
|
||||
|
||||
return this.blacklist.has(normalized);
|
||||
}
|
||||
|
||||
canAccess(candidate) {
|
||||
let npub = "";
|
||||
let hex = "";
|
||||
|
||||
const considerHexCandidate = (value) => {
|
||||
const normalizedHex = normalizeHexKey(value);
|
||||
if (!normalizedHex) {
|
||||
return false;
|
||||
}
|
||||
hex = normalizedHex;
|
||||
return true;
|
||||
};
|
||||
|
||||
if (typeof candidate === "string") {
|
||||
npub = candidate;
|
||||
if (!considerHexCandidate(candidate)) {
|
||||
npub = candidate;
|
||||
}
|
||||
} else if (candidate && typeof candidate === "object") {
|
||||
if (typeof candidate.npub === "string") {
|
||||
npub = candidate.npub;
|
||||
} else if (typeof candidate.pubkey === "string") {
|
||||
try {
|
||||
npub = window.NostrTools.nip19.npubEncode(candidate.pubkey);
|
||||
} catch (error) {
|
||||
npub = candidate.pubkey;
|
||||
if (!considerHexCandidate(candidate.pubkey)) {
|
||||
try {
|
||||
npub = window.NostrTools.nip19.npubEncode(candidate.pubkey);
|
||||
} catch (error) {
|
||||
npub = candidate.pubkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = normalizeNpub(npub);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
if (!hex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isLockdownActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.whitelistPubkeys.has(hex)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.blacklistPubkeys.has(hex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.whitelistEnabled && !this.whitelistPubkeys.has(hex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.isAdminEditor(normalized)) {
|
||||
@@ -530,6 +720,15 @@ class AccessControl {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hex) {
|
||||
if (this.whitelistPubkeys.has(hex)) {
|
||||
return true;
|
||||
}
|
||||
if (this.blacklistPubkeys.has(hex)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.whitelist.has(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import "./test-helpers/setup-localstorage.mjs";
|
||||
import { AccessControl } from "../js/accessControl.js";
|
||||
import { NostrService } from "../js/services/nostrService.js";
|
||||
|
||||
if (typeof globalThis.window === "undefined") {
|
||||
globalThis.window = {};
|
||||
}
|
||||
|
||||
const whitelistHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
const whitelistNpub = "npub1whitelistcandidate";
|
||||
const blacklistHex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
|
||||
const blacklistNpub = "npub1blacklistcandidate";
|
||||
|
||||
const toolkit = {
|
||||
nip19: {
|
||||
decode(value) {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("invalid value");
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === whitelistNpub) {
|
||||
return { type: "npub", data: whitelistHex };
|
||||
}
|
||||
if (trimmed === blacklistNpub) {
|
||||
return { type: "npub", data: blacklistHex };
|
||||
}
|
||||
throw new Error("unsupported npub");
|
||||
},
|
||||
npubEncode(value) {
|
||||
if (value === whitelistHex) {
|
||||
return whitelistNpub;
|
||||
}
|
||||
if (value === blacklistHex) {
|
||||
return blacklistNpub;
|
||||
}
|
||||
throw new Error("unsupported hex");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const originalWindowNostrTools = globalThis.window.NostrTools;
|
||||
const originalGlobalNostrTools = globalThis.NostrTools;
|
||||
|
||||
globalThis.window.NostrTools = toolkit;
|
||||
globalThis.NostrTools = toolkit;
|
||||
|
||||
test.after(() => {
|
||||
if (originalWindowNostrTools === undefined) {
|
||||
delete globalThis.window.NostrTools;
|
||||
} else {
|
||||
globalThis.window.NostrTools = originalWindowNostrTools;
|
||||
}
|
||||
|
||||
if (originalGlobalNostrTools === undefined) {
|
||||
delete globalThis.NostrTools;
|
||||
} else {
|
||||
globalThis.NostrTools = originalGlobalNostrTools;
|
||||
}
|
||||
});
|
||||
|
||||
function createServiceWithAccessControl() {
|
||||
const service = new NostrService();
|
||||
const accessControl = new AccessControl();
|
||||
accessControl._hydrateFromCache = () => {};
|
||||
accessControl._scheduleHydrateFromCache = () => {};
|
||||
accessControl.whitelist = new Set([whitelistNpub]);
|
||||
accessControl.whitelistPubkeys = new Set([whitelistHex]);
|
||||
accessControl.blacklist = new Set([blacklistNpub]);
|
||||
accessControl.blacklistPubkeys = new Set([blacklistHex]);
|
||||
accessControl.whitelistEnabled = true;
|
||||
service.accessControl = accessControl;
|
||||
return service;
|
||||
}
|
||||
|
||||
test("shouldIncludeVideo returns true for whitelist author when npubEncode throws", () => {
|
||||
const service = createServiceWithAccessControl();
|
||||
const originalEncode = globalThis.window.NostrTools.nip19.npubEncode;
|
||||
globalThis.window.NostrTools.nip19.npubEncode = () => {
|
||||
throw new Error("encode failure");
|
||||
};
|
||||
|
||||
try {
|
||||
const video = {
|
||||
id: "video-whitelist",
|
||||
kind: 30078,
|
||||
pubkey: whitelistHex,
|
||||
};
|
||||
|
||||
assert.equal(service.shouldIncludeVideo(video), true);
|
||||
} finally {
|
||||
globalThis.window.NostrTools.nip19.npubEncode = originalEncode;
|
||||
}
|
||||
});
|
||||
|
||||
test("shouldIncludeVideo rejects blacklisted authors provided as npub", () => {
|
||||
const service = createServiceWithAccessControl();
|
||||
const video = {
|
||||
id: "video-blacklist-npub",
|
||||
kind: 30078,
|
||||
pubkey: blacklistHex,
|
||||
npub: blacklistNpub,
|
||||
};
|
||||
|
||||
assert.equal(service.shouldIncludeVideo(video), false);
|
||||
});
|
||||
|
||||
test("shouldIncludeVideo rejects blacklisted authors provided as hex when npubEncode throws", () => {
|
||||
const service = createServiceWithAccessControl();
|
||||
const originalEncode = globalThis.window.NostrTools.nip19.npubEncode;
|
||||
globalThis.window.NostrTools.nip19.npubEncode = () => {
|
||||
throw new Error("encode failure");
|
||||
};
|
||||
|
||||
try {
|
||||
const video = {
|
||||
id: "video-blacklist-hex",
|
||||
kind: 30078,
|
||||
pubkey: blacklistHex,
|
||||
};
|
||||
|
||||
assert.equal(service.shouldIncludeVideo(video), false);
|
||||
} finally {
|
||||
globalThis.window.NostrTools.nip19.npubEncode = originalEncode;
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user