mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
1 parent
37e2e6dfc6
commit
b49a294dc0
6 files changed
+100
-13
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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
@@ -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):`,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user