From 2e825a9d3317bcfbd0cbd0d1d37e0bafa059e8b0 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Tue, 28 Apr 2026 21:58:14 -0400 Subject: [PATCH] feat(ext/sw): restore_backup handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unpacks .relbak via WASM, writes every vault artifact to the user-specified fresh remote via writeFileCreateOnly (refuses to clobber), and updates chrome.storage.local so subsequent unlocks hit the restored vault. The reference image — when bundled — is restored to imageBase64; otherwise the user keeps using their existing reference.jpg. --- .../src/service-worker/router/popup-only.ts | 90 ++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/extension/src/service-worker/router/popup-only.ts b/extension/src/service-worker/router/popup-only.ts index ad6958a..b7ca4a4 100644 --- a/extension/src/service-worker/router/popup-only.ts +++ b/extension/src/service-worker/router/popup-only.ts @@ -418,8 +418,94 @@ export async function handle( } } - case 'restore_backup': - return { ok: false, error: 'restore_backup not yet implemented' }; + case 'restore_backup': { + try { + const bytes = new Uint8Array(msg.bytes); + const outJson: string = state.wasm.unpack_backup_json(bytes, msg.passphrase); + const out = JSON.parse(outJson) as { + salt: string; + params_json: string; + devices_json: string; + manifest_enc: string; + settings_enc: string; + items: Array<{ id: string; ciphertext: string }>; + attachments: Array<{ item_id: string; attachment_id: string; ciphertext: string }>; + reference_jpg: string | null; + }; + + // Build a GitHost for the new remote. + const newHost = createGitHost( + msg.newRemote.hostType, + msg.newRemote.hostUrl, + msg.newRemote.repoPath, + msg.newRemote.apiToken, + ); + + // Refuse if the remote already has a vault. + try { + const meta = await vault.fetchVaultMeta(newHost); + if (meta.salt && meta.paramsJson) { + return { ok: false, error: 'remote already contains a relicario vault' }; + } + } catch { + // No vault present — expected for a fresh remote. + } + + // Write the layout via writeFileCreateOnly. Refuses to clobber. + const b64 = (s: string) => Uint8Array.from(atob(s), c => c.charCodeAt(0)); + await newHost.writeFileCreateOnly('.relicario/salt', b64(out.salt), 'restore: salt'); + await newHost.writeFileCreateOnly('.relicario/params.json', new TextEncoder().encode(out.params_json), 'restore: params.json'); + await newHost.writeFileCreateOnly('.relicario/devices.json', new TextEncoder().encode(out.devices_json), 'restore: devices.json'); + await newHost.writeFileCreateOnly('manifest.enc', b64(out.manifest_enc), 'restore: manifest.enc'); + await newHost.writeFileCreateOnly('settings.enc', b64(out.settings_enc), 'restore: settings.enc'); + + for (const it of out.items) { + await newHost.writeFileCreateOnly( + `items/${it.id}.enc`, b64(it.ciphertext), `restore: item ${it.id}`, + ); + } + // Translate canonical envelope keys (/) back to the + // extension's flat layout (attachments/.bin). The aid is + // already content-addressed and globally unique; the item_id segment + // is recorded only in the manifest's attachment_summaries. + for (const a of out.attachments) { + await newHost.writeFileCreateOnly( + `attachments/${a.attachment_id}.bin`, + b64(a.ciphertext), + `restore: attachment ${a.attachment_id}`, + ); + } + + // Update local config so subsequent unlocks work. + const cfg = { + hostType: msg.newRemote.hostType, + hostUrl: msg.newRemote.hostUrl, + repoPath: msg.newRemote.repoPath, + apiToken: msg.newRemote.apiToken, + }; + await chrome.storage.local.set({ vaultConfig: cfg }); + if (out.reference_jpg) { + await chrome.storage.local.set({ imageBase64: out.reference_jpg }); + } + + // Make sure the SW's gitHost cache picks up the new config. + state.gitHost = newHost; + state.manifest = null; // user must unlock to populate + + return { + ok: true, + data: { + summary: { + itemCount: out.items.length, + attachmentCount: out.attachments.length, + hasImage: out.reference_jpg != null, + }, + }, + }; + } catch (e) { + return { ok: false, error: (e as Error).message }; + } + } } }