Files
bitvid/js/nostr/publishHelpers.js
T

1063 lines
30 KiB
JavaScript

import {
ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS,
} from "../config.js";
import {
publishEventToRelays,
assertAnyRelayAccepted,
summarizePublishResults,
} from "../nostrPublish.js";
import { buildRepostEvent, buildVideoMirrorEvent } from "../nostrEventSchemas.js";
import { normalizePointerInput } from "./watchHistory.js";
import { sanitizeRelayList } from "./nip46Client.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" ||
(!signFn && !finalizeFn)
) {
throw new Error("Missing signing primitives");
}
const tags = Array.isArray(event.tags)
? event.tags.map((tag) => (Array.isArray(tag) ? [...tag] : tag))
: [];
const prepared = {
kind: event.kind,
pubkey: event.pubkey,
created_at: event.created_at,
tags,
content: typeof event.content === "string" ? event.content : "",
};
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) {
const windowSeconds = Math.max(
1,
Number(ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS) || 0,
);
const baseSeconds = Number.isFinite(referenceSeconds)
? Math.max(0, Math.floor(referenceSeconds))
: Math.floor(Date.now() / 1000);
return Math.floor(baseSeconds / windowSeconds);
}
export function getRebroadcastCooldownWindowMs() {
const windowSeconds = Math.max(
1,
Number(ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS) || 0,
);
return windowSeconds * 1000;
}
export function deriveRebroadcastScope(pubkey, eventId) {
const normalizedPubkey =
typeof pubkey === "string" && pubkey.trim()
? pubkey.trim().toLowerCase()
: "";
const normalizedEventId =
typeof eventId === "string" && eventId.trim()
? eventId.trim().toLowerCase()
: "";
if (!normalizedPubkey || !normalizedEventId) {
return "";
}
return `${normalizedPubkey}:${normalizedEventId}`;
}
function readRebroadcastGuardEntry(scope) {
if (!scope) {
return null;
}
const windowMs = getRebroadcastCooldownWindowMs();
const now = Date.now();
const entry = rebroadcastAttemptMemory.get(scope);
if (entry) {
const age = now - Number(entry.seenAt);
if (!Number.isFinite(entry.seenAt) || age >= windowMs) {
rebroadcastAttemptMemory.delete(scope);
} else {
return entry;
}
}
if (typeof localStorage === "undefined") {
return null;
}
const storageKey = `${REBROADCAST_GUARD_PREFIX}:${scope}`;
let rawValue = null;
try {
rawValue = localStorage.getItem(storageKey);
} catch (error) {
devLogger.warn("[nostr] Failed to read rebroadcast guard entry:", error);
return null;
}
if (typeof rawValue !== "string" || !rawValue) {
return null;
}
const [storedBucketRaw, storedSeenRaw] = rawValue.split(":", 2);
const storedBucket = Number(storedBucketRaw);
const storedSeenAt = Number(storedSeenRaw);
if (!Number.isFinite(storedBucket) || !Number.isFinite(storedSeenAt)) {
try {
localStorage.removeItem(storageKey);
} catch (error) {
devLogger.warn(
"[nostr] Failed to clear corrupt rebroadcast guard entry:",
error,
);
}
return null;
}
if (now - storedSeenAt >= windowMs) {
try {
localStorage.removeItem(storageKey);
} catch (error) {
devLogger.warn(
"[nostr] Failed to remove expired rebroadcast guard entry:",
error,
);
}
return null;
}
const normalizedEntry = {
bucket: storedBucket,
seenAt: storedSeenAt,
};
rebroadcastAttemptMemory.set(scope, normalizedEntry);
return normalizedEntry;
}
export function hasRecentRebroadcastAttempt(scope, bucketIndex) {
if (!scope || !Number.isFinite(bucketIndex)) {
return false;
}
const entry = readRebroadcastGuardEntry(scope);
return entry ? Number(entry.bucket) === bucketIndex : false;
}
export function rememberRebroadcastAttempt(scope, bucketIndex) {
if (!scope || !Number.isFinite(bucketIndex)) {
return;
}
const now = Date.now();
const windowMs = getRebroadcastCooldownWindowMs();
const entry = rebroadcastAttemptMemory.get(scope);
if (entry && Number.isFinite(entry.seenAt) && now - entry.seenAt >= windowMs) {
rebroadcastAttemptMemory.delete(scope);
}
const normalizedEntry = { bucket: bucketIndex, seenAt: now };
rebroadcastAttemptMemory.set(scope, normalizedEntry);
if (typeof localStorage === "undefined") {
return;
}
const storageKey = `${REBROADCAST_GUARD_PREFIX}:${scope}`;
try {
localStorage.setItem(storageKey, `${bucketIndex}:${now}`);
} catch (error) {
devLogger.warn("[nostr] Failed to persist rebroadcast guard entry:", error);
}
}
export function getRebroadcastCooldownState(scope) {
if (!scope) {
return null;
}
const entry = readRebroadcastGuardEntry(scope);
if (!entry || !Number.isFinite(entry.seenAt)) {
return null;
}
const windowMs = getRebroadcastCooldownWindowMs();
const expiresAt = entry.seenAt + windowMs;
const remainingMs = Math.max(0, expiresAt - Date.now());
if (remainingMs <= 0) {
return null;
}
return {
scope,
seenAt: entry.seenAt,
bucket: entry.bucket,
expiresAt,
remainingMs,
};
}
export async function signAndPublishEvent({
client,
event,
options = {},
resolveActiveSigner,
shouldRequestExtensionPermissions,
signEventWithPrivateKey: signWithPrivateKey,
}) {
const {
context = "event",
logName = context,
devLogLabel = logName,
rejectionLogLevel = "error",
relaysOverride = null,
} = options;
const normalizedEventPubkey =
event && typeof event.pubkey === "string"
? event.pubkey.toLowerCase()
: "";
const normalizedLogged =
typeof client?.pubkey === "string" ? client.pubkey.toLowerCase() : "";
const usingSessionActor =
normalizedEventPubkey &&
normalizedLogged &&
normalizedEventPubkey !== normalizedLogged;
const signer =
typeof resolveActiveSigner === "function"
? resolveActiveSigner(normalizedEventPubkey || client?.pubkey)
: null;
const canUseActiveSigner =
!usingSessionActor && signer && typeof signer.signEvent === "function";
let eventToSign = event;
let signedEvent = null;
let signerPubkey = null;
if (canUseActiveSigner) {
let permissionResult = { ok: true };
if (
typeof shouldRequestExtensionPermissions === "function" &&
shouldRequestExtensionPermissions(signer) &&
typeof client?.ensureExtensionPermissions === "function"
) {
permissionResult = await client.ensureExtensionPermissions(
DEFAULT_NIP07_PERMISSION_METHODS,
);
}
if (permissionResult.ok) {
try {
signedEvent = await signer.signEvent(event);
} catch (error) {
userLogger.warn(
"[nostr] Failed to sign event with active signer:",
error,
);
}
} else {
userLogger.warn(
"[nostr] Active signer permissions missing; falling back to session signer.",
permissionResult.error,
);
}
}
if (!signedEvent) {
try {
const currentSessionPubkey =
typeof client?.sessionActor?.pubkey === "string"
? client.sessionActor.pubkey.toLowerCase()
: "";
if (
typeof client?.ensureSessionActor === "function" &&
usingSessionActor &&
normalizedEventPubkey &&
normalizedEventPubkey !== currentSessionPubkey
) {
await client.ensureSessionActor(true);
} else if (typeof client?.ensureSessionActor === "function") {
await client.ensureSessionActor();
}
const sessionActor = client?.sessionActor;
if (
!sessionActor ||
typeof sessionActor.pubkey !== "string" ||
!sessionActor.pubkey ||
typeof sessionActor.privateKey !== "string" ||
!sessionActor.privateKey
) {
throw new Error("session-actor-unavailable");
}
const normalizedSessionPubkey = sessionActor.pubkey.toLowerCase();
if (
!normalizedEventPubkey ||
normalizedEventPubkey !== normalizedSessionPubkey ||
event.pubkey !== sessionActor.pubkey
) {
eventToSign = { ...event, pubkey: sessionActor.pubkey };
}
signedEvent = signWithPrivateKey(eventToSign, sessionActor.privateKey);
signerPubkey = sessionActor.pubkey;
} catch (error) {
userLogger.warn("[nostr] Failed to sign event with session key:", error);
throw error;
}
}
if (!signerPubkey && signedEvent && typeof signedEvent.pubkey === "string") {
signerPubkey = signedEvent.pubkey;
}
devLogger.log(`Signed ${devLogLabel} event:`, signedEvent);
let targetRelays = sanitizeRelayList(
Array.isArray(relaysOverride) && relaysOverride.length
? relaysOverride
: Array.isArray(client?.writeRelays) && client.writeRelays.length
? client.writeRelays
: client?.relays,
);
if (!targetRelays.length) {
targetRelays = Array.from(RELAY_URLS);
}
const publishResults = await publishEventToRelays(
client?.pool,
targetRelays,
signedEvent,
);
let publishSummary;
try {
publishSummary = assertAnyRelayAccepted(publishResults, { context });
} catch (publishError) {
if (publishError?.relayFailures?.length) {
const logLevel = rejectionLogLevel === "warn" ? "warn" : "error";
publishError.relayFailures.forEach(({ url, error: relayError, reason }) => {
const logFn = logLevel === "warn" ? userLogger.warn : userLogger.error;
logFn(`[nostr] ${logName} rejected by ${url}: ${reason}`, relayError || reason);
});
}
throw publishError;
}
publishSummary.accepted.forEach(({ url }) => {
devLogger.log(`${logName} published to ${url}`);
});
if (publishSummary.failed.length) {
publishSummary.failed.forEach(({ url, error: relayError }) => {
const reason =
relayError instanceof Error
? relayError.message
: relayError
? String(relayError)
: "publish failed";
userLogger.warn(
`[nostr] ${logName} not accepted by ${url}: ${reason}`,
relayError,
);
});
}
return {
signedEvent,
summary: publishSummary,
signerPubkey,
relays: targetRelays,
};
}
export async function repostEvent({
client,
eventId,
options = {},
resolveActiveSigner,
shouldRequestExtensionPermissions,
signEventWithPrivateKey: signWithPrivateKey,
eventToAddressPointer,
}) {
const normalizedId =
typeof eventId === "string" && eventId.trim() ? eventId.trim() : "";
if (!normalizedId) {
return { ok: false, error: "invalid-event-id" };
}
let pointer = null;
if (options.pointer) {
pointer = normalizePointerInput(options.pointer);
}
if (!pointer) {
const type =
typeof options.pointerType === "string" ? options.pointerType.trim() : "";
const value =
typeof options.pointerValue === "string" ? options.pointerValue.trim() : "";
if (type && value) {
const candidate = [type, value];
const relay =
typeof options.pointerRelay === "string"
? options.pointerRelay.trim()
: "";
if (relay) {
candidate.push(relay);
}
pointer = normalizePointerInput(candidate);
}
}
const cachedVideo = client?.allEvents?.get(normalizedId) || null;
const cachedRaw = client?.rawEvents?.get(normalizedId) || null;
let authorPubkey =
typeof options.authorPubkey === "string" && options.authorPubkey.trim()
? options.authorPubkey.trim().toLowerCase()
: "";
if (!authorPubkey && cachedVideo?.pubkey) {
authorPubkey = cachedVideo.pubkey.trim().toLowerCase();
}
if (!authorPubkey && cachedRaw?.pubkey) {
authorPubkey = cachedRaw.pubkey.trim().toLowerCase();
}
let address = typeof options.address === "string" ? options.address.trim() : "";
if (!address && pointer?.type === "a") {
address = pointer.value;
}
let addressRelay =
typeof options.addressRelay === "string" ? options.addressRelay.trim() : "";
if (!addressRelay && pointer?.type === "a" && pointer.relay) {
addressRelay = pointer.relay;
}
let eventRelay =
typeof options.eventRelay === "string" ? options.eventRelay.trim() : "";
if (!eventRelay && pointer?.type === "e" && pointer.relay) {
eventRelay = pointer.relay;
}
let targetKind = Number.isFinite(options.kind)
? Math.floor(options.kind)
: null;
const parseAddressMetadata = (candidate) => {
if (typeof candidate !== "string" || !candidate) {
return;
}
const parts = candidate.split(":");
if (parts.length >= 3) {
const maybeKind = Number.parseInt(parts[0], 10);
if (Number.isFinite(maybeKind) && !Number.isFinite(targetKind)) {
targetKind = maybeKind;
}
const maybePubkey = parts[1];
if (
maybePubkey &&
!authorPubkey &&
/^[0-9a-f]{64}$/i.test(maybePubkey)
) {
authorPubkey = maybePubkey.toLowerCase();
}
}
};
if (address) {
parseAddressMetadata(address);
}
if (!Number.isFinite(targetKind)) {
if (Number.isFinite(cachedRaw?.kind)) {
targetKind = Math.floor(cachedRaw.kind);
} else if (Number.isFinite(cachedVideo?.kind)) {
targetKind = Math.floor(cachedVideo.kind);
} else {
targetKind = 30078;
}
}
const deriveIdentifierFromVideo = () => {
if (!cachedVideo || typeof cachedVideo !== "object") {
return "";
}
if (
typeof cachedVideo.videoRootId === "string" &&
cachedVideo.videoRootId.trim()
) {
return cachedVideo.videoRootId.trim();
}
if (Array.isArray(cachedVideo.tags)) {
for (const tag of cachedVideo.tags) {
if (!Array.isArray(tag) || tag.length < 2) {
continue;
}
if (tag[0] === "d" && typeof tag[1] === "string" && tag[1].trim()) {
return tag[1].trim();
}
}
}
return "";
};
if (!address) {
const identifier = deriveIdentifierFromVideo();
const ownerPubkey =
authorPubkey ||
(cachedVideo?.pubkey ? cachedVideo.pubkey.trim().toLowerCase() : "") ||
(cachedRaw?.pubkey ? cachedRaw.pubkey.trim().toLowerCase() : "");
if (identifier && ownerPubkey) {
address = `${targetKind}:${ownerPubkey}:${identifier}`;
parseAddressMetadata(address);
} else if (cachedRaw && typeof eventToAddressPointer === "function") {
const fallbackAddress = eventToAddressPointer(cachedRaw);
if (fallbackAddress) {
address = fallbackAddress;
parseAddressMetadata(address);
}
}
}
const relaysOverride = sanitizeRelayList(
Array.isArray(options.relays) && options.relays.length
? options.relays
: Array.isArray(client?.writeRelays) && client.writeRelays.length
? client.writeRelays
: client?.relays,
);
const relays = relaysOverride.length ? relaysOverride : Array.from(RELAY_URLS);
let actorPubkey =
typeof options.actorPubkey === "string" && options.actorPubkey.trim()
? options.actorPubkey.trim()
: typeof client?.pubkey === "string" && client.pubkey.trim()
? client.pubkey.trim()
: "";
if (!actorPubkey) {
try {
const ensured =
typeof client?.ensureSessionActor === "function"
? await client.ensureSessionActor()
: null;
actorPubkey = ensured || "";
} catch (error) {
devLogger.warn("[nostr] Failed to ensure session actor before repost:", error);
return { ok: false, error: "missing-actor", details: error };
}
}
if (!actorPubkey) {
return { ok: false, error: "missing-actor" };
}
if (!client?.pool) {
try {
if (typeof client?.ensurePool === "function") {
await client.ensurePool();
}
} catch (error) {
devLogger.warn("[nostr] Failed to ensure pool before repost:", error);
return { ok: false, error: "pool-unavailable", details: error };
}
}
const createdAt =
typeof options.created_at === "number" && Number.isFinite(options.created_at)
? Math.max(0, Math.floor(options.created_at))
: Math.floor(Date.now() / 1000);
const additionalTags = Array.isArray(options.additionalTags)
? options.additionalTags.filter((tag) => Array.isArray(tag) && tag.length >= 2)
: [];
const repostEventPayload = buildRepostEvent({
pubkey: actorPubkey,
created_at: createdAt,
eventId: normalizedId,
eventRelay,
address,
addressRelay,
authorPubkey,
additionalTags,
});
try {
const { signedEvent, summary, signerPubkey } = await signAndPublishEvent({
client,
event: repostEventPayload,
resolveActiveSigner,
shouldRequestExtensionPermissions,
signEventWithPrivateKey: signWithPrivateKey,
options: {
context: "repost",
logName: "Repost",
devLogLabel: "repost",
relaysOverride: relays,
},
});
const normalizedSigner =
typeof signerPubkey === "string" ? signerPubkey.toLowerCase() : "";
const normalizedLogged =
typeof client?.pubkey === "string" ? client.pubkey.toLowerCase() : "";
const sessionPubkey =
typeof client?.sessionActor?.pubkey === "string"
? client.sessionActor.pubkey.toLowerCase()
: "";
const usedSessionActor =
normalizedSigner &&
normalizedSigner !== normalizedLogged &&
normalizedSigner === sessionPubkey;
return {
ok: true,
event: signedEvent,
summary,
relays,
sessionActor: usedSessionActor,
signerPubkey,
};
} catch (error) {
devLogger.warn("[nostr] Repost publish failed:", error);
const relayFailure =
error && typeof error === "object" && Array.isArray(error.relayFailures);
return {
ok: false,
error: relayFailure ? "publish-rejected" : "signing-failed",
details: error,
};
}
}
export async function mirrorVideoEvent({
client,
eventId,
options = {},
resolveActiveSigner,
shouldRequestExtensionPermissions,
signEventWithPrivateKey: signWithPrivateKey,
inferMimeTypeFromUrl,
}) {
const normalizedId =
typeof eventId === "string" && eventId.trim() ? eventId.trim() : "";
if (!normalizedId) {
return { ok: false, error: "invalid-event-id" };
}
const cachedVideo = client?.allEvents?.get(normalizedId) || null;
const sanitize = (value) => (typeof value === "string" ? value.trim() : "");
let url = sanitize(options.url);
if (!url && cachedVideo?.url) {
url = sanitize(cachedVideo.url);
}
if (!url) {
return { ok: false, error: "missing-url" };
}
const isPrivate =
options.isPrivate === true ||
options.isPrivate === "true" ||
cachedVideo?.isPrivate === true;
let magnet = sanitize(options.magnet);
if (!magnet && cachedVideo?.magnet) {
magnet = sanitize(cachedVideo.magnet);
}
if (!magnet && cachedVideo?.rawMagnet) {
magnet = sanitize(cachedVideo.rawMagnet);
}
if (!magnet && cachedVideo?.originalMagnet) {
magnet = sanitize(cachedVideo.originalMagnet);
}
let thumbnail = sanitize(options.thumbnail);
if (!thumbnail && cachedVideo?.thumbnail) {
thumbnail = sanitize(cachedVideo.thumbnail);
}
let description = sanitize(options.description);
if (!description && cachedVideo?.description) {
description = sanitize(cachedVideo.description);
}
let title = sanitize(options.title);
if (!title && cachedVideo?.title) {
title = sanitize(cachedVideo.title);
}
const providedMimeType = sanitize(options.mimeType);
const inferredMimeType = inferMimeTypeFromUrl
? inferMimeTypeFromUrl(url)
: "";
const mimeType = providedMimeType || inferredMimeType || "application/octet-stream";
const explicitAlt = sanitize(options.altText);
const altText = explicitAlt || description || title || "";
const tags = [];
tags.push(["url", url]);
if (mimeType) {
tags.push(["m", mimeType]);
}
if (thumbnail) {
tags.push(["thumb", thumbnail]);
}
if (altText) {
tags.push(["alt", altText]);
}
if (!isPrivate && magnet) {
tags.push(["magnet", magnet]);
}
const additionalTags = Array.isArray(options.additionalTags)
? options.additionalTags.filter((tag) => Array.isArray(tag) && tag.length >= 2)
: [];
tags.push(...additionalTags);
let actorPubkey =
typeof options.actorPubkey === "string" && options.actorPubkey.trim()
? options.actorPubkey.trim()
: typeof client?.pubkey === "string" && client.pubkey.trim()
? client.pubkey.trim()
: "";
if (!actorPubkey) {
try {
const ensured =
typeof client?.ensureSessionActor === "function"
? await client.ensureSessionActor()
: null;
actorPubkey = ensured || "";
} catch (error) {
devLogger.warn("[nostr] Failed to ensure session actor before mirror:", error);
return { ok: false, error: "missing-actor", details: error };
}
}
if (!actorPubkey) {
return { ok: false, error: "missing-actor" };
}
if (!client?.pool) {
try {
if (typeof client?.ensurePool === "function") {
await client.ensurePool();
}
} catch (error) {
devLogger.warn("[nostr] Failed to ensure pool before mirror:", error);
return { ok: false, error: "pool-unavailable", details: error };
}
}
const createdAt =
typeof options.created_at === "number" && Number.isFinite(options.created_at)
? Math.max(0, Math.floor(options.created_at))
: Math.floor(Date.now() / 1000);
const relaysOverride = sanitizeRelayList(
Array.isArray(options.relays) && options.relays.length
? options.relays
: Array.isArray(client?.writeRelays) && client.writeRelays.length
? client.writeRelays
: client?.relays,
);
const relays = relaysOverride.length ? relaysOverride : Array.from(RELAY_URLS);
const mirrorEventPayload = buildVideoMirrorEvent({
pubkey: actorPubkey,
created_at: createdAt,
tags,
content: altText,
});
try {
const { signedEvent, summary, signerPubkey } = await signAndPublishEvent({
client,
event: mirrorEventPayload,
resolveActiveSigner,
shouldRequestExtensionPermissions,
signEventWithPrivateKey: signWithPrivateKey,
options: {
context: "mirror",
logName: "NIP-94 mirror",
devLogLabel: "NIP-94 mirror",
rejectionLogLevel: "warn",
relaysOverride: relays,
},
});
const normalizedSigner =
typeof signerPubkey === "string" ? signerPubkey.toLowerCase() : "";
const normalizedLogged =
typeof client?.pubkey === "string" ? client.pubkey.toLowerCase() : "";
const sessionPubkey =
typeof client?.sessionActor?.pubkey === "string"
? client.sessionActor.pubkey.toLowerCase()
: "";
const usedSessionActor =
normalizedSigner &&
normalizedSigner !== normalizedLogged &&
normalizedSigner === sessionPubkey;
return {
ok: true,
event: signedEvent,
summary,
relays,
sessionActor: usedSessionActor,
signerPubkey,
};
} catch (error) {
devLogger.warn("[nostr] Mirror publish failed:", error);
const relayFailure =
error && typeof error === "object" && Array.isArray(error.relayFailures);
return {
ok: false,
error: relayFailure ? "publish-rejected" : "signing-failed",
details: error,
};
}
}
export async function rebroadcastEvent({ client, eventId, options = {} }) {
const normalizedId =
typeof eventId === "string" && eventId.trim() ? eventId.trim() : "";
if (!normalizedId) {
return { ok: false, error: "invalid-event-id" };
}
const candidatePubkeys = [];
if (typeof options.pubkey === "string" && options.pubkey.trim()) {
candidatePubkeys.push(options.pubkey.trim().toLowerCase());
}
const cachedVideo = client?.allEvents?.get(normalizedId);
if (cachedVideo?.pubkey) {
candidatePubkeys.push(cachedVideo.pubkey.toLowerCase());
}
const cachedRaw = client?.rawEvents?.get(normalizedId);
if (cachedRaw?.pubkey) {
candidatePubkeys.push(cachedRaw.pubkey.toLowerCase());
}
let normalizedPubkey = "";
for (const candidate of candidatePubkeys) {
if (typeof candidate === "string" && candidate) {
normalizedPubkey = candidate;
break;
}
}
let guardScope = deriveRebroadcastScope(normalizedPubkey, normalizedId);
const guardBucket = deriveRebroadcastBucketIndex();
if (guardScope && hasRecentRebroadcastAttempt(guardScope, guardBucket)) {
const cooldown = getRebroadcastCooldownState(guardScope);
return { ok: false, error: "cooldown-active", throttled: true, cooldown };
}
const relayCandidates =
Array.isArray(options.relays) && options.relays.length
? options.relays
: Array.isArray(client?.relays) && client.relays.length
? client.relays
: RELAY_URLS;
const relays = relayCandidates
.map((url) => (typeof url === "string" ? url.trim() : ""))
.filter(Boolean);
if (!client?.pool) {
try {
if (typeof client?.ensurePool === "function") {
await client.ensurePool();
}
} catch (error) {
devLogger.warn("[nostr] Failed to ensure pool before rebroadcast:", error);
return { ok: false, error: "pool-unavailable", details: error };
}
}
let rawEvent =
options.rawEvent ||
cachedRaw ||
(typeof client?.fetchRawEventById === "function"
? await client.fetchRawEventById(normalizedId, { relays })
: null);
if (!rawEvent) {
return { ok: false, error: "event-not-found" };
}
if (!normalizedPubkey && typeof rawEvent.pubkey === "string") {
normalizedPubkey = rawEvent.pubkey.trim().toLowerCase();
}
const effectiveScope = deriveRebroadcastScope(normalizedPubkey, normalizedId);
if (effectiveScope && !guardScope) {
guardScope = effectiveScope;
if (hasRecentRebroadcastAttempt(guardScope, guardBucket)) {
const cooldown = getRebroadcastCooldownState(guardScope);
return { ok: false, error: "cooldown-active", throttled: true, cooldown };
}
}
if (guardScope) {
rememberRebroadcastAttempt(guardScope, guardBucket);
}
let countResult = null;
if (options.skipCount !== true) {
try {
countResult =
typeof client?.countEventsAcrossRelays === "function"
? await client.countEventsAcrossRelays([
{ ids: [normalizedId] },
], {
relays,
timeoutMs: options.timeoutMs,
})
: null;
} catch (error) {
devLogger.warn("[nostr] COUNT request for rebroadcast failed:", error);
}
if (countResult?.total && Number(countResult.total) > 0) {
return {
ok: true,
alreadyPresent: true,
count: countResult,
cooldown: guardScope ? getRebroadcastCooldownState(guardScope) : null,
};
}
}
const publishResults = await publishEventToRelays(client?.pool, relays, rawEvent);
try {
const summary = assertAnyRelayAccepted(publishResults, { context: "rebroadcast" });
return {
ok: true,
rebroadcast: true,
summary,
count: countResult,
cooldown: guardScope ? getRebroadcastCooldownState(guardScope) : null,
};
} catch (error) {
devLogger.warn("[nostr] Rebroadcast rejected by relays:", error);
return {
ok: false,
error: "publish-rejected",
details: error,
results: publishResults,
cooldown: guardScope ? getRebroadcastCooldownState(guardScope) : null,
};
}
}
export { summarizePublishResults };