[upload] Auto-unlock storage in the upload modal for a kept-unlocked session (#51)

The upload modal only READ storage's unlock state (isUnlocked) on load/open/refresh
— it never auto-unlocked the way the profile Storage pane does. So after a refresh
with "keep unlocked" ticked, the upload modal still opened with storage Locked even
though the signer had been restored.

Add UploadModal.maybeAutoUnlockStorage(pubkey), called on load(), open(), and
refreshState(): if storage is locked but a decrypt-capable signer is already present
(e.g. a kept-unlocked nsec restored on refresh), unlock silently. Guarded two ways:
  - only when the account ALREADY exists (new storageService.hasStoredAccount) so it
    never auto-creates storage for magnet/URL-only uploaders, and
  - only when the signer is already available, so it never triggers a fresh
    permission prompt on modal open.

Tests: storageService.hasStoredAccount (false before setup, true after unlock,
never creates an account, survives lock). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-01 20:20:57 -04:00
1 parent cebd3dc1e7
commit 6bdf28fb30
3 files changed
+84 -1

No files matched your search

+17
View File
@@ -526,6 +526,23 @@ export class StorageService {
return this.masterKeys.has(pubkey);
}
/**
* True if an encrypted storage account already exists for this pubkey. Lets
* callers auto-unlock existing storage WITHOUT unlock() creating a brand-new
* account for users who never set storage up (e.g. magnet/URL-only uploaders).
*/
async hasStoredAccount(pubkey) {
if (!pubkey) {
return false;
}
try {
const account = await this._getAccount(pubkey);
return Boolean(account && account.encryptedMasterKey);
} catch (error) {
return false;
}
}
/**
* Saves a connection configuration.
* Requires unlock() to be called first.
+42 -1
View File
@@ -192,6 +192,7 @@ export class UploadModal {
// Initial State
if (this.storageService) {
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
await this.maybeAutoUnlockStorage(pubkey);
this.isStorageUnlocked = pubkey ? this.storageService.isUnlocked(pubkey) : false;
await this.loadFromStorage();
}
@@ -713,6 +714,7 @@ export class UploadModal {
async refreshState() {
if (this.storageService) {
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
await this.maybeAutoUnlockStorage(pubkey);
this.isStorageUnlocked = pubkey ? this.storageService.isUnlocked(pubkey) : false;
this.updateLockUi();
@@ -720,6 +722,43 @@ export class UploadModal {
}
}
// Silently unlock existing storage when a decrypt-capable signer is already
// available (e.g. a kept-unlocked nsec restored on refresh, TODO #51) so the
// upload modal opens Unlocked instead of Locked. Guarded on an EXISTING account
// so it never creates storage for magnet/URL-only uploaders, and on the signer
// already being present so it never triggers a fresh permission prompt.
async maybeAutoUnlockStorage(pubkey) {
if (!this.storageService || !pubkey) {
return;
}
if (this.storageService.isUnlocked(pubkey)) {
return;
}
const signer = getActiveSigner();
if (
!signer ||
(typeof signer.nip44Decrypt !== "function" &&
typeof signer.nip04Decrypt !== "function")
) {
return;
}
try {
const hasAccount =
typeof this.storageService.hasStoredAccount === "function"
? await this.storageService.hasStoredAccount(pubkey)
: false;
if (!hasAccount) {
return;
}
await this.storageService.unlock(pubkey, { signer });
} catch (error) {
devLogger?.log?.(
"[UploadModal] Auto-unlock storage skipped:",
error?.message || error,
);
}
}
async loadFromStorage() {
if (!this.storageService) return;
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
@@ -1222,9 +1261,11 @@ export class UploadModal {
this.setGlobalModalState("upload", true);
this.isVisible = true;
// Refresh lock state on open
// Refresh lock state on open. Auto-unlock first so a kept-unlocked nsec
// (restored on refresh) opens Unlocked instead of Locked (TODO #51).
if (this.storageService) {
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
await this.maybeAutoUnlockStorage(pubkey);
this.isStorageUnlocked = pubkey ? this.storageService.isUnlocked(pubkey) : false;
this.updateLockUi();
// Always attempt load to refresh configuration status (even if locked)
+25
View File
@@ -277,4 +277,29 @@ describe("StorageService", () => {
/Storage is locked/,
);
});
// hasStoredAccount lets the upload modal auto-unlock EXISTING storage on refresh
// without unlock() creating a fresh account for users who never set it up (#51).
test("hasStoredAccount() is false before setup, true after, and never creates an account", async () => {
assert.strictEqual(
await storageService.hasStoredAccount(pubkey),
false,
"no account before any unlock",
);
// A read-only existence check must not create an account.
assert.strictEqual(await storageService.hasStoredAccount(pubkey), false);
await storageService.unlock(pubkey, { signer: mockSigner });
assert.strictEqual(
await storageService.hasStoredAccount(pubkey),
true,
"account exists after unlock persisted the encrypted master key",
);
// Survives a lock (existence is on-disk, not the in-memory master key).
storageService.lock(pubkey);
assert.strictEqual(await storageService.hasStoredAccount(pubkey), true);
assert.strictEqual(await storageService.hasStoredAccount(""), false);
});
});