import { devLogger, userLogger } from "./utils/logger.js"; const REMOTE_IMPORT_TIMEOUT = 4500; const LOCAL_IMPORT_TIMEOUT = 2500; const NODE_IMPORT_TIMEOUT = 1500; const SCRIPT_FALLBACK_TIMEOUT = 6000; const CDN_BUNDLE_URL = "https://cdn.jsdelivr.net/npm/nostr-tools@2.17.0/lib/nostr.bundle.min.js"; const LOCAL_BUNDLE_RELATIVE_URL = "../vendor/nostr-tools.bundle.min.js"; const moduleUrl = typeof import.meta !== "undefined" && import.meta?.url ? import.meta.url : null; const resolveRelativeUrl = (relativePath) => { if (!relativePath) { return relativePath; } if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(relativePath)) { return relativePath; } if (moduleUrl) { try { return new URL(relativePath, moduleUrl).href; } catch (error) { // Fall through to other resolution strategies } } if (typeof window !== "undefined" && window?.location?.href) { try { return new URL(relativePath, window.location.href).href; } catch (error) { return relativePath; } } return relativePath; }; const LOCAL_BUNDLE_URL = resolveRelativeUrl(LOCAL_BUNDLE_RELATIVE_URL); let bootstrapPromise = null; const getGlobalScope = () => { if (typeof window !== "undefined") { return window; } if (typeof globalThis !== "undefined") { return globalThis; } return null; }; const hasWorkingNip04 = (candidate) => !!( candidate && typeof candidate === "object" && typeof candidate.encrypt === "function" && typeof candidate.decrypt === "function" ); const hasWorkingNip44 = (candidate) => !!( candidate && typeof candidate === "object" && (( typeof candidate.encrypt === "function" && typeof candidate.decrypt === "function" ) || (candidate.v2 && typeof candidate.v2 === "object" && typeof candidate.v2.encrypt === "function" && typeof candidate.v2.decrypt === "function")) ); const selectNip44Candidate = (moduleLike) => { if (!moduleLike || typeof moduleLike !== "object") { return null; } if (hasWorkingNip44(moduleLike.nip44)) { return moduleLike.nip44; } if (hasWorkingNip44(moduleLike.default)) { return moduleLike.default; } if (hasWorkingNip44(moduleLike)) { return moduleLike; } return null; }; const toSerializableError = (error) => { if (!error) { return null; } if (error instanceof Error) { return { message: error.message, name: error.name, }; } if (typeof error === "string") { return { message: error }; } try { return { ...error }; } catch (serializationError) { return { message: String(error) }; } }; const withTimeout = (factory, ms, label) => new Promise((resolve) => { const start = Date.now(); let settled = false; let timeoutId; const finalize = (result) => { if (settled) { return; } settled = true; clearTimeout(timeoutId); resolve({ label, duration: Date.now() - start, ...result, }); }; const runner = Promise.resolve().then(factory); timeoutId = setTimeout(() => { finalize({ ok: false, timedOut: true, error: new Error( `[bitvid][nostr-tools] ${label} timed out after ${ms}ms` ), }); runner.catch(() => {}); }, ms); runner.then( (value) => finalize({ ok: true, value, timedOut: false }), (error) => finalize({ ok: false, error, timedOut: false }) ); }); const discoverLocalEntrypoints = (scope) => { if (!scope || typeof scope !== "object") { return []; } const entrypoints = []; const configuredEntrypoints = scope.__BITVID_LOCAL_NOSTR_TOOLS_ENTRY__; if (typeof configuredEntrypoints === "string") { entrypoints.push(configuredEntrypoints); } else if (Array.isArray(configuredEntrypoints)) { configuredEntrypoints .filter((candidate) => typeof candidate === "string" && candidate) .forEach((candidate) => entrypoints.push(candidate)); } if (scope.document) { const localScripts = scope.document.querySelectorAll( "script[data-nostr-tools-entry]" ); localScripts.forEach((script) => { const candidate = script.getAttribute("data-nostr-tools-entry"); if (candidate) { entrypoints.push(candidate); } }); } return Array.from(new Set(entrypoints)); }; const extractToolsFromModule = (moduleLike) => { if (!moduleLike || typeof moduleLike !== "object") { return null; } const { default: defaultExport, ...named } = moduleLike; if (defaultExport && typeof defaultExport === "object") { return { ...defaultExport, ...named }; } return { ...named }; }; const ensureGlobalReadyPromise = (scope, promise) => { if (!scope || typeof scope !== "object") { return; } try { Object.defineProperty(scope, "nostrToolsReady", { configurable: true, enumerable: false, writable: true, value: promise, }); } catch (error) { scope.nostrToolsReady = promise; } }; const loadScriptBundle = (scope, url, label) => { if (!scope?.document) { return Promise.resolve({ label, ok: false, timedOut: false, duration: 0, error: new Error("Document is unavailable for script loading."), }); } return withTimeout( () => new Promise((resolve, reject) => { const { document } = scope; const target = document.head || document.body || document.documentElement; if (!target) { reject(new Error("Unable to locate a DOM target for script injection.")); return; } const script = document.createElement("script"); script.async = true; script.src = url; script.crossOrigin = "anonymous"; const cleanup = () => { script.onload = null; script.onerror = null; if (script.parentNode) { script.parentNode.removeChild(script); } }; script.onload = () => { cleanup(); resolve(scope?.NostrTools || null); }; script.onerror = (event) => { cleanup(); if (event?.error instanceof Error) { reject(event.error); } else { reject(new Error(`nostr-tools bundle failed to load: ${url}`)); } }; target.appendChild(script); }), SCRIPT_FALLBACK_TIMEOUT, label ); }; const createMockToolkit = () => ({ SimplePool: class MockSimplePool { sub() { return { on: () => {}, off: () => {}, unsub: () => {} }; } list() { return Promise.resolve([]); } publish() { return Promise.resolve(); } get() { return Promise.resolve(null); } ensureRelay() { return Promise.resolve({ connect: () => Promise.resolve() }); } }, nip19: { decode: () => ({ type: "npub", data: "mock" }), npubEncode: () => "npub1mock", neventEncode: () => "nevent1mock", naddrEncode: () => "naddr1mock", nprofileEncode: () => "nprofile1mock", }, nip04: { encrypt: async () => "mock_ciphertext", decrypt: async () => "mock_plaintext", }, nip44: { v2: { encrypt: () => "mock_ciphertext", decrypt: () => "mock_plaintext", }, encrypt: () => "mock_ciphertext", decrypt: () => "mock_plaintext", }, getPublicKey: () => "mock_pubkey", generateSecretKey: () => new Uint8Array(32), generatePrivateKey: () => "mock_private_key", finalizeEvent: () => ({ id: "mock_id", sig: "mock_sig" }), verifyEvent: () => true, }); export function bootstrapNostrTools() { if (bootstrapPromise) { return bootstrapPromise; } const scope = getGlobalScope(); bootstrapPromise = (async () => { // Test Environment / Fallback Guard // If we are clearly in a test runner that doesn't support remote fetching, // we return a mock immediately to avoid timeouts. const isTestEnv = (typeof process !== "undefined" && process.env?.NODE_ENV === "test") || (scope && scope.__TEST_MODE__); if (!scope) { if (isTestEnv) { return createMockToolkit(); } const error = { ok: false, reason: "Global scope is unavailable.", attempts: [], }; return error; } const attempts = []; const isNodeLike = typeof process !== "undefined" && !!process?.versions?.node && (!scope || typeof scope.document === "undefined"); const nodeImports = isNodeLike ? [ withTimeout( () => import("nostr-tools"), NODE_IMPORT_TIMEOUT, "node-main" ), withTimeout( () => import("nostr-tools/nip04"), NODE_IMPORT_TIMEOUT, "node-nip04" ), withTimeout( () => import("nostr-tools/nip44"), NODE_IMPORT_TIMEOUT, "node-nip44" ), ] : []; const remoteImports = [ withTimeout( () => import("https://cdn.jsdelivr.net/npm/nostr-tools@2.17.0/+esm"), REMOTE_IMPORT_TIMEOUT, "jsdelivr-main" ), withTimeout( () => import("https://esm.sh/nostr-tools@2.17.0"), REMOTE_IMPORT_TIMEOUT, "esm-main" ), withTimeout( () => import( "https://esm.sh/nostr-tools@2.17.0?target=es2022&exports=nip04" ), REMOTE_IMPORT_TIMEOUT, "esm-nip04" ), withTimeout( () => import( "https://esm.sh/nostr-tools@2.17.0/nip44?target=es2022" ), REMOTE_IMPORT_TIMEOUT, "esm-nip44" ), ]; const localEntrypoints = discoverLocalEntrypoints(scope); const localImports = localEntrypoints.map((entry, index) => withTimeout( () => import(entry), LOCAL_IMPORT_TIMEOUT, `local-${index}` ) ); const dynamicResults = await Promise.allSettled([ ...nodeImports, ...remoteImports, ...localImports, ]); const resolvedModules = []; let resolvedNip04 = null; let resolvedNip44 = null; dynamicResults.forEach((settledResult) => { if (settledResult.status !== "fulfilled") { return; } const result = settledResult.value; attempts.push({ target: result.label, ok: result.ok, timedOut: !!result.timedOut, duration: result.duration, error: result.ok ? null : toSerializableError(result.error), }); if (!result.ok) { return; } if (result.label === "esm-nip04" && hasWorkingNip04(result.value?.nip04)) { resolvedNip04 = result.value.nip04; return; } if (!resolvedNip44 && result.label === "esm-nip44") { const nip44Candidate = selectNip44Candidate(result.value); if (nip44Candidate) { resolvedNip44 = nip44Candidate; return; } } const extracted = extractToolsFromModule(result.value); if (extracted && Object.keys(extracted).length > 0) { resolvedModules.push({ source: result.label, tools: extracted }); if (!resolvedNip04 && hasWorkingNip04(extracted?.nip04)) { resolvedNip04 = extracted.nip04; } if (!resolvedNip44) { const nip44Candidate = selectNip44Candidate(extracted); if (nip44Candidate) { resolvedNip44 = nip44Candidate; } } } }); dynamicResults.forEach((settledResult) => { if (settledResult.status !== "fulfilled") { const { reason } = settledResult; attempts.push({ target: "unknown", ok: false, timedOut: false, duration: 0, error: toSerializableError(reason), }); return; } const result = settledResult.value; if (result && !attempts.find((entry) => entry.target === result.label)) { attempts.push({ target: result.label, ok: result.ok, timedOut: !!result.timedOut, duration: result.duration, error: result.ok ? null : toSerializableError(result.error), }); } }); const needsAdditionalCrypto = !resolvedNip04 || !resolvedNip44; const allDynamicTimedOut = attempts.length > 0 && attempts.every( (attempt) => attempt && attempt.target && attempt.target.startsWith("esm") ? attempt.timedOut : true ); const shouldAttemptScriptFallback = !!scope.document && (resolvedModules.length === 0 || allDynamicTimedOut || needsAdditionalCrypto); if (shouldAttemptScriptFallback) { const scriptSources = [ LOCAL_BUNDLE_URL ? { url: LOCAL_BUNDLE_URL, label: "local-bundle" } : null, { url: CDN_BUNDLE_URL, label: "cdn-bundle" }, ].filter(Boolean); for (const source of scriptSources) { const scriptLoadResult = await loadScriptBundle( scope, source.url, source.label ); attempts.push({ target: scriptLoadResult.label, ok: scriptLoadResult.ok, timedOut: !!scriptLoadResult.timedOut, duration: scriptLoadResult.duration, error: scriptLoadResult.ok ? null : toSerializableError(scriptLoadResult.error), }); if (!scriptLoadResult.ok) { continue; } const extracted = extractToolsFromModule(scriptLoadResult.value); if (extracted && Object.keys(extracted).length > 0) { resolvedModules.push({ source: source.label, tools: extracted }); if (!resolvedNip04 && hasWorkingNip04(extracted?.nip04)) { resolvedNip04 = extracted.nip04; } if (!resolvedNip44) { const nip44Candidate = selectNip44Candidate(extracted); if (nip44Candidate) { resolvedNip44 = nip44Candidate; } } break; } } } const existingGlobalTools = scope?.NostrTools; if (existingGlobalTools && typeof existingGlobalTools === "object") { resolvedModules.push({ source: "global", tools: existingGlobalTools }); if (!resolvedNip04 && hasWorkingNip04(existingGlobalTools?.nip04)) { resolvedNip04 = existingGlobalTools.nip04; } if (!resolvedNip44) { const nip44Candidate = selectNip44Candidate(existingGlobalTools); if (nip44Candidate) { resolvedNip44 = nip44Candidate; } } } if (resolvedModules.length === 0 && !resolvedNip04 && !resolvedNip44) { if (isTestEnv) { return createMockToolkit(); } const failure = { ok: false, reason: "Failed to resolve any nostr-tools helpers.", attempts, }; userLogger.warn("[bitvid] nostr-tools bootstrap failed", failure); return failure; } const toolSources = resolvedModules.map((entry) => entry.tools); const canonicalTools = Object.assign({}, ...toolSources); const normalizedGeneratePrivateKey = typeof canonicalTools.generatePrivateKey === "function" ? canonicalTools.generatePrivateKey : typeof canonicalTools.generateSecretKey === "function" ? canonicalTools.generateSecretKey : undefined; const normalizedGenerateSecretKey = typeof canonicalTools.generateSecretKey === "function" ? canonicalTools.generateSecretKey : typeof canonicalTools.generatePrivateKey === "function" ? canonicalTools.generatePrivateKey : undefined; if (normalizedGeneratePrivateKey) { canonicalTools.generatePrivateKey = normalizedGeneratePrivateKey; } if (normalizedGenerateSecretKey) { canonicalTools.generateSecretKey = normalizedGenerateSecretKey; } if (resolvedNip04) { canonicalTools.nip04 = resolvedNip04; devLogger.info("[bitvid] Initialized nostr nip04 helpers."); } else { userLogger.warn( "[bitvid] NIP-04 helpers unavailable after bootstrap attempts." ); } if (resolvedNip44) { canonicalTools.nip44 = resolvedNip44; devLogger.info("[bitvid] Initialized nostr nip44 helpers."); } else { userLogger.warn( "[bitvid] NIP-44 helpers unavailable after bootstrap attempts." ); } const frozenCanonical = Object.freeze({ ...canonicalTools }); try { Object.defineProperty(scope, "__BITVID_CANONICAL_NOSTR_TOOLS__", { configurable: true, enumerable: false, writable: false, value: frozenCanonical, }); } catch (error) { scope.__BITVID_CANONICAL_NOSTR_TOOLS__ = frozenCanonical; } let activeTools = canonicalTools; const mergeWithCanonical = (candidate) => { if (!candidate || typeof candidate !== "object") { return activeTools; } const merged = Object.assign({}, frozenCanonical, candidate); if (!merged.nip04 && frozenCanonical.nip04) { merged.nip04 = frozenCanonical.nip04; } if (!merged.nip44 && frozenCanonical.nip44) { merged.nip44 = frozenCanonical.nip44; } if ( typeof merged.generatePrivateKey !== "function" && typeof merged.generateSecretKey === "function" ) { merged.generatePrivateKey = merged.generateSecretKey; } if ( typeof merged.generateSecretKey !== "function" && typeof merged.generatePrivateKey === "function" ) { merged.generateSecretKey = merged.generatePrivateKey; } return merged; }; const applyActive = (candidate) => { activeTools = mergeWithCanonical(candidate); return activeTools; }; const existingDescriptor = scope && typeof scope === "object" ? Object.getOwnPropertyDescriptor(scope, "NostrTools") : null; const canInstallGuard = !existingDescriptor || existingDescriptor.configurable === true; if (canInstallGuard) { try { Object.defineProperty(scope, "NostrTools", { configurable: true, enumerable: true, get() { return activeTools; }, set(value) { applyActive(value); }, }); } catch (error) { userLogger.warn("[bitvid] Failed to install NostrTools guard.", error); } } else { devLogger.info( "[bitvid] Skipping NostrTools guard; existing property is non-configurable." ); } applyActive(existingGlobalTools); scope.NostrTools = canonicalTools; return frozenCanonical; })() .catch((error) => ({ ok: false, reason: "nostr-tools bootstrap threw unexpectedly.", error: toSerializableError(error), })); ensureGlobalReadyPromise(scope, bootstrapPromise); return bootstrapPromise; } const nostrToolsReady = bootstrapNostrTools(); export { nostrToolsReady }; export default nostrToolsReady; await nostrToolsReady;