diff --git a/js/nostr/client.js b/js/nostr/client.js index 8df98750..4dad63b2 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -123,6 +123,7 @@ import { encryptSessionPrivateKey, persistSessionActor as persistSessionActorEntry, readStoredSessionActorEntry, + isSessionActor, } from "./sessionActor.js"; import { HEX64_REGEX, @@ -3646,6 +3647,13 @@ export class NostrClient { ); } async publishWatchHistorySnapshot(rawItems, options = {}) { + if (isSessionActor(this)) { + const error = new Error( + "Publishing watch history is not allowed for session actors." + ); + error.code = "session-actor-publish-blocked"; + throw error; + } return publishWatchHistorySnapshotWithManager( this.watchHistory, rawItems, @@ -3653,6 +3661,13 @@ export class NostrClient { ); } async updateWatchHistoryList(rawItems = [], options = {}) { + if (isSessionActor(this)) { + const error = new Error( + "Publishing watch history is not allowed for session actors." + ); + error.code = "session-actor-publish-blocked"; + throw error; + } return updateWatchHistoryListWithManager( this.watchHistory, rawItems, @@ -3660,6 +3675,13 @@ export class NostrClient { ); } async removeWatchHistoryItem(pointerInput, options = {}) { + if (isSessionActor(this)) { + const error = new Error( + "Publishing watch history is not allowed for session actors." + ); + error.code = "session-actor-publish-blocked"; + throw error; + } return removeWatchHistoryItemWithManager( this.watchHistory, pointerInput, diff --git a/js/nostr/commentEvents.js b/js/nostr/commentEvents.js index 687cb05b..d108f15f 100644 --- a/js/nostr/commentEvents.js +++ b/js/nostr/commentEvents.js @@ -10,6 +10,7 @@ import { normalizePointerInput } from "./watchHistory.js"; import { devLogger, userLogger } from "../utils/logger.js"; import { LRUCache } from "../utils/lruCache.js"; import { CACHE_POLICIES } from "./cachePolicies.js"; +import { isSessionActor } from "./sessionActor.js"; const COMMENT_EVENT_SCHEMA = getNostrEventSchema(NOTE_TYPES.VIDEO_COMMENT); const CACHE_POLICY = CACHE_POLICIES[NOTE_TYPES.VIDEO_COMMENT]; @@ -1114,6 +1115,14 @@ export async function publishComment( return { ok: false, error: "nostr-uninitialized" }; } + if (isSessionActor(client)) { + const error = new Error( + "Publishing comments is not allowed for session actors." + ); + error.code = "session-actor-publish-blocked"; + return { ok: false, error }; + } + const descriptor = normalizeCommentTarget(targetInput, options); if (!descriptor) { return { ok: false, error: "invalid-target" }; diff --git a/js/nostr/sessionActor.js b/js/nostr/sessionActor.js index 02a5ea15..b8374673 100644 --- a/js/nostr/sessionActor.js +++ b/js/nostr/sessionActor.js @@ -380,6 +380,11 @@ export function clearStoredSessionActor() { } } +export function isSessionActor(nostrClient) { + const sa = nostrClient?.sessionActor; + return !!sa && sa.source !== "nsec"; +} + export const __testExports = { arrayBufferToBase64, base64ToUint8Array, diff --git a/js/nostr/viewEvents.js b/js/nostr/viewEvents.js index 872e80e1..4a1b4068 100644 --- a/js/nostr/viewEvents.js +++ b/js/nostr/viewEvents.js @@ -150,19 +150,30 @@ function generateViewEventDedupeTag(actorPubkey, pointer, createdAtSeconds) { return `${scope}:${normalizedActor}:${timestamp}:${entropy}`; } -export function hasRecentViewPublish(scope, bucketIndex) { +export function hasRecentViewPublish(scope, bucketIndex, actorPubkey) { if (!scope || !Number.isFinite(bucketIndex)) { return false; } const windowMs = getViewEventGuardWindowMs(); const now = Date.now(); - const entry = viewEventPublishMemory.get(scope); + const safeActor = + typeof actorPubkey === "string" && actorPubkey.trim() + ? actorPubkey.trim().toLowerCase() + : "anon"; + + let actorMemory = viewEventPublishMemory.get(safeActor); + if (!actorMemory) { + actorMemory = new Map(); + viewEventPublishMemory.set(safeActor, actorMemory); + } + + const entry = actorMemory.get(scope); if (entry) { const age = now - Number(entry.seenAt); if (!Number.isFinite(entry.seenAt) || age >= windowMs) { - viewEventPublishMemory.delete(scope); + actorMemory.delete(scope); } else if (Number(entry.bucket) === bucketIndex) { return true; } @@ -172,7 +183,7 @@ export function hasRecentViewPublish(scope, bucketIndex) { return false; } - const storageKey = `${VIEW_EVENT_GUARD_PREFIX}:${scope}`; + const storageKey = `${VIEW_EVENT_GUARD_PREFIX}:${safeActor}:${scope}`; let rawValue = null; try { rawValue = localStorage.getItem(storageKey); @@ -207,7 +218,7 @@ export function hasRecentViewPublish(scope, bucketIndex) { return false; } - viewEventPublishMemory.set(scope, { + actorMemory.set(scope, { bucket: storedBucket, seenAt: storedSeenAt, }); @@ -215,19 +226,30 @@ export function hasRecentViewPublish(scope, bucketIndex) { return storedBucket === bucketIndex; } -export function rememberViewPublish(scope, bucketIndex) { +export function rememberViewPublish(scope, bucketIndex, actorPubkey) { if (!scope || !Number.isFinite(bucketIndex)) { return; } const now = Date.now(); const windowMs = getViewEventGuardWindowMs(); - const entry = viewEventPublishMemory.get(scope); - if (entry && Number.isFinite(entry.seenAt) && now - entry.seenAt >= windowMs) { - viewEventPublishMemory.delete(scope); + const safeActor = + typeof actorPubkey === "string" && actorPubkey.trim() + ? actorPubkey.trim().toLowerCase() + : "anon"; + + let actorMemory = viewEventPublishMemory.get(safeActor); + if (!actorMemory) { + actorMemory = new Map(); + viewEventPublishMemory.set(safeActor, actorMemory); } - viewEventPublishMemory.set(scope, { + const entry = actorMemory.get(scope); + if (entry && Number.isFinite(entry.seenAt) && now - entry.seenAt >= windowMs) { + actorMemory.delete(scope); + } + + actorMemory.set(scope, { bucket: bucketIndex, seenAt: now, }); @@ -236,7 +258,7 @@ export function rememberViewPublish(scope, bucketIndex) { return; } - const storageKey = `${VIEW_EVENT_GUARD_PREFIX}:${scope}`; + const storageKey = `${VIEW_EVENT_GUARD_PREFIX}:${safeActor}:${scope}`; try { localStorage.setItem(storageKey, `${bucketIndex}:${now}`); } catch (error) { @@ -666,6 +688,7 @@ export async function publishViewEvent( } const actorPubkey = await client.ensureSessionActor(); + // actorPubkey derived via client.ensureSessionActor() if (!actorPubkey) { return { ok: false, error: "missing-actor" }; } @@ -677,7 +700,7 @@ export async function publishViewEvent( const guardScope = deriveViewEventPointerScope(pointer); const guardBucket = deriveViewEventBucketIndex(createdAt); - if (guardScope && hasRecentViewPublish(guardScope, guardBucket)) { + if (guardScope && hasRecentViewPublish(guardScope, guardBucket, actorPubkey)) { devLogger.info("[nostr] Skipping duplicate view publish for scope", guardScope); return { ok: true, @@ -815,7 +838,7 @@ export async function publishViewEvent( const success = acceptedRelays.length > 0; if (success) { if (guardScope) { - rememberViewPublish(guardScope, guardBucket); + rememberViewPublish(guardScope, guardBucket, actorPubkey); } devLogger.info( `[nostr] View event accepted by ${acceptedRelays.length} relay(s):`, diff --git a/js/services/hashtagPreferencesService.js b/js/services/hashtagPreferencesService.js index 4b0dc22f..7f268ffe 100644 --- a/js/services/hashtagPreferencesService.js +++ b/js/services/hashtagPreferencesService.js @@ -4,6 +4,7 @@ import { requestDefaultExtensionPermissions, } from "../nostrClientFacade.js"; import { getActiveSigner } from "../nostr/index.js"; +import { isSessionActor } from "../nostr/sessionActor.js"; import { buildHashtagPreferenceEvent, getNostrEventSchema, @@ -700,6 +701,15 @@ class HashtagPreferencesService { } async publish(options = {}) { + // nostrClient imported from nostrClientFacade + if (isSessionActor(nostrClient)) { + const error = new Error( + "Publishing preferences is not allowed for session actors." + ); + error.code = "session-actor-publish-blocked"; + throw error; + } + const targetPubkey = normalizeHexPubkey( typeof options?.pubkey === "string" ? options.pubkey : this.activePubkey, ); diff --git a/js/userBlocks.js b/js/userBlocks.js index 1b156db4..525cb385 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -4,6 +4,7 @@ import { requestDefaultExtensionPermissions, } from "./nostrClientFacade.js"; import { getActiveSigner } from "./nostr/index.js"; +import { isSessionActor } from "./nostr/sessionActor.js"; import { normalizeNostrPubkey } from "./nostr/nip46Client.js"; import { buildBlockListEvent, BLOCK_LIST_IDENTIFIER, NOTE_TYPES } from "./nostrEventSchemas.js"; import { CACHE_POLICIES, STORAGE_TIERS } from "./nostr/cachePolicies.js"; @@ -1521,6 +1522,15 @@ class UserBlockListManager { plaintext = "", onStatus, } = {}) { + // nostrClient imported from nostrClientFacade + if (isSessionActor(nostrClient)) { + const error = new Error( + "Publishing mute lists is not allowed for session actors." + ); + error.code = "session-actor-publish-blocked"; + throw error; + } + const owner = normalizeHex(ownerPubkey); if (!owner || !signer || typeof signer.signEvent !== "function") { return null; @@ -1622,6 +1632,14 @@ class UserBlockListManager { } async publishBlockList(userPubkey, options = {}) { + if (isSessionActor(nostrClient)) { + const error = new Error( + "Publishing block lists is not allowed for session actors." + ); + error.code = "session-actor-publish-blocked"; + throw error; + } + const onStatus = options && typeof options.onStatus === "function" ? options.onStatus : null;