From a3374f0569e492c45900e4074051d63e285ef0f9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:38:27 -0500 Subject: [PATCH] Guard upload when info hash is unavailable --- components/upload-modal.html | 4 ++-- js/services/r2Service.js | 38 ++++++++++++++++++++++++++------- js/ui/components/UploadModal.js | 35 +++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/components/upload-modal.html b/components/upload-modal.html index a22e1bc3..1e7cb32d 100644 --- a/components/upload-modal.html +++ b/components/upload-modal.html @@ -209,12 +209,12 @@
-

Direct link to a hosted video file.

+

Primary hosted video source (URL-first playback).

-

Optional P2P fallback or primary source.

+

Optional WebTorrent fallback when the URL is unavailable.

diff --git a/js/services/r2Service.js b/js/services/r2Service.js index 3055297f..5c289192 100644 --- a/js/services/r2Service.js +++ b/js/services/r2Service.js @@ -31,6 +31,15 @@ import { } from "./videoNotePayload.js"; const STATUS_VARIANTS = new Set(["info", "success", "error", "warning"]); +const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/; + +function normalizeInfoHash(value) { + return typeof value === "string" ? value.trim().toLowerCase() : ""; +} + +function isValidInfoHash(value) { + return INFO_HASH_PATTERN.test(value); +} function createDefaultSettings() { return { @@ -187,10 +196,10 @@ class R2Service { primaryVariant.m = mimeType; } - const normalizedHash = - typeof infoHash === "string" ? infoHash.trim().toLowerCase() : ""; + const normalizedHash = normalizeInfoHash(infoHash); if ( normalizedHash && + isValidInfoHash(normalizedHash) && (!primaryVariant.x || !String(primaryVariant.x).trim()) ) { primaryVariant.x = normalizedHash; @@ -705,14 +714,27 @@ class R2Service { } else { // Construct the Magnet Link // magnet:?xt=urn:btih:&dn=&ws= + const normalizedInfoHash = normalizeInfoHash(infoHash); + const hasValidInfoHash = isValidInfoHash(normalizedInfoHash); let generatedMagnet = ""; let generatedWs = ""; - if (infoHash) { - const encodedDn = encodeURIComponent(file.name); - const encodedWs = encodeURIComponent(publicUrl); - generatedMagnet = `magnet:?xt=urn:btih:${infoHash}&dn=${encodedDn}&ws=${encodedWs}`; - generatedWs = publicUrl; + if (hasValidInfoHash) { + const encodedDn = encodeURIComponent(file.name); + const encodedWs = encodeURIComponent(publicUrl); + generatedMagnet = `magnet:?xt=urn:btih:${normalizedInfoHash}&dn=${encodedDn}&ws=${encodedWs}`; + generatedWs = publicUrl; + } else { + if (infoHash) { + userLogger.warn( + "Invalid info hash provided. Skipping magnet and webseed generation.", + infoHash + ); + } + this.setCloudflareUploadStatus( + "Info hash missing or invalid. Publishing URL-first without WebTorrent fallback.", + "warning" + ); } const rawVideoPayload = { @@ -731,7 +753,7 @@ class R2Service { const mergedNip71 = this.buildNip71MetadataForUpload(metadata?.nip71, { publicUrl, file, - infoHash, + infoHash: hasValidInfoHash ? normalizedInfoHash : "", }); if (mergedNip71 && Object.keys(mergedNip71).length) { rawVideoPayload.nip71 = mergedNip71; diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js index 642553ae..06c0d0cf 100644 --- a/js/ui/components/UploadModal.js +++ b/js/ui/components/UploadModal.js @@ -10,6 +10,16 @@ import { import { createTorrentMetadata } from "../../utils/torrentHash.js"; import { sanitizeBucketName } from "../../storage/r2-mgmt.js"; +const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/; + +function normalizeInfoHash(value) { + return typeof value === "string" ? value.trim().toLowerCase() : ""; +} + +function isValidInfoHash(value) { + return INFO_HASH_PATTERN.test(value); +} + export class UploadModal { constructor({ authService, @@ -671,6 +681,29 @@ export class UploadModal { userLogger.warn("Failed to calculate info hash:", hashErr); } + const normalizedInfoHash = normalizeInfoHash(infoHash); + const hasValidInfoHash = isValidInfoHash(normalizedInfoHash); + + if (!hasValidInfoHash) { + const proceed = confirm( + "We couldn't calculate a valid info hash. Publishing will continue with URL-first playback only, and WebTorrent fallback will be unavailable. Continue?" + ); + if (!proceed) { + this.updateUploadStatus( + "Upload canceled. A valid info hash is required for WebTorrent fallback.", + "warning" + ); + this.isUploading = false; + this.submitButton.disabled = false; + this.updateProgress(null); + return; + } + this.updateUploadStatus( + "Continuing without WebTorrent fallback (info hash unavailable).", + "warning" + ); + } + // 2. Upload await this.r2Service.uploadVideo({ npub, @@ -678,7 +711,7 @@ export class UploadModal { thumbnailFile, torrentFile, metadata, - infoHash, + infoHash: hasValidInfoHash ? normalizedInfoHash : "", settingsInput: this.collectSettingsForm(), publishVideoNote: this.publishVideoNote, onReset: () => this.resetForm(),