This commit is contained in:
thePR0M3TH3AN committed 2026-02-21 15:46:35 -05:00
1 parent 1165099397
commit 00f96bbe0e
3 files changed
+260 -9

No files matched your search

+66
View File
@@ -2,6 +2,10 @@
import watchHistoryService from "./watchHistoryService.js";
import { nostrClient } from "./nostrClientFacade.js";
import {
createFeedEngine,
createWatchHistoryFeedDefinition,
} from "./feedEngine/index.js";
import {
updateWatchHistoryListWithDefaultClient as updateWatchHistoryList,
} from "./nostrWatchHistoryFacade.js";
@@ -86,6 +90,21 @@ function getAppInstance() {
return getApplication();
}
let localWatchHistoryFallbackEngine = null;
function getLocalWatchHistoryFallbackEngine() {
if (localWatchHistoryFallbackEngine) {
return localWatchHistoryFallbackEngine;
}
const engine = createFeedEngine();
engine.registerFeed(
"watch-history",
createWatchHistoryFeedDefinition({ service: watchHistoryService }),
);
localWatchHistoryFallbackEngine = engine;
return localWatchHistoryFallbackEngine;
}
function buildWatchHistoryFeedRuntime({
actor,
cursor = 0,
@@ -228,8 +247,17 @@ function normalizeHistoryItems(rawItems) {
if (!key) {
continue;
}
const baseMetadata =
candidate &&
typeof candidate === "object" &&
candidate.metadata &&
typeof candidate.metadata === "object"
? { ...candidate.metadata }
: {};
const watchedAtRaw = Number.isFinite(candidate?.watchedAt)
? candidate.watchedAt
: Number.isFinite(baseMetadata?.watchedAt)
? baseMetadata.watchedAt
: Number.isFinite(candidate?.timestamp)
? candidate.timestamp
: Number.isFinite(pointer?.watchedAt)
@@ -237,10 +265,23 @@ function normalizeHistoryItems(rawItems) {
: null;
const watchedAt =
watchedAtRaw !== null ? Math.max(0, Math.floor(watchedAtRaw)) : 0;
const video = candidate?.video || baseMetadata.video || null;
const profile = candidate?.profile || baseMetadata.profile || null;
const metadata = {
...baseMetadata,
pointerKey: key,
watchedAt: Number.isFinite(baseMetadata?.watchedAt)
? baseMetadata.watchedAt
: watchedAt || null,
video: video || null,
profile: profile || null,
};
normalized.push({
pointer,
pointerKey: key,
watchedAt,
video: metadata.video,
metadata,
raw: candidate
});
}
@@ -1439,6 +1480,31 @@ export function createWatchHistoryRenderer(config = {}) {
);
}
}
try {
const runtime = buildWatchHistoryFeedRuntime({
actor: actorInput,
cursor,
forceRefresh,
});
const fallbackEngine = getLocalWatchHistoryFallbackEngine();
const hydrated = await fallbackEngine.run("watch-history", { runtime });
return {
...(hydrated && typeof hydrated === "object" ? hydrated : {}),
metadata: {
...(hydrated?.metadata && typeof hydrated.metadata === "object"
? hydrated.metadata
: {}),
engine: "local-feed-fallback",
},
};
} catch (fallbackError) {
if (isDevEnv) {
devLogger.warn(
"[historyView] local feed fallback failed; using raw service data.",
fallbackError,
);
}
}
const items = await watchHistoryService.loadLatest(actorInput, {
allowStale: !forceRefresh,
forceRefresh,
+133 -9
View File
@@ -693,6 +693,101 @@ async function resolveNostrToolkit(deps = {}, cacheRef = null) {
return cacheRef?.current || null;
}
async function decryptWatchHistoryCiphertext(
{
event = null,
ciphertext = "",
actorKey = "",
decryptSigner = null,
sessionPrivateKey = "",
deps = {},
} = {},
toolkitCacheRef = null,
) {
const normalizedCiphertext =
typeof ciphertext === "string" ? ciphertext.trim() : "";
if (!normalizedCiphertext || !actorKey) {
return "";
}
const decryptors = new Map();
const registerDecryptor = (scheme, decrypt) => {
if (typeof scheme !== "string" || !scheme || typeof decrypt !== "function") {
return;
}
if (!decryptors.has(scheme)) {
decryptors.set(scheme, decrypt);
}
};
if (decryptSigner && typeof decryptSigner === "object") {
if (typeof decryptSigner.nip44Decrypt === "function") {
registerDecryptor("nip44", (value) =>
decryptSigner.nip44Decrypt(actorKey, value),
);
}
if (typeof decryptSigner.nip04Decrypt === "function") {
registerDecryptor("nip04", (value) =>
decryptSigner.nip04Decrypt(actorKey, value),
);
}
}
const normalizedSessionPrivateKey =
typeof sessionPrivateKey === "string" ? sessionPrivateKey.trim().toLowerCase() : "";
const canUseSessionPrivateKey = /^[0-9a-f]{64}$/.test(normalizedSessionPrivateKey);
if (canUseSessionPrivateKey) {
const tools = await resolveNostrToolkit(deps, toolkitCacheRef);
if (tools?.nip04 && typeof tools.nip04.decrypt === "function") {
registerDecryptor("nip04", (value) =>
tools.nip04.decrypt(normalizedSessionPrivateKey, actorKey, value),
);
}
const nip44Suite = createNip44CipherSuite(
tools,
normalizedSessionPrivateKey,
actorKey,
);
if (nip44Suite?.v2 && typeof nip44Suite.v2.decrypt === "function") {
registerDecryptor("nip44_v2", (value) => nip44Suite.v2.decrypt(value));
}
if (nip44Suite?.legacy && typeof nip44Suite.legacy.decrypt === "function") {
registerDecryptor("nip44", (value) => nip44Suite.legacy.decrypt(value));
}
}
if (!decryptors.size) {
return "";
}
const availableSchemes = Array.from(decryptors.keys());
const order = determineWatchHistoryDecryptionOrder(
event,
normalizedCiphertext,
availableSchemes,
);
for (const scheme of order) {
const decrypt = decryptors.get(scheme);
if (typeof decrypt !== "function") {
continue;
}
try {
const plaintext = await decrypt(normalizedCiphertext);
if (typeof plaintext === "string" && plaintext.trim()) {
return plaintext;
}
} catch (error) {
devLogger.warn(
`[nostr] Failed to decrypt watch history payload with ${scheme}:`,
error,
);
}
}
return "";
}
function mergeWatchHistoryItemsWithFallback(parsed, fallbackItems) {
if (!parsed || typeof parsed !== "object") {
return {
@@ -1900,8 +1995,13 @@ class WatchHistoryManager {
}
}
const sessionPrivateKey =
typeof sessionActor?.privateKey === "string" ? sessionActor.privateKey : "";
const canAttemptDecrypt = Boolean(decryptSigner) || Boolean(sessionPrivateKey);
const toolkitCacheRef = { current: null };
let decryptedItems = [];
if (chunkIdentifiers.length && decryptSigner) {
if (chunkIdentifiers.length && canAttemptDecrypt) {
try {
const results = await pool.list(readRelays, [
{
@@ -1920,13 +2020,19 @@ class WatchHistoryManager {
if (!ciphertext) {
return [];
}
let plaintext = "";
if (typeof decryptSigner.nip04Decrypt === "function") {
try {
plaintext = await decryptSigner.nip04Decrypt(actorKey, ciphertext);
} catch (error) {
devLogger.warn("[nostr] Failed to decrypt watch history chunk:", error);
}
let plaintext = looksLikeJsonStructure(ciphertext) ? ciphertext : "";
if (!plaintext) {
plaintext = await decryptWatchHistoryCiphertext(
{
event,
ciphertext,
actorKey,
decryptSigner,
sessionPrivateKey,
deps: this.deps,
},
toolkitCacheRef,
);
}
if (!plaintext) {
return [];
@@ -1962,9 +2068,27 @@ class WatchHistoryManager {
const dTag = event.tags.find(t => t[0] === 'd');
const eventSnapshotId = dTag ? dTag[1] : "";
let payloadContent = ciphertext;
if (!looksLikeJsonStructure(payloadContent) && canAttemptDecrypt) {
const decrypted = await decryptWatchHistoryCiphertext(
{
event,
ciphertext: payloadContent,
actorKey,
decryptSigner,
sessionPrivateKey,
deps: this.deps,
},
toolkitCacheRef,
);
if (decrypted) {
payloadContent = decrypted;
}
}
// Try parsing as plaintext first
const payload = parseWatchHistoryContentWithFallback(
ciphertext,
payloadContent,
fallbackPointers,
{
version: 0,
+61
View File
@@ -311,6 +311,67 @@ test("fetchWatchHistory falls back to pointer payload when nip04 decrypt fails",
}
});
test("fetchWatchHistory decrypts encrypted pointer events with nip44 signer support", async () => {
const actorHex = "5".repeat(64);
const ciphertext = "nip44-ciphertext";
const decryptCalls = [];
const pointerEvent = {
id: "pointer-nip44",
pubkey: actorHex,
created_at: 1_700_200_000,
content: ciphertext,
tags: [
["encrypted", "nip44"],
["a", "30078:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:fallback"],
["d", "watch-history-month"],
],
};
const signer = {
nip44Decrypt: async (pubkey, payload) => {
decryptCalls.push({ pubkey, payload });
assert.equal(pubkey, actorHex, "nip44 decrypt should target the actor pubkey");
assert.equal(payload, ciphertext, "nip44 decrypt should receive encrypted content");
return JSON.stringify({
version: 2,
items: [{ type: "e", value: "decrypted-from-pointer", watchedAt: 33 }],
});
},
};
const pool = {
async list() {
return [pointerEvent];
},
};
const manager = createWatchHistoryManager({
getActivePubkey: () => actorHex,
resolveActiveSigner: () => signer,
shouldRequestExtensionPermissions: () => false,
getPool: () => pool,
getReadRelays: () => ["wss://relay.example"],
});
try {
const result = await fetchWatchHistory(manager, actorHex, { forceRefresh: true });
assert.equal(decryptCalls.length, 1, "nip44 decrypt should be attempted once");
assert.deepStrictEqual(
result.items.map((item) => item.value),
["decrypted-from-pointer"],
"decrypted pointer event payload should be used",
);
assert.equal(
result.items[0]?.watchedAt,
33,
"watched timestamp from decrypted payload should be preserved",
);
} finally {
manager.clear();
}
});
test("publishWatchHistorySnapshot uses injected nostr-tools helpers when signer cannot encrypt", async () => {
const actorPubkey = "f".repeat(64);
const sessionPrivateKey = "a".repeat(64);