mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 00:42:30 +00:00
Improve Nostr watch history snapshot caching
This commit is contained in:
1 parent
9f79983d5d
commit
1ff3523bd0
7 files changed
+777
-39
No files matched your search
+14
-4
@@ -2452,6 +2452,11 @@ export class NostrClient {
|
||||
!!normalizedLogged &&
|
||||
activeSigner &&
|
||||
typeof activeSigner.signEvent === "function";
|
||||
const canSignWithExtension =
|
||||
!!normalizedLogged &&
|
||||
activeSigner &&
|
||||
typeof activeSigner.signEvent === "function" &&
|
||||
shouldRequestExtensionPermissions(activeSigner);
|
||||
|
||||
if (!forceRenew && canSignWithActiveSigner) {
|
||||
return normalizedLogged;
|
||||
@@ -3237,6 +3242,10 @@ export class NostrClient {
|
||||
if (!pubkey) throw new Error("Not logged in to publish video.");
|
||||
|
||||
const { videoData, nip71Metadata } = extractVideoPublishPayload(videoPayload);
|
||||
const nip71EditedFlag =
|
||||
videoPayload && typeof videoPayload === "object"
|
||||
? videoPayload.nip71Edited
|
||||
: null;
|
||||
|
||||
// NOTE: Keep the Upload, Edit, and Revert flows synchronized when
|
||||
// updating shared fields. Changes here must be reflected in the modal
|
||||
@@ -3274,6 +3283,7 @@ export class NostrClient {
|
||||
const finalIsNsfw = videoData.isNsfw === true;
|
||||
const finalIsForKids =
|
||||
videoData.isForKids === true && !finalIsNsfw;
|
||||
const wantPrivate = videoData.isPrivate === true;
|
||||
const finalWs =
|
||||
typeof videoData.ws === "string" ? videoData.ws.trim() : "";
|
||||
const finalXs =
|
||||
@@ -3389,10 +3399,10 @@ export class NostrClient {
|
||||
description: contentObject.description,
|
||||
url: contentObject.url,
|
||||
magnet: contentObject.magnet,
|
||||
thumbnail: contentObject.thumbnail,
|
||||
mode: contentObject.mode,
|
||||
isPrivate: contentObject.isPrivate,
|
||||
isNsfw: contentObject.isNsfw,
|
||||
thumbnail: contentObject.thumbnail,
|
||||
mode: contentObject.mode,
|
||||
isPrivate: wantPrivate,
|
||||
isNsfw: contentObject.isNsfw,
|
||||
isForKids: contentObject.isForKids,
|
||||
};
|
||||
|
||||
|
||||
@@ -9,20 +9,61 @@ import {
|
||||
import { buildRepostEvent, buildVideoMirrorEvent } from "../nostrEventSchemas.js";
|
||||
import { normalizePointerInput } from "./watchHistory.js";
|
||||
import { sanitizeRelayList } from "./nip46Client.js";
|
||||
import { RELAY_URLS, getCachedNostrTools } from "./toolkit.js";
|
||||
import {
|
||||
RELAY_URLS,
|
||||
getCachedNostrTools,
|
||||
readToolkitFromScope,
|
||||
} from "./toolkit.js";
|
||||
import { DEFAULT_NIP07_PERMISSION_METHODS } from "./nip07Permissions.js";
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
|
||||
const REBROADCAST_GUARD_PREFIX = "bitvid:rebroadcast:v1";
|
||||
const rebroadcastAttemptMemory = new Map();
|
||||
|
||||
const HEX_PRIVATE_KEY_REGEX = /^[0-9a-f]{64}$/i;
|
||||
|
||||
export function signEventWithPrivateKey(event, privateKey) {
|
||||
const tools = getCachedNostrTools();
|
||||
const scopedTools = readToolkitFromScope();
|
||||
const canonicalTools =
|
||||
typeof globalThis !== "undefined" &&
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__
|
||||
? globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__
|
||||
: null;
|
||||
const signFn =
|
||||
typeof canonicalTools?.signEvent === "function"
|
||||
? canonicalTools.signEvent
|
||||
: typeof tools?.signEvent === "function"
|
||||
? tools.signEvent
|
||||
: typeof scopedTools?.signEvent === "function"
|
||||
? scopedTools.signEvent
|
||||
: null;
|
||||
// console.debug('signEventWithPrivateKey', {
|
||||
// signFn: typeof signFn,
|
||||
// scopedSign: typeof scopedTools?.signEvent,
|
||||
// cachedSign: typeof tools?.signEvent,
|
||||
// });
|
||||
const finalizeFn =
|
||||
typeof canonicalTools?.finalizeEvent === "function"
|
||||
? canonicalTools.finalizeEvent
|
||||
: typeof tools?.finalizeEvent === "function"
|
||||
? tools.finalizeEvent
|
||||
: typeof scopedTools?.finalizeEvent === "function"
|
||||
? scopedTools.finalizeEvent
|
||||
: null;
|
||||
const hashFn =
|
||||
typeof canonicalTools?.getEventHash === "function"
|
||||
? canonicalTools.getEventHash
|
||||
: typeof tools?.getEventHash === "function"
|
||||
? tools.getEventHash
|
||||
: typeof scopedTools?.getEventHash === "function"
|
||||
? scopedTools.getEventHash
|
||||
: null;
|
||||
|
||||
if (
|
||||
!privateKey ||
|
||||
typeof privateKey !== "string" ||
|
||||
!tools?.getEventHash ||
|
||||
typeof tools.signEvent !== "function"
|
||||
(!signFn && !finalizeFn)
|
||||
) {
|
||||
throw new Error("Missing signing primitives");
|
||||
}
|
||||
@@ -38,10 +79,50 @@ export function signEventWithPrivateKey(event, privateKey) {
|
||||
tags,
|
||||
content: typeof event.content === "string" ? event.content : "",
|
||||
};
|
||||
const id = tools.getEventHash(prepared);
|
||||
const sig = tools.signEvent(prepared, privateKey);
|
||||
|
||||
return { ...prepared, id, sig };
|
||||
if (signFn) {
|
||||
const signature = signFn(prepared, privateKey);
|
||||
if (signature && typeof signature === "object") {
|
||||
const idValue =
|
||||
typeof signature.id === "string"
|
||||
? signature.id
|
||||
: hashFn
|
||||
? hashFn(prepared)
|
||||
: null;
|
||||
const sigValue =
|
||||
typeof signature.sig === "string"
|
||||
? signature.sig
|
||||
: typeof signature.signature === "string"
|
||||
? signature.signature
|
||||
: null;
|
||||
if (!idValue || !sigValue) {
|
||||
throw new Error("Missing signing primitives");
|
||||
}
|
||||
return { ...prepared, id: idValue, sig: sigValue };
|
||||
}
|
||||
|
||||
const sigValue = typeof signature === "string" ? signature : null;
|
||||
if (!sigValue || !hashFn) {
|
||||
throw new Error("Missing signing primitives");
|
||||
}
|
||||
const idValue = hashFn(prepared);
|
||||
return { ...prepared, id: idValue, sig: sigValue };
|
||||
}
|
||||
|
||||
const normalizedKey = privateKey.trim();
|
||||
if (!HEX_PRIVATE_KEY_REGEX.test(normalizedKey)) {
|
||||
throw new Error("Missing signing primitives");
|
||||
}
|
||||
|
||||
const finalized = finalizeFn(prepared, normalizedKey);
|
||||
if (
|
||||
!finalized ||
|
||||
typeof finalized.id !== "string" ||
|
||||
typeof finalized.sig !== "string"
|
||||
) {
|
||||
throw new Error("Missing signing primitives");
|
||||
}
|
||||
return finalized;
|
||||
}
|
||||
|
||||
export function deriveRebroadcastBucketIndex(referenceSeconds = null) {
|
||||
|
||||
+91
-12
@@ -88,7 +88,9 @@ function mergePointerDetails(target, source) {
|
||||
target.watchedAt = Math.max(0, Math.floor(source.watchedAt));
|
||||
}
|
||||
if (typeof source.relay === "string" && source.relay.trim()) {
|
||||
target.relay = source.relay.trim();
|
||||
if (!target.relay || !target.relay.trim()) {
|
||||
target.relay = source.relay.trim();
|
||||
}
|
||||
}
|
||||
if (!target.metadata && source.metadata && typeof source.metadata === "object") {
|
||||
target.metadata = clonePointerMetadata(source.metadata);
|
||||
@@ -175,11 +177,7 @@ export function pointerKey(pointer) {
|
||||
if (!type || !value) {
|
||||
return "";
|
||||
}
|
||||
const relay =
|
||||
typeof pointer.relay === "string" && pointer.relay.trim()
|
||||
? pointer.relay.trim().toLowerCase()
|
||||
: "";
|
||||
return relay ? `${type}:${value}:${relay}` : `${type}:${value}`;
|
||||
return `${type}:${value}`;
|
||||
}
|
||||
|
||||
function normalizePointerTag(tag) {
|
||||
@@ -466,7 +464,10 @@ export function normalizeActorKey(actor) {
|
||||
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
const decoder = getCachedNostrTools()?.nip19?.decode;
|
||||
const toolkit = getCachedNostrTools();
|
||||
const decoder =
|
||||
(typeof window !== "undefined" ? window?.NostrTools?.nip19?.decode : null) ||
|
||||
toolkit?.nip19?.decode;
|
||||
if (typeof decoder === "function") {
|
||||
try {
|
||||
const decoded = decoder(trimmed);
|
||||
@@ -729,7 +730,11 @@ export function isNip04EncryptedWatchHistoryEvent(pointerEvent, ciphertext) {
|
||||
|
||||
class WatchHistoryManager {
|
||||
constructor(deps) {
|
||||
this.deps = deps || {};
|
||||
this.deps = {
|
||||
ensureNostrTools,
|
||||
getCachedNostrTools,
|
||||
...(deps || {}),
|
||||
};
|
||||
this.cache = new Map();
|
||||
this.storage = null;
|
||||
this.republishTimers = new Map();
|
||||
@@ -1225,11 +1230,51 @@ class WatchHistoryManager {
|
||||
if (cachedNip04Tools) {
|
||||
return cachedNip04Tools;
|
||||
}
|
||||
const tools = await ensureNostrTools();
|
||||
if (tools?.nip04 && typeof tools.nip04.encrypt === "function") {
|
||||
cachedNip04Tools = tools;
|
||||
return cachedNip04Tools;
|
||||
|
||||
const ensureToolkit =
|
||||
typeof this.deps.ensureNostrTools === "function"
|
||||
? this.deps.ensureNostrTools
|
||||
: ensureNostrTools;
|
||||
if (ensureToolkit) {
|
||||
try {
|
||||
const ensured = await ensureToolkit();
|
||||
if (
|
||||
ensured?.nip04 &&
|
||||
typeof ensured.nip04.encrypt === "function"
|
||||
) {
|
||||
cachedNip04Tools = ensured;
|
||||
return cachedNip04Tools;
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[nostr] Failed to resolve nostr-tools for watch history:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const readCachedToolkit =
|
||||
typeof this.deps.getCachedNostrTools === "function"
|
||||
? this.deps.getCachedNostrTools
|
||||
: getCachedNostrTools;
|
||||
if (readCachedToolkit) {
|
||||
try {
|
||||
const cached = await readCachedToolkit();
|
||||
if (
|
||||
cached?.nip04 &&
|
||||
typeof cached.nip04.encrypt === "function"
|
||||
) {
|
||||
cachedNip04Tools = cached;
|
||||
return cachedNip04Tools;
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[nostr] Failed to read cached nostr-tools for watch history:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
const encryptChunk = async (plaintext) => {
|
||||
@@ -1483,6 +1528,40 @@ class WatchHistoryManager {
|
||||
if (!success && errorCode) {
|
||||
result.error = errorCode;
|
||||
}
|
||||
|
||||
const shouldPersistLocally =
|
||||
pointerAcceptedCount > 0 && !chunkRejectedEverywhere;
|
||||
|
||||
if (shouldPersistLocally) {
|
||||
const storage = this.getStorage();
|
||||
const previousEntry =
|
||||
this.cache.get(actorKey) || storage.actors?.[actorKey] || {};
|
||||
const metadata = sanitizeWatchHistoryMetadata(previousEntry.metadata);
|
||||
metadata.updatedAt = Date.now();
|
||||
metadata.status = success ? "ok" : "partial";
|
||||
metadata.lastPublishResults = result.publishResults;
|
||||
metadata.skippedCount = result.skippedCount || 0;
|
||||
if (success) {
|
||||
delete metadata.lastError;
|
||||
} else {
|
||||
metadata.lastError = result.error || "publish-partial";
|
||||
}
|
||||
|
||||
const entry = {
|
||||
actor: actorPubkey,
|
||||
items: canonicalItems,
|
||||
snapshotId,
|
||||
pointerEvent: result.pointerEvent,
|
||||
chunkEvents: result.chunkEvents,
|
||||
savedAt: Date.now(),
|
||||
fingerprint: await this.getFingerprint(actorPubkey, canonicalItems),
|
||||
metadata,
|
||||
};
|
||||
|
||||
this.cache.set(actorKey, entry);
|
||||
this.persistEntry(actorKey, entry);
|
||||
}
|
||||
|
||||
devLogger.info("[nostr] Watch history snapshot publish result.", {
|
||||
actor: actorKey,
|
||||
snapshotId,
|
||||
|
||||
@@ -5,7 +5,14 @@ if (typeof globalThis.window === "undefined") {
|
||||
globalThis.window = {};
|
||||
}
|
||||
|
||||
if (!window.crypto || !window.crypto.getRandomValues) {
|
||||
const { webcrypto } = await import("node:crypto");
|
||||
window.crypto = webcrypto;
|
||||
}
|
||||
|
||||
const originalWindowNostr = window.nostr;
|
||||
const originalWindowNostrTools = window.NostrTools;
|
||||
const nostrToolsModule = await import("nostr-tools");
|
||||
|
||||
window.nostr = {
|
||||
...(originalWindowNostr || {}),
|
||||
@@ -20,12 +27,43 @@ window.nostr = {
|
||||
},
|
||||
};
|
||||
|
||||
window.NostrTools = {
|
||||
...(originalWindowNostrTools || {}),
|
||||
...nostrToolsModule,
|
||||
generatePrivateKey: () => {
|
||||
const raw = nostrToolsModule.generateSecretKey();
|
||||
const bytes = raw instanceof Uint8Array ? raw : Uint8Array.from(raw || []);
|
||||
return Array.from(bytes)
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
},
|
||||
getEventHash: nostrToolsModule.getEventHash,
|
||||
signEvent: (event, priv) => {
|
||||
const signed = nostrToolsModule.finalizeEvent(event, priv);
|
||||
return signed.sig;
|
||||
},
|
||||
};
|
||||
|
||||
let activeSignerCleanup = null;
|
||||
|
||||
try {
|
||||
const { RelayPublishError } = await import("../js/nostrPublish.js");
|
||||
const { nostrClient } = await import("../js/nostr.js");
|
||||
const { nostrClient, setActiveSigner, clearActiveSigner } = await import("../js/nostr.js");
|
||||
const { subscriptions } = await import("../js/subscriptions.js");
|
||||
const { userBlocks } = await import("../js/userBlocks.js");
|
||||
|
||||
setActiveSigner({
|
||||
type: "session",
|
||||
pubkey: "f".repeat(64),
|
||||
signEvent: async (event) => ({
|
||||
...event,
|
||||
id: event?.id || `signed-${event.kind}`,
|
||||
sig: `sig-${event.kind}`,
|
||||
}),
|
||||
nip04Encrypt: async (_pubkey, plaintext) => `cipher:${plaintext}`,
|
||||
});
|
||||
activeSignerCleanup = clearActiveSigner;
|
||||
|
||||
function createRejectingPool(reason = "relay rejected") {
|
||||
const calls = [];
|
||||
return {
|
||||
@@ -245,9 +283,21 @@ try {
|
||||
|
||||
console.log("nostr publish rejection tests passed");
|
||||
} finally {
|
||||
if (typeof activeSignerCleanup === "function") {
|
||||
try {
|
||||
activeSignerCleanup();
|
||||
} catch (_) {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
}
|
||||
if (typeof originalWindowNostr === "undefined") {
|
||||
delete window.nostr;
|
||||
} else {
|
||||
window.nostr = originalWindowNostr;
|
||||
}
|
||||
if (typeof originalWindowNostrTools === "undefined") {
|
||||
delete window.NostrTools;
|
||||
} else {
|
||||
window.NostrTools = originalWindowNostrTools;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { webcrypto } from "node:crypto";
|
||||
import {
|
||||
finalizeEvent,
|
||||
generateSecretKey,
|
||||
getEventHash,
|
||||
getPublicKey,
|
||||
nip04,
|
||||
} from "nostr-tools";
|
||||
|
||||
const {
|
||||
nostrClient,
|
||||
clearActiveSigner,
|
||||
} = await import("../../js/nostr.js");
|
||||
const { accessControl } = await import("../../js/accessControl.js");
|
||||
const { WATCH_HISTORY_KIND } = await import("../../js/config.js");
|
||||
const { normalizeActorKey } = await import("../../js/nostr/watchHistory.js");
|
||||
const { VIEW_EVENT_KIND } = await import("../../js/nostr/viewEvents.js");
|
||||
const { watchHistoryService } = await import("../../js/watchHistoryService.js");
|
||||
|
||||
function finalizeWithKey(eventTemplate, secretKey, pubkeyOverride = null) {
|
||||
const tags = Array.isArray(eventTemplate?.tags)
|
||||
? eventTemplate.tags.map((tag) => Array.isArray(tag) ? [...tag] : [])
|
||||
: [];
|
||||
const base = {
|
||||
kind: Number.isFinite(eventTemplate?.kind)
|
||||
? eventTemplate.kind
|
||||
: 1,
|
||||
content: typeof eventTemplate?.content === "string"
|
||||
? eventTemplate.content
|
||||
: "",
|
||||
created_at: Number.isFinite(eventTemplate?.created_at)
|
||||
? Math.floor(eventTemplate.created_at)
|
||||
: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
pubkey:
|
||||
typeof pubkeyOverride === "string" && pubkeyOverride
|
||||
? pubkeyOverride
|
||||
: typeof eventTemplate?.pubkey === "string" && eventTemplate.pubkey
|
||||
? eventTemplate.pubkey
|
||||
: getPublicKey(secretKey),
|
||||
};
|
||||
return finalizeEvent(base, secretKey);
|
||||
}
|
||||
|
||||
test("nostr login + remote signing + publish + watch history integration", async (t) => {
|
||||
const originalWindow = globalThis.window;
|
||||
const windowRef = originalWindow || {};
|
||||
globalThis.window = windowRef;
|
||||
|
||||
if (!windowRef.crypto || !windowRef.crypto.subtle) {
|
||||
windowRef.crypto = webcrypto;
|
||||
}
|
||||
|
||||
const originalNostr = windowRef.nostr;
|
||||
const originalNostrTools = windowRef.NostrTools;
|
||||
const originalGlobalNostrTools = globalThis.NostrTools;
|
||||
const originalCanonicalTools = globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
||||
const originalToolsReady = globalThis.nostrToolsReady;
|
||||
|
||||
const originalPool = nostrClient.pool;
|
||||
const originalRelays = Array.isArray(nostrClient.relays)
|
||||
? [...nostrClient.relays]
|
||||
: nostrClient.relays;
|
||||
const originalReadRelays = Array.isArray(nostrClient.readRelays)
|
||||
? [...nostrClient.readRelays]
|
||||
: nostrClient.readRelays;
|
||||
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
|
||||
? [...nostrClient.writeRelays]
|
||||
: nostrClient.writeRelays;
|
||||
const originalPubkey = nostrClient.pubkey;
|
||||
const originalSessionActor = nostrClient.sessionActor;
|
||||
const originalWatchHistoryCacheEntries = Array.from(
|
||||
nostrClient.watchHistory?.cache?.entries?.() || [],
|
||||
);
|
||||
const originalWatchHistoryStorage = nostrClient.watchHistory?.storage;
|
||||
|
||||
const userSecret = generateSecretKey();
|
||||
const userPubkey = getPublicKey(userSecret);
|
||||
|
||||
let extensionSignCount = 0;
|
||||
let remoteSignCount = 0;
|
||||
let remoteEncryptCount = 0;
|
||||
|
||||
const publishedEvents = [];
|
||||
const poolStub = {
|
||||
publish(urls, event) {
|
||||
publishedEvents.push({
|
||||
urls: Array.isArray(urls) ? [...urls] : [],
|
||||
event,
|
||||
});
|
||||
return {
|
||||
on(eventName, handler) {
|
||||
if (eventName === "ok") {
|
||||
setTimeout(() => handler(), 0);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
};
|
||||
},
|
||||
sub() {
|
||||
return { on() {}, unsub() {} };
|
||||
},
|
||||
close() {},
|
||||
};
|
||||
|
||||
nostrClient.pool = poolStub;
|
||||
nostrClient.relays = ["wss://relay.integration"];
|
||||
nostrClient.readRelays = [...nostrClient.relays];
|
||||
nostrClient.writeRelays = [...nostrClient.relays];
|
||||
nostrClient.watchHistory.cache?.clear?.();
|
||||
nostrClient.watchHistory.fingerprints?.clear?.();
|
||||
nostrClient.watchHistory.republishTimers?.forEach?.((entry) => {
|
||||
if (entry?.timer) {
|
||||
clearTimeout(entry.timer);
|
||||
}
|
||||
});
|
||||
nostrClient.watchHistory.republishTimers?.clear?.();
|
||||
nostrClient.watchHistory.storage = null;
|
||||
watchHistoryService.resetProgress?.();
|
||||
if (typeof localStorage?.clear === "function") {
|
||||
localStorage.clear();
|
||||
}
|
||||
|
||||
const toolkit = {
|
||||
finalizeEvent,
|
||||
getEventHash,
|
||||
getPublicKey,
|
||||
nip04,
|
||||
nip19: {
|
||||
npubEncode: (hex) => `npub${hex}`,
|
||||
},
|
||||
};
|
||||
windowRef.NostrTools = toolkit;
|
||||
globalThis.NostrTools = toolkit;
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = toolkit;
|
||||
globalThis.nostrToolsReady = Promise.resolve(toolkit);
|
||||
|
||||
const userNpub = toolkit.nip19.npubEncode(userPubkey);
|
||||
const originalAccessState = {
|
||||
whitelistEnabled: accessControl.whitelistEnabled,
|
||||
whitelist: new Set(accessControl.whitelist),
|
||||
blacklist: new Set(accessControl.blacklist),
|
||||
editors: new Set(accessControl.editors),
|
||||
};
|
||||
accessControl.whitelistEnabled = false;
|
||||
accessControl.whitelist = new Set([userNpub]);
|
||||
accessControl.blacklist = new Set();
|
||||
accessControl.editors = new Set([userNpub]);
|
||||
|
||||
windowRef.nostr = {
|
||||
enable: () => Promise.resolve(),
|
||||
getPublicKey: () => Promise.resolve(userPubkey),
|
||||
signEvent: async (event) => {
|
||||
extensionSignCount += 1;
|
||||
return finalizeWithKey(event, userSecret, userPubkey);
|
||||
},
|
||||
nip04: {
|
||||
encrypt: (pubkey, plaintext) => nip04.encrypt(userSecret, pubkey, plaintext),
|
||||
decrypt: (pubkey, ciphertext) => nip04.decrypt(userSecret, pubkey, ciphertext),
|
||||
},
|
||||
};
|
||||
|
||||
t.after(() => {
|
||||
clearActiveSigner();
|
||||
accessControl.whitelistEnabled = originalAccessState.whitelistEnabled;
|
||||
accessControl.whitelist = new Set(originalAccessState.whitelist);
|
||||
accessControl.blacklist = new Set(originalAccessState.blacklist);
|
||||
accessControl.editors = new Set(originalAccessState.editors);
|
||||
nostrClient.pool = originalPool;
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.readRelays = originalReadRelays;
|
||||
nostrClient.writeRelays = originalWriteRelays;
|
||||
nostrClient.pubkey = originalPubkey;
|
||||
nostrClient.sessionActor = originalSessionActor;
|
||||
nostrClient.watchHistory.cache = new Map(originalWatchHistoryCacheEntries);
|
||||
nostrClient.watchHistory.storage = originalWatchHistoryStorage || null;
|
||||
nostrClient.watchHistory.fingerprints?.clear?.();
|
||||
nostrClient.watchHistory.republishTimers?.forEach?.((entry) => {
|
||||
if (entry?.timer) {
|
||||
clearTimeout(entry.timer);
|
||||
}
|
||||
});
|
||||
nostrClient.watchHistory.republishTimers?.clear?.();
|
||||
watchHistoryService.resetProgress?.();
|
||||
if (typeof localStorage?.clear === "function") {
|
||||
localStorage.clear();
|
||||
}
|
||||
|
||||
if (originalNostr === undefined) {
|
||||
delete windowRef.nostr;
|
||||
} else {
|
||||
windowRef.nostr = originalNostr;
|
||||
}
|
||||
|
||||
if (originalNostrTools === undefined) {
|
||||
delete windowRef.NostrTools;
|
||||
} else {
|
||||
windowRef.NostrTools = originalNostrTools;
|
||||
}
|
||||
|
||||
if (originalGlobalNostrTools === undefined) {
|
||||
delete globalThis.NostrTools;
|
||||
} else {
|
||||
globalThis.NostrTools = originalGlobalNostrTools;
|
||||
}
|
||||
|
||||
if (originalCanonicalTools === undefined) {
|
||||
delete globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
||||
} else {
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = originalCanonicalTools;
|
||||
}
|
||||
|
||||
if (originalToolsReady === undefined) {
|
||||
delete globalThis.nostrToolsReady;
|
||||
} else {
|
||||
globalThis.nostrToolsReady = originalToolsReady;
|
||||
}
|
||||
|
||||
if (!originalWindow) {
|
||||
delete globalThis.window;
|
||||
}
|
||||
});
|
||||
|
||||
const loginResult = await nostrClient.login();
|
||||
assert.equal(loginResult, userPubkey);
|
||||
extensionSignCount = 0;
|
||||
|
||||
const remoteSigner = {
|
||||
type: "nip46",
|
||||
pubkey: userPubkey,
|
||||
async signEvent(event) {
|
||||
remoteSignCount += 1;
|
||||
return finalizeWithKey(event, userSecret, userPubkey);
|
||||
},
|
||||
async nip04Encrypt(pubkey, plaintext) {
|
||||
remoteEncryptCount += 1;
|
||||
return nip04.encrypt(userSecret, pubkey, plaintext);
|
||||
},
|
||||
nip04Decrypt(pubkey, ciphertext) {
|
||||
return nip04.decrypt(userSecret, pubkey, ciphertext);
|
||||
},
|
||||
};
|
||||
|
||||
nostrClient.installNip46Client(
|
||||
{
|
||||
getActiveSigner: () => remoteSigner,
|
||||
destroy() {},
|
||||
},
|
||||
{ userPubkey },
|
||||
);
|
||||
|
||||
const viewPointer = {
|
||||
type: "e",
|
||||
value: "view-event-integration",
|
||||
watchedAt: 1_700_000_123,
|
||||
};
|
||||
|
||||
const viewResult = await nostrClient.recordVideoView(viewPointer, {
|
||||
created_at: 1_700_000_125,
|
||||
});
|
||||
assert.equal(viewResult.ok, true);
|
||||
|
||||
const publishResult = await nostrClient.publishVideo(
|
||||
{
|
||||
legacyFormData: {
|
||||
title: "Integration Remote Video",
|
||||
description: "remote signer flow",
|
||||
magnet: "magnet:?xt=urn:btih:REMOTEINTEGRATION123456789",
|
||||
mode: "live",
|
||||
},
|
||||
},
|
||||
userPubkey,
|
||||
);
|
||||
assert.equal(typeof publishResult?.id, "string");
|
||||
|
||||
const snapshotItems = [
|
||||
{ type: "e", value: viewPointer.value, watchedAt: viewPointer.watchedAt },
|
||||
];
|
||||
const snapshotResult = await nostrClient.publishWatchHistorySnapshot(
|
||||
snapshotItems,
|
||||
{ actorPubkey: userPubkey, snapshotId: "integration-snapshot" },
|
||||
);
|
||||
assert.equal(snapshotResult.ok, true);
|
||||
|
||||
assert.equal(extensionSignCount, 0, "extension signer should not handle publish operations");
|
||||
assert(remoteSignCount > 0, "remote signer should sign nostr events");
|
||||
assert(remoteEncryptCount > 0, "remote signer should encrypt watch history payloads");
|
||||
|
||||
const publishedKinds = publishedEvents.map((entry) => entry.event?.kind);
|
||||
assert(
|
||||
publishedKinds.some((kind) => kind === VIEW_EVENT_KIND),
|
||||
"view events should be published",
|
||||
);
|
||||
assert(
|
||||
publishedKinds.some((kind) => kind === WATCH_HISTORY_KIND),
|
||||
"watch history events should be published",
|
||||
);
|
||||
|
||||
const actorKey = normalizeActorKey(userPubkey);
|
||||
const cachedEntry = nostrClient.watchHistory.cache.get(actorKey);
|
||||
assert.ok(cachedEntry, "watch history cache should contain the published snapshot");
|
||||
assert.equal(cachedEntry.items.length, 1);
|
||||
assert.equal(cachedEntry.items[0]?.value, viewPointer.value);
|
||||
|
||||
const latest = await watchHistoryService.loadLatest(userPubkey);
|
||||
assert(Array.isArray(latest), "watch history service should resolve an array");
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
@@ -304,3 +305,136 @@ test("fetchWatchHistory falls back to pointer payload when nip04 decrypt fails",
|
||||
manager.clear();
|
||||
}
|
||||
});
|
||||
|
||||
test("publishWatchHistorySnapshot uses injected nostr-tools helpers when signer cannot encrypt", async () => {
|
||||
const actorPubkey = "f".repeat(64);
|
||||
const sessionPrivateKey = "a".repeat(64);
|
||||
let encryptCalls = 0;
|
||||
const toolkit = {
|
||||
nip04: {
|
||||
encrypt: async (_priv, _pub, plaintext) => {
|
||||
encryptCalls += 1;
|
||||
return `encrypted:${plaintext}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const manager = createWatchHistoryManager({
|
||||
getActivePubkey: () => actorPubkey,
|
||||
getSessionActor: () => ({ pubkey: actorPubkey, privateKey: sessionPrivateKey }),
|
||||
ensureSessionActor: async () => actorPubkey,
|
||||
resolveActiveSigner: () => null,
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
ensureExtensionPermissions: async () => ({ ok: true }),
|
||||
ensureNostrTools: async () => toolkit,
|
||||
getCachedNostrTools: () => toolkit,
|
||||
signEventWithPrivateKey: (event) => ({
|
||||
...event,
|
||||
id: `signed-${event.kind}`,
|
||||
sig: `sig-${event.kind}`,
|
||||
}),
|
||||
getReadRelays: () => ["wss://relay.integration"],
|
||||
getWriteRelays: () => ["wss://relay.integration"],
|
||||
getRelayFallback: () => ["wss://relay.integration"],
|
||||
getPool: () => ({
|
||||
publish: () => ({
|
||||
on(eventName, handler) {
|
||||
if (eventName === "ok") {
|
||||
handler();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await manager.publishSnapshot(
|
||||
[{ type: "e", value: "pointer", watchedAt: 123 }],
|
||||
{ actorPubkey, snapshotId: "snapshot-injected" },
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
result.ok,
|
||||
true,
|
||||
"snapshot publish should succeed with injected toolkit helpers",
|
||||
);
|
||||
assert(
|
||||
encryptCalls > 0,
|
||||
"fallback nostr-tools encrypt helper should be invoked",
|
||||
);
|
||||
} finally {
|
||||
manager.clear();
|
||||
}
|
||||
});
|
||||
|
||||
test("publishWatchHistorySnapshot caches successful snapshot results", async () => {
|
||||
const actorPubkey = "c".repeat(64);
|
||||
const sessionPrivateKey = "s".repeat(64);
|
||||
const relayUrls = ["wss://relay.cache"];
|
||||
const publishCalls = [];
|
||||
|
||||
const manager = createWatchHistoryManager({
|
||||
getActivePubkey: () => actorPubkey,
|
||||
getSessionActor: () => ({ pubkey: actorPubkey, privateKey: sessionPrivateKey }),
|
||||
ensureSessionActor: async () => actorPubkey,
|
||||
resolveActiveSigner: () => null,
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
signEventWithPrivateKey: async (event, key) => ({
|
||||
...event,
|
||||
id: `${event.kind}:${event.created_at}:${event.content?.length || 0}`,
|
||||
sig: `sig-${key}`,
|
||||
}),
|
||||
ensureNostrTools: async () => ({
|
||||
nip04: {
|
||||
encrypt: async (priv, pub, plaintext) => `enc:${priv}:${pub}:${plaintext}`,
|
||||
},
|
||||
}),
|
||||
getWriteRelays: () => relayUrls,
|
||||
getRelayFallback: () => relayUrls,
|
||||
getPool: () => ({
|
||||
publish(urls, event) {
|
||||
publishCalls.push({ urls, event });
|
||||
return {
|
||||
on(eventName, handler) {
|
||||
if (eventName === "ok") {
|
||||
setTimeout(handler, 0);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
const items = [{ type: "e", value: "cached-pointer", watchedAt: 234 }];
|
||||
const result = await manager.publishSnapshot(items, {
|
||||
actorPubkey,
|
||||
relays: relayUrls,
|
||||
snapshotId: "snapshot-cache",
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true, "snapshot publish should succeed");
|
||||
assert(publishCalls.length > 0, "publish helper should be invoked");
|
||||
|
||||
const actorKey = actorPubkey.toLowerCase();
|
||||
const cacheEntry = manager.cache.get(actorKey);
|
||||
assert.ok(cacheEntry, "cache entry should be stored for the actor");
|
||||
assert.equal(cacheEntry.items.length, 1, "cache should retain canonical items");
|
||||
assert.equal(cacheEntry.items[0]?.value, "cached-pointer");
|
||||
assert.equal(cacheEntry.snapshotId, result.snapshotId);
|
||||
assert.deepStrictEqual(cacheEntry.items, result.items);
|
||||
|
||||
const storage = manager.getStorage();
|
||||
assert.ok(storage.actors[actorKey], "storage should persist the cache entry");
|
||||
assert.equal(storage.actors[actorKey].snapshotId, result.snapshotId);
|
||||
assert.equal(storage.actors[actorKey].items.length, 1);
|
||||
|
||||
const fingerprint = manager.fingerprints.get(actorKey);
|
||||
assert.ok(fingerprint, "fingerprint cache should be populated");
|
||||
assert.equal(fingerprint, cacheEntry.fingerprint);
|
||||
} finally {
|
||||
manager.clear();
|
||||
}
|
||||
});
|
||||
@@ -17,7 +17,10 @@ const {
|
||||
getWatchHistoryV2Enabled,
|
||||
setWatchHistoryV2Enabled,
|
||||
} = await import("../js/constants.js");
|
||||
const { nostrClient } = await import("../js/nostr.js");
|
||||
const { nostrClient, setActiveSigner, getActiveSigner, clearActiveSigner } = await import(
|
||||
"../js/nostr.js",
|
||||
);
|
||||
const { rememberNostrTools } = await import("../js/nostr/toolkit.js");
|
||||
const { normalizeActorKey } = await import("../js/nostr/watchHistory.js");
|
||||
const { watchHistoryService } = await import("../js/watchHistoryService.js");
|
||||
const { buildHistoryCard } = await import("../js/historyView.js");
|
||||
@@ -39,6 +42,10 @@ setWatchHistoryV2Enabled(true);
|
||||
|
||||
const originalWindowNostr = window.nostr;
|
||||
const originalNostrTools = window.NostrTools || {};
|
||||
const originalWatchHistoryEnsureTools =
|
||||
nostrClient.watchHistory?.deps?.ensureNostrTools || null;
|
||||
const originalWatchHistoryGetCachedTools =
|
||||
nostrClient.watchHistory?.deps?.getCachedNostrTools || null;
|
||||
const originalPool = nostrClient.pool;
|
||||
const originalRelays = Array.isArray(nostrClient.relays)
|
||||
? [...nostrClient.relays]
|
||||
@@ -302,19 +309,30 @@ nostrClient.relays = ["wss://relay.test"];
|
||||
nostrClient.readRelays = ["wss://relay.test"];
|
||||
nostrClient.writeRelays = ["wss://relay.test"];
|
||||
|
||||
function installSessionCrypto({ privateKey }) {
|
||||
async function installSessionCrypto({ privateKey }) {
|
||||
const original = window.NostrTools || {};
|
||||
let encryptCalls = 0;
|
||||
let decryptCalls = 0;
|
||||
const { createHash, randomBytes } = await import("node:crypto");
|
||||
const deriveHex = (input) =>
|
||||
createHash("sha256").update(String(input ?? ""), "utf8").digest("hex");
|
||||
const previousEnsure =
|
||||
nostrClient.watchHistory?.deps?.ensureNostrTools || null;
|
||||
const previousGetCached =
|
||||
nostrClient.watchHistory?.deps?.getCachedNostrTools || null;
|
||||
window.NostrTools = {
|
||||
...original,
|
||||
generatePrivateKey: () => randomBytes(32).toString("hex"),
|
||||
getPublicKey: (secret) => {
|
||||
const normalized = typeof secret === "string" ? secret.trim() : "";
|
||||
if (!normalized) {
|
||||
throw new Error("missing-secret");
|
||||
}
|
||||
return deriveHex(`pub:${normalized}`);
|
||||
},
|
||||
getEventHash: (event) =>
|
||||
`hash-${event.kind}-${event.created_at}-${event.tags?.length || 0}`,
|
||||
signEvent: (event, key) => ({
|
||||
...event,
|
||||
id: `signed-${event.kind}-${event.created_at}-${event.tags?.length || 0}`,
|
||||
sig: `sig-${key}`,
|
||||
}),
|
||||
signEvent: (_event, key) => `sig-${key}`,
|
||||
nip04: {
|
||||
...(original.nip04 || {}),
|
||||
encrypt: async (secret, pub, plaintext) => {
|
||||
@@ -333,9 +351,28 @@ function installSessionCrypto({ privateKey }) {
|
||||
},
|
||||
},
|
||||
};
|
||||
rememberNostrTools(window.NostrTools);
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = window.NostrTools;
|
||||
globalThis.nostrToolsReady = Promise.resolve(window.NostrTools);
|
||||
if (nostrClient.watchHistory?.deps) {
|
||||
nostrClient.watchHistory.deps.ensureNostrTools = async () => window.NostrTools;
|
||||
nostrClient.watchHistory.deps.getCachedNostrTools = () => window.NostrTools;
|
||||
}
|
||||
return {
|
||||
restore() {
|
||||
window.NostrTools = original;
|
||||
if (nostrClient.watchHistory?.deps) {
|
||||
if (previousEnsure) {
|
||||
nostrClient.watchHistory.deps.ensureNostrTools = previousEnsure;
|
||||
} else {
|
||||
delete nostrClient.watchHistory.deps.ensureNostrTools;
|
||||
}
|
||||
if (previousGetCached) {
|
||||
nostrClient.watchHistory.deps.getCachedNostrTools = previousGetCached;
|
||||
} else {
|
||||
delete nostrClient.watchHistory.deps.getCachedNostrTools;
|
||||
}
|
||||
}
|
||||
},
|
||||
getEncryptCalls: () => encryptCalls,
|
||||
getDecryptCalls: () => decryptCalls,
|
||||
@@ -396,8 +433,24 @@ function installExtensionCrypto({ actor }) {
|
||||
},
|
||||
},
|
||||
};
|
||||
rememberNostrTools(window.NostrTools);
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = window.NostrTools;
|
||||
globalThis.nostrToolsReady = Promise.resolve(window.NostrTools);
|
||||
const previousSigner = getActiveSigner();
|
||||
setActiveSigner({
|
||||
type: "extension",
|
||||
pubkey: actor,
|
||||
signEvent: window.nostr.signEvent,
|
||||
nip04Encrypt: window.nostr.nip04.encrypt,
|
||||
nip04Decrypt: window.nostr.nip04.decrypt,
|
||||
});
|
||||
return {
|
||||
restore() {
|
||||
if (previousSigner) {
|
||||
setActiveSigner(previousSigner);
|
||||
} else {
|
||||
clearActiveSigner();
|
||||
}
|
||||
window.nostr = originalNostr;
|
||||
window.NostrTools = originalTools;
|
||||
},
|
||||
@@ -644,7 +697,7 @@ async function testFetchWatchHistoryExtensionDecryptsHexAndNpub() {
|
||||
const runVariant = async (label, actorInput, pubkeyInput) => {
|
||||
await publishEvents();
|
||||
|
||||
const sessionCrypto = installSessionCrypto({ privateKey: `session-${label}` });
|
||||
const sessionCrypto = await installSessionCrypto({ privateKey: `session-${label}` });
|
||||
const extensionCrypto = installExtensionCrypto({ actor: actorHex });
|
||||
|
||||
const previousEnsure = nostrClient.ensureSessionActor;
|
||||
@@ -727,7 +780,7 @@ async function testPublishSnapshotCanonicalizationAndChunking() {
|
||||
poolHarness.setResolver(() => ({ ok: true }));
|
||||
|
||||
const actor = "session-pubkey";
|
||||
const restoreCrypto = installSessionCrypto({ privateKey: "session-priv" });
|
||||
const restoreCrypto = await installSessionCrypto({ privateKey: "session-priv" });
|
||||
|
||||
const originalEnsure = nostrClient.ensureSessionActor;
|
||||
const originalSession = nostrClient.sessionActor;
|
||||
@@ -756,6 +809,9 @@ async function testPublishSnapshotCanonicalizationAndChunking() {
|
||||
{ type: "e", value: "pointer-small", watchedAt: 80 },
|
||||
];
|
||||
|
||||
const { getCachedNostrTools } = await import("../js/nostr/toolkit.js");
|
||||
getCachedNostrTools();
|
||||
|
||||
const firstResult = await nostrClient.publishWatchHistorySnapshot(rawItems, {
|
||||
actorPubkey: actor,
|
||||
snapshotId: "session-snapshot",
|
||||
@@ -890,7 +946,7 @@ async function testPublishSnapshotUsesExtensionCrypto() {
|
||||
poolHarness.setResolver(() => ({ ok: true }));
|
||||
|
||||
const actor = "ext-pubkey";
|
||||
const sessionRestore = installSessionCrypto({ privateKey: "session-priv" });
|
||||
const sessionRestore = await installSessionCrypto({ privateKey: "session-priv" });
|
||||
const extension = installExtensionCrypto({ actor });
|
||||
const originalEnsure = nostrClient.ensureSessionActor;
|
||||
const originalSession = nostrClient.sessionActor;
|
||||
@@ -1013,7 +1069,7 @@ async function testPublishSnapshotFailureRetry() {
|
||||
nostrClient.watchHistoryLastCreatedAt = 0;
|
||||
|
||||
const actor = "retry-actor";
|
||||
const restoreCrypto = installSessionCrypto({ privateKey: "retry-priv" });
|
||||
const restoreCrypto = await installSessionCrypto({ privateKey: "retry-priv" });
|
||||
const originalEnsure = nostrClient.ensureSessionActor;
|
||||
const originalSession = nostrClient.sessionActor;
|
||||
const originalPub = nostrClient.pubkey;
|
||||
@@ -1070,7 +1126,7 @@ async function testWatchHistoryPartialRelayRetry() {
|
||||
nostrClient.watchHistoryLastCreatedAt = 0;
|
||||
|
||||
const actor = "partial-actor";
|
||||
const restoreCrypto = installSessionCrypto({ privateKey: "partial-priv" });
|
||||
const restoreCrypto = await installSessionCrypto({ privateKey: "partial-priv" });
|
||||
const originalEnsure = nostrClient.ensureSessionActor;
|
||||
const originalSession = nostrClient.sessionActor;
|
||||
const originalPub = nostrClient.pubkey;
|
||||
@@ -1270,10 +1326,11 @@ async function testWatchHistoryPartialRelayRetry() {
|
||||
}
|
||||
|
||||
async function testResolveWatchHistoryBatchingWindow() {
|
||||
const actor = "batch-window-actor";
|
||||
const actor = "b".repeat(64);
|
||||
const originalEnsure = nostrClient.ensureSessionActor;
|
||||
const originalPub = nostrClient.pubkey;
|
||||
const originalFetch = nostrClient.fetchWatchHistory;
|
||||
const originalManagerFetch = nostrClient.watchHistory.fetch;
|
||||
const originalCache = nostrClient.watchHistoryCache;
|
||||
const originalStorage = nostrClient.watchHistoryStorage;
|
||||
|
||||
@@ -1289,11 +1346,13 @@ async function testResolveWatchHistoryBatchingWindow() {
|
||||
watchedAt: 1_700_100_000 + index,
|
||||
}));
|
||||
|
||||
nostrClient.fetchWatchHistory = async () => ({
|
||||
const fetchResult = {
|
||||
items: syntheticItems.map((item) => ({ ...item })),
|
||||
snapshotId: "batch-snapshot",
|
||||
pointerEvent: null,
|
||||
});
|
||||
};
|
||||
nostrClient.fetchWatchHistory = async () => fetchResult;
|
||||
nostrClient.watchHistory.fetch = async () => fetchResult;
|
||||
|
||||
const resolved = await nostrClient.resolveWatchHistory(actor, {
|
||||
forceRefresh: true,
|
||||
@@ -1332,6 +1391,7 @@ async function testResolveWatchHistoryBatchingWindow() {
|
||||
nostrClient.ensureSessionActor = originalEnsure;
|
||||
nostrClient.pubkey = originalPub;
|
||||
nostrClient.fetchWatchHistory = originalFetch;
|
||||
nostrClient.watchHistory.fetch = originalManagerFetch;
|
||||
nostrClient.watchHistoryCache = originalCache;
|
||||
nostrClient.watchHistoryStorage = originalStorage;
|
||||
}
|
||||
@@ -1351,7 +1411,7 @@ async function testWatchHistoryServiceIntegration() {
|
||||
infoHash: "89abcdef0123456789abcdef0123456789abcdef",
|
||||
legacyInfoHash: "89abcdef0123456789abcdef0123456789abcdef",
|
||||
};
|
||||
const restoreCrypto = installSessionCrypto({ privateKey: "service-priv" });
|
||||
const restoreCrypto = await installSessionCrypto({ privateKey: "service-priv" });
|
||||
const originalEnsure = nostrClient.ensureSessionActor;
|
||||
const originalSession = nostrClient.sessionActor;
|
||||
const originalPub = nostrClient.pubkey;
|
||||
@@ -2143,6 +2203,20 @@ console.log("watch-history.test.mjs completed successfully");
|
||||
|
||||
window.nostr = originalWindowNostr;
|
||||
window.NostrTools = originalNostrTools;
|
||||
if (nostrClient.watchHistory?.deps) {
|
||||
if (originalWatchHistoryEnsureTools) {
|
||||
nostrClient.watchHistory.deps.ensureNostrTools =
|
||||
originalWatchHistoryEnsureTools;
|
||||
} else {
|
||||
delete nostrClient.watchHistory.deps.ensureNostrTools;
|
||||
}
|
||||
if (originalWatchHistoryGetCachedTools) {
|
||||
nostrClient.watchHistory.deps.getCachedNostrTools =
|
||||
originalWatchHistoryGetCachedTools;
|
||||
} else {
|
||||
delete nostrClient.watchHistory.deps.getCachedNostrTools;
|
||||
}
|
||||
}
|
||||
nostrClient.pool = originalPool;
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.readRelays = originalReadRelays;
|
||||
|
||||
Reference in new issue
Block a user