fix(storage): make Blossom actually usable in the upload modal [LOCAL] (#30)

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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-09 21:40:18 -04:00
1 parent 5935702e75
commit ba62b35ada
4 files changed
+78 -21

No files matched your search

+36 -7
View File
@@ -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";
}