From ba62b35ada81bb5f5609de78ac51a8e89936d8b6 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:40:18 -0400 Subject: [PATCH] fix(storage): make Blossom actually usable in the upload modal [LOCAL] (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two root causes for "Blossom set as default but not showing / not usable": 1. SAVE required unlock. saveConnection throws "Storage is locked" without a master key — but Blossom has no secret. Added storageService.saveKeylessConnection() (writes plaintext meta, no encryption, no unlock) and the Blossom save branch now uses it. So a Blossom connection saves even if the user never unlocked storage. 2. UploadModal treated Blossom like a locked S3 connection: it only set activeCredentials when isStorageUnlocked, showed "Locked 🔒", blocked the file-pick behind an unlock prompt, and mislabeled it "S3 Compatible". Now: getProviderLabel → "Blossom"; loadFromStorage resolves servers from the plaintext meta (no unlock); the summary shows "N servers / Nostr-native"; the two upload gates and updateLockUi bypass unlock for Blossom (activeProviderIsBlossom). Uploads authorize with the Nostr signer instead. Bumped UploadModal size baseline (+gating code). Lint + build clean. Local only. Co-Authored-By: Claude Opus 4.8 --- js/services/storageService.js | 35 +++++++++++++++ js/ui/components/UploadModal.js | 43 ++++++++++++++++--- .../profileModal/ProfileStorageController.js | 19 +++----- scripts/check-file-size.mjs | 2 +- 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/js/services/storageService.js b/js/services/storageService.js index f4cbdb01..aeb2c9db 100644 --- a/js/services/storageService.js +++ b/js/services/storageService.js @@ -591,6 +591,41 @@ export class StorageService { userLogger.log(`[StorageService] Saved connection ${connectionId}`); } + /** + * Save a KEYLESS connection (e.g. Blossom): all config lives in the plaintext + * meta and there is no secret to encrypt, so this does NOT require unlock(). + * Used for providers whose auth is the user's Nostr signer, not a stored key. + */ + async saveKeylessConnection(pubkey, connectionId, meta = {}) { + const account = (await this._getAccount(pubkey)) || { + pubkey, + connections: {}, + }; + if (!account.connections) { + account.connections = {}; + } + if (meta.defaultForUploads) { + for (const key in account.connections) { + const conn = account.connections[key]; + if (conn.meta) { + conn.meta.defaultForUploads = false; + } + } + } + account.connections[connectionId] = { + id: connectionId, + provider: meta.provider || connectionId, + meta: { + ...meta, + lastSaved: Date.now(), + provider: meta.provider || connectionId, + }, + encrypted: null, + }; + await this._saveAccount(account); + userLogger.log(`[StorageService] Saved keyless connection ${connectionId}`); + } + /** * Retrieves and decrypts a connection. * Requires unlock() to be called first. diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js index 971b0632..a0073f38 100644 --- a/js/ui/components/UploadModal.js +++ b/js/ui/components/UploadModal.js @@ -16,6 +16,10 @@ import { sanitizeBucketName } from "../../storage/r2-mgmt.js"; import { buildR2Key, buildPublicUrl } from "../../r2.js"; import { buildS3ObjectUrl } from "../../services/s3Service.js"; import { PROVIDERS } from "../../services/storageService.js"; +import { + BLOSSOM_PROVIDER, + isBlossomProvider, +} from "../../services/blossomService.js"; import { buildStoragePointerValue, buildStoragePrefixFromKey, @@ -119,6 +123,9 @@ export class UploadModal { this.activeProvider = null; this.isStorageUnlocked = false; this.storageConfigured = false; + // Blossom connections have no encrypted secret, so they never need a PIN + // unlock — the upload authorizes with the Nostr signer instead. + this.activeProviderIsBlossom = false; // #44: per-modal upload-destination override (null = account default). this.selectedConnectionId = null; @@ -556,9 +563,10 @@ export class UploadModal { e.target.value = ""; // Clear selection return; } - if (!this.isStorageUnlocked) { + if (!this.isStorageUnlocked && !this.activeProviderIsBlossom) { // #56: unlock inline (silent restore or one passphrase prompt) and - // continue with this same file pick instead of erroring. + // continue with this same file pick instead of erroring. Blossom has no + // secret to unlock — it authorizes with the Nostr signer at upload time. const unlocked = await this.ensureStorageUnlockedForUpload(); if (!unlocked) { e.target.value = ""; @@ -664,8 +672,9 @@ export class UploadModal { this.resetThumbnailPicker(); return; } - if (!this.isStorageUnlocked) { + if (!this.isStorageUnlocked && !this.activeProviderIsBlossom) { // #56: prompt to unlock inline instead of silently dropping the pick. + // Blossom needs no unlock (signer-authorized at upload time). const unlocked = await this.ensureStorageUnlockedForUpload(); if (!unlocked) { this.resetThumbnailPicker(); @@ -934,12 +943,20 @@ export class UploadModal { if (targetConn) { this.storageConfigured = true; this.activeProvider = targetConn.provider; + this.activeProviderIsBlossom = isBlossomProvider(targetConn.provider); // UI Updates this.toggleStorageView("summary"); const providerName = this.getProviderLabel(targetConn.provider || targetConn.meta?.provider); - const bucketName = targetConn.meta?.bucket || "Unknown Bucket"; - const urlStyle = this.describeUrlStyle(targetConn.provider, targetConn.meta?.forcePathStyle); + const blossomServers = Array.isArray(targetConn.meta?.servers) + ? targetConn.meta.servers + : []; + const bucketName = this.activeProviderIsBlossom + ? `${blossomServers.length} server${blossomServers.length === 1 ? "" : "s"}` + : targetConn.meta?.bucket || "Unknown Bucket"; + const urlStyle = this.activeProviderIsBlossom + ? "Nostr-native (content-addressed)" + : this.describeUrlStyle(targetConn.provider, targetConn.meta?.forcePathStyle); if (this.statusText.summaryProvider) { this.statusText.summaryProvider.textContent = providerName; @@ -955,7 +972,14 @@ export class UploadModal { } if (this.statusText.summaryCopy) this.statusText.summaryCopy.textContent = this.getSummaryCopy(targetConn.provider); - if (this.isStorageUnlocked) { + if (this.activeProviderIsBlossom) { + // No secret to decrypt — servers live in the plaintext meta. + this.activeCredentials = { + provider: BLOSSOM_PROVIDER, + servers: blossomServers, + meta: targetConn.meta || {}, + }; + } else if (this.isStorageUnlocked) { const details = await this.storageService.getConnection(pubkey, targetConn.id); if (details) { this.activeCredentials = details; @@ -970,6 +994,7 @@ export class UploadModal { this.storageConfigured = false; this.activeCredentials = null; this.activeProvider = null; + this.activeProviderIsBlossom = false; this.toggleStorageView("empty"); } this.updateLockUi(); @@ -1064,7 +1089,8 @@ export class UploadModal { } updateLockUi() { - const locked = !this.isStorageUnlocked; + // Blossom has no secret to unlock — never show it as "locked". + const locked = !this.isStorageUnlocked && !this.activeProviderIsBlossom; if (this.statusText?.storageLock) { this.statusText.storageLock.textContent = locked ? "Locked 🔒" : "Unlocked 🔓"; this.statusText.storageLock.className = locked ? "text-xs text-critical" : "text-xs text-success"; @@ -1080,6 +1106,9 @@ export class UploadModal { } getProviderLabel(provider) { + if (isBlossomProvider(provider)) { + return "Blossom"; + } if (provider === PROVIDERS.R2 || provider === "cloudflare_r2") { return "Cloudflare R2"; } diff --git a/js/ui/profileModal/ProfileStorageController.js b/js/ui/profileModal/ProfileStorageController.js index cf56279c..2939c752 100644 --- a/js/ui/profileModal/ProfileStorageController.js +++ b/js/ui/profileModal/ProfileStorageController.js @@ -805,25 +805,18 @@ export class ProfileStorageController { const isDefaultBlossom = this.storageDefaultInput?.checked || false; this.setStorageFormStatus("Saving...", "info"); try { - await saveProviderConnection(storageService, pubkey, { + // Keyless: no secret to encrypt, so no storage unlock needed. + await storageService.saveKeylessConnection(pubkey, BLOSSOM_PROVIDER, { provider: BLOSSOM_PROVIDER, - payload: { provider: BLOSSOM_PROVIDER }, - meta: { - provider: BLOSSOM_PROVIDER, - servers, - defaultForUploads: isDefaultBlossom, - label: `Blossom (${servers.length} server${servers.length === 1 ? "" : "s"})`, - }, - isDefault: isDefaultBlossom, + servers, + defaultForUploads: isDefaultBlossom, + label: `Blossom (${servers.length} server${servers.length === 1 ? "" : "s"})`, }); this.setStorageFormStatus("Blossom servers saved.", "success"); this.mainController.showSuccess("Blossom storage saved."); } catch (error) { devLogger.error("Failed to save Blossom connection:", error); - this.setStorageFormStatus( - "Failed to save Blossom servers. Unlock storage first if prompted.", - "error", - ); + this.setStorageFormStatus("Failed to save Blossom servers.", "error"); } return; } diff --git a/scripts/check-file-size.mjs b/scripts/check-file-size.mjs index da035e72..756722dc 100644 --- a/scripts/check-file-size.mjs +++ b/scripts/check-file-size.mjs @@ -57,7 +57,7 @@ const GRANDFATHERED = { "js/app/playbackCoordinator.js": 1376, "js/feedEngine/stages.js": 1370, "js/ui/views/VideoListView.js": 1359, - "js/ui/components/UploadModal.js": 1319, + "js/ui/components/UploadModal.js": 1345, "js/ui/components/SimilarContentCard.js": 1303, "js/services/playbackService.js": 1274, "js/services/authService.js": 1335,