diff --git a/index.html b/index.html
index 8e86ca16..3d2af74c 100644
--- a/index.html
+++ b/index.html
@@ -198,156 +198,7 @@
-
+
diff --git a/js/nostrToolsBootstrap.js b/js/nostrToolsBootstrap.js
new file mode 100644
index 00000000..6e496f68
--- /dev/null
+++ b/js/nostrToolsBootstrap.js
@@ -0,0 +1,424 @@
+const REMOTE_IMPORT_TIMEOUT = 4500;
+const LOCAL_IMPORT_TIMEOUT = 2500;
+const SCRIPT_FALLBACK_TIMEOUT = 6000;
+const CDN_BUNDLE_URL = "https://cdn.jsdelivr.net/npm/nostr-tools@2.10.4/lib/nostr.bundle.min.js";
+
+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 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;
+ }
+};
+
+export function bootstrapNostrTools() {
+ if (bootstrapPromise) {
+ return bootstrapPromise;
+ }
+
+ const scope = getGlobalScope();
+
+ bootstrapPromise = (async () => {
+ if (!scope) {
+ const error = {
+ ok: false,
+ reason: "Global scope is unavailable.",
+ attempts: [],
+ };
+ return error;
+ }
+
+ const attempts = [];
+
+ const remoteImports = [
+ withTimeout(
+ () => import("https://esm.sh/nostr-tools@1.8.3"),
+ REMOTE_IMPORT_TIMEOUT,
+ "esm-main"
+ ),
+ withTimeout(
+ () =>
+ import(
+ "https://esm.sh/nostr-tools@1.8.3?target=es2022&exports=nip04"
+ ),
+ REMOTE_IMPORT_TIMEOUT,
+ "esm-nip04"
+ ),
+ ];
+
+ const localEntrypoints = discoverLocalEntrypoints(scope);
+ const localImports = localEntrypoints.map((entry, index) =>
+ withTimeout(
+ () => import(entry),
+ LOCAL_IMPORT_TIMEOUT,
+ `local-${index}`
+ )
+ );
+
+ const dynamicResults = await Promise.allSettled([
+ ...remoteImports,
+ ...localImports,
+ ]);
+
+ const resolvedModules = [];
+ let resolvedNip04 = 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;
+ }
+ 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;
+ }
+ }
+ });
+
+ 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 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);
+
+ if (shouldAttemptScriptFallback) {
+ const scriptLoadResult = await withTimeout(
+ () =>
+ new Promise((resolve, reject) => {
+ const script = scope.document.createElement("script");
+ script.src = CDN_BUNDLE_URL;
+ script.async = true;
+ script.onload = () => resolve(scope?.NostrTools || null);
+ script.onerror = () =>
+ reject(new Error("nostr-tools CDN bundle failed to load"));
+ scope.document.head.appendChild(script);
+ }),
+ SCRIPT_FALLBACK_TIMEOUT,
+ "cdn-bundle"
+ );
+
+ attempts.push({
+ target: scriptLoadResult.label,
+ ok: scriptLoadResult.ok,
+ timedOut: !!scriptLoadResult.timedOut,
+ duration: scriptLoadResult.duration,
+ error: scriptLoadResult.ok
+ ? null
+ : toSerializableError(scriptLoadResult.error),
+ });
+
+ if (scriptLoadResult.ok) {
+ const extracted = extractToolsFromModule(scriptLoadResult.value);
+ if (extracted && Object.keys(extracted).length > 0) {
+ resolvedModules.push({ source: "cdn-bundle", tools: extracted });
+ if (!resolvedNip04 && hasWorkingNip04(extracted?.nip04)) {
+ resolvedNip04 = extracted.nip04;
+ }
+ }
+ }
+ }
+
+ const existingGlobalTools = scope?.NostrTools;
+ if (existingGlobalTools && typeof existingGlobalTools === "object") {
+ resolvedModules.push({ source: "global", tools: existingGlobalTools });
+ if (!resolvedNip04 && hasWorkingNip04(existingGlobalTools?.nip04)) {
+ resolvedNip04 = existingGlobalTools.nip04;
+ }
+ }
+
+ if (resolvedModules.length === 0 && !resolvedNip04) {
+ const failure = {
+ ok: false,
+ reason: "Failed to resolve any nostr-tools helpers.",
+ attempts,
+ };
+ console.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;
+ console.info("[bitvid] Initialized nostr nip04 helpers.");
+ } else {
+ console.warn(
+ "[bitvid] NIP-04 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 (
+ 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;
+ };
+
+ try {
+ Object.defineProperty(scope, "NostrTools", {
+ configurable: true,
+ enumerable: true,
+ get() {
+ return activeTools;
+ },
+ set(value) {
+ applyActive(value);
+ },
+ });
+ } catch (error) {
+ console.warn("[bitvid] Failed to install NostrTools guard.", error);
+ }
+
+ 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;