Ensure trusted seeds hydrate before app creation

This commit is contained in:
thePR0M3TH3AN committed 2026-01-02 12:18:57 -05:00
1 parent 192df53959
commit 9310929f0d
3 files changed
+147 -22

No files matched your search

+98 -10
View File
@@ -12,6 +12,9 @@ import nostrService from "./services/nostrService.js";
import r2Service from "./services/r2Service.js";
import { loadView } from "./viewManager.js";
import hashtagPreferences from "./services/hashtagPreferencesService.js";
import { devLogger, userLogger } from "./utils/logger.js";
const TRUST_SEED_READY_TIMEOUT_MS = 3500;
function normalizeNpub(value) {
return typeof value === "string" ? value.trim() : "";
@@ -44,6 +47,53 @@ function buildTrustedSeeds({ superAdmin, editors, fallbackSeeds }) {
return seeds;
}
function delay(ms = 0) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForRelaysReady({ timeoutMs = TRUST_SEED_READY_TIMEOUT_MS } = {}) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (
nostrService?.nostrClient?.relays &&
Array.isArray(nostrService.nostrClient.relays) &&
nostrService.nostrClient.relays.length > 0
) {
return true;
}
await delay(125);
}
return false;
}
async function waitForAccessControl({ timeoutMs = TRUST_SEED_READY_TIMEOUT_MS } = {}) {
if (!accessControl || typeof accessControl.ensureReady !== "function") {
return { ok: true, timedOut: false };
}
let timedOut = false;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
timedOut = true;
reject(new Error("accessControl ensureReady timed out"));
}, timeoutMs);
});
try {
await Promise.race([accessControl.ensureReady(), timeoutPromise]);
return { ok: true, timedOut: false };
} catch (error) {
if (timedOut) {
devLogger.warn(
"[bootstrap] access control hydration timed out; falling back to default trust seeds.",
error,
);
return { ok: true, timedOut: true };
}
throw error;
}
}
async function bootstrapTrustedSeeds() {
if (
!(
@@ -67,20 +117,49 @@ async function bootstrapTrustedSeeds() {
fallbackSeeds: DEFAULT_TRUST_SEED_NPUBS,
});
moderationService.setTrustedSeeds(seeds);
} catch {
// Swallow bootstrap errors so login flow can continue; flag enables quick rollback.
if (
moderationService &&
typeof moderationService.recomputeAllSummaries === "function"
) {
moderationService.recomputeAllSummaries();
}
} catch (error) {
userLogger.warn("[bootstrap] Failed to apply trusted seeds", error);
}
};
try {
if (accessControl && typeof accessControl.ensureReady === "function") {
await accessControl.ensureReady();
let attempts = 0;
const hydrate = async () => {
attempts += 1;
try {
const result = await waitForAccessControl();
applySeeds();
return result;
} catch (error) {
userLogger.warn(
"[bootstrap] Failed to hydrate admin lists for trusted seeds",
error,
);
return { ok: false, timedOut: false, error };
}
};
let hydrateResult = await hydrate();
if (!hydrateResult?.ok && attempts < 2) {
const relaysReady = await waitForRelaysReady();
if (relaysReady) {
hydrateResult = await hydrate();
} else {
devLogger.warn(
"[bootstrap] Skipping trusted seed retry because relays were not ready in time.",
);
}
} catch {
// Swallow bootstrap errors so login flow can continue; flag enables quick rollback.
}
applySeeds();
if (!hydrateResult?.ok) {
applySeeds();
}
const applyOnChange = () => {
applySeeds();
@@ -95,7 +174,7 @@ async function bootstrapTrustedSeeds() {
}
}
bootstrapTrustedSeeds();
export const trustedSeedsReadyPromise = bootstrapTrustedSeeds();
function mergeServices(overrides = {}) {
const merged = { nostrService, r2Service, hashtagPreferences };
@@ -115,7 +194,16 @@ function mergeServices(overrides = {}) {
return merged;
}
export function createApplication({ services, loadView: loadViewOverride } = {}) {
export async function createApplication({ services, loadView: loadViewOverride } = {}) {
try {
await trustedSeedsReadyPromise;
} catch (error) {
userLogger.warn(
"[bootstrap] Failed to await trusted seed hydration before creating application",
error,
);
}
return new Application({
services: mergeServices(services),
loadView: typeof loadViewOverride === "function" ? loadViewOverride : loadView,
+11 -11
View File
@@ -3,7 +3,6 @@
import { validateInstanceConfig } from "../config/validate-config.js";
import { ASSET_VERSION } from "../config/asset-version.js";
import "./bufferPolyfill.js";
import Application from "./app.js";
import { setApplication, setApplicationReady } from "./applicationContext.js";
import nostrService from "./services/nostrService.js";
import r2Service from "./services/r2Service.js";
@@ -37,6 +36,7 @@ import { accessControl } from "./accessControl.js";
import { nostrClient } from "./nostrClientFacade.js";
import { userBlocks } from "./userBlocks.js";
import { relayManager } from "./relayManager.js";
import createApplication from "./bootstrap.js";
validateInstanceConfig();
@@ -354,17 +354,17 @@ function startApplication() {
return applicationReadyPromise;
}
application = new Application({
services: {
nostrService,
r2Service,
},
loadView,
});
setApplication(application);
const startupPromise = (async () => {
application = await createApplication({
services: {
nostrService,
r2Service,
},
loadView,
});
setApplication(application);
await application.init();
if (typeof application.start === "function") {
await application.start();
+38 -1
View File
@@ -87,6 +87,37 @@ test("trusted seeds contribute to trusted report counts", (t) => {
assert.deepEqual(reporters.map((entry) => entry.pubkey), [seedHex]);
});
test("trusted seed updates recompute guest report summaries", (t) => {
withMockedNostrTools(t);
const eventId = "a".repeat(64);
const reporterHex = "b".repeat(64);
const reporterNpub = `npub${reporterHex}`;
const { service } = createModerationServiceHarness(t);
const report = createReportEvent({
id: "c".repeat(64),
reporter: reporterHex,
eventId,
createdAt: 1_700_000_999,
type: "spam",
});
service.ingestReportEvent(report);
const preSeedSummary = service.getTrustedReportSummary(eventId);
assert.equal(preSeedSummary.totalTrusted, 0);
assert.equal(preSeedSummary.types.spam?.trusted ?? 0, 0);
service.setTrustedSeeds([reporterNpub]);
const postSeedSummary = service.getTrustedReportSummary(eventId);
assert.equal(postSeedSummary.totalTrusted, 1);
assert.equal(postSeedSummary.types.spam.trusted, 1);
assert.equal(service.trustedReportCount(eventId, "spam"), 1);
});
test("moderator seeds populate trusted contacts", (t) => {
withMockedNostrTools(t);
@@ -150,6 +181,7 @@ test("bootstrap seeds track editor roster and ignore whitelist-only changes", as
};
const setTrustedSeedsCalls = [];
const recomputeCalls = [];
const moderationServiceMock = {
setTrustedSeeds: (seeds) => {
const snapshot = [];
@@ -160,12 +192,16 @@ test("bootstrap seeds track editor roster and ignore whitelist-only changes", as
}
setTrustedSeedsCalls.push(snapshot);
},
recomputeAllSummaries: () => {
recomputeCalls.push(true);
},
};
globalThis.__bootstrapAccessControlMock = accessControlMock;
globalThis.__bootstrapModerationServiceMock = moderationServiceMock;
await import("../../js/bootstrap.js");
const bootstrapModule = await import("../../js/bootstrap.js");
await bootstrapModule.trustedSeedsReadyPromise;
assert.equal(ensureReadyCalls, 1);
assert.ok(
@@ -174,6 +210,7 @@ test("bootstrap seeds track editor roster and ignore whitelist-only changes", as
);
assert.equal(setTrustedSeedsCalls.length, 1);
assert.equal(recomputeCalls.length, 1, "summaries should recompute after trusted seeds apply");
const [initialSeeds] = setTrustedSeedsCalls;
assert.ok(
initialSeeds.includes(ADMIN_SUPER_NPUB),