feat(ext/sw): restore_backup handler

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.
This commit is contained in:
adlee-was-taken
2026-04-28 21:58:14 -04:00
parent 5d9ea37b7f
commit 2e825a9d33

View File

@@ -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 (<item_id>/<aid>) back to the
// extension's flat layout (attachments/<aid>.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 };
}
}
}
}