wallet import/export API and QRcode scanning from Tauri plugin

pull/31/head^2
Niko PLP 5 months ago
parent 104c796f68
commit 65b91ffc3f
  1. 48
      Cargo.lock
  2. 8
      README.md
  3. 1
      nextgraph/.gitignore
  4. 3
      nextgraph/Cargo.toml
  5. 3
      nextgraph/src/lib.rs
  6. 259
      nextgraph/src/local_broker.rs
  7. 1
      nextgraph/src/local_broker_dev_env.rs
  8. 1
      ng-app/package.json
  9. 3
      ng-app/src-tauri/Cargo.toml
  10. 1
      ng-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml
  11. 68
      ng-app/src-tauri/src/lib.rs
  12. 2
      ng-app/src/App.svelte
  13. 5
      ng-app/src/api.ts
  14. 3
      ng-app/src/lib/Login.svelte
  15. 9
      ng-app/src/locales/en.json
  16. 4
      ng-app/src/routes/AccountInfo.svelte
  17. 51
      ng-app/src/routes/ScanQR.svelte
  18. 2
      ng-app/src/routes/User.svelte
  19. 3
      ng-app/src/routes/UserRegistered.svelte
  20. 8
      ng-app/src/routes/WalletCreate.svelte
  21. 16
      ng-app/src/routes/WalletInfo.svelte
  22. 66
      ng-app/src/routes/WalletLogin.svelte
  23. 13
      ng-app/src/store.ts
  24. 101
      ng-broker/src/server_broker.rs
  25. 2
      ng-net/src/actors/client/mod.rs
  26. 89
      ng-net/src/actors/client/wallet_put_export.rs
  27. 2
      ng-net/src/actors/ext/mod.rs
  28. 109
      ng-net/src/actors/ext/wallet_get_export.rs
  29. 2
      ng-net/src/actors/mod.rs
  30. 64
      ng-net/src/actors/start.rs
  31. 28
      ng-net/src/broker.rs
  32. 103
      ng-net/src/connection.rs
  33. 13
      ng-net/src/server_broker.rs
  34. 117
      ng-net/src/types.rs
  35. 19
      ng-repo/src/errors.rs
  36. 75
      ng-sdk-js/src/lib.rs
  37. 20
      ng-verifier/src/verifier.rs
  38. 1
      ng-wallet/Cargo.toml
  39. 24
      ng-wallet/src/types.rs
  40. 8
      pnpm-lock.yaml

48
Cargo.lock generated

@ -2797,14 +2797,13 @@ checksum = "f850fafca79ebacd70eab9d80cb75a33aeda38bde8f3dd784c1837cdf0bde631"
[[package]]
name = "json-patch"
version = "1.0.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f54898088ccb91df1b492cc80029a6fdf1c48ca0db7c6822a8babad69c94658"
checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b"
dependencies = [
"serde",
"serde_json",
"thiserror",
"treediff",
]
[[package]]
@ -2942,9 +2941,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.19"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
dependencies = [
"value-bag",
]
@ -3264,6 +3263,7 @@ dependencies = [
"async-trait",
"base64-url",
"futures",
"lazy_static",
"ng-client-ws",
"ng-net",
"ng-repo",
@ -3271,7 +3271,9 @@ dependencies = [
"ng-verifier",
"ng-wallet",
"once_cell",
"qrcode",
"serde_bare",
"serde_bytes",
"serde_json",
"web-time",
"zeroize",
@ -3293,7 +3295,9 @@ dependencies = [
"sys-locale",
"tauri",
"tauri-build",
"tauri-plugin-barcode-scanner",
"tauri-plugin-window",
"tauri-utils",
]
[[package]]
@ -3547,6 +3551,7 @@ dependencies = [
"aes-gcm-siv",
"argon2",
"async-std",
"base64-url",
"blake3",
"chacha20poly1305",
"crypto_box",
@ -4436,6 +4441,12 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "qrcode"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
[[package]]
name = "quick-xml"
version = "0.28.2"
@ -5575,6 +5586,20 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin-barcode-scanner"
version = "2.0.0-alpha.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058922dd9cafc89a865593c99507177c4cdbdad9d22a911ac41872ed7dbf0348"
dependencies = [
"log",
"serde",
"serde_json",
"tauri",
"tauri-build",
"thiserror",
]
[[package]]
name = "tauri-plugin-window"
version = "2.0.0-alpha.1"
@ -6043,15 +6068,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "treediff"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303"
dependencies = [
"serde_json",
]
[[package]]
name = "try-lock"
version = "0.2.4"
@ -6198,9 +6214,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.4.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e"
checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101"
[[package]]
name = "vcpkg"

@ -57,6 +57,14 @@ cargo install wasm-pack --git https://github.com/rustwasm/wasm-pack.git --rev c2
then :
create a file called `nextgraph/src/local_broker_dev_env.rs` with the content :
```
pub const PEER_ID: &str = "FtdzuDYGewfXWdoPuXIPb0wnd0SAg1WoA2B14S7jW3MA";
```
once your ngd server will run in your dev env, replace the above string with the actual PEER ID of your ngd server.
```
cargo install cargo-watch
// optionally, if you want a Rust REPL: cargo install evcxr_repl

@ -1 +1,2 @@
tests
local_broker_dev_env_peer_id.rs

@ -18,6 +18,7 @@ maintenance = { status = "actively-developed" }
[dependencies]
serde_bare = "0.5.0"
serde_json = "1.0"
serde_bytes = "0.11.7"
base64-url = "2.0.0"
once_cell = "1.17.1"
zeroize = { version = "1.7.0", features = ["zeroize_derive"] }
@ -25,7 +26,9 @@ futures = "0.3.24"
async-std = { version = "1.12.0", features = [ "attributes", "unstable" ] }
async-trait = "0.1.64"
async-once-cell = "0.5.3"
lazy_static = "1.4.0"
web-time = "0.2.0"
qrcode = { version = "0.14.1", default-features = false, features = ["svg"] }
ng-repo = { path = "../ng-repo", version = "0.1.0-preview.1" }
ng-net = { path = "../ng-net", version = "0.1.0-preview.1" }
ng-wallet = { path = "../ng-wallet", version = "0.1.0-preview.5" }

@ -98,3 +98,6 @@ pub mod verifier {
pub mod wallet {
pub use ng_wallet::*;
}
#[cfg(debug_assertions)]
mod local_broker_dev_env;

@ -20,6 +20,8 @@ use once_cell::sync::Lazy;
use serde_bare::to_vec;
use serde_json::json;
use zeroize::Zeroize;
use qrcode::{render::svg, QrCode};
use lazy_static::lazy_static;
use ng_repo::block_storage::BlockStorage;
use ng_repo::block_storage::HashMapBlockStorage;
@ -27,7 +29,7 @@ use ng_repo::errors::{NgError, ProtocolError};
use ng_repo::log::*;
use ng_repo::os_info::get_os_info;
use ng_repo::types::*;
use ng_repo::utils::{derive_key, generate_keypair};
use ng_repo::utils::{derive_key, generate_keypair, encrypt_in_place};
use ng_net::{actor::*,actors::admin::*};
use ng_net::broker::*;
@ -1492,6 +1494,238 @@ pub async fn wallet_add(lws: LocalWalletStorageV0) -> Result<(), NgError> {
}
Ok(())
}
#[cfg(debug_assertions)]
lazy_static! {
static ref NEXTGRAPH_EU: BrokerServerV0 = BrokerServerV0 {
server_type: BrokerServerTypeV0::Localhost(14400),
can_verify: false,
can_forward: false,
peer_id: ng_repo::utils::decode_key({use crate::local_broker_dev_env::PEER_ID; PEER_ID}).unwrap(),
};
}
#[cfg(not(debug_assertions))]
lazy_static! {
static ref NEXTGRAPH_EU: BrokerServerV0 = BrokerServerV0 {
server_type: BrokerServerTypeV0::Domain("nextgraph.eu".to_string()),
can_verify: false,
can_forward: false,
peer_id: ng_repo::utils::decode_key("FtdzuDYGewfXWdoPuXIPb0wnd0SAg1WoA2B14S7jW3MA").unwrap(),
};
}
/// Obtains a Wallet object from a QRCode or a TextCode.
///
/// The returned object can be used to import the wallet into a new Device
/// with the help of the function [wallet_open_with_pazzle_words]
/// followed by [wallet_import]
pub async fn wallet_import_from_code(code: String) -> Result<Wallet, NgError> {
let qr = NgQRCode::from_code(code)?;
match qr {
NgQRCode::V0(NgQRCodeV0{broker, rendezvous, secret_key, is_rendezvous}) => {
let wallet: ExportedWallet = do_ext_call(
&broker,
ExtWalletGetExportV0 {
id:rendezvous,
is_rendezvous
}).await?;
let mut buf = wallet.0.into_vec();
encrypt_in_place(&mut buf,*secret_key.slice(), [0;12]);
let wallet: Wallet = serde_bare::from_slice(&buf)?;
let broker = match LOCAL_BROKER.get() {
None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized),
Some(Ok(broker)) => broker.read().await,
};
// check that the wallet is not already present in local_broker
let wallet_name = wallet.name();
if broker.wallets.get(&wallet_name).is_none() {
Ok(wallet)
} else {
Err(NgError::WalletAlreadyAdded)
}
}
}
}
/// Starts a rendez-vous to obtain a wallet from other device.
///
/// A rendezvous is used when the device that is importing, doesn't have a camera.
/// The QRCode is displayed on that device, and another device (with camera, and with the wallet) will scan it.
///
/// Returns the QRcode in SVG format, and the code (a string) to be used with [wallet_import_from_code]
pub async fn wallet_import_rendezvous(size: u32) -> Result<(String,String), NgError> {
let code = NgQRCode::V0(NgQRCodeV0 {
broker: NEXTGRAPH_EU.clone(),
rendezvous: SymKey::random(),
secret_key: SymKey::random(),
is_rendezvous: true
});
let code_string = code.to_code();
let code_svg = match QrCode::with_error_correction_level(&code_string, qrcode::EcLevel::M) {
Ok(qr) => {
let svg = qr
.render()
.max_dimensions(size, size)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.build();
svg
},
Err(_e) => {return Err(NgError::BrokerError)}
};
Ok((code_svg,code_string))
}
/// Gets the TextCode to display in order to export the wallet of the current session ID
///
/// The ExportedWallet is valid for 5 min.
///
/// Returns the TextCode
pub async fn wallet_export_get_textcode(session_id: u64) -> Result<String, NgError> {
let broker = match LOCAL_BROKER.get() {
None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized),
Some(Ok(broker)) => broker.read().await,
};
match &broker.config {
LocalBrokerConfig::Headless(_) => {
return Err(NgError::LocalBrokerIsHeadless)
},
_ => {
let (real_session_id, is_remote) = broker.get_real_session_id_for_mut(session_id)?;
if is_remote {
return Err(NgError::NotImplemented);
} else {
let session = broker.opened_sessions_list[real_session_id].as_ref()
.ok_or(NgError::SessionNotFound)?;
let wallet_name = session.config.wallet_name();
match broker.wallets.get(&wallet_name) {
None => Err(NgError::WalletNotFound),
Some(lws) => {
let broker = lws.bootstrap.servers().first().unwrap();
let wallet = &lws.wallet;
let secret_key = SymKey::random();
let rendezvous = SymKey::random();
let code = NgQRCode::V0(NgQRCodeV0 {
broker: broker.clone(),
rendezvous: rendezvous.clone(),
secret_key: secret_key.clone(),
is_rendezvous: false
});
let code_string = code.to_code();
let mut wallet_ser = serde_bare::to_vec(wallet)?;
encrypt_in_place(&mut wallet_ser,*secret_key.slice(), [0;12]);
let exported_wallet = ExportedWallet(serde_bytes::ByteBuf::from(wallet_ser));
match session.verifier.client_request::<WalletPutExport, ()>(WalletPutExport::V0(WalletPutExportV0{wallet:exported_wallet, rendezvous_id:rendezvous, is_rendezvous:false})).await {
Err(e) => Err(e),
Ok(SoS::Stream(_)) => Err(NgError::InvalidResponse),
Ok(SoS::Single(_)) => Ok(code_string)
}
}
}
}
}
}
}
/// Gets the QRcode to display in order to export a wallet of the current session ID
///
/// The ExportedWallet is valid for 5 min.
///
/// Returns the QRcode in SVG format
pub async fn wallet_export_get_qrcode(session_id: u64, size: u32) -> Result<String, NgError> {
let code_string = wallet_export_get_textcode(session_id).await?;
let code_svg = match QrCode::with_error_correction_level(&code_string, qrcode::EcLevel::M) {
Ok(qr) => {
let svg = qr
.render()
.max_dimensions(size, size)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.build();
svg
},
Err(_e) => {return Err(NgError::BrokerError)}
};
Ok(code_svg)
}
/// Puts the Wallet to the rendezvous ID that was scanned
///
/// The rendezvous ID is valid for 5 min.
pub async fn wallet_export_rendezvous(session_id: u64, code: String) -> Result<(), NgError> {
let qr = NgQRCode::from_code(code)?;
match qr {
NgQRCode::V0(NgQRCodeV0{broker: _, rendezvous, secret_key, is_rendezvous}) => {
if !is_rendezvous {
return Err(NgError::NotARendezVous);
}
let broker = match LOCAL_BROKER.get() {
None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized),
Some(Ok(broker)) => broker.read().await,
};
match &broker.config {
LocalBrokerConfig::Headless(_) => {
return Err(NgError::LocalBrokerIsHeadless)
},
_ => {
let (real_session_id, is_remote) = broker.get_real_session_id_for_mut(session_id)?;
if is_remote {
return Err(NgError::NotImplemented);
} else {
let session = broker.opened_sessions_list[real_session_id].as_ref()
.ok_or(NgError::SessionNotFound)?;
let wallet_name = session.config.wallet_name();
match broker.wallets.get(&wallet_name) {
None => Err(NgError::WalletNotFound),
Some(lws) => {
//let broker = lws.bootstrap.servers().first().unwrap();
let wallet = &lws.wallet;
let mut wallet_ser = serde_bare::to_vec(wallet)?;
encrypt_in_place(&mut wallet_ser,*secret_key.slice(), [0;12]);
let exported_wallet = ExportedWallet(serde_bytes::ByteBuf::from(wallet_ser));
// TODO: send the WalletPutExport client request to the broker received from QRcode. for now it is cheer luck that all clients are connected to nextgraph.eu.
// if the user doesn't have an account with nextgraph.eu, their broker should relay the request (core protocol ?)
match session.verifier.client_request::<WalletPutExport, ()>(WalletPutExport::V0(WalletPutExportV0{wallet:exported_wallet, rendezvous_id:rendezvous, is_rendezvous:true})).await {
Err(e) => Err(e),
Ok(SoS::Stream(_)) => Err(NgError::InvalidResponse),
Ok(SoS::Single(_)) => Ok(())
}
}
}
}
}
}
}
}
}
/// Reads a binary Wallet File and decodes it to a Wallet object.
///
@ -2214,6 +2448,29 @@ async fn do_admin_call<
.await
}
async fn do_ext_call<
A: Into<ProtocolMessage> + Into<ExtRequestContentV0> + std::fmt::Debug + Sync + Send + 'static,
B: TryFrom<ProtocolMessage, Error = ProtocolError>
+ std::fmt::Debug
+ Sync
+ Send
+ 'static,
>(
broker_server: &BrokerServerV0,
cmd: A,
) -> Result<B, NgError> {
let (peer_privk, peer_pubk) = generate_keypair();
Broker::ext(
Box::new(ConnectionWebSocket {}),
peer_privk,
peer_pubk,
broker_server.peer_id,
broker_server.get_ws_url(&None).await.unwrap().0, // for now we are only connecting to NextGraph SaaS cloud (nextgraph.eu) so it is safe.
cmd,
)
.await
}
#[doc(hidden)]
pub async fn admin_create_user(server_peer_id: DirectPeerId, admin_user_key: PrivKey, server_addr: BindAddress) -> Result<UserId, ProtocolError> {

@ -0,0 +1 @@
pub const PEER_ID: &str = "FtdzuDYGewfXWdoPuXIPb0wnd0SAg1WoA2B14S7jW3MA";

@ -18,6 +18,7 @@
"dependencies": {
"@popperjs/core": "^2.11.8",
"@tauri-apps/api": "2.0.0-alpha.8",
"@tauri-apps/plugin-barcode-scanner": "2.0.0-alpha.0",
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
"async-proxy": "^0.4.1",
"classnames": "^2.3.2",

@ -21,7 +21,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2.0.0-alpha.8", features = [] }
# tauri-macros = { version = "=2.0.0-alpha.6" }
# tauri-codegen = { version = "=2.0.0-alpha.6" }
# tauri-utils = { version = "=2.0.0-alpha.6" }
tauri-utils = { version = "=2.0.0-alpha.7" }
[dependencies]
serde = { version = "1.0", features = ["derive"] }
@ -32,6 +32,7 @@ sys-locale = { version = "0.3.1" }
ng-async-tungstenite = { git = "https://git.nextgraph.org/NextGraph/async-tungstenite.git", branch = "nextgraph", features = ["async-std-runtime", "async-native-tls"] }
tauri = { version = "2.0.0-alpha.14", features = [] }
tauri-plugin-window = "2.0.0-alpha.1"
tauri-plugin-barcode-scanner = "=2.0.0-alpha.0"
# tauri-plugin-window = { git = "https://git.nextgraph.org/NextGraph/plugins-workspace.git", branch="window-alpha.1-nextgraph" }
# tauri = { git = "https://git.nextgraph.org/NextGraph/tauri.git", branch="alpha.11-nextgraph", features = ["no-ipc-custom-protocol"] }
# tauri = { git = "https://github.com/simonhyll/tauri.git", branch="fix/ipc-mixup", features = [] }

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"

@ -127,7 +127,8 @@ async fn wallet_open_with_mnemonic_words(
pin: [u8; 4],
_app: tauri::AppHandle,
) -> Result<SensitiveWallet, String> {
let wallet = nextgraph::local_broker::wallet_open_with_mnemonic_words(&wallet, &mnemonic_words, pin)
let wallet =
nextgraph::local_broker::wallet_open_with_mnemonic_words(&wallet, &mnemonic_words, pin)
.map_err(|e| e.to_string())?;
Ok(wallet)
}
@ -205,6 +206,55 @@ async fn wallet_import(
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_export_rendezvous(
session_id: u64,
code: String,
_app: tauri::AppHandle,
) -> Result<(), String> {
nextgraph::local_broker::wallet_export_rendezvous(session_id, code)
.await
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_export_get_qrcode(
session_id: u64,
size: u32,
_app: tauri::AppHandle,
) -> Result<String, String> {
nextgraph::local_broker::wallet_export_get_qrcode(session_id, size)
.await
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_export_get_textcode(
session_id: u64,
_app: tauri::AppHandle,
) -> Result<String, String> {
nextgraph::local_broker::wallet_export_get_textcode(session_id)
.await
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_import_rendezvous(
size: u32,
_app: tauri::AppHandle,
) -> Result<(String, String), String> {
nextgraph::local_broker::wallet_import_rendezvous(size)
.await
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn wallet_import_from_code(code: String, _app: tauri::AppHandle) -> Result<Wallet, String> {
nextgraph::local_broker::wallet_import_from_code(code)
.await
.map_err(|e: NgError| e.to_string())
}
#[tauri::command(rename_all = "snake_case")]
async fn get_wallets(
app: tauri::AppHandle,
@ -517,7 +567,7 @@ impl AppBuilder {
pub fn run(self) {
let setup = self.setup;
tauri::Builder::default()
let builder = tauri::Builder::default()
.setup(move |app| {
if let Some(setup) = setup {
(setup)(app)?;
@ -533,7 +583,14 @@ impl AppBuilder {
}
Ok(())
})
.plugin(tauri_plugin_window::init())
.plugin(tauri_plugin_window::init());
#[cfg(mobile)]
{
let builder = builder.plugin(tauri_plugin_barcode_scanner::init());
}
builder
.invoke_handler(tauri::generate_handler![
test,
locales,
@ -547,6 +604,11 @@ impl AppBuilder {
wallet_read_file,
wallet_get_file,
wallet_import,
wallet_export_rendezvous,
wallet_export_get_qrcode,
wallet_export_get_textcode,
wallet_import_rendezvous,
wallet_import_from_code,
wallet_close,
encode_create_account,
session_start,

@ -35,6 +35,7 @@
import User from "./routes/User.svelte";
import UserRegistered from "./routes/UserRegistered.svelte";
import Install from "./routes/Install.svelte";
import ScanQR from "./routes/ScanQR.svelte";
import ng from "./api";
import AccountInfo from "./routes/AccountInfo.svelte";
@ -49,6 +50,7 @@
routes.set("/user/registered", UserRegistered);
routes.set("/wallet", WalletInfo);
routes.set("/user/accounts", AccountInfo);
routes.set("/wallet/scanqr", ScanQR);
if (import.meta.env.NG_APP_WEB) routes.set("/install", Install);
routes.set(/^\/did:ng(.*)/i, NURI);
routes.set("*", NotFound);

@ -22,6 +22,11 @@ const mapping = {
"wallet_read_file": ["file"],
"wallet_get_file": ["wallet_name"],
"wallet_import": ["encrypted_wallet","opened_wallet","in_memory"],
"wallet_export_rendezvous": ["session_id", "code"],
"wallet_export_get_qrcode": ["session_id", "size"],
"wallet_export_get_textcode": ["session_id"],
"wallet_import_rendezvous": ["size"],
"wallet_import_from_code": ["code"],
"wallet_close": ["wallet_name"],
"encode_create_account": ["payload"],
"session_start": ["wallet_name","user"],

@ -32,6 +32,7 @@
ArrowLeft,
} from "svelte-heros-v2";
import PasswordInput from "./components/PasswordInput.svelte";
import { display_error } from "../store";
//import Worker from "../worker.js?worker&inline";
export let wallet;
export let for_import = false;
@ -685,7 +686,7 @@
/>
</svg>
<Alert color="red" class="mt-5">
{$t("errors." + error)}
{display_error(error)}
</Alert>
</div>
<div class="flex justify-between mt-auto gap-4">

@ -295,18 +295,19 @@
"VerifierError": "Error during verification.",
"SiteNotFoundOnBroker": "The site cannot be found on the broker",
"BrokerConfigErrorStr": "{error}",
"BrokerConfigError": "Error in the broker configuration",
"BrokerConfigError": "Error in the broker configuration.",
"MalformedEvent": "The event has an invalid format.",
"InvalidPayload": "The payload is invalid.",
"WrongUploadId": "The upload ID is incorrect.",
"FileError": "Error with file.",
"InternalError": "Internal Error",
"OxiGraphError": "Error in OxiGraph database.",
"ConfigError": "Error in configuration",
"ConfigError": "Error in configuration.",
"LocalBrokerIsHeadless": "The local broker is headless.",
"LocalBrokerIsNotHeadless": "The local broker is not headless.",
"InvalidNuri": "Invalid NextGraph URI",
"InvalidTarget": "Cannot resolve target"
"InvalidNuri": "Invalid NextGraph URI.",
"InvalidTarget": "Cannot resolve target.",
"ExportWalletTimeOut": "Export of wallet has expired."
},
"connectivity": {
"stopped": "Stopped",

@ -22,7 +22,7 @@
import { onMount, tick } from "svelte";
import { Sidebar, SidebarGroup, SidebarWrapper } from "flowbite-svelte";
import { t } from "svelte-i18n";
import { active_session, active_wallet, connections } from "../store";
import { active_session, active_wallet, connections, display_error } from "../store";
import { default as ng } from "../api";
import DeviceIcon from "../lib/components/DeviceIcon.svelte";
@ -343,7 +343,7 @@
{:else}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.error_occurred", {
values: { message: $t("errors." + error) },
values: { message: display_error(error) },
})}
</p>
<a use:link href="/">

@ -0,0 +1,51 @@
<!--
// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
-->
<!--
Home page to display for logged in users.
Redirects to no-wallet or login page, if not logged in.
-->
<script>
import { onMount, onDestroy } from "svelte";
import {
wallet_import_qrcode
} from "../store";
let tauri_platform = import.meta.env.TAURI_PLATFORM;
let mobile = tauri_platform == "android" || tauri_platform == "ios";
onMount(async () => {
//TODO: here we should also take into account the case of a webapp with camera feature, and sue the lib https://www.npmjs.com/package/html5-qrcode
if (mobile) {
const scanner = await import("@tauri-apps/plugin-barcode-scanner");
let perms = await scanner.requestPermissions();
console.log(perms);
wallet_import_qrcode.set("");
let result = await scanner.scan({ windowed: false, cameraDirection: "back", formats: [scanner.Format.QRCode] })
console.log(result)
wallet_import_qrcode.set(result.content);
window.history.go(-1);
}
});
onDestroy(async () => {
if (mobile) {
const scanner = await import("@tauri-apps/plugin-barcode-scanner");
await scanner.cancel();
}
});
</script>
<div class="text-center">
<!-- please translate this too. i didnt want to do it to avoid a merge conflict-->
Scanning the QRcode
</div>

@ -365,7 +365,7 @@
{:else}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.error_occurred", {
values: { message: $t("errors." + error) },
values: { message: display_error(error) },
})}
</p>
<a use:link href="/">

@ -19,6 +19,7 @@
import { onMount, tick } from "svelte";
import { default as ng } from "../api";
import { display_error } from "../store";
const param = new URLSearchParams($querystring);
@ -79,7 +80,7 @@
{:else}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.error_occurred", {
values: { message: $t("errors." + error) },
values: { message: display_error(error) },
})}
</p>
<a use:link href="/">

@ -44,7 +44,7 @@
} from "../wallet_emojis";
import { onMount, onDestroy, tick } from "svelte";
import { wallets, set_active_session, has_wallets } from "../store";
import { wallets, set_active_session, has_wallets, display_error } from "../store";
const param = new URLSearchParams($querystring);
@ -315,7 +315,7 @@
unsub_register_accepted = undefined;
};
onDestroy(() => {
onDestroy(async () => {
unsub_register();
});
@ -589,7 +589,7 @@
{:else}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.error_occurred", {
values: { message: $t("errors." + registration_error) },
values: { message: display_error(registration_error) },
})}
</p>
<a use:link href="/">
@ -1743,7 +1743,7 @@
/>
</svg>
<Alert color="red" class="mt-5">
{$t("errors." + error)}
{display_error(error)}
</Alert>
<button
class="mt-10 select-none"

@ -31,7 +31,7 @@
import { onMount, tick } from "svelte";
import { Sidebar, SidebarGroup, SidebarWrapper } from "flowbite-svelte";
import { t } from "svelte-i18n";
import { close_active_wallet, active_session, active_wallet } from "../store";
import { close_active_wallet, active_session, active_wallet, display_error } from "../store";
import { default as ng } from "../api";
@ -52,6 +52,7 @@
} else {
await scrollToTop();
}
text_code = await ng.wallet_export_get_textcode($active_session.session_id);
});
let downloading = false;
@ -78,9 +79,12 @@
}
}
let text_code;
let wallet_remove_modal_open = false;
function remove_wallet_clicked() {
wallet_remove_modal_open = true;
async function remove_wallet_clicked() {
//wallet_remove_modal_open = true;
await ng.wallet_export_rendezvous($active_session.session_id, "AABAOAAAAHNb4y7hdWADqFWDgER3J0xvD3K5D9pZ1wd7Bja4c9cWAGLFmUlRYG3D2ULZKhHltZY9IhE2wzBbOqRL-PLw7ZiKAJPyRr_TGnHd-9Uh2Zsv9ahfOWD6tB3q8tVPUS54qdrdAQ");
}
const close_modal = () => {
@ -209,6 +213,10 @@
</li>
{/if}
<li class="break-all">
{text_code}
</li>
<!-- Remove Wallet -->
<li
tabindex="0"
@ -337,7 +345,7 @@
{:else}
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.error_occurred", {
values: { message: $t("errors." + error) },
values: { message: display_error(error) },
})}
</p>
<a use:link href="/">

@ -32,6 +32,8 @@
active_session,
set_active_session,
has_wallets,
wallet_import_qrcode,
display_error,
} from "../store";
let tauri_platform = import.meta.env.TAURI_PLATFORM;
@ -55,7 +57,10 @@
return imageUrl;
}
let qrcode;
onMount(async () => {
step = "open";
wallets_unsub = wallets.subscribe((value) => {
wallet = selected && $wallets[selected]?.wallet;
@ -92,6 +97,25 @@
}
}
});
if ($wallet_import_qrcode) {
let code = $wallet_import_qrcode;
wallet_import_qrcode.set("");
try {
wallet = await ng.wallet_import_from_code(code);
importing = true;
} catch(e) {
error = e;
}
}
// example of rendezvous for desktop and web without cam (please remove it)
qrcode = await ng.wallet_import_rendezvous(300);
try {
wallet = await ng.wallet_import_from_code(qrcode[1]);
importing = true;
} catch (e) {
error = e;
}
});
function loggedin() {
step = "loggedin";
@ -214,7 +238,7 @@
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.error_occurred", {
values: { message: $t("errors." + error) },
values: { message: display_error(error) },
})}
</p>
<button
@ -267,11 +291,35 @@
/>
</div>
{/each}
<!-- remove all this-->
<div
class="wallet-box"
role="button"
tabindex="0"
>
{#if qrcode}
{@html qrcode[0]}
{/if}
</div>
<div
class="wallet-box break-all"
role="button"
tabindex="0"
>
{#if qrcode}
{qrcode[1]}
{/if}
</div>
<!-- remove until here -->
<div class="wallet-box">
{#if $has_wallets}<p class="mt-1">
{#if $has_wallets}
<p class="mt-1">
{$t("pages.wallet_login.with_another_wallet")}
</p>
{:else}<p class="mt-1">{$t("pages.wallet_login.import_wallet")}</p>
{:else}
<p class="mt-1">
{$t("pages.wallet_login.import_wallet")}
</p>
{/if}
<Fileupload
style="display:none;"
@ -302,9 +350,9 @@
</svg>
{$t("pages.wallet_login.import_file")}
</button>
<Button
<a href="/wallet/scanqr" use:link>
<button
style="min-width: 250px;justify-content: left;"
disabled
class="disabled mt-1 text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center justify-center dark:focus:ring-primary-100/55 mb-2"
>
<svg
@ -328,10 +376,10 @@
/>
</svg>
{$t("pages.wallet_login.import_qr")}
</Button>
<Button
</button>
</a>
<button
style="min-width: 250px;justify-content: left;"
disabled
class="mt-1 text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mb-2"
>
<svg
@ -351,7 +399,7 @@
</svg>
{$t("pages.wallet_login.import_link")}
</Button>
</button>
<a href="/wallet/create" use:link>
<button
tabindex="-1"

@ -15,7 +15,7 @@ import {
get,
type Writable,
} from "svelte/store";
import { register, init, locale } from "svelte-i18n";
import { register, init, locale, format } from "svelte-i18n";
import ng from "./api";
import { official_classes } from "./classes";
import { official_apps, official_services } from "./zeras";
@ -43,6 +43,15 @@ init({
initialLocale: "en",
});
export const display_error = (error:string) => {
const parts = error.split(":");
let res = get(format)("errors."+parts[0]);
if (parts[1]) {
res += " "+get(format)("errors."+parts[1]);
}
return res;
}
export const select_default_lang = async () => {
let locales = await ng.locales();
for (let lo of locales) {
@ -148,6 +157,8 @@ export const cur_tab = writable({
});
export const wallet_import_qrcode = writable("");
export const opened_wallets = writable({});
/// { wallet:, id: }

@ -12,14 +12,15 @@
//! Implementation of the Server Broker
use std::{
collections::{HashMap, HashSet},
collections::{BTreeMap, HashMap, HashSet},
path::PathBuf,
sync::Arc,
time::{Duration, SystemTime},
};
use async_std::sync::{Mutex, RwLock};
use either::Either;
use futures::StreamExt;
use futures::{channel::mpsc, SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use ng_repo::{
@ -35,6 +36,7 @@ use ng_net::{
connection::NoiseFSM,
server_broker::IServerBroker,
types::*,
utils::{spawn_and_log_error, Receiver, ResultSend, Sender},
};
use ng_verifier::{
@ -147,6 +149,10 @@ pub struct ServerBrokerState {
verifiers: HashMap<UserId, Arc<RwLock<DetachableVerifier>>>,
remote_apps: HashMap<(DirectPeerId, u64), UserId>,
wallet_rendezvous: HashMap<SymKey, Sender<ExportedWallet>>,
wallet_exports: HashMap<SymKey, ExportedWallet>,
wallet_exports_timestamp: BTreeMap<SystemTime, SymKey>,
}
pub struct ServerBroker {
@ -167,6 +173,9 @@ impl ServerBroker {
local_subscriptions: HashMap::new(),
verifiers: HashMap::new(),
remote_apps: HashMap::new(),
wallet_rendezvous: HashMap::new(),
wallet_exports: HashMap::new(),
wallet_exports_timestamp: BTreeMap::new(),
}),
path_users,
@ -273,10 +282,98 @@ impl ServerBroker {
}
}
use async_std::future::timeout;
async fn wait_for_wallet(
mut internal_receiver: Receiver<ExportedWallet>,
mut sender: Sender<Result<ExportedWallet, ServerError>>,
rendezvous: SymKey,
) -> ResultSend<()> {
let wallet_future = internal_receiver.next();
let _ = sender
.send(
match timeout(Duration::from_millis(5 * 60_000), wallet_future).await {
Err(_) => Err(ServerError::ExportWalletTimeOut),
Ok(Some(w)) => Ok(w),
Ok(None) => Err(ServerError::BrokerError),
},
)
.await;
BROKER
.read()
.await
.get_server_broker()?
.read()
.await
.remove_rendezvous(&rendezvous)
.await;
Ok(())
}
//TODO: the purpose of this trait is to have a level of indirection so we can keep some data in memory (cache) and avoid hitting the storage backend (rocksdb) at every call.
//for now this cache is not implemented, but the structs are ready (see above), and it would just require to change slightly the implementation of the trait functions here below.
#[async_trait::async_trait]
impl IServerBroker for ServerBroker {
async fn remove_rendezvous(&self, rendezvous: &SymKey) {
let mut lock = self.state.write().await;
let _ = lock.wallet_rendezvous.remove(&rendezvous);
}
async fn wait_for_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
) -> Receiver<Result<ExportedWallet, ServerError>> {
let (internal_sender, internal_receiver) = mpsc::unbounded();
let (mut sender, receiver) = mpsc::unbounded();
{
let mut state = self.state.write().await;
if state.wallet_rendezvous.contains_key(&rendezvous) {
let _ = sender.send(Err(ServerError::BrokerError));
sender.close_channel();
return receiver;
} else {
let _ = state
.wallet_rendezvous
.insert(rendezvous.clone(), internal_sender);
}
}
spawn_and_log_error(wait_for_wallet(internal_receiver, sender, rendezvous));
receiver
}
async fn get_wallet_export(&self, rendezvous: SymKey) -> Result<ExportedWallet, ServerError> {
let mut state = self.state.write().await;
match state.wallet_exports.remove(&rendezvous) {
Some(wallet) => Ok(wallet),
None => Err(ServerError::NotFound),
}
}
async fn put_wallet_export(&self, rendezvous: SymKey, export: ExportedWallet) {
let mut state = self.state.write().await;
let _ = state.wallet_exports.insert(rendezvous.clone(), export);
let _ = state
.wallet_exports_timestamp
.insert(SystemTime::now(), rendezvous);
}
// TODO: periodically (every 5 min) remove entries in wallet_exports_timestamp and wallet_exports
async fn put_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
export: ExportedWallet,
) -> Result<(), ServerError> {
let mut state = self.state.write().await;
match state.wallet_rendezvous.remove(&rendezvous) {
None => Err(ServerError::NotFound),
Some(mut sender) => {
let _ = sender.send(export).await;
Ok(())
}
}
}
fn get_block_storage(
&self,
) -> std::sync::Arc<std::sync::RwLock<dyn BlockStorage + Send + Sync>> {

@ -15,3 +15,5 @@ pub mod blocks_put;
pub mod blocks_exist;
pub mod blocks_get;
pub mod wallet_put_export;

@ -0,0 +1,89 @@
/*
* Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::OverlayId;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl WalletPutExport {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<WalletPutExport, ()>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for WalletPutExport {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::WalletPutExport(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<WalletPutExport> for ProtocolMessage {
fn from(msg: WalletPutExport) -> ProtocolMessage {
ProtocolMessage::from_client_request_v0(
ClientRequestContentV0::WalletPutExport(msg),
OverlayId::nil(),
)
}
}
impl Actor<'_, WalletPutExport, ()> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, WalletPutExport, ()> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = WalletPutExport::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let mut res: Result<(), ServerError> = Ok(());
match req {
WalletPutExport::V0(v0) => {
if v0.is_rendezvous {
res = sb
.read()
.await
.put_wallet_at_rendezvous(v0.rendezvous_id, v0.wallet)
.await;
} else {
sb.read()
.await
.put_wallet_export(v0.rendezvous_id, v0.wallet)
.await;
}
}
}
fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,2 @@
pub mod wallet_get_export;
pub use wallet_get_export::*;

@ -0,0 +1,109 @@
/*
* Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::stream::StreamExt;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use super::super::StartProtocol;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl ExtWalletGetExportV0 {
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<ExtWalletGetExportV0, ExportedWallet>::new_responder(0)
}
}
impl TryFrom<ProtocolMessage> for ExtWalletGetExportV0 {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Ext(ExtRequest::V0(ExtRequestV0 {
content: ExtRequestContentV0::WalletGetExport(a),
..
}))) = msg
{
Ok(a)
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<ExtWalletGetExportV0> for ProtocolMessage {
fn from(_msg: ExtWalletGetExportV0) -> ProtocolMessage {
unimplemented!();
}
}
impl From<ExtWalletGetExportV0> for ExtRequestContentV0 {
fn from(msg: ExtWalletGetExportV0) -> ExtRequestContentV0 {
ExtRequestContentV0::WalletGetExport(msg)
}
}
impl TryFrom<ProtocolMessage> for ExportedWallet {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<ExportedWallet, Self::Error> {
let content: ExtResponseContentV0 = msg.try_into()?;
if let ExtResponseContentV0::Wallet(res) = content {
Ok(res)
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl Actor<'_, ExtWalletGetExportV0, ExportedWallet> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, ExtWalletGetExportV0, ExportedWallet> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = ExtWalletGetExportV0::try_from(msg)?;
let result = if req.is_rendezvous {
let mut receiver = {
let broker = BROKER.read().await;
let sb = broker.get_server_broker()?;
let lock = sb.read().await;
lock.wait_for_wallet_at_rendezvous(req.id).await
};
match receiver.next().await {
None => Err(ServerError::BrokerError),
Some(Err(e)) => Err(e),
Some(Ok(w)) => Ok(ExtResponseContentV0::Wallet(w)),
}
} else {
{
let broker = BROKER.read().await;
let sb = broker.get_server_broker()?;
let lock = sb.read().await;
lock.get_wallet_export(req.id).await
}
.map(|wallet| ExtResponseContentV0::Wallet(wallet))
};
let response: ExtResponseV0 = result.into();
fsm.lock().await.send(response.into()).await?;
Ok(())
}
}

@ -21,3 +21,5 @@ pub mod client;
pub mod admin;
pub mod app;
pub mod ext;

@ -23,7 +23,7 @@ use crate::actors::noise::Noise;
use crate::connection::NoiseFSM;
use crate::types::{
AdminRequest, ClientInfo, CoreBrokerConnect, CoreBrokerConnectResponse, CoreMessage,
CoreMessageV0, CoreResponse, CoreResponseContentV0, CoreResponseV0, ExtResponse,
CoreMessageV0, CoreResponse, CoreResponseContentV0, CoreResponseV0, ExtRequest,
};
use crate::{actor::*, types::ProtocolMessage};
@ -34,7 +34,7 @@ use crate::{actor::*, types::ProtocolMessage};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum StartProtocol {
Client(ClientHello),
Ext(ExtHello),
Ext(ExtRequest),
Core(CoreHello),
Admin(AdminRequest),
App(AppHello),
@ -136,28 +136,28 @@ impl EActor for Actor<'_, CoreBrokerConnect, CoreBrokerConnectResponse> {
}
}
/// External Hello (finalizes the Noise handshake and sends first ExtRequest)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExtHello {
// contains the 3rd Noise handshake message "s,se"
pub noise: Noise,
// /// External Hello (finalizes the Noise handshake and sends first ExtRequest)
// #[derive(Clone, Debug, Serialize, Deserialize)]
// pub struct ExtHello {
// // contains the 3rd Noise handshake message "s,se"
// pub noise: Noise,
/// Noise encrypted payload (an ExtRequest)
#[serde(with = "serde_bytes")]
pub payload: Vec<u8>,
}
// /// Noise encrypted payload (an ExtRequest)
// #[serde(with = "serde_bytes")]
// pub payload: Vec<u8>,
// }
impl ExtHello {
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<ExtHello, ExtResponse>::new_responder(0)
}
}
// impl ExtHello {
// pub fn get_actor(&self) -> Box<dyn EActor> {
// Actor::<ExtHello, ExtResponse>::new_responder(0)
// }
// }
impl From<ExtHello> for ProtocolMessage {
fn from(msg: ExtHello) -> ProtocolMessage {
ProtocolMessage::Start(StartProtocol::Ext(msg))
}
}
// impl From<ExtHello> for ProtocolMessage {
// fn from(msg: ExtHello) -> ProtocolMessage {
// ProtocolMessage::Start(StartProtocol::Ext(msg))
// }
// }
/// Client Hello
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -251,18 +251,18 @@ impl EActor for Actor<'_, ClientHello, ServerHello> {
}
}
impl Actor<'_, ExtHello, ExtResponse> {}
// impl Actor<'_, ExtHello, ExtResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, ExtHello, ExtResponse> {
async fn respond(
&mut self,
_msg: ProtocolMessage,
_fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
Ok(())
}
}
// #[async_trait::async_trait]
// impl EActor for Actor<'_, ExtHello, ExtResponse> {
// async fn respond(
// &mut self,
// _msg: ProtocolMessage,
// _fsm: Arc<Mutex<NoiseFSM>>,
// ) -> Result<(), ProtocolError> {
// Ok(())
// }
// }
// ///////////// APP HELLO ///////////////

@ -894,6 +894,34 @@ impl Broker {
connection.admin::<A>().await
}
pub async fn ext<
A: Into<ProtocolMessage> + Into<ExtRequestContentV0> + std::fmt::Debug + Sync + Send + 'static,
B: TryFrom<ProtocolMessage, Error = ProtocolError> + std::fmt::Debug + Sync + Send + 'static,
>(
cnx: Box<dyn IConnect>,
peer_privk: PrivKey,
peer_pubk: PubKey,
remote_peer_id: DirectPeerId,
url: String,
request: A,
) -> Result<B, NgError> {
let config = StartConfig::Ext(ExtConfig {
url,
request: request.into(),
});
let remote_peer_id_dh = remote_peer_id.to_dh_from_ed();
let mut connection = cnx
.open(
config.get_url(),
peer_privk.clone(),
peer_pubk,
remote_peer_id_dh,
config.clone(),
)
.await?;
connection.ext::<A, B>().await
}
#[doc(hidden)]
pub fn connect_local(&mut self, peer_pubk: PubKey, user: UserId) -> Result<(), ProtocolError> {
if self.closing {

@ -112,7 +112,7 @@ pub enum FSMstate {
Noise3,
AdminRequest,
ExtRequest,
ExtResponse,
//ExtResponse,
ClientHello,
ServerHello,
ClientAuth,
@ -177,7 +177,10 @@ pub struct ClientConfig {
}
#[derive(Debug, Clone)]
pub struct ExtConfig {}
pub struct ExtConfig {
pub url: String,
pub request: ExtRequestContentV0,
}
#[derive(Debug, Clone)]
pub struct CoreConfig {
@ -215,10 +218,14 @@ pub enum StartConfig {
impl StartConfig {
pub fn get_url(&self) -> String {
match self {
Self::Client(config) => config.url.clone(),
Self::Admin(config) => format!("ws://{}:{}", config.addr.ip, config.addr.port),
Self::Core(config) => format!("ws://{}:{}", config.addr.ip, config.addr.port),
Self::App(config) => format!("ws://{}:{}", config.addr.ip, config.addr.port),
Self::Client(ClientConfig { url, .. }) | Self::Ext(ExtConfig { url, .. }) => {
url.clone()
}
Self::Admin(AdminConfig { addr, .. })
| Self::Core(CoreConfig { addr, .. })
| Self::App(AppConfig { addr, .. }) => {
format!("ws://{}:{}", addr.ip, addr.port)
}
_ => unimplemented!(),
}
}
@ -235,9 +242,9 @@ impl StartConfig {
_ => false,
}
}
pub fn is_admin(&self) -> bool {
pub fn is_oneshot(&self) -> bool {
match self {
StartConfig::Admin(_) => true,
StartConfig::Admin(_) | StartConfig::Ext(_) => true,
_ => false,
}
}
@ -635,7 +642,10 @@ impl NoiseFSM {
self.state = FSMstate::ClientHello;
}
StartConfig::Ext(_ext_config) => {
todo!();
let noise = Noise::V0(NoiseV0 { data: payload });
self.send(noise.into()).await?;
self.state = FSMstate::Noise3;
next_step = StepReply::ReEnter;
}
StartConfig::Core(_core_config) => {
todo!();
@ -774,8 +784,18 @@ impl NoiseFSM {
StartConfig::Client(_) => {
return Err(ProtocolError::InvalidState);
}
StartConfig::Ext(_ext_config) => {
todo!();
StartConfig::Ext(ext_config) => {
let ext_req = ExtRequestV0 {
content: ext_config.request.clone(),
id: 0,
overlay: None,
};
let protocol_start = StartProtocol::Ext(ExtRequest::V0(ext_req));
self.send(protocol_start.into()).await?;
self.state = FSMstate::ExtRequest;
return Ok(StepReply::NONE);
}
StartConfig::Core(_core_config) => {
todo!();
@ -807,8 +827,9 @@ impl NoiseFSM {
StartProtocol::Client(_) => {
return Err(ProtocolError::InvalidState);
}
StartProtocol::Ext(_ext_config) => {
todo!();
StartProtocol::Ext(_ext_req) => {
self.state = FSMstate::Closing;
return Ok(StepReply::Responder(msg_opt.unwrap()));
}
// StartProtocol::Core(core_config) => {
// todo!();
@ -852,8 +873,15 @@ impl NoiseFSM {
return Ok(StepReply::Response(msg));
}
}
FSMstate::ExtRequest => {}
FSMstate::ExtResponse => {}
FSMstate::ExtRequest => {
// CLIENT side receiving ExtResponse
if let Some(msg) = msg_opt {
if self.dir.is_server() || msg.type_id() != TypeId::of::<ExtResponse>() {
return Err(ProtocolError::InvalidState);
}
return Ok(StepReply::Response(msg));
}
}
FSMstate::ClientHello => {
if let Some(msg) = msg_opt.as_ref() {
if !self.dir.is_server() {
@ -1371,6 +1399,47 @@ impl ConnectionBase {
}
}
pub async fn ext<
A: Into<ProtocolMessage> + Into<ExtRequestContentV0> + std::fmt::Debug + Sync + Send + 'static,
B: TryFrom<ProtocolMessage, Error = ProtocolError> + std::fmt::Debug + Sync + Send + 'static,
>(
&mut self,
) -> Result<B, NgError> {
if !self.dir.is_server() {
let mut actor = Box::new(Actor::<A, B>::new(0, true));
self.actors.lock().await.insert(0, actor.get_receiver_tx());
let mut receiver = actor.detach_receiver();
match receiver.next().await {
Some(ConnectionCommand::Msg(msg)) => {
self.fsm
.as_ref()
.unwrap()
.lock()
.await
.remove_actor(0)
.await;
let server_error: Result<ServerError, NgError> = (&msg).try_into();
let response: B = match msg.try_into() {
Ok(b) => b,
Err(ProtocolError::ServerError) => {
return Err(NgError::ServerError(server_error?));
}
Err(e) => return Err(NgError::ProtocolError(e)),
};
self.close().await;
Ok(response)
}
Some(ConnectionCommand::ProtocolError(e)) => Err(e.into()),
Some(ConnectionCommand::Error(e)) => Err(ProtocolError::from(e).into()),
Some(ConnectionCommand::Close) => Err(ProtocolError::Closing.into()),
_ => Err(ProtocolError::ActorError.into()),
}
} else {
panic!("cannot call ext on a server-side connection");
}
}
pub async fn probe(&mut self) -> Result<Option<PubKey>, ProtocolError> {
if !self.dir.is_server() {
let config = StartConfig::Probe;
@ -1431,7 +1500,7 @@ impl ConnectionBase {
pub async fn start(&mut self, config: StartConfig) -> Result<(), ProtocolError> {
// BOOTSTRAP the protocol from client-side
if !self.dir.is_server() {
let is_admin = config.is_admin();
let is_oneshot = config.is_oneshot();
let res;
{
let mut fsm = self.fsm.as_ref().unwrap().lock().await;
@ -1442,7 +1511,7 @@ impl ConnectionBase {
self.send(ConnectionCommand::ProtocolError(err.clone()))
.await;
Err(err)
} else if !is_admin {
} else if !is_oneshot {
let mut actor = Box::new(Actor::<Connecting, ()>::new(0, true));
self.actors.lock().await.insert(0, actor.get_receiver_tx());

@ -24,9 +24,22 @@ use crate::app_protocol::{AppRequest, AppSessionStart, AppSessionStartResponse,
use crate::broker::ClientPeerId;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::utils::Receiver;
#[async_trait::async_trait]
pub trait IServerBroker: Send + Sync {
async fn remove_rendezvous(&self, rendezvous: &SymKey);
async fn put_wallet_export(&self, rendezvous: SymKey, export: ExportedWallet);
async fn get_wallet_export(&self, rendezvous: SymKey) -> Result<ExportedWallet, ServerError>;
async fn put_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
export: ExportedWallet,
) -> Result<(), ServerError>;
async fn wait_for_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
) -> Receiver<Result<ExportedWallet, ServerError>>;
fn get_path_users(&self) -> PathBuf;
fn get_block_storage(&self) -> Arc<std::sync::RwLock<dyn BlockStorage + Send + Sync>>;
fn put_block(&self, overlay_id: &OverlayId, block: Block) -> Result<(), ServerError>;

@ -3249,6 +3249,20 @@ impl CommitGet {
}
}
/// Request to store one or more blocks
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WalletPutExportV0 {
pub wallet: ExportedWallet,
pub rendezvous_id: SymKey,
pub is_rendezvous: bool,
}
/// Request to store one or more blocks
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum WalletPutExport {
V0(WalletPutExportV0),
}
/// Request to store one or more blocks
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BlocksPutV0 {
@ -3414,6 +3428,8 @@ pub enum ClientRequestContentV0 {
// For InnerOverlay's only :
BlocksPut(BlocksPut),
PublishEvent(PublishEvent),
WalletPutExport(WalletPutExport),
}
impl ClientRequestContentV0 {
@ -3428,6 +3444,7 @@ impl ClientRequestContentV0 {
ClientRequestContentV0::BlocksPut(a) => a.set_overlay(overlay),
ClientRequestContentV0::BlocksExist(a) => a.set_overlay(overlay),
ClientRequestContentV0::BlocksGet(a) => a.set_overlay(overlay),
ClientRequestContentV0::WalletPutExport(_a) => {}
_ => unimplemented!(),
}
}
@ -3479,6 +3496,7 @@ impl ClientRequest {
ClientRequestContentV0::BlocksPut(r) => r.get_actor(self.id()),
ClientRequestContentV0::BlocksExist(r) => r.get_actor(self.id()),
ClientRequestContentV0::BlocksGet(r) => r.get_actor(self.id()),
ClientRequestContentV0::WalletPutExport(r) => r.get_actor(self.id()),
_ => unimplemented!(),
},
}
@ -3946,23 +3964,41 @@ pub enum ExtObjectGet {
V0(ExtObjectGetV0),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExtWalletGetExportV0 {
pub id: SymKey,
pub is_rendezvous: bool,
}
/// Topic synchronization request
pub type ExtTopicSyncReq = TopicSyncReq;
/// Content of ExtRequestV0
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ExtRequestContentV0 {
WalletGetExport(ExtWalletGetExportV0),
ExtObjectGet(ExtObjectGet),
ExtTopicSyncReq(ExtTopicSyncReq),
// TODO inbox requests
// TODO subreq ?
}
impl ExtRequestContentV0 {
pub fn get_actor(&self) -> Box<dyn EActor> {
match self {
Self::WalletGetExport(a) => a.get_actor(),
_ => unimplemented!()
// Self::ExtObjectGet(a) => a.get_actor(),
// Self::ExtTopicSyncReq(a) => a.get_actor(),
}
}
}
/// External request with its request ID
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExtRequestV0 {
/// outer overlayId
pub overlay: Digest,
pub overlay: Option<Digest>,
/// Request ID
pub id: i64,
@ -3993,16 +4029,48 @@ impl ExtRequest {
}
}
}
pub fn get_actor(&self) -> Box<dyn EActor> {
match self {
Self::V0(a) => a.content.get_actor(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExportedWallet(pub serde_bytes::ByteBuf);
/// Content of ExtResponseV0
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ExtResponseContentV0 {
EmptyResponse,
Block(Block),
Wallet(ExportedWallet),
// TODO inbox related replies
// TODO event ?
}
impl TryFrom<ProtocolMessage> for ExtResponseContentV0 {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::ExtResponse(ExtResponse::V0(ExtResponseV0 {
content,
result,
..
})) = msg
{
let err = ServerError::try_from(result).unwrap();
if !err.is_err() {
Ok(content)
} else {
Err(ProtocolError::ServerError)
}
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
/// Response to an ExtRequest
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExtResponseV0 {
@ -4013,7 +4081,7 @@ pub struct ExtResponseV0 {
pub result: u16,
/// Response content
pub content: Option<ExtResponseContentV0>,
pub content: ExtResponseContentV0,
}
/// Response to an ExtRequest
@ -4035,6 +4103,16 @@ impl ExtResponse {
}
}
}
pub fn result(&self) -> u16 {
match self {
Self::V0(o) => o.result,
}
}
pub fn content_v0(&self) -> ExtResponseContentV0 {
match self {
Self::V0(o) => o.content.clone(),
}
}
}
impl TryFrom<ProtocolMessage> for ExtResponse {
@ -4048,6 +4126,29 @@ impl TryFrom<ProtocolMessage> for ExtResponse {
}
}
impl From<Result<ExtResponseContentV0, ServerError>> for ExtResponseV0 {
fn from(res: Result<ExtResponseContentV0, ServerError>) -> ExtResponseV0 {
match res {
Err(e) => ExtResponseV0 {
id: 0,
result: e.into(),
content: ExtResponseContentV0::EmptyResponse,
},
Ok(content) => ExtResponseV0 {
id: 0,
result: 0,
content,
},
}
}
}
impl From<ExtResponseV0> for ProtocolMessage {
fn from(msg: ExtResponseV0) -> ProtocolMessage {
ProtocolMessage::ExtResponse(ExtResponse::V0(msg))
}
}
//
// PROTOCOL MESSAGES
//
@ -4149,6 +4250,12 @@ impl TryFrom<&ProtocolMessage> for ServerError {
return Ok(ServerError::try_from(res).unwrap());
}
}
if let ProtocolMessage::ExtResponse(ref bm) = msg {
let res = bm.result();
if res != 0 {
return Ok(ServerError::try_from(res).unwrap());
}
}
if let ProtocolMessage::AppMessage(ref bm) = msg {
let res = bm.result();
if res != 0 {
@ -4205,12 +4312,6 @@ impl ProtocolMessage {
match self {
ProtocolMessage::ClientMessage(s) => Some(s as &dyn IStreamable),
ProtocolMessage::AppMessage(s) => Some(s as &dyn IStreamable),
// ProtocolMessage::ServerHello(a) => a.get_actor(),
// ProtocolMessage::ClientAuth(a) => a.get_actor(),
// ProtocolMessage::AuthResult(a) => a.get_actor(),
// ProtocolMessage::ExtRequest(a) => a.get_actor(),
// ProtocolMessage::ExtResponse(a) => a.get_actor(),
// ProtocolMessage::BrokerMessage(a) => a.get_actor(),
_ => None,
}
}

@ -17,6 +17,7 @@ use num_enum::TryFromPrimitive;
pub use crate::commit::{CommitLoadError, CommitVerifyError};
use crate::file::FileError;
use crate::log::*;
use crate::object::Object;
use crate::types::BlockId;
@ -83,6 +84,9 @@ pub enum NgError {
LocalBrokerIsNotHeadless,
InvalidNuri,
InvalidTarget,
InvalidQrCode,
NotImplemented,
NotARendezVous,
}
impl Error for NgError {}
@ -264,6 +268,8 @@ pub enum ServerError {
OxiGraphError,
InvalidNuri,
InvalidTarget,
ExportWalletTimeOut,
NetError,
}
impl From<StorageError> for ServerError {
@ -275,12 +281,23 @@ impl From<StorageError> for ServerError {
}
}
impl From<NetError> for ServerError {
fn from(e: NetError) -> Self {
match e {
_ => ServerError::NetError,
}
}
}
impl From<ProtocolError> for ServerError {
fn from(e: ProtocolError) -> Self {
match e {
ProtocolError::NotFound => ServerError::NotFound,
ProtocolError::BrokerError => ServerError::BrokerError,
_ => ServerError::ProtocolError,
_ => {
log_err!("{:?}", e);
ServerError::ProtocolError
}
}
}
}

@ -535,6 +535,81 @@ pub async fn wallet_read_file(file: JsValue) -> Result<JsValue, String> {
Ok(serde_wasm_bindgen::to_value(&wallet).unwrap())
}
#[wasm_bindgen]
pub async fn wallet_import_from_code(code: JsValue) -> Result<JsValue, String> {
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
let code = serde_wasm_bindgen::from_value::<String>(code)
.map_err(|_| "Deserialization error of code".to_string())?;
let wallet = nextgraph::local_broker::wallet_import_from_code(code)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(serde_wasm_bindgen::to_value(&wallet).unwrap())
}
#[wasm_bindgen]
pub async fn wallet_import_rendezvous(size: JsValue) -> Result<JsValue, String> {
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
let size: u32 = serde_wasm_bindgen::from_value::<u32>(size)
.map_err(|_| "Deserialization error of size".to_string())?;
let res = nextgraph::local_broker::wallet_import_rendezvous(size)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(serde_wasm_bindgen::to_value(&res).unwrap())
}
#[wasm_bindgen]
pub async fn wallet_export_get_qrcode(
session_id: JsValue,
size: JsValue,
) -> Result<JsValue, String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
let size: u32 = serde_wasm_bindgen::from_value::<u32>(size)
.map_err(|_| "Deserialization error of size".to_string())?;
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
let res = nextgraph::local_broker::wallet_export_get_qrcode(session_id, size)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(serde_wasm_bindgen::to_value(&res).unwrap())
}
#[wasm_bindgen]
pub async fn wallet_export_get_textcode(session_id: JsValue) -> Result<JsValue, String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
let res = nextgraph::local_broker::wallet_export_get_textcode(session_id)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(serde_wasm_bindgen::to_value(&res).unwrap())
}
#[wasm_bindgen]
pub async fn wallet_export_rendezvous(session_id: JsValue, code: JsValue) -> Result<(), String> {
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
let code = serde_wasm_bindgen::from_value::<String>(code)
.map_err(|_| "Deserialization error of code".to_string())?;
init_local_broker_with_lazy(&INIT_LOCAL_BROKER).await;
nextgraph::local_broker::wallet_export_rendezvous(session_id, code)
.await
.map_err(|e: NgError| e.to_string())?;
Ok(())
}
#[wasm_bindgen]
pub async fn wallet_was_opened(
opened_wallet: JsValue, //SensitiveWallet

@ -813,6 +813,26 @@ impl Verifier {
}
}
pub async fn client_request<
A: Into<ProtocolMessage> + std::fmt::Debug + Sync + Send + 'static,
B: TryFrom<ProtocolMessage, Error = ProtocolError> + std::fmt::Debug + Sync + Send + 'static,
>(
&self,
msg: A,
) -> Result<SoS<B>, NgError> {
if self.connected_broker.is_some() {
let connected_broker = self.connected_broker.clone();
let broker = BROKER.read().await;
let user = self.user_id().clone();
broker
.request::<A, B>(&Some(user), &connected_broker.into(), msg)
.await
} else {
Err(NgError::NotConnected)
}
}
async fn send_or_save_event_to_outbox<'a>(
&'a mut self,
commit_ref: ObjectRef,

@ -25,6 +25,7 @@ rand = { version = "0.7", features = ["getrandom"] }
aes-gcm-siv = {version = "0.11.1", features = ["aes","heapless","getrandom","std"] }
zeroize = { version = "1.7.0", features = ["zeroize_derive"] }
crypto_box = { version = "0.8.2", features = ["seal"] }
base64-url = "2.0.0"
blake3 = "1.3.1"
argon2 = "0.5.0"
chacha20poly1305 = "0.10.1"

@ -1426,3 +1426,27 @@ pub struct ShuffledPazzle {
pub category_indices: Vec<u8>,
pub emoji_indices: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NgQRCodeV0 {
pub broker: BrokerServerV0,
pub rendezvous: SymKey, // Rendez-vous ID
pub secret_key: SymKey,
pub is_rendezvous: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum NgQRCode {
V0(NgQRCodeV0),
}
impl NgQRCode {
pub fn from_code(code: String) -> Result<Self, NgError> {
let decoded = base64_url::decode(&code).map_err(|_| NgError::SerializationError)?;
Ok(serde_bare::from_slice(&decoded)?)
}
pub fn to_code(&self) -> String {
let ser = serde_bare::to_vec(self).unwrap();
base64_url::encode(&ser)
}
}

@ -16,6 +16,7 @@ importers:
'@sveltejs/vite-plugin-svelte': ^2.0.0
'@tauri-apps/api': 2.0.0-alpha.8
'@tauri-apps/cli': 2.0.0-alpha.14
'@tauri-apps/plugin-barcode-scanner': 2.0.0-alpha.0
'@tauri-apps/plugin-window': 2.0.0-alpha.1
'@tsconfig/svelte': ^3.0.0
'@types/node': ^18.7.10
@ -50,6 +51,7 @@ importers:
dependencies:
'@popperjs/core': 2.11.8
'@tauri-apps/api': 2.0.0-alpha.8
'@tauri-apps/plugin-barcode-scanner': 2.0.0-alpha.0
'@tauri-apps/plugin-window': 2.0.0-alpha.1
async-proxy: 0.4.1
classnames: 2.3.2
@ -892,6 +894,12 @@ packages:
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-alpha.14
dev: true
/@tauri-apps/plugin-barcode-scanner/2.0.0-alpha.0:
resolution: {integrity: sha512-qXc/HfGdWI2x2vUEfgf65kb4Bw3PEDMLz6tNizLdzQ5Q4wJLMkaPZxFsqS6ZbuRjILAqM0lfp/otgR93OGVOgA==}
dependencies:
'@tauri-apps/api': 2.0.0-alpha.8
dev: false
/@tauri-apps/plugin-window/2.0.0-alpha.1:
resolution: {integrity: sha512-dFOAgal/3Txz3SQ+LNQq0AK1EPC+acdaFlwPVB/6KXUZYmaFleIlzgxDVoJCQ+/xOhxvYrdQaFLefh0I/Kldbg==}
dependencies:

Loading…
Cancel
Save