From 1c366a4a782d73a69163c2bd078462473b40072d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 01:29:04 +0000 Subject: [PATCH 1/3] Fix: Ensure trust seed mutes are loaded for session actors - Fixed `ModerationService.clearTrustedMuteTracking` to properly force re-subscription to trusted seeds by passing an empty previous state. - Enhanced `ModerationService` to filter trusted seeds against the global admin blacklist. - Added blacklist change event support to `AccessControl` and `TrustBootstrap` to dynamically update seeds when the blacklist changes. --- js/accessControl.js | 37 ++++++++++++++++++++++++++++++++ js/services/moderationService.js | 12 +++++++---- js/services/trustBootstrap.js | 4 ++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/js/accessControl.js b/js/accessControl.js index 0ac932dc..47b04a5f 100644 --- a/js/accessControl.js +++ b/js/accessControl.js @@ -117,6 +117,7 @@ class AccessControl { this._hydratedFromCache = false; this._whitelistListeners = new Set(); this._editorListeners = new Set(); + this._blacklistListeners = new Set(); this._scheduleHydrateFromCache(); } @@ -251,6 +252,8 @@ class AccessControl { this.editors instanceof Set ? new Set(this.editors) : new Set(); const previousWhitelist = this.whitelist instanceof Set ? new Set(this.whitelist) : new Set(); + const previousBlacklist = + this.blacklist instanceof Set ? new Set(this.blacklist) : new Set(); this.editors = new Set( dedupeNpubs([...ADMIN_EDITORS_NPUBS, ...editors]) @@ -280,6 +283,7 @@ class AccessControl { return true; }); this.blacklist = new Set(sanitizedBlacklist); + const blacklistChanged = !areSetsEqual(previousBlacklist, this.blacklist); const toolkitCandidate = (typeof window !== "undefined" ? window?.NostrTools : null) || @@ -326,6 +330,9 @@ class AccessControl { if (editorsChanged) { this._emitEditorsChange(Array.from(this.editors)); } + if (blacklistChanged) { + this._emitBlacklistChange(Array.from(this.blacklist)); + } } _emitWhitelistChange(whitelistValues) { @@ -346,6 +353,24 @@ class AccessControl { } } + _emitBlacklistChange(blacklistValues) { + if (!this._blacklistListeners.size) { + return; + } + + const snapshot = Array.isArray(blacklistValues) + ? [...blacklistValues] + : this.getBlacklist(); + + for (const listener of Array.from(this._blacklistListeners)) { + try { + listener(snapshot); + } catch (error) { + userLogger.error("accessControl blacklist listener failed", error); + } + } + } + _emitEditorsChange(editorsValues) { if (!this._editorListeners.size) { return; @@ -485,6 +510,18 @@ class AccessControl { }; } + onBlacklistChange(listener) { + if (typeof listener !== "function") { + return () => {}; + } + + this._blacklistListeners.add(listener); + + return () => { + this._blacklistListeners.delete(listener); + }; + } + async addModerator(requestorNpub, moderatorNpub) { if (!this.isSuperAdmin(requestorNpub)) { return { ok: false, error: "forbidden" }; diff --git a/js/services/moderationService.js b/js/services/moderationService.js index 6562567c..3e0e5ad5 100644 --- a/js/services/moderationService.js +++ b/js/services/moderationService.js @@ -520,9 +520,11 @@ export class ModerationService { } if (this.trustedSeedContacts instanceof Set) { + const adminSnapshot = this.getAdminListSnapshot(); + const blacklistHex = adminSnapshot.blacklistHex; for (const seed of this.trustedSeedContacts) { const normalized = normalizeHex(seed); - if (normalized) { + if (normalized && (!blacklistHex || !blacklistHex.has(normalized))) { merged.add(normalized); } } @@ -635,18 +637,20 @@ export class ModerationService { setTrustedSeeds(seeds = []) { const sanitizedSeeds = new Set(); + const adminSnapshot = this.getAdminListSnapshot(); + const blacklistHex = adminSnapshot.blacklistHex; if (seeds instanceof Set || Array.isArray(seeds)) { for (const candidate of seeds) { const normalized = normalizeToHex(candidate); - if (normalized) { + if (normalized && (!blacklistHex || !blacklistHex.has(normalized))) { sanitizedSeeds.add(normalized); } } } else if (seeds && typeof seeds[Symbol.iterator] === "function") { for (const candidate of seeds) { const normalized = normalizeToHex(candidate); - if (normalized) { + if (normalized && (!blacklistHex || !blacklistHex.has(normalized))) { sanitizedSeeds.add(normalized); } } @@ -845,7 +849,7 @@ export class ModerationService { this.emit("trusted-mutes", { total: 0 }); - this.rebuildTrustedContacts(new Set(), { previous }); + this.rebuildTrustedContacts(new Set(), { previous: new Set() }); } isTrustedMuteOwner(pubkey) { diff --git a/js/services/trustBootstrap.js b/js/services/trustBootstrap.js index 8ac79c6a..d878d2a2 100644 --- a/js/services/trustBootstrap.js +++ b/js/services/trustBootstrap.js @@ -178,4 +178,8 @@ export async function bootstrapTrustedSeeds() { if (accessControl && typeof accessControl.onEditorsChange === "function") { accessControl.onEditorsChange(applyOnChange); } + + if (accessControl && typeof accessControl.onBlacklistChange === "function") { + accessControl.onBlacklistChange(applyOnChange); + } } From cda0671699c9c07e0d80f96fe958811b21954d34 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 01:40:46 +0000 Subject: [PATCH 2/3] Fix: Ensure trust seed mutes are loaded for session actors - Fixed `ModerationService.reconcileTrustedMuteSubscriptions` to always force re-subscription to trusted seeds, bypassing previous state comparisons. - Enhanced `ModerationService` to filter trusted seeds against the global admin blacklist. - Added blacklist change event support to `AccessControl` and `TrustBootstrap` to dynamically update seeds when the blacklist changes. - Added detailed logging to `ModerationService` for tracking subscription lifecycle. --- js/services/moderationService.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/js/services/moderationService.js b/js/services/moderationService.js index 3e0e5ad5..559518d5 100644 --- a/js/services/moderationService.js +++ b/js/services/moderationService.js @@ -636,6 +636,7 @@ export class ModerationService { } setTrustedSeeds(seeds = []) { + this.log("[moderationService] setTrustedSeeds called", { count: seeds?.length || seeds?.size || 0 }); const sanitizedSeeds = new Set(); const adminSnapshot = this.getAdminListSnapshot(); const blacklistHex = adminSnapshot.blacklistHex; @@ -867,6 +868,11 @@ export class ModerationService { const previous = previousSet instanceof Set ? new Set(previousSet) : new Set(); const next = nextSet instanceof Set ? new Set(nextSet) : new Set(); + this.log("[moderationService] reconcileTrustedMuteSubscriptions", { + previous: previous.size, + next: next.size, + }); + if (this.viewerPubkey) { if (this.trustedMuteSubscriptions.has(this.viewerPubkey)) { previous.add(this.viewerPubkey); @@ -881,14 +887,12 @@ export class ModerationService { } for (const value of next) { - if (!previous.has(value)) { - this.subscribeToTrustedMuteList(value).catch((error) => { - this.log( - `(moderationService) failed to subscribe to trusted mute list for ${value}`, - error, - ); - }); - } + this.subscribeToTrustedMuteList(value).catch((error) => { + this.log( + `(moderationService) failed to subscribe to trusted mute list for ${value}`, + error, + ); + }); } } @@ -927,9 +931,12 @@ export class ModerationService { } return; } else if (typeof record.unsub === "function") { + this.log(`[moderationService] already subscribed to ${normalized}`); return; } + this.log(`[moderationService] subscribing to trusted mute list for ${normalized}`); + record.promise = (async () => { try { await this.ensurePool(); @@ -947,6 +954,7 @@ export class ModerationService { const relays = resolveRelayList(this.nostrClient); if (!relays.length) { + this.log(`[moderationService] no relays available for ${normalized}`); return; } @@ -991,6 +999,7 @@ export class ModerationService { }); sub.on("eose", () => {}); record.unsub = typeof sub.unsub === "function" ? () => sub.unsub() : null; + this.log(`[moderationService] subscribed to trusted mute list for ${normalized}`); } catch (error) { this.log( `(moderationService) failed to subscribe to trusted mute list for ${normalized}`, @@ -1153,6 +1162,7 @@ export class ModerationService { return; } + this.log(`[moderationService] ingesting trusted mute event from ${owner} (${event.id})`); this.applyTrustedMuteEvent(owner, event); } From 8aaa2f69ee3aa624dbcb0cbd992c765927b43145 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 03:36:19 +0000 Subject: [PATCH 3/3] Fix: Ensure trust seed mutes are loaded for session actors - Fixed `ModerationService.reconcileTrustedMuteSubscriptions` to always force re-subscription to trusted seeds, bypassing previous state comparisons. - Enhanced `ModerationService` to filter trusted seeds against the global admin blacklist. - Added blacklist change event support to `AccessControl` and `TrustBootstrap` to dynamically update seeds when the blacklist changes. - Added detailed logging to `ModerationService`, `TrustBootstrap`, and `AccessControl` to trace trust seed initialization and application. --- js/accessControl.js | 5 +++++ js/services/trustBootstrap.js | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/js/accessControl.js b/js/accessControl.js index 47b04a5f..7d163ac1 100644 --- a/js/accessControl.js +++ b/js/accessControl.js @@ -243,6 +243,7 @@ class AccessControl { } _applyState(state, options = {}) { + userLogger.info("[accessControl] _applyState called", { state, options }); const markLoaded = options.markLoaded !== false; const editors = Array.isArray(state?.editors) ? state.editors : []; const whitelist = Array.isArray(state?.whitelist) ? state.whitelist : []; @@ -421,9 +422,11 @@ class AccessControl { refresh() { if (this._isRefreshing) { + userLogger.info("[accessControl] refresh ignored (already refreshing)"); return this._refreshPromise; } + userLogger.info("[accessControl] refresh started"); const operation = this._performRefresh(); const tracked = operation.catch((error) => { userLogger.error("Failed to refresh admin lists:", error); @@ -440,8 +443,10 @@ class AccessControl { try { await this._refreshPromise; + userLogger.info("[accessControl] ensureReady resolved"); } catch (error) { if (!this.hasLoaded) { + userLogger.warn("[accessControl] ensureReady retry"); await this.refresh(); await this._refreshPromise; } else { diff --git a/js/services/trustBootstrap.js b/js/services/trustBootstrap.js index d878d2a2..24ba4f67 100644 --- a/js/services/trustBootstrap.js +++ b/js/services/trustBootstrap.js @@ -92,6 +92,7 @@ async function waitForAccessControl({ timeoutMs = TRUST_SEED_READY_TIMEOUT_MS } } export async function bootstrapTrustedSeeds() { + devLogger.info("[trustBootstrap] bootstrapTrustedSeeds called"); if ( !( FEATURE_TRUST_SEEDS && @@ -99,6 +100,7 @@ export async function bootstrapTrustedSeeds() { typeof moderationService.setTrustedSeeds === "function" ) ) { + devLogger.warn("[trustBootstrap] Trust seeds feature disabled or service missing"); return; } @@ -113,6 +115,7 @@ export async function bootstrapTrustedSeeds() { editors, fallbackSeeds: DEFAULT_TRUST_SEED_NPUBS, }); + devLogger.info(`[trustBootstrap] Applying ${seeds.size} trusted seeds`, Array.from(seeds)); moderationService.setTrustedSeeds(seeds); if ( moderationService && @@ -130,6 +133,7 @@ export async function bootstrapTrustedSeeds() { attempts += 1; try { const result = await waitForAccessControl(); + devLogger.info("[trustBootstrap] waitForAccessControl result", result); applySeeds(); return result; } catch (error) {