feat(wasm): pack_backup_json / unpack_backup_json

JSON bridge for the SW. Binary fields are base64 in the JSON wrapper;
core gets borrowed byte slices.
This commit is contained in:
adlee-was-taken
2026-04-28 19:52:36 -04:00
parent 6d96ca8288
commit 7407fe512f

View File

@@ -305,6 +305,133 @@ pub fn totp_compute(
Ok(TotpCode { code, expires_at })
}
// ── Backup container bridge ─────────────────────────────────────────────────
use relicario_core::backup::{
pack_backup as core_pack_backup,
unpack_backup as core_unpack_backup,
BackupInput, BackupItem, BackupAttachment,
};
/// Pack a vault into a `.relbak` byte vector.
///
/// `input_json` shape:
/// ```json
/// {
/// "salt": "<base64>",
/// "params_json": "...",
/// "devices_json": "...",
/// "manifest_enc": "<base64>",
/// "settings_enc": "<base64>",
/// "items": [{"id": "<hex>", "ciphertext": "<base64>"}, ...],
/// "attachments": [{"item_id": "<hex>", "attachment_id": "<hex>", "ciphertext": "<base64>"}, ...],
/// "reference_jpg": "<base64>" | null,
/// "git_archive": "<base64>" | null
/// }
/// ```
#[wasm_bindgen]
pub fn pack_backup_json(input_json: &str, passphrase: &str) -> Result<Vec<u8>, JsError> {
#[derive(serde::Deserialize)]
struct InJson {
salt: String,
params_json: String,
devices_json: String,
manifest_enc: String,
settings_enc: String,
items: Vec<InItem>,
attachments: Vec<InAttachment>,
reference_jpg: Option<String>,
git_archive: Option<String>,
}
#[derive(serde::Deserialize)]
struct InItem { id: String, ciphertext: String }
#[derive(serde::Deserialize)]
struct InAttachment { item_id: String, attachment_id: String, ciphertext: String }
let parsed: InJson = serde_json::from_str(input_json)
.map_err(|e| JsError::new(&format!("backup input: {e}")))?;
let b64 = base64::engine::general_purpose::STANDARD;
let salt = b64.decode(&parsed.salt).map_err(|e| JsError::new(&e.to_string()))?;
let manifest = b64.decode(&parsed.manifest_enc).map_err(|e| JsError::new(&e.to_string()))?;
let settings = b64.decode(&parsed.settings_enc).map_err(|e| JsError::new(&e.to_string()))?;
let items_bytes: Vec<(String, Vec<u8>)> = parsed.items.iter()
.map(|i| {
let ct = b64.decode(&i.ciphertext).map_err(|e| JsError::new(&e.to_string()))?;
Ok((i.id.clone(), ct))
})
.collect::<Result<Vec<_>, JsError>>()?;
let attach_bytes: Vec<(String, String, Vec<u8>)> = parsed.attachments.iter()
.map(|a| {
let ct = b64.decode(&a.ciphertext).map_err(|e| JsError::new(&e.to_string()))?;
Ok((a.item_id.clone(), a.attachment_id.clone(), ct))
})
.collect::<Result<Vec<_>, JsError>>()?;
let ref_bytes = parsed.reference_jpg.as_deref()
.map(|s| b64.decode(s))
.transpose()
.map_err(|e| JsError::new(&e.to_string()))?;
let git_bytes = parsed.git_archive.as_deref()
.map(|s| b64.decode(s))
.transpose()
.map_err(|e| JsError::new(&e.to_string()))?;
let items_refs: Vec<BackupItem> = items_bytes.iter()
.map(|(id, ct)| BackupItem { id: id.clone(), ciphertext: ct })
.collect();
let attach_refs: Vec<BackupAttachment> = attach_bytes.iter()
.map(|(iid, aid, ct)| BackupAttachment {
item_id: iid.clone(),
attachment_id: aid.clone(),
ciphertext: ct,
})
.collect();
let input = BackupInput {
salt: &salt,
params_json: &parsed.params_json,
devices_json: &parsed.devices_json,
manifest_enc: &manifest,
settings_enc: &settings,
items: items_refs,
attachments: attach_refs,
reference_jpg: ref_bytes.as_deref(),
git_archive: git_bytes.as_deref(),
};
core_pack_backup(input, passphrase).map_err(|e| JsError::new(&e.to_string()))
}
/// Unpack `.relbak` bytes; returns the JSON shape that mirrors `BackupOutput`,
/// with binary fields base64-encoded.
#[wasm_bindgen]
pub fn unpack_backup_json(bytes: &[u8], passphrase: &str) -> Result<String, JsError> {
let out = core_unpack_backup(bytes, passphrase)
.map_err(|e| JsError::new(&e.to_string()))?;
let b64 = base64::engine::general_purpose::STANDARD;
let json = serde_json::json!({
"salt": b64.encode(out.salt),
"params_json": out.params_json,
"devices_json": out.devices_json,
"manifest_enc": b64.encode(&out.manifest_enc),
"settings_enc": b64.encode(&out.settings_enc),
"items": out.items.iter().map(|i| serde_json::json!({
"id": i.id,
"ciphertext": b64.encode(&i.ciphertext),
})).collect::<Vec<_>>(),
"attachments": out.attachments.iter().map(|a| serde_json::json!({
"item_id": a.item_id,
"attachment_id": a.attachment_id,
"ciphertext": b64.encode(&a.ciphertext),
})).collect::<Vec<_>>(),
"reference_jpg": out.reference_jpg.as_ref().map(|b| b64.encode(b)),
"git_archive": out.git_archive.as_ref().map(|b| b64.encode(b)),
"created_at": out.created_at,
});
Ok(json.to_string())
}
#[cfg(test)]
mod session_tests {
use super::*;