Refactor publishVideo method in NostrClient

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-09 01:54:07 +00:00
1 parent 286e676280
commit b246e431e5
6 files changed
+499 -399

No files matched your search

+45 -399
View File
@@ -19,7 +19,6 @@
import { isDevMode } from "../config.js";
import { FEATURE_PUBLISH_NIP71 } from "../constants.js";
import { bytesToHex, sha256 } from "../../vendor/crypto-helpers.bundle.min.js";
import { infoHashFromMagnet } from "../magnets.js";
// 🔧 merged conflicting changes from codex/update-video-publishing-and-parsing-logic vs unstable
import {
@@ -163,7 +162,7 @@ import {
readStoredSessionActorEntry,
isSessionActor,
} from "./sessionActor.js";
import { HEX64_REGEX } from "../utils/hex.js";
import { HEX64_REGEX, normalizeHexHash } from "../utils/hex.js";
import {
NIP46_RPC_KIND,
NIP46_HANDSHAKE_TIMEOUT_MS,
@@ -215,6 +214,11 @@ import { EventsMap } from "./eventsMap.js";
import { PersistenceManager } from "./managers/PersistenceManager.js";
import { ConnectionManager } from "./managers/ConnectionManager.js";
import { SignerManager } from "./managers/SignerManager.js";
import {
prepareVideoPublishPayload,
prepareVideoMirrorOptions,
} from "./videoPayloadBuilder.js";
import { inferMimeTypeFromUrl } from "../utils/mime.js";
function normalizeProfileFromEvent(event) {
if (!event || !event.content) return null;
@@ -304,119 +308,6 @@ export const __testExports = {
attemptDecryptNip46HandshakePayload,
};
function extractVideoPublishPayload(rawPayload) {
let videoData = rawPayload;
let nip71Metadata = null;
if (rawPayload && typeof rawPayload === "object") {
if (rawPayload.nip71 && typeof rawPayload.nip71 === "object") {
nip71Metadata = rawPayload.nip71;
}
if (
rawPayload.legacyFormData &&
typeof rawPayload.legacyFormData === "object"
) {
videoData = rawPayload.legacyFormData;
} else if (
Object.prototype.hasOwnProperty.call(rawPayload, "legacyFormData")
) {
videoData = rawPayload.legacyFormData || {};
}
}
if (!videoData || typeof videoData !== "object") {
videoData = {};
}
const normalizedVideoData = { ...videoData };
const isNsfw = videoData.isNsfw === true;
normalizedVideoData.isNsfw = isNsfw;
normalizedVideoData.isForKids =
videoData.isForKids === true && !isNsfw;
return { videoData: normalizedVideoData, nip71Metadata };
}
function normalizeHexHash(candidate) {
if (candidate === undefined || candidate === null) {
return "";
}
const stringValue =
typeof candidate === "string" ? candidate : String(candidate);
const trimmed = stringValue.trim().toLowerCase();
if (!trimmed) {
return "";
}
return HEX64_REGEX.test(trimmed) ? trimmed : "";
}
const BlobConstructor = typeof Blob !== "undefined" ? Blob : null;
let sharedTextEncoder = null;
function getSharedTextEncoder() {
if (!sharedTextEncoder && typeof TextEncoder !== "undefined") {
sharedTextEncoder = new TextEncoder();
}
return sharedTextEncoder;
}
async function valueToUint8Array(value) {
if (!value) {
return null;
}
try {
if (
BlobConstructor &&
value instanceof BlobConstructor &&
typeof value.arrayBuffer === "function"
) {
const buffer = await value.arrayBuffer();
return new Uint8Array(buffer);
}
} catch (error) {
devLogger.warn("[nostr] Failed to read Blob while computing hash:", error);
return null;
}
if (typeof ArrayBuffer !== "undefined") {
if (value instanceof ArrayBuffer) {
return new Uint8Array(value);
}
if (ArrayBuffer.isView?.(value)) {
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
}
}
if (typeof Buffer !== "undefined" && Buffer.isBuffer?.(value)) {
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
}
if (typeof value === "string") {
const encoder = getSharedTextEncoder();
return encoder ? encoder.encode(value) : null;
}
return null;
}
async function computeSha256HexFromValue(value) {
const data = await valueToUint8Array(value);
if (!data) {
return "";
}
try {
const digest = sha256(data);
const hex = typeof digest === "string" ? digest : bytesToHex(digest);
return hex ? hex.toLowerCase() : "";
} catch (error) {
devLogger.warn("[nostr] Failed to compute SHA-256 for mirror payload:", error);
return "";
}
}
function eventToAddressPointer(event) {
if (!event || typeof event !== "object") {
return "";
@@ -494,57 +385,6 @@ function buildDmFilters(actorPubkey, { since, until, limit } = {}) {
return filters;
}
const EXTENSION_MIME_MAP = Object.freeze(
Object.fromEntries(
Object.entries({
mp4: "video/mp4",
m4v: "video/x-m4v",
webm: "video/webm",
mkv: "video/x-matroska",
mov: "video/quicktime",
avi: "video/x-msvideo",
ogv: "video/ogg",
ogg: "video/ogg",
m3u8: "application/x-mpegurl",
mpd: "application/dash+xml",
ts: "video/mp2t",
mpg: "video/mpeg",
mpeg: "video/mpeg",
flv: "video/x-flv",
"3gp": "video/3gpp",
}).map(([extension, mimeType]) => [
extension,
typeof mimeType === "string" ? mimeType.toLowerCase() : "",
]),
),
);
function inferMimeTypeFromUrl(url) {
if (!url || typeof url !== "string") {
return "";
}
let pathname = "";
try {
const parsed = new URL(url);
pathname = parsed.pathname || "";
} catch (err) {
const sanitized = url.split("?")[0].split("#")[0];
pathname = sanitized || "";
}
const lastSegment = pathname.split("/").pop() || "";
const match = lastSegment.match(/\.([a-z0-9]+)$/i);
if (!match) {
return "";
}
const extension = match[1].toLowerCase();
const mimeType = EXTENSION_MIME_MAP[extension];
return typeof mimeType === "string" ? mimeType : "";
}
function getActiveKey(video) {
if (video.videoRootId) {
return `ROOT:${video.videoRootId}`;
@@ -2487,154 +2327,28 @@ export class NostrClient {
* @throws {Error} If not logged in or if the primary publish fails.
*/
async publishVideo(videoPayload, pubkey) {
if (!pubkey) throw new Error("Not logged in to publish video.");
const normalizedPubkey =
typeof pubkey === "string" ? pubkey.trim() : "";
if (!normalizedPubkey) {
throw new Error("Not logged in to publish video.");
}
const {
event,
videoData,
nip71Metadata,
finalUrl,
finalMagnet,
finalThumbnail,
finalDescription,
finalTitle,
mimeType,
fileSha256,
originalFileSha256,
videoRootId,
dTagValue,
createdAt,
contentObject,
wantPrivate,
normalizedPubkey,
} = await prepareVideoPublishPayload(videoPayload, pubkey);
const userPubkeyLower = normalizedPubkey.toLowerCase();
const { videoData, nip71Metadata } = extractVideoPublishPayload(videoPayload);
const nip71EditedFlag =
videoPayload && typeof videoPayload === "object"
? videoPayload.nip71Edited
: null;
// NOTE: Keep the Upload, Edit, and Revert flows synchronized when
// updating shared fields. Changes here must be reflected in the modal
// controllers and revert helpers so all paths stay in lockstep.
devLogger.log("Publishing new video with data:", videoData);
if (nip71Metadata) {
devLogger.log("Including NIP-71 metadata:", nip71Metadata);
}
const rawMagnet = typeof videoData.magnet === "string" ? videoData.magnet : "";
const finalMagnet = rawMagnet.trim();
const finalUrl =
typeof videoData.url === "string" ? videoData.url.trim() : "";
const finalThumbnail =
typeof videoData.thumbnail === "string" ? videoData.thumbnail.trim() : "";
const finalDescription =
typeof videoData.description === "string"
? videoData.description.trim()
: "";
const finalTitle =
typeof videoData.title === "string" ? videoData.title.trim() : "";
const providedMimeType =
typeof videoData.mimeType === "string"
? videoData.mimeType.trim().toLowerCase()
: "";
const createdAt = Math.floor(Date.now() / 1000);
const seriesIdentifierCandidates = [
videoData.videoRootId,
videoData.seriesId,
videoData.seriesIdentifier,
];
let seriesIdentifier = "";
for (const candidate of seriesIdentifierCandidates) {
const normalized = typeof candidate === "string" ? candidate.trim() : "";
if (normalized) {
seriesIdentifier = normalized;
break;
}
}
if (!seriesIdentifier) {
seriesIdentifier = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
const videoRootId = seriesIdentifier;
const dTagValue = seriesIdentifier;
const finalEnableComments =
videoData.enableComments === false ? false : true;
const finalIsNsfw = videoData.isNsfw === true;
const finalIsForKids =
videoData.isForKids === true && !finalIsNsfw;
const wantPrivate = videoData.isPrivate === true;
const finalWs =
typeof videoData.ws === "string" ? videoData.ws.trim() : "";
const finalXs =
typeof videoData.xs === "string" ? videoData.xs.trim() : "";
const infoHashCandidates = [videoData.infoHash];
let infoHash = "";
for (const candidate of infoHashCandidates) {
const normalized = infoHashFromMagnet(candidate);
if (normalized) {
infoHash = normalized;
break;
}
}
if (!infoHash && finalMagnet) {
infoHash = infoHashFromMagnet(finalMagnet) || "";
}
const fileSha256 = normalizeHexHash(videoData.fileSha256);
const originalFileSha256 = normalizeHexHash(videoData.originalFileSha256);
const storagePointer = resolveStoragePointerValue({
storagePointer: videoData.storagePointer,
url: finalUrl,
infoHash,
fallbackId: videoRootId,
provider: videoData.storageProvider,
});
const contentObject = {
version: 3,
title: finalTitle,
url: finalUrl,
magnet: finalMagnet,
thumbnail: finalThumbnail,
description: finalDescription,
mode: videoData.mode || "live",
videoRootId,
deleted: false,
isPrivate: videoData.isPrivate ?? false,
isNsfw: finalIsNsfw,
isForKids: finalIsForKids,
enableComments: finalEnableComments,
};
if (infoHash) {
contentObject.infoHash = infoHash;
}
if (fileSha256) {
contentObject.fileSha256 = fileSha256;
}
if (originalFileSha256) {
contentObject.originalFileSha256 = originalFileSha256;
}
if (finalWs) {
contentObject.ws = finalWs;
}
if (finalXs) {
contentObject.xs = finalXs;
}
const nip71Tags = buildNip71MetadataTags(
nip71Metadata && typeof nip71Metadata === "object" ? nip71Metadata : null
);
const additionalTags = storagePointer
? [["s", storagePointer], ...nip71Tags]
: nip71Tags;
const event = buildVideoPostEvent({
pubkey: normalizedPubkey,
created_at: createdAt,
dTagValue,
content: contentObject,
additionalTags,
});
devLogger.log("Publish event with series identifier:", videoRootId);
devLogger.log("Event content:", event.content);
@@ -2648,90 +2362,22 @@ export class NostrClient {
});
// 2. Publish NIP-94 Mirror (Kind 1063) if a hosted URL is present.
// This allows clients that only understand NIP-94 to find the file.
if (finalUrl) {
const inferredMimeType = inferMimeTypeFromUrl(finalUrl);
const mimeTypeSource =
providedMimeType ||
inferredMimeType ||
"application/octet-stream";
const mimeType = mimeTypeSource.toLowerCase();
const fileHashCandidates = [
videoData.fileSha256,
videoData.uploadedFileSha256,
videoPayload?.legacyFormData?.fileSha256,
videoPayload?.fileSha256,
];
let fileSha256 = "";
for (const candidate of fileHashCandidates) {
const normalized = normalizeHexHash(candidate);
if (normalized) {
fileSha256 = normalized;
break;
}
}
const originalHashCandidates = [
videoData.originalFileSha256,
videoPayload?.legacyFormData?.originalFileSha256,
videoPayload?.originalFileSha256,
];
let originalFileSha256 = "";
for (const candidate of originalHashCandidates) {
const normalized = normalizeHexHash(candidate);
if (normalized) {
originalFileSha256 = normalized;
break;
}
}
const uploadedFile =
videoData?.uploadedFile ||
videoData?.file ||
videoPayload?.legacyFormData?.uploadedFile ||
videoPayload?.legacyFormData?.file ||
videoPayload?.uploadedFile ||
videoPayload?.file ||
null;
const originalFile =
videoData?.originalFile ||
videoPayload?.legacyFormData?.originalFile ||
videoPayload?.originalFile ||
uploadedFile;
if (!fileSha256 && uploadedFile) {
fileSha256 = await computeSha256HexFromValue(uploadedFile);
}
if (!originalFileSha256 && originalFile) {
originalFileSha256 = await computeSha256HexFromValue(originalFile);
}
if (!originalFileSha256 && fileSha256) {
originalFileSha256 = fileSha256;
}
const mirrorOptions = {
url: finalUrl,
magnet: finalMagnet,
thumbnail: finalThumbnail,
description: finalDescription,
title: finalTitle,
const mirrorOptions = await prepareVideoMirrorOptions({
videoData,
videoPayload,
finalUrl,
finalMagnet,
finalThumbnail,
finalDescription,
finalTitle,
mimeType,
fileSha256,
originalFileSha256,
pubkey: normalizedPubkey,
createdAt,
isPrivate: contentObject.isPrivate,
actorPubkey: normalizedPubkey,
created_at: createdAt,
};
if (fileSha256) {
mirrorOptions.fileSha256 = fileSha256;
}
if (originalFileSha256) {
mirrorOptions.originalFileSha256 = originalFileSha256;
}
});
try {
const mirrorResult = await this.mirrorVideoEvent(
@@ -2741,10 +2387,7 @@ export class NostrClient {
if (mirrorResult?.ok) {
devLogger.log("Prepared NIP-94 mirror event:", mirrorResult.event);
devLogger.log(
"NIP-94 mirror dispatched for hosted URL:",
finalUrl,
);
devLogger.log("NIP-94 mirror dispatched for hosted URL:", finalUrl);
} else if (mirrorResult) {
devLogger.warn(
"[nostr] NIP-94 mirror rejected:",
@@ -2761,7 +2404,10 @@ export class NostrClient {
} else devLogger.log("Skipping NIP-94 mirror: no hosted URL provided.");
// 3. Publish NIP-71 Metadata (Kind 22) if categories/tags were added.
// This is a separate event linked to the video via `a` tag or `e` tag.
const nip71EditedFlag =
videoPayload && typeof videoPayload === "object"
? videoPayload.nip71Edited
: null;
const hasMetadataObject =
nip71Metadata && typeof nip71Metadata === "object";
const metadataWasEdited =
@@ -2801,12 +2447,12 @@ export class NostrClient {
videoRootId,
dTag: dTagValue,
eventId: signedEvent.id,
}
},
);
} catch (nip71Error) {
userLogger.warn(
"[nostr] Failed to publish NIP-71 metadata for edit:",
nip71Error
nip71Error,
);
}
}
+304
View File
@@ -0,0 +1,304 @@
import { HEX64_REGEX, normalizeHexHash } from "../utils/hex.js";
import { infoHashFromMagnet } from "../magnets.js";
import { resolveStoragePointerValue } from "../utils/storagePointer.js";
import { buildNip71MetadataTags } from "./nip71.js";
import { buildVideoPostEvent } from "../nostrEventSchemas.js";
import { computeSha256HexFromValue } from "../utils/cryptoUtils.js";
import { inferMimeTypeFromUrl } from "../utils/mime.js";
import { devLogger } from "../utils/logger.js";
export function extractVideoPublishPayload(rawPayload) {
let videoData = rawPayload;
let nip71Metadata = null;
if (rawPayload && typeof rawPayload === "object") {
if (rawPayload.nip71 && typeof rawPayload.nip71 === "object") {
nip71Metadata = rawPayload.nip71;
}
if (
rawPayload.legacyFormData &&
typeof rawPayload.legacyFormData === "object"
) {
videoData = rawPayload.legacyFormData;
} else if (
Object.prototype.hasOwnProperty.call(rawPayload, "legacyFormData")
) {
videoData = rawPayload.legacyFormData || {};
}
}
if (!videoData || typeof videoData !== "object") {
videoData = {};
}
const normalizedVideoData = { ...videoData };
const isNsfw = videoData.isNsfw === true;
normalizedVideoData.isNsfw = isNsfw;
normalizedVideoData.isForKids =
videoData.isForKids === true && !isNsfw;
return { videoData: normalizedVideoData, nip71Metadata };
}
export async function prepareVideoPublishPayload(videoPayload, pubkey, { timestamp = Math.floor(Date.now() / 1000) } = {}) {
const normalizedPubkey = typeof pubkey === "string" ? pubkey.trim() : "";
if (!normalizedPubkey) {
throw new Error("Not logged in to publish video.");
}
const { videoData, nip71Metadata } = extractVideoPublishPayload(videoPayload);
devLogger.log("Publishing new video with data:", videoData);
if (nip71Metadata) {
devLogger.log("Including NIP-71 metadata:", nip71Metadata);
}
const rawMagnet = typeof videoData.magnet === "string" ? videoData.magnet : "";
const finalMagnet = rawMagnet.trim();
const finalUrl =
typeof videoData.url === "string" ? videoData.url.trim() : "";
const finalThumbnail =
typeof videoData.thumbnail === "string" ? videoData.thumbnail.trim() : "";
const finalDescription =
typeof videoData.description === "string"
? videoData.description.trim()
: "";
const finalTitle =
typeof videoData.title === "string" ? videoData.title.trim() : "";
const providedMimeType =
typeof videoData.mimeType === "string"
? videoData.mimeType.trim().toLowerCase()
: "";
const createdAt = timestamp;
const seriesIdentifierCandidates = [
videoData.videoRootId,
videoData.seriesId,
videoData.seriesIdentifier,
];
let seriesIdentifier = "";
for (const candidate of seriesIdentifierCandidates) {
const normalized = typeof candidate === "string" ? candidate.trim() : "";
if (normalized) {
seriesIdentifier = normalized;
break;
}
}
if (!seriesIdentifier) {
seriesIdentifier = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
const videoRootId = seriesIdentifier;
const dTagValue = seriesIdentifier;
const finalEnableComments =
videoData.enableComments === false ? false : true;
const finalIsNsfw = videoData.isNsfw === true;
const finalIsForKids =
videoData.isForKids === true && !finalIsNsfw;
const wantPrivate = videoData.isPrivate === true;
const finalWs =
typeof videoData.ws === "string" ? videoData.ws.trim() : "";
const finalXs =
typeof videoData.xs === "string" ? videoData.xs.trim() : "";
const infoHashCandidates = [videoData.infoHash];
let infoHash = "";
for (const candidate of infoHashCandidates) {
const normalized = infoHashFromMagnet(candidate);
if (normalized) {
infoHash = normalized;
break;
}
}
if (!infoHash && finalMagnet) {
infoHash = infoHashFromMagnet(finalMagnet) || "";
}
const fileSha256 = normalizeHexHash(videoData.fileSha256);
const originalFileSha256 = normalizeHexHash(videoData.originalFileSha256);
const storagePointer = resolveStoragePointerValue({
storagePointer: videoData.storagePointer,
url: finalUrl,
infoHash,
fallbackId: videoRootId,
provider: videoData.storageProvider,
});
const contentObject = {
version: 3,
title: finalTitle,
url: finalUrl,
magnet: finalMagnet,
thumbnail: finalThumbnail,
description: finalDescription,
mode: videoData.mode || "live",
videoRootId,
deleted: false,
isPrivate: videoData.isPrivate ?? false,
isNsfw: finalIsNsfw,
isForKids: finalIsForKids,
enableComments: finalEnableComments,
};
if (infoHash) {
contentObject.infoHash = infoHash;
}
if (fileSha256) {
contentObject.fileSha256 = fileSha256;
}
if (originalFileSha256) {
contentObject.originalFileSha256 = originalFileSha256;
}
if (finalWs) {
contentObject.ws = finalWs;
}
if (finalXs) {
contentObject.xs = finalXs;
}
const nip71Tags = buildNip71MetadataTags(
nip71Metadata && typeof nip71Metadata === "object" ? nip71Metadata : null
);
const additionalTags = storagePointer
? [["s", storagePointer], ...nip71Tags]
: nip71Tags;
const event = buildVideoPostEvent({
pubkey: normalizedPubkey,
created_at: createdAt,
dTagValue,
content: contentObject,
additionalTags,
});
return {
event,
videoData,
nip71Metadata,
finalUrl,
finalMagnet,
finalThumbnail,
finalDescription,
finalTitle,
mimeType: providedMimeType,
fileSha256,
originalFileSha256,
videoRootId,
dTagValue,
createdAt,
contentObject,
wantPrivate,
normalizedPubkey
};
}
export async function prepareVideoMirrorOptions(params) {
const {
videoData,
videoPayload,
finalUrl,
finalMagnet,
finalThumbnail,
finalDescription,
finalTitle,
mimeType: providedMimeType,
fileSha256: initialFileSha256,
originalFileSha256: initialOriginalFileSha256,
pubkey,
createdAt,
isPrivate
} = params;
const inferredMimeType = inferMimeTypeFromUrl(finalUrl);
const mimeTypeSource =
providedMimeType ||
inferredMimeType ||
"application/octet-stream";
const mimeType = mimeTypeSource.toLowerCase();
const fileHashCandidates = [
initialFileSha256,
videoData.fileSha256,
videoData.uploadedFileSha256,
videoPayload?.legacyFormData?.fileSha256,
videoPayload?.fileSha256,
];
let fileSha256 = "";
for (const candidate of fileHashCandidates) {
const normalized = normalizeHexHash(candidate);
if (normalized) {
fileSha256 = normalized;
break;
}
}
const originalHashCandidates = [
initialOriginalFileSha256,
videoData.originalFileSha256,
videoPayload?.legacyFormData?.originalFileSha256,
videoPayload?.originalFileSha256,
];
let originalFileSha256 = "";
for (const candidate of originalHashCandidates) {
const normalized = normalizeHexHash(candidate);
if (normalized) {
originalFileSha256 = normalized;
break;
}
}
const uploadedFile =
videoData?.uploadedFile ||
videoData?.file ||
videoPayload?.legacyFormData?.uploadedFile ||
videoPayload?.legacyFormData?.file ||
videoPayload?.uploadedFile ||
videoPayload?.file ||
null;
const originalFile =
videoData?.originalFile ||
videoPayload?.legacyFormData?.originalFile ||
videoPayload?.originalFile ||
uploadedFile;
if (!fileSha256 && uploadedFile) {
fileSha256 = await computeSha256HexFromValue(uploadedFile);
}
if (!originalFileSha256 && originalFile) {
originalFileSha256 = await computeSha256HexFromValue(originalFile);
}
if (!originalFileSha256 && fileSha256) {
originalFileSha256 = fileSha256;
}
const mirrorOptions = {
url: finalUrl,
magnet: finalMagnet,
thumbnail: finalThumbnail,
description: finalDescription,
title: finalTitle,
mimeType,
isPrivate: isPrivate,
actorPubkey: pubkey,
created_at: createdAt,
};
if (fileSha256) {
mirrorOptions.fileSha256 = fileSha256;
}
if (originalFileSha256) {
mirrorOptions.originalFileSha256 = originalFileSha256;
}
return mirrorOptions;
}
+69
View File
@@ -0,0 +1,69 @@
import { bytesToHex, sha256 } from "../../vendor/crypto-helpers.bundle.min.js";
import { devLogger } from "./logger.js";
let sharedTextEncoder = null;
export function getSharedTextEncoder() {
if (!sharedTextEncoder && typeof TextEncoder !== "undefined") {
sharedTextEncoder = new TextEncoder();
}
return sharedTextEncoder;
}
const BlobConstructor = typeof Blob !== "undefined" ? Blob : null;
export async function valueToUint8Array(value) {
if (!value) {
return null;
}
try {
if (
BlobConstructor &&
value instanceof BlobConstructor &&
typeof value.arrayBuffer === "function"
) {
const buffer = await value.arrayBuffer();
return new Uint8Array(buffer);
}
} catch (error) {
devLogger.warn("[nostr] Failed to read Blob while computing hash:", error);
return null;
}
if (typeof ArrayBuffer !== "undefined") {
if (value instanceof ArrayBuffer) {
return new Uint8Array(value);
}
if (ArrayBuffer.isView?.(value)) {
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
}
}
if (typeof Buffer !== "undefined" && Buffer.isBuffer?.(value)) {
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
}
if (typeof value === "string") {
const encoder = getSharedTextEncoder();
return encoder ? encoder.encode(value) : null;
}
return null;
}
export async function computeSha256HexFromValue(value) {
const data = await valueToUint8Array(value);
if (!data) {
return "";
}
try {
const digest = sha256(data);
const hex = typeof digest === "string" ? digest : bytesToHex(digest);
return hex ? hex.toLowerCase() : "";
} catch (error) {
devLogger.warn("[nostr] Failed to compute SHA-256 for mirror payload:", error);
return "";
}
}
+12
View File
@@ -11,3 +11,15 @@ export function normalizeHexString(value) {
export const normalizeHexId = normalizeHexString;
export const normalizeHexPubkey = normalizeHexString;
export function normalizeHexHash(value) {
if (value === undefined || value === null) {
return "";
}
const stringValue = typeof value === "string" ? value : String(value);
const trimmed = stringValue.trim().toLowerCase();
if (!trimmed) {
return "";
}
return HEX64_REGEX.test(trimmed) ? trimmed : "";
}
+49
View File
@@ -0,0 +1,49 @@
export const EXTENSION_MIME_MAP = Object.freeze(
Object.fromEntries(
Object.entries({
mp4: "video/mp4",
m4v: "video/x-m4v",
webm: "video/webm",
mkv: "video/x-matroska",
mov: "video/quicktime",
avi: "video/x-msvideo",
ogv: "video/ogg",
ogg: "video/ogg",
m3u8: "application/x-mpegurl",
mpd: "application/dash+xml",
ts: "video/mp2t",
mpg: "video/mpeg",
mpeg: "video/mpeg",
flv: "video/x-flv",
"3gp": "video/3gpp",
}).map(([extension, mimeType]) => [
extension,
typeof mimeType === "string" ? mimeType.toLowerCase() : "",
]),
),
);
export function inferMimeTypeFromUrl(url) {
if (!url || typeof url !== "string") {
return "";
}
let pathname = "";
try {
const parsed = new URL(url);
pathname = parsed.pathname || "";
} catch (err) {
const sanitized = url.split("?")[0].split("#")[0];
pathname = sanitized || "";
}
const lastSegment = pathname.split("/").pop() || "";
const match = lastSegment.match(/\.([a-z0-9]+)$/i);
if (!match) {
return "";
}
const extension = match[1].toLowerCase();
const mimeType = EXTENSION_MIME_MAP[extension];
return typeof mimeType === "string" ? mimeType : "";
}
+20
View File
@@ -4,6 +4,7 @@ import {
normalizeHexString,
normalizeHexId,
normalizeHexPubkey,
normalizeHexHash,
HEX64_REGEX,
} from "../../js/utils/hex.js";
@@ -60,4 +61,23 @@ describe("hex utils", () => {
assert.doesNotMatch("", HEX64_REGEX);
});
});
describe("normalizeHexHash", () => {
it("should return empty string for non-string inputs", () => {
assert.equal(normalizeHexHash(null), "");
assert.equal(normalizeHexHash(undefined), "");
assert.equal(normalizeHexHash(123), "");
});
it("should return empty string for invalid hex", () => {
assert.equal(normalizeHexHash("invalid"), "");
assert.equal(normalizeHexHash("g".repeat(64)), "");
});
it("should return normalized hex for valid inputs", () => {
const valid = "A".repeat(64);
const expected = "a".repeat(64);
assert.equal(normalizeHexHash(valid), expected);
});
});
});