mod events;
pub(crate) use crate::protocol::pending_action_wire;
mod stores;
use std::collections::HashMap;
use std::time::Duration;
use crate::{
protocol::{
DeRecFlow, DeRecProtocol, DeRecProtocolBuilder, UnpairAck,
types::{Target, UserSecret},
},
types::ChannelId,
wasm::ts_bindings_utils::{js_error, js_error_from_lib},
};
use crate::wasm::primitives::pairing::ContactMessage as PairingContactMessage;
use derec_proto::{SenderKind, TransportProtocol};
use js_sys::{Array, Uint8Array};
use stores::{
JsChannelStore, JsSecretStore, JsShareStore, JsStateStore, JsTransport, JsUserSecretStore,
};
use wasm_bindgen::prelude::*;
type WasmProtocol = DeRecProtocol<
JsChannelStore,
JsShareStore,
JsSecretStore,
JsUserSecretStore,
JsStateStore,
JsTransport,
>;
#[wasm_bindgen]
pub struct DeRecProtocolWasm {
inner: WasmProtocol,
}
#[wasm_bindgen(js_name = DeRecProtocolBuilder)]
pub struct DeRecProtocolBuilderWasm {
secret_id: u64,
channel_store: Option<JsValue>,
share_store: Option<JsValue>,
secret_store: Option<JsValue>,
user_secret_store: Option<JsValue>,
state_store: Option<JsValue>,
transport: Option<JsValue>,
own_transport_uri: Option<String>,
own_transport_protocol_num: Option<i32>,
threshold: u32,
keep_versions_count: u32,
communication_info: HashMap<String, String>,
timeout_in_secs: u32,
auto_respond_on_failure: bool,
unpair_ack: UnpairAck,
auto_reply_to: bool,
auto_accept: crate::protocol::AutoAcceptPolicy,
replica_id: Option<u64>,
parameter_range: Option<derec_proto::ParameterRange>,
}
#[wasm_bindgen(js_class = DeRecProtocolBuilder)]
impl DeRecProtocolBuilderWasm {
#[wasm_bindgen(constructor)]
pub fn new(secret_id: JsValue) -> Result<DeRecProtocolBuilderWasm, JsValue> {
let secret_id = js_value_to_u64(secret_id)
.map_err(|e| js_error("INVALID_SECRET_ID", format!("{e:?}")))?;
Ok(DeRecProtocolBuilderWasm {
secret_id,
channel_store: None,
share_store: None,
secret_store: None,
user_secret_store: None,
state_store: None,
transport: None,
own_transport_uri: None,
own_transport_protocol_num: None,
threshold: 3,
keep_versions_count: 3,
communication_info: HashMap::new(),
timeout_in_secs: 300,
auto_respond_on_failure: false,
unpair_ack: UnpairAck::Required,
auto_reply_to: false,
auto_accept: crate::protocol::AutoAcceptPolicy::default(),
replica_id: None,
parameter_range: None,
})
}
#[wasm_bindgen(js_name = withChannelStore)]
pub fn with_channel_store(mut self, store: JsValue) -> DeRecProtocolBuilderWasm {
self.channel_store = Some(store);
self
}
#[wasm_bindgen(js_name = withShareStore)]
pub fn with_share_store(mut self, store: JsValue) -> DeRecProtocolBuilderWasm {
self.share_store = Some(store);
self
}
#[wasm_bindgen(js_name = withSecretStore)]
pub fn with_secret_store(mut self, store: JsValue) -> DeRecProtocolBuilderWasm {
self.secret_store = Some(store);
self
}
#[wasm_bindgen(js_name = withUserSecretStore)]
pub fn with_user_secret_store(mut self, store: JsValue) -> DeRecProtocolBuilderWasm {
self.user_secret_store = Some(store);
self
}
#[wasm_bindgen(js_name = withStateStore)]
pub fn with_state_store(mut self, store: JsValue) -> DeRecProtocolBuilderWasm {
self.state_store = Some(store);
self
}
#[wasm_bindgen(js_name = withTransport)]
pub fn with_transport(mut self, transport: JsValue) -> DeRecProtocolBuilderWasm {
self.transport = Some(transport);
self
}
#[wasm_bindgen(js_name = withOwnTransport)]
pub fn with_own_transport(
mut self,
endpoint: JsValue,
) -> Result<DeRecProtocolBuilderWasm, JsValue> {
#[derive(serde::Deserialize)]
struct EndpointShape {
uri: String,
protocol: String,
}
let parsed: EndpointShape = serde_wasm_bindgen::from_value(endpoint)
.map_err(|e| js_error("INVALID_OWN_TRANSPORT", e.to_string()))?;
let protocol_num = match parsed.protocol.to_lowercase().as_str() {
"https" => 0i32,
other => {
return Err(js_error(
"INVALID_PROTOCOL",
format!("unknown protocol: {other}"),
));
}
};
self.own_transport_uri = Some(parsed.uri);
self.own_transport_protocol_num = Some(protocol_num);
Ok(self)
}
#[wasm_bindgen(js_name = withThreshold)]
pub fn with_threshold(mut self, threshold: u32) -> DeRecProtocolBuilderWasm {
self.threshold = threshold;
self
}
#[wasm_bindgen(js_name = withKeepVersionsCount)]
pub fn with_keep_versions_count(mut self, count: u32) -> DeRecProtocolBuilderWasm {
self.keep_versions_count = count;
self
}
#[wasm_bindgen(js_name = withTimeout)]
pub fn with_timeout(mut self, timeout_in_secs: u32) -> DeRecProtocolBuilderWasm {
self.timeout_in_secs = timeout_in_secs.max(1);
self
}
#[wasm_bindgen(js_name = withCommunicationInfo)]
pub fn with_communication_info(
mut self,
info: JsValue,
) -> Result<DeRecProtocolBuilderWasm, JsValue> {
let parsed: HashMap<String, String> = serde_wasm_bindgen::from_value(info)
.map_err(|e| js_error("INVALID_COMMUNICATION_INFO", e.to_string()))?;
self.communication_info = parsed;
Ok(self)
}
#[wasm_bindgen(js_name = withAutoRespondOnFailure)]
pub fn with_auto_respond_on_failure(mut self, enabled: bool) -> DeRecProtocolBuilderWasm {
self.auto_respond_on_failure = enabled;
self
}
#[wasm_bindgen(js_name = withUnpairAck)]
pub fn with_unpair_ack(
mut self,
ack: String,
) -> Result<DeRecProtocolBuilderWasm, JsValue> {
self.unpair_ack = match ack.to_ascii_lowercase().as_str() {
"required" => UnpairAck::Required,
"not_required" | "notrequired" | "fire_and_forget" => UnpairAck::NotRequired,
other => {
return Err(js_error(
"INVALID_UNPAIR_ACK",
format!(
"unknown unpair_ack value: {other:?}; expected \"required\" or \"not_required\""
),
));
}
};
Ok(self)
}
#[wasm_bindgen(js_name = withAutoReplyTo)]
pub fn with_auto_reply_to(mut self, enabled: bool) -> DeRecProtocolBuilderWasm {
self.auto_reply_to = enabled;
self
}
#[wasm_bindgen(js_name = withAutoAccept)]
pub fn with_auto_accept(
mut self,
policy: JsValue,
) -> Result<DeRecProtocolBuilderWasm, JsValue> {
#[derive(serde::Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
struct AutoAcceptPolicyShape {
pairing: bool,
pre_pair: bool,
store_share: bool,
verify_share: bool,
discovery: bool,
get_share: bool,
unpair: bool,
update_channel_info: bool,
}
let parsed: AutoAcceptPolicyShape = serde_wasm_bindgen::from_value(policy)
.map_err(|e| js_error("INVALID_AUTO_ACCEPT_POLICY", e.to_string()))?;
self.auto_accept = crate::protocol::AutoAcceptPolicy {
pairing: parsed.pairing,
pre_pair: parsed.pre_pair,
store_share: parsed.store_share,
verify_share: parsed.verify_share,
discovery: parsed.discovery,
get_share: parsed.get_share,
unpair: parsed.unpair,
update_channel_info: parsed.update_channel_info,
};
Ok(self)
}
#[wasm_bindgen(js_name = withReplicaId)]
pub fn with_replica_id(
mut self,
id: JsValue,
) -> Result<DeRecProtocolBuilderWasm, JsValue> {
let v = js_value_to_u64(id)
.map_err(|e| js_error("INVALID_REPLICA_ID", format!("{e:?}")))?;
self.replica_id = Some(v);
Ok(self)
}
#[wasm_bindgen(js_name = withParameterRange)]
pub fn with_parameter_range(
mut self,
range: JsValue,
) -> Result<DeRecProtocolBuilderWasm, JsValue> {
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct In {
#[serde(default)]
min_share_size: i64,
#[serde(default)]
max_share_size: i64,
#[serde(default)]
min_time_between_verifications: i64,
#[serde(default)]
max_time_between_verifications: i64,
#[serde(default)]
min_time_between_share_updates: i64,
#[serde(default)]
max_time_between_share_updates: i64,
#[serde(default)]
min_unresponsive_deletion_timeout: i64,
#[serde(default)]
max_unresponsive_deletion_timeout: i64,
#[serde(default)]
min_unresponsive_deactivation_timeout: i64,
#[serde(default)]
max_unresponsive_deactivation_timeout: i64,
}
let parsed: In = serde_wasm_bindgen::from_value(range)
.map_err(|e| js_error("INVALID_PARAMETER_RANGE", e.to_string()))?;
self.parameter_range = Some(derec_proto::ParameterRange {
min_share_size: parsed.min_share_size,
max_share_size: parsed.max_share_size,
min_time_between_verifications: parsed.min_time_between_verifications,
max_time_between_verifications: parsed.max_time_between_verifications,
min_time_between_share_updates: parsed.min_time_between_share_updates,
max_time_between_share_updates: parsed.max_time_between_share_updates,
min_unresponsive_deletion_timeout: parsed.min_unresponsive_deletion_timeout,
max_unresponsive_deletion_timeout: parsed.max_unresponsive_deletion_timeout,
min_unresponsive_deactivation_timeout: parsed.min_unresponsive_deactivation_timeout,
max_unresponsive_deactivation_timeout: parsed.max_unresponsive_deactivation_timeout,
});
Ok(self)
}
pub fn build(self) -> Result<DeRecProtocolWasm, JsValue> {
let channel_store = self
.channel_store
.ok_or_else(|| js_error("BUILDER_MISSING", "withChannelStore is required"))?;
let share_store = self
.share_store
.ok_or_else(|| js_error("BUILDER_MISSING", "withShareStore is required"))?;
let secret_store = self
.secret_store
.ok_or_else(|| js_error("BUILDER_MISSING", "withSecretStore is required"))?;
let user_secret_store = self
.user_secret_store
.ok_or_else(|| js_error("BUILDER_MISSING", "withUserSecretStore is required"))?;
let state_store = self
.state_store
.ok_or_else(|| js_error("BUILDER_MISSING", "withStateStore is required"))?;
let transport = self
.transport
.ok_or_else(|| js_error("BUILDER_MISSING", "withTransport is required"))?;
let own_transport_uri = self
.own_transport_uri
.ok_or_else(|| js_error("BUILDER_MISSING", "withOwnTransport is required"))?;
let own_transport_protocol = self
.own_transport_protocol_num
.ok_or_else(|| js_error("BUILDER_MISSING", "withOwnTransport is required"))?;
let proto_tp = TransportProtocol {
uri: own_transport_uri,
protocol: own_transport_protocol,
};
let own_transport = crate::transport::TransportProtocol::try_from(&proto_tp)
.map_err(|e| js_error("INVALID_OWN_TRANSPORT", e.to_string()))?;
let mut builder = DeRecProtocolBuilder::new(self.secret_id)
.with_channel_store(JsChannelStore(channel_store))
.with_share_store(JsShareStore(share_store))
.with_secret_store(JsSecretStore(secret_store))
.with_user_secret_store(JsUserSecretStore(user_secret_store))
.with_state_store(JsStateStore(state_store))
.with_transport(JsTransport(transport))
.with_own_transport(own_transport)
.with_threshold(self.threshold as usize)
.with_keep_versions_count(self.keep_versions_count as usize)
.with_communication_info(self.communication_info)
.with_timeout(Duration::from_secs(u64::from(self.timeout_in_secs)))
.with_auto_respond_on_failure(self.auto_respond_on_failure)
.with_unpair_ack(self.unpair_ack)
.with_auto_reply_to(self.auto_reply_to)
.with_auto_accept(self.auto_accept);
if let Some(id) = self.replica_id {
builder = builder.with_replica_id(id);
}
if let Some(range) = self.parameter_range {
builder = builder.with_parameter_range(range);
}
let inner = builder.build().map_err(js_error_from_lib)?;
Ok(DeRecProtocolWasm { inner })
}
}
#[wasm_bindgen]
impl DeRecProtocolWasm {
#[wasm_bindgen(js_name = "secretId")]
pub fn secret_id(&self) -> u64 {
self.inner.secret_id()
}
#[wasm_bindgen(js_name = "createContact")]
pub async fn create_contact(
&mut self,
channel_id: JsValue,
contact_mode: u32,
nonce: JsValue,
) -> Result<JsValue, JsValue> {
let id = parse_optional_channel_id(channel_id)?;
let mode = match contact_mode {
0 => derec_proto::ContactMode::InlineKeys,
1 => derec_proto::ContactMode::HashedKeys,
2 => derec_proto::ContactMode::NoKeys,
other => {
return Err(js_error(
"INVALID_CONTACT_MODE",
format!("unknown contact_mode: {other}; expected 0 (InlineKeys), 1 (HashedKeys), or 2 (NoKeys)"),
));
}
};
let nonce = if nonce.is_null() || nonce.is_undefined() {
None
} else {
Some(js_value_to_u64(nonce)?)
};
let contact = self
.inner
.create_contact(id, mode, nonce)
.await
.map_err(|e| js_error("DEREC_ERROR", e.to_string()))?;
let contact: PairingContactMessage = contact.into();
let serializer = serde_wasm_bindgen::Serializer::new()
.serialize_large_number_types_as_bigints(true);
use serde::Serialize as _;
contact
.serialize(&serializer)
.map_err(|e| js_error("WASM_SERIALIZE_ERROR", e.to_string()))
}
#[wasm_bindgen(js_name = "setCommunicationInfo")]
pub fn set_communication_info(&mut self, info: JsValue) -> Result<(), JsValue> {
let map: HashMap<String, String> = if info.is_null() || info.is_undefined() {
HashMap::new()
} else {
serde_wasm_bindgen::from_value(info)
.map_err(|e| js_error("INVALID_COMMUNICATION_INFO", e.to_string()))?
};
self.inner.set_communication_info(map);
Ok(())
}
#[wasm_bindgen(js_name = "setOwnTransport")]
pub fn set_own_transport(
&mut self,
uri: String,
protocol: String,
) -> Result<(), JsValue> {
let protocol_num = match protocol.to_lowercase().as_str() {
"https" => 0i32,
other => {
return Err(js_error(
"INVALID_PROTOCOL",
format!("unknown protocol: {other}"),
));
}
};
let proto_tp = TransportProtocol {
uri,
protocol: protocol_num,
};
let lib_tp = crate::transport::TransportProtocol::try_from(&proto_tp)
.map_err(|e| js_error("INVALID_OWN_TRANSPORT", e.to_string()))?;
self.inner
.set_own_transport(lib_tp)
.map_err(|e| js_error("INVALID_OWN_TRANSPORT", e.to_string()))?;
Ok(())
}
#[wasm_bindgen(js_name = "start")]
pub async fn start(&mut self, flow_kind: u32, params: JsValue) -> Result<JsValue, JsValue> {
let flow = parse_flow(flow_kind, params)?;
let rust_events = self
.inner
.start(flow)
.await
.map_err(|e| js_error("DEREC_ERROR", e.to_string()))?;
let js_events = Array::new();
for event in rust_events {
js_events.push(&events::event_to_js(event)?);
}
Ok(js_events.into())
}
#[wasm_bindgen(js_name = "getFingerprint")]
pub async fn get_fingerprint(&mut self, channel_id: JsValue) -> Result<String, JsValue> {
let id = js_value_to_u64(channel_id)?;
self.inner
.get_fingerprint(ChannelId(id))
.await
.map_err(|e| js_error("DEREC_ERROR", e.to_string()))
}
#[wasm_bindgen(js_name = "verifyFingerprint")]
pub async fn verify_fingerprint(
&mut self,
channel_id: JsValue,
fingerprint: String,
) -> Result<bool, JsValue> {
let id = js_value_to_u64(channel_id)?;
self.inner
.verify_fingerprint(ChannelId(id), &fingerprint)
.await
.map_err(|e| js_error("DEREC_ERROR", e.to_string()))
}
pub async fn accept(&mut self, action_bytes: &[u8]) -> Result<JsValue, JsValue> {
let action = pending_action_wire::deserialize(action_bytes)
.map_err(|e| js_error("DECODE_ERROR", e))?;
let rust_events = self
.inner
.accept(action)
.await
.map_err(|e| js_error("DEREC_ERROR", e.to_string()))?;
let js_events = Array::new();
for event in rust_events {
js_events.push(&events::event_to_js(event)?);
}
Ok(js_events.into())
}
pub async fn reject(
&mut self,
action_bytes: &[u8],
status: i32,
memo: &str,
) -> Result<(), JsValue> {
let action = pending_action_wire::deserialize(action_bytes)
.map_err(|e| js_error("DECODE_ERROR", e))?;
let status_enum =
derec_proto::StatusEnum::try_from(status).map_err(|_| {
js_error(
"INVALID_STATUS",
format!("invalid StatusEnum value: {status}"),
)
})?;
self.inner
.reject(action, status_enum, memo)
.await
.map_err(|e| js_error("DEREC_ERROR", e.to_string()))
}
pub async fn process(&mut self, message: &[u8]) -> Result<JsValue, JsValue> {
let rust_events = self
.inner
.process(message)
.await
.map_err(|e| {
web_sys::console::error_1(
&format!("[wasm-process] error: {e}").into(),
);
let channel_id_str = e.channel_id.map(|c| c.0.to_string());
if let Some((status, memo)) = e.as_non_ok_status() {
non_ok_status_error(status, memo, channel_id_str.as_deref())
} else {
process_error(e.to_string(), channel_id_str.as_deref())
}
})?;
let js_events = Array::new();
for event in rust_events {
js_events.push(&events::event_to_js(event)?);
}
Ok(js_events.into())
}
#[wasm_bindgen(js_name = "restore")]
pub async fn restore(
&mut self,
recovered_secret: JsValue,
version: u32,
) -> Result<JsValue, JsValue> {
let secret = parse_recovered_secret(recovered_secret)?;
let rust_events = self
.inner
.restore(&secret, version)
.await
.map_err(|e| match e {
crate::Error::Restore(inner) => restore_error_to_js(inner),
other => js_error_from_lib(other),
})?;
let js_events = Array::new();
for event in rust_events {
js_events.push(&events::event_to_js(event)?);
}
Ok(js_events.into())
}
}
fn parse_recovered_secret(
value: JsValue,
) -> Result<crate::protocol::types::Secret, JsValue> {
#[derive(serde::Deserialize)]
struct HelperIn {
channel_id: String,
transport_uri: String,
shared_key: Vec<u8>,
#[serde(default)]
communication_info: HashMap<String, String>,
}
#[derive(serde::Deserialize)]
struct ReplicaIn {
channel_id: String,
transport_uri: String,
#[serde(default)]
communication_info: HashMap<String, String>,
replica_id: String,
sender_kind: i32,
}
#[derive(serde::Deserialize)]
struct UserSecretIn {
id: Vec<u8>,
name: String,
data: Vec<u8>,
}
#[derive(serde::Deserialize)]
struct SecretIn {
#[serde(default)]
helpers: Vec<HelperIn>,
#[serde(default)]
secrets: Vec<UserSecretIn>,
#[serde(default)]
replicas: Option<ReplicasIn>,
#[serde(default)]
owner_replica_id: String,
}
#[derive(serde::Deserialize)]
struct ReplicasIn {
#[serde(default)]
replicas: Vec<ReplicaIn>,
#[serde(default)]
shared_key: Vec<u8>,
}
let input: SecretIn = serde_wasm_bindgen::from_value(value)
.map_err(|e| js_error("INVALID_RECOVERED_SECRET", e.to_string()))?;
let parse_u64 = |s: &str, ctx: &str| -> Result<u64, JsValue> {
if s.is_empty() {
return Ok(0);
}
s.parse::<u64>().map_err(|e| {
js_error(
"INVALID_RECOVERED_SECRET",
format!("{ctx} must be a u64 decimal string: {e}"),
)
})
};
let helpers = input
.helpers
.into_iter()
.map(|h| -> Result<_, JsValue> {
Ok(crate::protocol::types::HelperInfo {
channel_id: parse_u64(&h.channel_id, "helper.channel_id")?,
transport_uri: h.transport_uri,
shared_key: h.shared_key,
communication_info: h.communication_info,
})
})
.collect::<Result<Vec<_>, _>>()?;
let replicas = input
.replicas
.map(|g| -> Result<_, JsValue> {
let replicas = g
.replicas
.into_iter()
.map(|r| -> Result<_, JsValue> {
Ok(crate::protocol::types::ReplicaInfo {
channel_id: parse_u64(&r.channel_id, "replica.channel_id")?,
transport_uri: r.transport_uri,
communication_info: r.communication_info,
replica_id: parse_u64(&r.replica_id, "replica.replica_id")?,
sender_kind: r.sender_kind,
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(crate::protocol::types::Replicas {
replicas,
shared_key: g.shared_key,
})
})
.transpose()?;
let secrets = input
.secrets
.into_iter()
.map(|s| crate::protocol::types::UserSecret {
id: s.id,
name: s.name,
data: s.data,
})
.collect();
let owner_replica_id = parse_u64(&input.owner_replica_id, "owner_replica_id")?;
Ok(crate::protocol::types::Secret {
helpers,
secrets,
replicas,
owner_replica_id,
})
}
fn restore_error_to_js(e: crate::protocol::RestoreError) -> JsValue {
use crate::protocol::RestoreError;
match e {
RestoreError::AlreadyRestored => js_error("ALREADY_RESTORED", e.to_string()),
RestoreError::Conflict(ids) => {
#[derive(serde::Serialize)]
struct ConflictError {
code: &'static str,
message: String,
channel_ids: Vec<String>,
}
serde_wasm_bindgen::to_value(&ConflictError {
code: "CONFLICT",
message: "restore blocked by pre-existing channels at canonical ids"
.to_owned(),
channel_ids: ids.iter().map(|c| c.0.to_string()).collect(),
})
.unwrap_or_else(|_| js_error("CONFLICT", "restore conflict"))
}
RestoreError::Invariant(msg) => js_error("INVARIANT", msg.to_string()),
}
}
fn non_ok_status_error(status: i32, memo: &str, channel_id: Option<&str>) -> JsValue {
#[derive(serde::Serialize)]
struct NonOkStatusError<'a> {
code: &'static str,
message: String,
status: i32,
memo: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
channel_id: Option<&'a str>,
}
serde_wasm_bindgen::to_value(&NonOkStatusError {
code: "NON_OK_STATUS",
message: format!("non-ok status (status={status}): {memo}"),
status,
memo,
channel_id,
})
.unwrap_or_else(|_| JsValue::from_str("failed to serialize non-ok status error"))
}
fn process_error(message: String, channel_id: Option<&str>) -> JsValue {
#[derive(serde::Serialize)]
struct ProcessErrorJs<'a> {
code: &'static str,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
channel_id: Option<&'a str>,
}
serde_wasm_bindgen::to_value(&ProcessErrorJs {
code: "DEREC_ERROR",
message,
channel_id,
})
.unwrap_or_else(|_| JsValue::from_str("failed to serialize process error"))
}
fn parse_optional_channel_id(val: JsValue) -> Result<Option<ChannelId>, JsValue> {
if val.is_null() || val.is_undefined() {
return Ok(None);
}
Ok(Some(ChannelId(js_value_to_u64(val)?)))
}
fn js_value_to_u64(val: JsValue) -> Result<u64, JsValue> {
if val.is_bigint() {
let s = js_sys::BigInt::from(val)
.to_string(10)
.map_err(|e| js_error("DECODE_ERROR", format!("{e:?}")))?
.as_string()
.ok_or_else(|| js_error("DECODE_ERROR", "BigInt.toString returned non-string"))?;
s.parse::<u64>()
.map_err(|e| js_error("DECODE_ERROR", e.to_string()))
} else if let Some(s) = val.as_string() {
s.parse::<u64>()
.map_err(|e| js_error("DECODE_ERROR", format!("string is not a decimal u64: {e}")))
} else {
val.as_f64()
.ok_or_else(|| {
js_error("DECODE_ERROR", "value must be BigInt, number, or decimal string")
})
.map(|f| f as u64)
}
}
fn parse_sender_kind(kind: u32) -> Result<SenderKind, JsValue> {
match kind {
0 => Ok(SenderKind::Owner),
1 => Ok(SenderKind::Helper),
3 => Ok(SenderKind::ReplicaSource),
4 => Ok(SenderKind::ReplicaDestination),
_ => Err(js_error(
"INVALID_SENDER_KIND",
format!("invalid sender kind: {kind}, valid values are 0 (Owner), 1 (Helper), 3 (ReplicaSource), 4 (ReplicaDestination)"),
)),
}
}
fn parse_target(val: JsValue) -> Result<Target, JsValue> {
if val.is_null() || val.is_undefined() {
return Ok(Target::All);
}
if val.is_bigint() || val.as_f64().is_some() {
let id = js_value_to_u64(val)?;
return Ok(Target::Single(ChannelId(id)));
}
if Array::is_array(&val) {
let arr = Array::from(&val);
let mut ids = Vec::with_capacity(arr.length() as usize);
for i in 0..arr.length() {
ids.push(ChannelId(js_value_to_u64(arr.get(i))?));
}
return Ok(Target::Many(ids));
}
Err(js_error(
"INVALID_DISCOVERY_TARGET",
"target must be null (all), a BigInt (single), or an array of BigInts (many)",
))
}
fn parse_user_secrets(val: JsValue) -> Result<Vec<UserSecret>, JsValue> {
let arr = Array::from(&val);
let mut result = Vec::with_capacity(arr.length() as usize);
for i in 0..arr.length() {
let entry = arr.get(i);
let id = js_sys::Reflect::get(&entry, &JsValue::from_str("id"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing id: {e:?}")))?;
let name = js_sys::Reflect::get(&entry, &JsValue::from_str("name"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing name: {e:?}")))?
.as_string()
.ok_or_else(|| js_error("DECODE_ERROR", "name must be a string"))?;
let data = js_sys::Reflect::get(&entry, &JsValue::from_str("data"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing data: {e:?}")))?;
result.push(UserSecret {
id: Uint8Array::new(&id).to_vec(),
name,
data: Uint8Array::new(&data).to_vec(),
});
}
Ok(result)
}
fn parse_flow(flow_kind: u32, params: JsValue) -> Result<DeRecFlow, JsValue> {
match flow_kind {
0 => {
let kind_val = js_sys::Reflect::get(¶ms, &JsValue::from_str("kind"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing kind: {e:?}")))?;
let kind = kind_val
.as_f64()
.ok_or_else(|| js_error("DECODE_ERROR", "kind must be a number"))?
as u32;
let sender_kind = parse_sender_kind(kind)?;
let contact_val = js_sys::Reflect::get(¶ms, &JsValue::from_str("contact"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing contact: {e:?}")))?;
let contact: PairingContactMessage = serde_wasm_bindgen::from_value(contact_val)
.map_err(|e| js_error("DECODE_ERROR", e.to_string()))?;
let contact: derec_proto::ContactMessage = contact.into();
let raw = js_sys::Reflect::get(¶ms, &JsValue::from_str("peerCommunicationInfo"))
.unwrap_or(JsValue::UNDEFINED);
let peer_communication_info: HashMap<String, String> =
if raw.is_null() || raw.is_undefined() {
HashMap::new()
} else {
serde_wasm_bindgen::from_value(raw).map_err(|e| {
js_error("INVALID_PEER_COMMUNICATION_INFO", e.to_string())
})?
};
Ok(DeRecFlow::Pairing {
kind: sender_kind,
contact,
peer_communication_info,
})
}
1 => {
let target_val = js_sys::Reflect::get(¶ms, &JsValue::from_str("target"))
.unwrap_or(JsValue::UNDEFINED);
let target = parse_target(target_val)?;
Ok(DeRecFlow::Discovery { target })
}
2 => {
let secrets_val = js_sys::Reflect::get(¶ms, &JsValue::from_str("secrets"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing secrets: {e:?}")))?;
let secrets = parse_user_secrets(secrets_val)?;
let description = js_sys::Reflect::get(¶ms, &JsValue::from_str("description"))
.unwrap_or(JsValue::UNDEFINED)
.as_string();
Ok(DeRecFlow::ProtectSecret {
secrets,
description,
})
}
3 => {
let secret_id_val = js_sys::Reflect::get(¶ms, &JsValue::from_str("secretId"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing secretId: {e:?}")))?;
let secret_id = js_value_to_u64(secret_id_val)?;
let version = js_sys::Reflect::get(¶ms, &JsValue::from_str("version"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing version: {e:?}")))?
.as_f64()
.ok_or_else(|| js_error("DECODE_ERROR", "version must be a number"))?
as u32;
let target_val = js_sys::Reflect::get(¶ms, &JsValue::from_str("target"))
.unwrap_or(JsValue::UNDEFINED);
let target = parse_target(target_val)?;
Ok(DeRecFlow::VerifyShares {
secret_id,
version,
target,
})
}
4 => {
let secret_id_val = js_sys::Reflect::get(¶ms, &JsValue::from_str("secretId"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing secretId: {e:?}")))?;
let secret_id = js_value_to_u64(secret_id_val)?;
let version = js_sys::Reflect::get(¶ms, &JsValue::from_str("version"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing version: {e:?}")))?
.as_f64()
.ok_or_else(|| js_error("DECODE_ERROR", "version must be a number"))?
as u32;
Ok(DeRecFlow::RecoverSecret { secret_id, version })
}
5 => {
let channel_id_val = js_sys::Reflect::get(¶ms, &JsValue::from_str("channel_id"))
.map_err(|e| js_error("DECODE_ERROR", format!("missing channel_id: {e:?}")))?;
let channel_id = ChannelId(js_value_to_u64(channel_id_val)?);
let memo = js_sys::Reflect::get(¶ms, &JsValue::from_str("memo"))
.unwrap_or(JsValue::UNDEFINED)
.as_string();
Ok(DeRecFlow::Unpair { channel_id, memo })
}
6 => {
let target_val = js_sys::Reflect::get(¶ms, &JsValue::from_str("target"))
.unwrap_or(JsValue::UNDEFINED);
let target = parse_target(target_val)?;
let communication_info_val =
js_sys::Reflect::get(¶ms, &JsValue::from_str("communication_info"))
.unwrap_or(JsValue::UNDEFINED);
let communication_info: Option<HashMap<String, String>> =
if communication_info_val.is_null() || communication_info_val.is_undefined() {
None
} else {
Some(
serde_wasm_bindgen::from_value(communication_info_val).map_err(|e| {
js_error("INVALID_COMMUNICATION_INFO", e.to_string())
})?,
)
};
let transport_protocol_val =
js_sys::Reflect::get(¶ms, &JsValue::from_str("transport_protocol"))
.unwrap_or(JsValue::UNDEFINED);
let transport_protocol = if transport_protocol_val.is_null()
|| transport_protocol_val.is_undefined()
{
None
} else {
#[derive(serde::Deserialize)]
struct TransportShape {
uri: String,
protocol: i32,
}
let parsed: TransportShape = serde_wasm_bindgen::from_value(transport_protocol_val)
.map_err(|e| js_error("INVALID_TRANSPORT_PROTOCOL", e.to_string()))?;
Some(TransportProtocol {
uri: parsed.uri,
protocol: parsed.protocol,
})
};
Ok(DeRecFlow::UpdateChannelInfo {
target,
communication_info,
transport_protocol,
})
}
_ => Err(js_error(
"INVALID_FLOW_KIND",
format!("invalid flow kind: {flow_kind}, must be 0..6"),
)),
}
}