feat: isolate view telemetry and restrict session actor publishing

- Add `isSessionActor` helper to `js/nostr/sessionActor.js` to identify ephemeral keys.
- Update `js/nostr/viewEvents.js` to namespace view deduplication caches by `actorPubkey` to prevent data leakage between sessions.
- Block comment publishing for session actors in `js/nostr/commentEvents.js`.
- Block watch history list publishing for session actors in `js/nostr/client.js`.
- Block user block list publishing for session actors in `js/userBlocks.js`.
- Block hashtag preference publishing for session actors in `js/services/hashtagPreferencesService.js`.
- Add inline documentation clarifying variable sources.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-17 22:48:31 +00:00
1 parent 37e2e6dfc6
commit b49a294dc0
6 files changed
+100 -13

No files matched your search

+22
View File
@@ -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,
+9
View File
@@ -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" };
+5
View File
@@ -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,
+36 -13
View File
@@ -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):`,
+10
View File
@@ -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,
);
+18
View File
@@ -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;