From 51072e48710dc47304a3b6da58850a3a7b758d58 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 12 Jul 2026 10:22:22 -0400 Subject: [PATCH] fix(storage): let a Blossom-only (keyless) account sync across devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit importAccountRecord hard-required encryptedMasterKey, but a Blossom-only account is keyless (no encrypted payloads) and legitimately has none — so its synced record failed to import. Require the envelope ONLY when the record carries an encrypted connection that actually needs it to unlock. Co-Authored-By: Claude Opus 4.8 --- js/services/storageService.js | 16 +++++++++++- tests/storage-connections.test.mjs | 40 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/js/services/storageService.js b/js/services/storageService.js index 5d7d1fe3..9a729c10 100644 --- a/js/services/storageService.js +++ b/js/services/storageService.js @@ -735,7 +735,21 @@ export class StorageService { if (!pubkey) { throw new Error("Pubkey required to import storage account record."); } - if (!record || typeof record !== "object" || !record.encryptedMasterKey) { + if (!record || typeof record !== "object") { + throw new Error("Invalid storage account record."); + } + // A Blossom-only account is keyless (no encrypted payloads), so it legitimately + // has no encryptedMasterKey. Require the envelope ONLY when the record actually + // carries an encrypted connection that would need it to unlock — otherwise a + // Blossom-only account can't sync across devices. + const connections = + record.connections && typeof record.connections === "object" + ? record.connections + : {}; + const hasEncryptedConnection = Object.values(connections).some( + (conn) => conn && conn.encrypted != null, + ); + if (hasEncryptedConnection && !record.encryptedMasterKey) { throw new Error("Invalid storage account record."); } const toSave = { ...record, pubkey }; diff --git a/tests/storage-connections.test.mjs b/tests/storage-connections.test.mjs index 6d7ae3d2..a5019c23 100644 --- a/tests/storage-connections.test.mjs +++ b/tests/storage-connections.test.mjs @@ -240,4 +240,44 @@ describe("saving multiple providers does not clash", () => { await storageService.importAccountRecord(pubkey, record); assert.equal(fired, null, "off() detached the observer"); }); + + test("a Blossom-only (keyless) account syncs without an encryptedMasterKey", async () => { + // Blossom connections are keyless (encrypted: null), so a Blossom-only account + // legitimately has no encryptedMasterKey. Importing it must still work. + const record = { + pubkey, + connections: { + blossom: { + id: "blossom", + provider: "blossom", + meta: { provider: "blossom", servers: ["https://blossom.band"] }, + encrypted: null, + }, + }, + }; + await storageService.importAccountRecord(pubkey, record); // must not throw + const conns = await storageService.listConnections(pubkey); + assert.deepEqual(conns.map((c) => c.id), ["blossom"]); + assert.deepEqual(conns[0].meta.servers, ["https://blossom.band"]); + }); + + test("an encrypted (R2/S3) connection still requires the master-key envelope", async () => { + // A record carrying an encrypted payload can't be unlocked without the + // envelope — importing it without one must still be rejected. + const record = { + pubkey, + connections: { + default: { + id: "default", + provider: "cloudflare_r2", + meta: { provider: "cloudflare_r2" }, + encrypted: { cipher: "deadbeef", iv: "00" }, + }, + }, + }; + await assert.rejects( + () => storageService.importAccountRecord(pubkey, record), + /Invalid storage account record/, + ); + }); });