feat(wasm,ext): org_manifest_decrypt/encrypt over WASM + faithful OrgManifest TS mirror

- Add `org_manifest_decrypt` + `org_manifest_encrypt` to relicario-wasm/src/lib.rs,
  placed after `org_unwrap_key`; mirror the existing manifest_decrypt/manifest_encrypt
  pattern exactly (need_key guard, session::with, js_value_for). Core import path:
  relicario_core::decrypt_org_manifest / relicario_core::encrypt_org_manifest (crate root).
- Add round-trip test `org_manifest_round_trip_via_handle` in org_tests: encrypt via
  WASM wrapper, decrypt via core directly (avoids serde_wasm_bindgen off-wasm).
  Asserts collection/title/tags/schema_version survive the round-trip; asserts nonce
  uniqueness. TDD: RED (compile error) → GREEN confirmed.
- Declare org_manifest_decrypt/org_manifest_encrypt in extension/src/wasm.d.ts.
- Add faithful/lean OrgManifestEntry + OrgManifest TS interfaces to
  extension/src/shared/types.ts: {id, type, title, tags[], modified, trashed_at?,
  collection} — no personal-only fields; collection required. Mirrors org.rs:184-202.
- Plan-doc: rewrite Task 2 body (re-scope rationale + checked steps), fix File
  Structure + Architecture lines, amend Hand-off contract (org_list_items ->
  OrgManifestEntry[]; org items remain personal Item).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
This commit is contained in:
adlee-was-taken
2026-06-25 23:33:29 -04:00
parent f85dc6729e
commit 2ecd98208d
4 changed files with 112 additions and 43 deletions

View File

@@ -602,12 +602,73 @@ pub fn org_unwrap_key(
Ok(SessionHandle(handle))
}
/// Decrypt an org manifest blob (manifest.enc in the org repo) with an org
/// session handle. The org manifest is a dedicated OrgManifest type (NOT the
/// personal Manifest); core's decrypt_org_manifest deserializes it.
#[wasm_bindgen]
pub fn org_manifest_decrypt(handle: &SessionHandle, encrypted: &[u8]) -> Result<JsValue, JsError> {
need_key(handle)?;
let out = session::with(handle.0, |k| relicario_core::decrypt_org_manifest(encrypted, k))
.unwrap()
.map_err(|e| JsError::new(&e.to_string()))?;
js_value_for(&out)
}
/// Encrypt an OrgManifest (JSON) with an org session handle, for the write track
/// (Dev-C never touches WASM — this is exposed here so they don't have to).
#[wasm_bindgen]
pub fn org_manifest_encrypt(handle: &SessionHandle, manifest_json: &str) -> Result<Vec<u8>, JsError> {
need_key(handle)?;
let m: relicario_core::OrgManifest = serde_json::from_str(manifest_json)
.map_err(|e| JsError::new(&format!("org manifest json: {e}")))?;
session::with(handle.0, |k| relicario_core::encrypt_org_manifest(&m, k))
.unwrap()
.map_err(|e| JsError::new(&e.to_string()))
}
#[cfg(test)]
mod org_tests {
use super::*;
use relicario_core::org::wrap_org_key;
use zeroize::Zeroizing;
#[test]
fn org_manifest_round_trip_via_handle() {
use relicario_core::{OrgManifest, OrgManifestEntry, ItemType, ItemId, decrypt_org_manifest};
session::clear();
let key_bytes = [0xABu8; 32];
let h = session::insert(Zeroizing::new(key_bytes), Zeroizing::new([0u8; 32]));
let handle = SessionHandle(h);
let key = Zeroizing::new(key_bytes);
let mut manifest = OrgManifest::new();
manifest.entries.push(OrgManifestEntry {
id: ItemId::new(),
r#type: ItemType::SecureNote,
title: "Org Test Item".into(),
tags: vec!["tag1".into()],
modified: 1_234_567_890,
trashed_at: None,
collection: "prod".into(),
});
let manifest_json = serde_json::to_string(&manifest).unwrap();
let encrypted = org_manifest_encrypt(&handle, &manifest_json).unwrap();
assert!(!encrypted.is_empty());
// Decrypt via core directly (avoids js-sys/serde_wasm_bindgen on native).
let parsed: OrgManifest = decrypt_org_manifest(&encrypted, &key).unwrap();
assert_eq!(parsed.entries.len(), 1);
assert_eq!(parsed.entries[0].collection, "prod");
assert_eq!(parsed.entries[0].title, "Org Test Item");
assert_eq!(parsed.entries[0].tags, vec!["tag1".to_string()]);
assert_eq!(parsed.schema_version, 1);
// Random nonces: two encryptions of the same plaintext must differ.
let encrypted2 = org_manifest_encrypt(&handle, &manifest_json).unwrap();
assert_ne!(encrypted, encrypted2, "nonces must differ");
}
/// Returns (public_openssh, private_openssh) using the real core keypair generator.
///
/// Device private keys are produced by `relicario_core::device::generate_keypair()`