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/, + ); + }); });