From bfa88bfcfd95d66417ba9444bee9551f87860fa1 Mon Sep 17 00:00:00 2001
From: Niko PLP <niko@nextgraph.org>
Date: Mon, 3 Jul 2023 23:02:07 +0300
Subject: [PATCH] ClientInfo

---
 Cargo.lock                                |   47 +
 Cargo.toml                                |    1 +
 ng-app/README.md                          |    6 +
 ng-app/package.json                       |    1 +
 ng-app/src-tauri/Cargo.toml               |    1 +
 ng-app/src-tauri/src/lib.rs               |   10 +-
 ng-app/src/api.ts                         |   49 +-
 ng-app/src/routes/Grid.svelte             |   10 +-
 ng-app/src/routes/Install.svelte          |   10 +-
 ng-app/src/routes/WalletCreate.svelte     |  445 +++-
 ng-app/src/styles.css                     |    4 +
 ng-sdk-js/README.md                       |   23 +-
 ng-sdk-js/app-node/index.js               |    2 +-
 ng-sdk-js/js/bowser.js                    | 2233 +++++++++++++++++++++
 ng-sdk-js/js/browser.js                   |   17 +-
 ng-sdk-js/js/node.js                      |  122 +-
 ng-sdk-js/src/lib.rs                      |   91 +-
 ng-wallet/src/lib.rs                      |   54 +-
 ng-wallet/src/types.rs                    |   64 +-
 ngaccount/Cargo.toml                      |   28 +
 ngaccount/README.md                       |   37 +
 ngaccount/src/main.rs                     |   82 +
 ngaccount/src/types.rs                    |   30 +
 ngaccount/web/.gitignore                  |   24 +
 ngaccount/web/.vscode/extensions.json     |    3 +
 ngaccount/web/index.html                  |   61 +
 ngaccount/web/jsconfig.json               |   32 +
 ngaccount/web/package.json                |   27 +
 ngaccount/web/pnpm-lock.yaml              | 1310 ++++++++++++
 ngaccount/web/postcss.config.cjs          |   13 +
 ngaccount/web/public/robots.txt           |    2 +
 ngaccount/web/public/vite.svg             |    1 +
 ngaccount/web/src/App.svelte              |   26 +
 ngaccount/web/src/app.postcss             |    4 +
 ngaccount/web/src/assets/EU.svg           |    4 +
 ngaccount/web/src/assets/nextgraph.svg    |   16 +
 ngaccount/web/src/main.js                 |    9 +
 ngaccount/web/src/routes/Home.svelte      |   27 +
 ngaccount/web/src/routes/NotFound.svelte  |   20 +
 ngaccount/web/src/vite-env.d.ts           |    2 +
 ngaccount/web/svelte.config.js            |    7 +
 ngaccount/web/tailwind.config.cjs         |   23 +
 ngaccount/web/vite.config.js              |   36 +
 ngd/src/cli.rs                            |    8 +
 ngd/src/main.rs                           |    9 +
 ngone/README.md                           |   13 +-
 ngone/src/main.rs                         |    5 +-
 ngone/web/src/routes/WalletCreate.svelte  |    6 +-
 p2p-broker/src/broker_store/account.rs    |  186 +-
 p2p-broker/src/broker_store/invitation.rs |  157 ++
 p2p-broker/src/broker_store/mod.rs        |    4 +-
 p2p-broker/src/broker_store/overlay.rs    |    4 +-
 p2p-broker/src/broker_store/topic.rs      |    2 +-
 p2p-broker/src/lib.rs                     |    2 +
 p2p-broker/src/server_ws.rs               |   83 +-
 p2p-broker/src/storage.rs                 |   37 +
 p2p-broker/src/types.rs                   |   10 +
 p2p-net/Cargo.toml                        |    3 +-
 p2p-net/src/broker.rs                     |    7 +
 p2p-net/src/broker_storage.rs             |   17 +
 p2p-net/src/lib.rs                        |    4 +
 p2p-net/src/types.rs                      |  287 ++-
 p2p-net/src/utils.rs                      |   56 +
 p2p-repo/src/errors.rs                    |    2 +
 p2p-repo/src/kcv_store.rs                 |    2 +-
 pnpm-lock.yaml                            |    8 +
 stores-lmdb/src/kcv_store.rs              |    5 +-
 67 files changed, 5674 insertions(+), 257 deletions(-)
 create mode 100644 ng-sdk-js/js/bowser.js
 create mode 100644 ngaccount/Cargo.toml
 create mode 100644 ngaccount/README.md
 create mode 100644 ngaccount/src/main.rs
 create mode 100644 ngaccount/src/types.rs
 create mode 100644 ngaccount/web/.gitignore
 create mode 100644 ngaccount/web/.vscode/extensions.json
 create mode 100644 ngaccount/web/index.html
 create mode 100644 ngaccount/web/jsconfig.json
 create mode 100644 ngaccount/web/package.json
 create mode 100644 ngaccount/web/pnpm-lock.yaml
 create mode 100644 ngaccount/web/postcss.config.cjs
 create mode 100644 ngaccount/web/public/robots.txt
 create mode 100644 ngaccount/web/public/vite.svg
 create mode 100644 ngaccount/web/src/App.svelte
 create mode 100644 ngaccount/web/src/app.postcss
 create mode 100755 ngaccount/web/src/assets/EU.svg
 create mode 100644 ngaccount/web/src/assets/nextgraph.svg
 create mode 100644 ngaccount/web/src/main.js
 create mode 100644 ngaccount/web/src/routes/Home.svelte
 create mode 100644 ngaccount/web/src/routes/NotFound.svelte
 create mode 100644 ngaccount/web/src/vite-env.d.ts
 create mode 100644 ngaccount/web/svelte.config.js
 create mode 100644 ngaccount/web/tailwind.config.cjs
 create mode 100644 ngaccount/web/vite.config.js
 create mode 100644 p2p-broker/src/broker_store/invitation.rs
 create mode 100644 p2p-broker/src/storage.rs
 create mode 100644 p2p-net/src/broker_storage.rs

diff --git a/Cargo.lock b/Cargo.lock
index cb9e557..928508d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2736,6 +2736,7 @@ dependencies = [
  "serde_json",
  "tauri",
  "tauri-build",
+ "tauri-plugin-window",
 ]
 
 [[package]]
@@ -2787,6 +2788,30 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "ngaccount"
+version = "0.1.0"
+dependencies = [
+ "base64-url",
+ "bytes",
+ "env_logger",
+ "log",
+ "ng-wallet",
+ "p2p-net",
+ "p2p-repo",
+ "rust-embed",
+ "serde",
+ "serde-big-array",
+ "serde_bare",
+ "serde_bytes",
+ "serde_json",
+ "slice_as_array",
+ "stores-lmdb",
+ "tokio",
+ "warp",
+ "warp-embed",
+]
+
 [[package]]
 name = "ngcli"
 version = "0.1.0"
@@ -3024,6 +3049,15 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
 
+[[package]]
+name = "openssl-src"
+version = "111.26.0+1.1.1u"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37"
+dependencies = [
+ "cc",
+]
+
 [[package]]
 name = "openssl-sys"
 version = "0.9.90"
@@ -3032,6 +3066,7 @@ checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
 dependencies = [
  "cc",
  "libc",
+ "openssl-src",
  "pkg-config",
  "vcpkg",
 ]
@@ -3129,6 +3164,7 @@ dependencies = [
  "unique_id",
  "url",
  "wasm-bindgen",
+ "web-time",
 ]
 
 [[package]]
@@ -4629,6 +4665,17 @@ dependencies = [
  "tauri-utils",
 ]
 
+[[package]]
+name = "tauri-plugin-window"
+version = "2.0.0-alpha.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "209fef1a00a981949e2440924b4be267c7639daeba51b29179004fa1c6d74900"
+dependencies = [
+ "serde",
+ "tauri",
+ "thiserror",
+]
+
 [[package]]
 name = "tauri-runtime"
 version = "0.13.0-alpha.6"
diff --git a/Cargo.toml b/Cargo.toml
index e3b7c21..56d8f5d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,7 @@ members = [
   "ngcli",
   "ngd",
   "ngone",
+  "ngaccount",
   "ng-sdk-js",
   "ng-app/src-tauri",
   "ng-wallet"
diff --git a/ng-app/README.md b/ng-app/README.md
index 77d2e1a..b7b3309 100644
--- a/ng-app/README.md
+++ b/ng-app/README.md
@@ -101,6 +101,12 @@ rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-andro
 
 - follow the steps for Android in the [Prerquisites guide of Tauri](https://next--tauri.netlify.app/next/guides/getting-started/prerequisites/)
 
+Until I find out how to do this properly, if you are compiling the android app from a macos station, you need to override an env var. this is due to reqwest needing SSL support, and on linux and android it compiles it from source. apparently the compiler (cc-rs) doesn't know that when cross compiling to android targets, the tool ranlib is called llvm-ranlib (and not [target]-ranlib)
+
+```
+export RANLIB=/Users/[user]/Library/Android/sdk/ndk/[yourNDKversion]/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ranlib
+```
+
 to launch the dev app :
 
 ```
diff --git a/ng-app/package.json b/ng-app/package.json
index 690b53e..61a2d7b 100644
--- a/ng-app/package.json
+++ b/ng-app/package.json
@@ -17,6 +17,7 @@
   "dependencies": {
     "@popperjs/core": "^2.11.8",
     "@tauri-apps/api": "2.0.0-alpha.4",
+    "@tauri-apps/plugin-window": "2.0.0-alpha.0",
     "async-proxy": "^0.4.1",
     "classnames": "^2.3.2",
     "flowbite": "^1.6.5",
diff --git a/ng-app/src-tauri/Cargo.toml b/ng-app/src-tauri/Cargo.toml
index b8d82b6..b2f7c6a 100644
--- a/ng-app/src-tauri/Cargo.toml
+++ b/ng-app/src-tauri/Cargo.toml
@@ -24,6 +24,7 @@ p2p-repo = { path = "../../p2p-repo" }
 p2p-net = { path = "../../p2p-net" }
 ng-wallet = { path = "../../ng-wallet" }
 async-std = {  version = "1.12.0", features = ["attributes","unstable"] }
+tauri-plugin-window = "2.0.0-alpha"
 
 [features]
 # this feature is used for production builds or when `devPath` points to the filesystem
diff --git a/ng-app/src-tauri/src/lib.rs b/ng-app/src-tauri/src/lib.rs
index 1e77b6c..d0f62d0 100644
--- a/ng-app/src-tauri/src/lib.rs
+++ b/ng-app/src-tauri/src/lib.rs
@@ -13,7 +13,7 @@ use p2p_net::broker::*;
 use p2p_net::utils::{spawn_and_log_error, Receiver, ResultSend};
 use p2p_repo::log::*;
 use p2p_repo::types::*;
-use tauri::{App, Manager};
+use tauri::{App, Manager, Window};
 
 #[cfg(mobile)]
 mod mobile;
@@ -78,6 +78,7 @@ async fn wallet_create_wallet(mut params: CreateWalletV0) -> Result<CreateWallet
 #[tauri::command(rename_all = "snake_case")]
 async fn doc_sync_branch(nuri: &str, stream_id: &str, app: tauri::AppHandle) -> Result<(), ()> {
     log_info!("doc_sync_branch {} {}", nuri, stream_id);
+    let main_window = app.get_window("main").unwrap();
 
     let mut reader;
     {
@@ -91,10 +92,10 @@ async fn doc_sync_branch(nuri: &str, stream_id: &str, app: tauri::AppHandle) ->
     async fn inner_task(
         mut reader: Receiver<Commit>,
         stream_id: String,
-        app: tauri::AppHandle,
+        main_window: tauri::Window,
     ) -> ResultSend<()> {
         while let Some(commit) = reader.next().await {
-            app.emit_all(&stream_id, commit).unwrap();
+            main_window.emit(&stream_id, commit).unwrap();
         }
 
         BROKER.write().await.tauri_stream_cancel(stream_id);
@@ -103,7 +104,7 @@ async fn doc_sync_branch(nuri: &str, stream_id: &str, app: tauri::AppHandle) ->
         Ok(())
     }
 
-    spawn_and_log_error(inner_task(reader, stream_id.to_string(), app));
+    spawn_and_log_error(inner_task(reader, stream_id.to_string(), main_window));
 
     Ok(())
 }
@@ -173,6 +174,7 @@ impl AppBuilder {
 
                 Ok(())
             })
+            .plugin(tauri_plugin_window::init())
             .invoke_handler(tauri::generate_handler![
                 test,
                 doc_sync_branch,
diff --git a/ng-app/src/api.ts b/ng-app/src/api.ts
index d187782..9b2d0d4 100644
--- a/ng-app/src/api.ts
+++ b/ng-app/src/api.ts
@@ -8,6 +8,8 @@
 // according to those terms.
 import {createAsyncProxy} from "async-proxy";
 import { writable } from "svelte/store";
+import { Bowser } from "../../ng-sdk-js/js/bowser.js"; 
+import {version} from '../package.json';
 
 const mapping = {
 
@@ -26,18 +28,51 @@ const handler = {
         
         if (import.meta.env.NG_APP_WEB) {
             let sdk = await import("ng-sdk-js")
-            return Reflect.apply(sdk[path], caller, args)
+            if (path[0] === "client_info") {
+                let client_info = await Reflect.apply(sdk[path], caller, args);
+                client_info.version=version;
+                //console.log(client_info);
+                return client_info;
+            } else {
+                return Reflect.apply(sdk[path], caller, args)
+            }
         } else {
             let tauri = await import("@tauri-apps/api/tauri");
+            if (path[0] === "client_info") {
 
-            if (path[0] === "doc_sync_branch") {
+                let tauri_platform = import.meta.env.TAURI_PLATFORM;
+                let client_type;
+                switch (tauri_platform) {
+                    case 'macos': client_type = "NativeMacOS";break;
+                    case 'linux': client_type = "NativeLinux";break;
+                    case 'windows': client_type = "NativeWindows";break;
+                    case 'android': client_type = "NativeAndroid";break;
+                    case 'ios': client_type = "NativeIos";break;
+                }
+                let info = Bowser.parse(window.navigator.userAgent);
+                info.platform.arch = import.meta.env.TAURI_ARCH;
+                info.platform.tauri = {
+                    family: import.meta.env.TAURI_FAMILY,
+                    os_version: import.meta.env.TAURI_PLATFORM_VERSION,
+                    type: import.meta.env.TAURI_PLATFORM_TYPE,
+                    debug: import.meta.env.TAURI_DEBUG,
+                    target: import.meta.env.TAURI_TARGET_TRIPLE
+                };
+                info.browser.ua = window.navigator.userAgent;
+                let res = {
+                    // TODO: install timestamp 
+                    ClientInfoV0 : { client_type, details: JSON.stringify(info), version, timestamp_install:0, timestamp_updated:0 }
+                };
+                //console.log(res);
+                return res;
+            } else if (path[0] === "doc_sync_branch") {
                 let stream_id = (lastStreamId += 1).toString();
                 console.log("stream_id",stream_id);
-                let { listen } = await import("@tauri-apps/api/event");
+                let { appWindow } = await import("@tauri-apps/plugin-window");
                 let nuri = args[0];
                 let callback = args[1];
 
-                let unlisten = await listen(stream_id, (event) => {
+                let unlisten = await appWindow.listen(stream_id, (event) => {
                     callback(event.payload).then(()=> {})
                 })
                 await tauri.invoke("doc_sync_branch",{nuri, stream_id});
@@ -58,6 +93,8 @@ const handler = {
                 params.result_with_wallet_file = false;
                 params.security_img = Array.from(new Uint8Array(params.security_img));
                 return await tauri.invoke(path[0],{params})
+            } else if (path[0] === "get_local_bootstrap") {
+                return false;
             }
             else {
                 let arg = {};
@@ -70,4 +107,8 @@ const handler = {
   
 const api = createAsyncProxy({}, handler);
 
+export const NG_EU_BSP = "https://nextgraph.eu";
+
+export const NG_NET_BSP = "https://nextgraph.net";
+
 export default api;
\ No newline at end of file
diff --git a/ng-app/src/routes/Grid.svelte b/ng-app/src/routes/Grid.svelte
index 235b5ff..87e8312 100644
--- a/ng-app/src/routes/Grid.svelte
+++ b/ng-app/src/routes/Grid.svelte
@@ -55,16 +55,10 @@
       security_txt: "   know     yourself  ",
       pin: [5, 2, 9, 1],
       pazzle_length: 9,
-      send_bootstrap: undefined,
+      send_bootstrap: false,
       send_wallet: false,
       result_with_wallet_file: true,
-      peer_id: {
-        Ed25519PubKey: [
-          119, 251, 253, 29, 135, 199, 254, 50, 134, 67, 1, 208, 117, 196, 167,
-          107, 2, 113, 98, 243, 49, 90, 7, 0, 157, 58, 14, 187, 14, 3, 116, 86,
-        ],
-      },
-      nonce: 0,
+      local_save: false,
     };
 
     try {
diff --git a/ng-app/src/routes/Install.svelte b/ng-app/src/routes/Install.svelte
index 5761e9d..df77f23 100644
--- a/ng-app/src/routes/Install.svelte
+++ b/ng-app/src/routes/Install.svelte
@@ -78,7 +78,7 @@
     </a>
   </div>
   <div class="row mt-5">
-    <a href="#">
+    <a href="https://nextgraph.org/download/#android">
       <button
         tabindex="-1"
         class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mr-2 mb-2"
@@ -119,7 +119,7 @@
   </div>
 
   <div class="row mt-5">
-    <a href="#">
+    <a href="https://nextgraph.org/download/#macos">
       <button
         tabindex="-1"
         class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mr-2 mb-2"
@@ -140,7 +140,7 @@
   </div>
 
   <div class="row mt-5">
-    <a href="#">
+    <a href="https://nextgraph.org/download/#linux">
       <button
         tabindex="-1"
         class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mr-2 mb-2"
@@ -161,7 +161,7 @@
   </div>
 
   <div class="row mt-5">
-    <a href="#">
+    <a href="https://nextgraph.org/download/#windows">
       <button
         tabindex="-1"
         class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mr-2 mb-2"
@@ -182,7 +182,7 @@
   </div>
 
   <div class="row mt-5 mb-12">
-    <a href="https://docs.nextgraph.org/en/self-hosted">
+    <a href="https://nextgraph.org/self-host">
       <button
         tabindex="-1"
         class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mr-2 mb-2"
diff --git a/ng-app/src/routes/WalletCreate.svelte b/ng-app/src/routes/WalletCreate.svelte
index f7c8990..34de688 100644
--- a/ng-app/src/routes/WalletCreate.svelte
+++ b/ng-app/src/routes/WalletCreate.svelte
@@ -11,17 +11,19 @@
 
 <script>
   import { Button, Alert, Dropzone, Toggle } from "flowbite-svelte";
-  import { link } from "svelte-spa-router";
+  import { link, querystring } from "svelte-spa-router";
   import EULogo from "../assets/EU.svg?component";
   import Logo from "../assets/nextgraph.svg?component";
-  import ng from "../api";
+  import { NG_EU_BSP, NG_NET_BSP, default as ng } from "../api";
   import { display_pazzle } from "../wallet_emojis";
 
   import { onMount, tick } from "svelte";
 
-  let mobile =
-    import.meta.env.TAURI_PLATFORM == "android" ||
-    import.meta.env.TAURI_PLATFORM == "ios";
+  const params = new URLSearchParams($querystring);
+
+  let tauri_platform = import.meta.env.TAURI_PLATFORM;
+
+  let mobile = tauri_platform == "android" || tauri_platform == "ios";
 
   const onFileSelected = (image) => {
     animate_bounce = false;
@@ -92,6 +94,7 @@
   let download_name;
   let cloud_link;
   let animateDownload = true;
+  let invitation;
 
   function scrollToTop() {
     top.scrollIntoView();
@@ -115,9 +118,18 @@
     ? "api/v1/"
     : "http://localhost:3030/api/v1/";
 
-  let display_note_on_local_wallets = true;
+  let display_note_on_local_wallets = false;
 
   async function bootstrap() {
+    console.log(await ng.client_info());
+    invitation = await ng.get_local_bootstrap(location.href, params.get("i"));
+    console.log(invitation);
+    // TODO: implement this error screen and link button
+    if (!invitation && params.get("i")) {
+      console.error(
+        "got an invitation for another broker. click on the link below to be redirected to the right broker"
+      );
+    }
     scrollToTop();
     let bs;
     try {
@@ -152,15 +164,8 @@
       security_txt,
       pin,
       pazzle_length: 9,
-      send_bootstrap: undefined, //options.cloud || options.bootstrap ?  : undefined,
+      send_bootstrap: false, //options.cloud || options.bootstrap ?  : undefined,
       send_wallet: options.cloud,
-      peer_id: {
-        Ed25519PubKey: [
-          119, 251, 253, 29, 135, 199, 254, 50, 134, 67, 1, 208, 117, 196, 167,
-          107, 2, 113, 98, 243, 49, 90, 7, 0, 157, 58, 14, 187, 14, 3, 116, 86,
-        ],
-      },
-      nonce: 0,
       local_save: options.trusted, // this is only used for tauri apps
       result_with_wallet_file: false, // this will be automatically changed to true for browser app
     };
@@ -203,7 +208,16 @@
     console.log("Result:", result);
   }
 
-  onMount(() => bootstrap());
+  onMount(async () => await bootstrap());
+
+  const selectEU = (event) => {
+    if (!tauri_platform) {
+      window.open(NG_EU_BSP + "/#/wallet/create", "_blank").focus();
+    }
+  };
+  const selectNET = (event) => {};
+  const enterINVITE = (event) => {};
+  const enterQRcode = (event) => {};
 </script>
 
 <main class="container3" bind:this={top}>
@@ -215,14 +229,14 @@
   {#if intro}
     <div class=" max-w-6xl lg:px-8 mx-auto px-4">
       <p class="max-w-xl md:mx-auto lg:max-w-2xl">
-        A <b>NextGraph Wallet</b> is unique to each individual. It stores your
+        A <b>NextGraph Wallet</b> is unique to each person. It stores your
         credentials and authorizations to access documents. <br /><br />If you
         already have a wallet, you should not create a new one, instead,
         <a href="/wallet/login" use:link
           >login here with your existing wallet.</a
         >
         If you never created a NextGraph Wallet before, please follow the instructions
-        below in order to create your personal wallet.
+        below in order to create your unique personal wallet.
       </p>
     </div>
     {#if display_note_on_local_wallets}
@@ -277,8 +291,8 @@
               />
             </svg>
             <span
-              >In it, we store all the permissions to access documents you have
-              been granted with, or that you have created yourself.</span
+              >In your wallet, we store all the permissions to access documents
+              you have been granted with, or that you have created yourself.</span
             >
           </li>
           <li class="flex space-x-3">
@@ -422,10 +436,10 @@
             </svg>
             <span
               >For the same reason, we won't be able to help you if you forget
-              your pazzle or PIN code. There is no "password recovery" option in
-              this case. You can note your pazzle down on a piece of paper until
-              you remember it, but don't forget to destroy this note after a
-              while.</span
+              your pazzle or PIN code, or if you loose the wallet file. There is
+              no "password recovery" option in this case. You can note your
+              pazzle down on a piece of paper until you remember it, but don't
+              forget to destroy this note after a while.</span
             >
           </li>
         </ul>
@@ -435,7 +449,7 @@
       <button
         on:click|once={create_wallet}
         role="button"
-        class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:outline-none 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-700/55 mr-2 mb-2"
+        class="text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:outline-none 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-700/55 mb-2"
       >
         <svg
           class="w-8 h-8 mr-2 -ml-1"
@@ -455,14 +469,374 @@
         Ok, I create my wallet now !
       </button>
     </div>
+  {:else if !invitation}
+    <div class=" max-w-6xl lg:px-8 mx-auto px-4">
+      <p class="max-w-xl md:mx-auto lg:max-w-2xl">
+        NextGraph is based on an efficient decentralized P2P network, and in
+        order to join this network and start using the app, you need to first
+        select a <b>broker&nbsp;server</b>.
+      </p>
+    </div>
+    <div class="px-4 pt-3 mx-auto max-w-6xl lg:px-8 lg:pt-10 dark:bg-slate-800">
+      <div class="max-w-xl md:mx-auto sm:text-center lg:max-w-2xl">
+        <h2 class="pb-5 text-xl">
+          What is a broker? <span class="text-sm">Please read</span>
+        </h2>
+        <ul class="mb-8 space-y-4 text-left text-gray-500 dark:text-gray-400">
+          <li class="flex space-x-3">
+            <svg
+              fill="none"
+              stroke="currentColor"
+              stroke-width="1.5"
+              viewBox="0 0 24 24"
+              xmlns="http://www.w3.org/2000/svg"
+              aria-hidden="true"
+              class="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
+              />
+            </svg>
+            <span>
+              The broker helps you keep all your data in <b>sync</b>, as it is
+              connected to the internet 24/7 and keeps a copy of the updates for
+              you. This way, even if the devices of the other participants are
+              offline, you can still see their changes</span
+            >
+          </li>
+          <li class="flex space-x-3">
+            <svg
+              class="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="1.5"
+              viewBox="0 0 24 24"
+              xmlns="http://www.w3.org/2000/svg"
+              aria-hidden="true"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
+              />
+            </svg>
+            <span>
+              All your data is secure and <b>end-to-end encrypted</b>, and the
+              broker cannot see the content of the documents as it does not have
+              the keys to decrypt them.</span
+            >
+          </li>
+          <li class="flex space-x-3">
+            <svg
+              class="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="1.5"
+              viewBox="0 0 24 24"
+              xmlns="http://www.w3.org/2000/svg"
+              aria-hidden="true"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
+              />
+            </svg>
+            <span>
+              The broker helps you enforce your <b>privacy</b> as it hides your internet
+              address (IP) from other users you share documents with.</span
+            >
+          </li>
+          <li class="flex space-x-3">
+            <svg
+              class="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="1.5"
+              viewBox="0 0 24 24"
+              xmlns="http://www.w3.org/2000/svg"
+              aria-hidden="true"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+              />
+            </svg>
+
+            <span>
+              It will be possible in the future to use NextGraph without any
+              broker and to have direct connections between peers, but this will
+              imply a less smooth experience.</span
+            >
+          </li>
+          <li class="flex space-x-3">
+            <svg
+              class="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="1.5"
+              viewBox="0 0 24 24"
+              xmlns="http://www.w3.org/2000/svg"
+              aria-hidden="true"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
+              />
+            </svg>
+            <span>
+              At anytime you can decide to switch to another broker service
+              provider or host it yourself. Your data is totally <b>portable</b>
+              and can freely move to another broker.</span
+            >
+          </li>
+          <li class="flex space-x-3">
+            <svg
+              class="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="1.5"
+              viewBox="0 0 24 24"
+              xmlns="http://www.w3.org/2000/svg"
+              aria-hidden="true"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
+              />
+            </svg>
+
+            <span>
+              Very soon we will offer you the opportunity to host your own
+              broker at <b>home</b> or <b>office</b>. Instead of using a "broker
+              service provider", you will own a small device that you connect
+              behind your internet router. It is called NG Box and will be
+              available soon.</span
+            >
+          </li>
+          <li class="flex space-x-3">
+            <svg
+              class="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
+              fill="none"
+              stroke="currentColor"
+              stroke-width="1.5"
+              viewBox="0 0 24 24"
+              xmlns="http://www.w3.org/2000/svg"
+              aria-hidden="true"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
+              />
+            </svg>
+
+            <span>
+              Large organizations and companies have the opportunity to host a
+              broker <b>on-premise</b> or in the cloud, as the software is open
+              source. Individuals can also <b>self-host</b> a broker on any VPS server
+              or at home.</span
+            >
+          </li>
+        </ul>
+        <h2 class="mt-3 text-xl">Please choose one broker among the list</h2>
+      </div>
+    </div>
+    <div class="row mt-5">
+      {#if !tauri_platform}
+        <a href="https://nextgraph.eu/#/wallet/create">
+          <button
+            tabindex="-1"
+            class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/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"
+          >
+            <EULogo class="mr-4 block h-10 w-10" alt="European Union flag" />
+            For European Union citizens
+          </button>
+        </a>
+      {:else}
+        <button
+          on:click|once={selectEU}
+          class="choice-button text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/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"
+        >
+          <EULogo class="mr-4 block h-10 w-10" alt="European Union flag" />
+          For European Union citizens
+        </button>
+      {/if}
+    </div>
+
+    <div class="row mt-5">
+      {#if !tauri_platform}
+        <a href="https://nextgraph.net/#/wallet/create">
+          <button
+            tabindex="-1"
+            class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/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
+              fill="none"
+              stroke="currentColor"
+              stroke-width="1.5"
+              viewBox="0 0 24 24"
+              xmlns="http://www.w3.org/2000/svg"
+              aria-hidden="true"
+              class="mr-4 block h-10 w-10"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
+              />
+            </svg>
+            For the rest of the world
+          </button>
+        </a>
+      {:else}
+        <button
+          on:click|once={selectNET}
+          class="choice-button text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/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
+            fill="none"
+            stroke="currentColor"
+            stroke-width="1.5"
+            viewBox="0 0 24 24"
+            xmlns="http://www.w3.org/2000/svg"
+            aria-hidden="true"
+            class="mr-4 block h-10 w-10"
+          >
+            <path
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
+            />
+          </svg>
+          For the rest of the world
+        </button>
+      {/if}
+    </div>
+
+    <div class="row mt-5">
+      <button
+        on:click|once={enterINVITE}
+        class="choice-button text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/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
+          fill="none"
+          stroke="currentColor"
+          stroke-width="1.5"
+          viewBox="0 0 24 24"
+          xmlns="http://www.w3.org/2000/svg"
+          aria-hidden="true"
+          class="mr-4 block h-10 w-10"
+        >
+          <path
+            stroke-linecap="round"
+            stroke-linejoin="round"
+            d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
+          />
+        </svg>
+
+        Enter an invitation link
+      </button>
+    </div>
+    {#if mobile}
+      <div class="row mt-5">
+        <button
+          on:click|once={enterQRcode}
+          class="choice-button text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/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
+            class="mr-4 block h-10 w-10"
+            fill="none"
+            stroke="currentColor"
+            stroke-width="1.5"
+            viewBox="0 0 24 24"
+            xmlns="http://www.w3.org/2000/svg"
+            aria-hidden="true"
+          >
+            <path
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"
+            />
+            <path
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z"
+            />
+          </svg>
+
+          Scan an invitation QRcode
+        </button>
+      </div>
+    {/if}
+    <div class="row mt-5">
+      <a href="https://nextgraph.org/self-host">
+        <button
+          tabindex="-1"
+          class="choice-button text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/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
+            fill="none"
+            stroke="currentColor"
+            stroke-width="1.5"
+            viewBox="0 0 24 24"
+            xmlns="http://www.w3.org/2000/svg"
+            aria-hidden="true"
+            class="mr-4 block h-10 w-10"
+          >
+            <path
+              stroke-linecap="round"
+              stroke-linejoin="round"
+              d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
+            />
+          </svg>
+          Self-hosted broker
+        </button>
+      </a>
+    </div>
+    <div class="row mt-5 mb-12">
+      <a href="https://nextgraph.org/ng-box/">
+        <button
+          tabindex="-1"
+          class="choice-button text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/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
+            xmlns="http://www.w3.org/2000/svg"
+            version="1.1"
+            viewBox="0 0 225 225"
+            class="mr-4 block h-10 w-10"
+            stroke="currentColor"
+            stroke-width="12"
+            fill="none"
+          >
+            <path
+              d="M 88.332599,179.77884 C 72.858008,177.42608 59.581081,170.564 48.8817,159.38898 36.800075,146.77026 30.396139,130.74266 30.396139,113.12381 c 0,-8.81477 1.466462,-16.772273 4.503812,-24.439156 3.697755,-9.333883 8.658122,-16.726264 15.988284,-23.827148 4.07992,-3.952299 5.699054,-5.267377 9.730928,-7.903581 10.263753,-6.710853 20.852276,-10.247623 32.861256,-10.976317 17.083161,-1.036581 33.737521,4.410501 47.100151,15.404873 1.30009,1.069669 2.35446,2.035155 2.34305,2.145524 -0.0114,0.110369 -3.32807,3.135042 -7.37038,6.721489 -4.04229,3.586437 -8.6667,7.731233 -10.27646,9.210635 -1.60975,1.479412 -3.05439,2.689839 -3.21032,2.689839 -0.15591,0 -1.2075,-0.642795 -2.33686,-1.428431 -6.49544,-4.518567 -13.79659,-6.747116 -22.104843,-6.747116 -10.982241,0 -20.054641,3.741852 -27.727158,11.435891 -5.517107,5.532575 -9.233107,12.555305 -10.782595,20.377588 -0.596045,3.00901 -0.594915,11.67153 0.0017,14.67182 3.195984,16.0665 15.801761,28.55358 31.607491,31.30987 3.592183,0.62643 10.334745,0.61437 13.792675,-0.0247 12.10383,-2.2368 22.30712,-9.80603 27.83192,-20.64689 0.66747,-1.30971 1.08703,-2.48825 0.93235,-2.61898 -0.1547,-0.13073 -5.9299,-1.01605 -12.83381,-1.96739 -8.43575,-1.16241 -12.87296,-1.9096 -13.52955,-2.27826 -1.31171,-0.73647 -2.44642,-2.49122 -2.44642,-3.78325 0,-1.012 1.74837,-13.68832 2.1486,-15.57814 0.25598,-1.20873 2.0923,-3.01339 3.3151,-3.25795 0.53677,-0.10735 7.61424,0.73799 15.7688,1.88346 8.13723,1.14303 14.89071,1.97925 15.00772,1.85826 0.11702,-0.12098 0.96445,-5.648553 1.88315,-12.283473 0.95557,-6.900944 1.90122,-12.59548 2.20977,-13.306594 0.29667,-0.683692 0.95765,-1.595052 1.46889,-2.025218 1.77972,-1.497534 2.7114,-1.539742 10.52745,-0.476938 8.31229,1.130266 9.2373,1.347581 10.59333,2.488613 1.41776,1.192951 1.96085,2.424677 1.94866,4.419342 -0.006,0.950347 -0.79507,7.156475 -1.75393,13.791395 -0.95885,6.634933 -1.70069,12.111623 -1.64854,12.170443 0.0522,0.0588 6.18174,0.95872 13.62132,1.99978 9.57969,1.34053 13.80866,2.0595 14.49353,2.46406 1.3199,0.77969 2.13943,2.28402 2.1135,3.87957 -0.0399,2.45278 -2.08103,15.63263 -2.5664,16.57122 -0.57073,1.10369 -2.24485,2.197 -3.38232,2.20889 -0.44831,0.004 -6.79249,-0.82755 -14.09817,-1.84941 -7.3057,-1.02186 -13.34942,-1.79161 -13.43049,-1.71053 -0.0811,0.0811 -1.02469,6.33285 -2.09694,13.89286 -1.24218,8.75802 -2.1547,14.1778 -2.51495,14.93697 -0.62565,1.31846 -2.38302,2.64205 -3.91461,2.94836 -0.8254,0.16509 -9.4024,-0.80047 -11.73007,-1.32049 -0.47193,-0.10544 -1.63157,0.58011 -3.8898,2.29957 -9.71515,7.39729 -20.99725,11.99799 -33.08692,13.49241 -3.79574,0.46921 -13.565667,0.37348 -17.125664,-0.16779 z"
+            />
+            <rect
+              ry="37.596001"
+              y="10.583322"
+              x="14.363095"
+              height="204.86308"
+              width="195.79167"
+            />
+          </svg>
+          NG Box (owned or invited)
+        </button>
+      </a>
+    </div>
   {:else if pin.length < 4}
     <div class=" max-w-6xl lg:px-8 mx-auto px-4">
       <p class="max-w-xl md:mx-auto lg:max-w-2xl">
         <span class="text-xl">Let's start by choosing a PIN code</span>
         <Alert color="yellow" class="mt-5">
-          We recommend you to choose a PIN code that you already know very well
-          :<br />
-          your credit card PIN, by example, is a good choice
+          We recommend you to choose a PIN code that you already know very well.
+          <br />
+          Your credit card PIN, by example, is a good choice. (We at NextGraph will
+          never see your PIN)
         </Alert>
       </p>
       <p class="text-left mt-5">Here are the rules for the PIN :</p>
@@ -610,7 +984,7 @@
           <button
             on:click|once={save_security}
             bind:this={validate_button}
-            class="animate-bounce mt-10 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:outline-none 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-700/55 mr-2 mb-2"
+            class="animate-bounce mt-10 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:outline-none 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-700/55 mb-2"
           >
             <svg
               class="w-8 h-8 mr-2 -ml-1"
@@ -695,8 +1069,8 @@
         with your security and privacy.<br /><br />
         Remember that in any case, once your wallet will be created, you will download
         a file that you should keep privately somewhere on your device, USB key or
-        harddisk. This is the default way you can use and keep your wallet. Now let's
-        look at some options that can make your life a bit easier.
+        hard-disk. This is the default way you can use and keep your wallet. Now
+        let's look at some options that can make your life a bit easier.
       </p>
       <p class="max-w-xl md:mx-auto lg:max-w-2xl text-left">
         <span class="text-xl">Do you trust this device? </span> <br />
@@ -704,8 +1078,8 @@
         family or workplace, and you would like to login again from this device in
         the future, then you can save your wallet on this device. To the contrary,
         if this device is public and shared by strangers, do not save your wallet
-        here. {#if !import.meta.env.TAURI_PLATFORM}By selecting this option, you
-          agree to save some cookies on your browser.{/if}<br />
+        here. {#if !tauri_platform}By selecting this option, you agree to save
+          some cookies on your browser.{/if}<br />
         <Toggle class="mt-3" bind:checked={options.trusted}
           >Save your wallet on this device?</Toggle
         >
@@ -755,7 +1129,7 @@
       {/if}
       <button
         on:click|once={do_wallet}
-        class="mt-10 mb-8 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:outline-none 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-700/55 mr-2"
+        class="mt-10 mb-8 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:outline-none 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-700/55"
       >
         <svg
           class="w-8 h-8 mr-2 -ml-1"
@@ -827,7 +1201,7 @@
               tabindex="-1"
               class:animate-bounce={animateDownload}
               on:click={() => (animateDownload = false)}
-              class="mt-10 mb-8 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:outline-none 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-700/55 mr-2"
+              class="mt-10 mb-8 text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:outline-none 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-700/55"
             >
               <svg
                 class="w-8 h-8 mr-2 -ml-1"
@@ -896,3 +1270,6 @@
     </div>
   {/if}
 </main>
+
+<style>
+</style>
diff --git a/ng-app/src/styles.css b/ng-app/src/styles.css
index 9324551..f50cfdb 100644
--- a/ng-app/src/styles.css
+++ b/ng-app/src/styles.css
@@ -29,6 +29,10 @@ div[role="alert"] div {
   display: block;
 }
 
+.choice-button {
+  min-width: 340px;
+}
+
 .row {
   display: flex;
   justify-content: center;
diff --git a/ng-sdk-js/README.md b/ng-sdk-js/README.md
index 050d6e4..0f1bc8f 100644
--- a/ng-sdk-js/README.md
+++ b/ng-sdk-js/README.md
@@ -37,17 +37,10 @@ npm i ng-sdk-js
 ## For contributors
 
 ```
-wasm-pack build --target bundler
-cd pkg
-// if you have access to npm registry and want to publish the package
-// npm publish --access=public
+wasm-pack build --dev --target bundler
 
-cd ..
-wasm-pack build -t nodejs -d pkg-node
+wasm-pack build --dev -t nodejs -d pkg-node
 node prepare-node.js
-cd pkg-node
-// if you have access to npm registry and want to publish the package
-// npm publish --access=public
 ```
 
 For testing in vanilla JS
@@ -65,6 +58,18 @@ Or automated testing with headless chrome:
 wasm-pack test --chrome --headless
 ```
 
+## Production built
+
+```
+wasm-pack build --target bundler
+wasm-pack build -t nodejs -d pkg-node
+node prepare-node.js
+cd pkg
+npm publish --access=public
+cd ../pkg-node
+npm publish --access=public
+```
+
 ### Plain JS web app
 
 ```
diff --git a/ng-sdk-js/app-node/index.js b/ng-sdk-js/app-node/index.js
index 4de3e0a..38b79eb 100644
--- a/ng-sdk-js/app-node/index.js
+++ b/ng-sdk-js/app-node/index.js
@@ -14,6 +14,6 @@ global.WebSocket = WebSocket;
 const test = require("./test")
 console.log("FROM INDEX");
 ng.test();
-test.random();
+//test.random();
 console.log(ng.start());
 
diff --git a/ng-sdk-js/js/bowser.js b/ng-sdk-js/js/bowser.js
new file mode 100644
index 0000000..2c684d2
--- /dev/null
+++ b/ng-sdk-js/js/bowser.js
@@ -0,0 +1,2233 @@
+// NOTE: this list must be up-to-date with browsers listed in
+// test/acceptance/useragentstrings.yml
+export const BROWSER_ALIASES_MAP = {
+    'Amazon Silk': 'amazon_silk',
+    'Android Browser': 'android',
+    Bada: 'bada',
+    BlackBerry: 'blackberry',
+    Chrome: 'chrome',
+    Chromium: 'chromium',
+    Electron: 'electron',
+    Epiphany: 'epiphany',
+    Firefox: 'firefox',
+    Focus: 'focus',
+    Generic: 'generic',
+    'Google Search': 'google_search',
+    Googlebot: 'googlebot',
+    'Internet Explorer': 'ie',
+    'K-Meleon': 'k_meleon',
+    Maxthon: 'maxthon',
+    'Microsoft Edge': 'edge',
+    'MZ Browser': 'mz',
+    'NAVER Whale Browser': 'naver',
+    Opera: 'opera',
+    'Opera Coast': 'opera_coast',
+    PhantomJS: 'phantomjs',
+    Puffin: 'puffin',
+    QupZilla: 'qupzilla',
+    QQ: 'qq',
+    QQLite: 'qqlite',
+    Safari: 'safari',
+    Sailfish: 'sailfish',
+    'Samsung Internet for Android': 'samsung_internet',
+    SeaMonkey: 'seamonkey',
+    Sleipnir: 'sleipnir',
+    Swing: 'swing',
+    Tizen: 'tizen',
+    'UC Browser': 'uc',
+    Vivaldi: 'vivaldi',
+    'WebOS Browser': 'webos',
+    WeChat: 'wechat',
+    'Yandex Browser': 'yandex',
+    Roku: 'roku',
+  };
+  
+  export const BROWSER_MAP = {
+    amazon_silk: 'Amazon Silk',
+    android: 'Android Browser',
+    bada: 'Bada',
+    blackberry: 'BlackBerry',
+    chrome: 'Chrome',
+    chromium: 'Chromium',
+    electron: 'Electron',
+    epiphany: 'Epiphany',
+    firefox: 'Firefox',
+    focus: 'Focus',
+    generic: 'Generic',
+    googlebot: 'Googlebot',
+    google_search: 'Google Search',
+    ie: 'Internet Explorer',
+    k_meleon: 'K-Meleon',
+    maxthon: 'Maxthon',
+    edge: 'Microsoft Edge',
+    mz: 'MZ Browser',
+    naver: 'NAVER Whale Browser',
+    opera: 'Opera',
+    opera_coast: 'Opera Coast',
+    phantomjs: 'PhantomJS',
+    puffin: 'Puffin',
+    qupzilla: 'QupZilla',
+    qq: 'QQ Browser',
+    qqlite: 'QQ Browser Lite',
+    safari: 'Safari',
+    sailfish: 'Sailfish',
+    samsung_internet: 'Samsung Internet for Android',
+    seamonkey: 'SeaMonkey',
+    sleipnir: 'Sleipnir',
+    swing: 'Swing',
+    tizen: 'Tizen',
+    uc: 'UC Browser',
+    vivaldi: 'Vivaldi',
+    webos: 'WebOS Browser',
+    wechat: 'WeChat',
+    yandex: 'Yandex Browser',
+  };
+  
+  export const PLATFORMS_MAP = {
+    tablet: 'tablet',
+    mobile: 'mobile',
+    desktop: 'desktop',
+    tv: 'tv',
+  };
+  
+  export const OS_MAP = {
+    WindowsPhone: 'Windows Phone',
+    Windows: 'Windows',
+    MacOS: 'macOS',
+    iOS: 'iOS',
+    Android: 'Android',
+    WebOS: 'WebOS',
+    BlackBerry: 'BlackBerry',
+    Bada: 'Bada',
+    Tizen: 'Tizen',
+    Linux: 'Linux',
+    ChromeOS: 'Chrome OS',
+    PlayStation4: 'PlayStation 4',
+    Roku: 'Roku',
+  };
+  
+  export const ENGINE_MAP = {
+    EdgeHTML: 'EdgeHTML',
+    Blink: 'Blink',
+    Trident: 'Trident',
+    Presto: 'Presto',
+    Gecko: 'Gecko',
+    WebKit: 'WebKit',
+  };
+
+  class Utils {
+    /**
+     * Get first matched item for a string
+     * @param {RegExp} regexp
+     * @param {String} ua
+     * @return {Array|{index: number, input: string}|*|boolean|string}
+     */
+    static getFirstMatch(regexp, ua) {
+      const match = ua.match(regexp);
+      return (match && match.length > 0 && match[1]) || '';
+    }
+  
+    /**
+     * Get second matched item for a string
+     * @param regexp
+     * @param {String} ua
+     * @return {Array|{index: number, input: string}|*|boolean|string}
+     */
+    static getSecondMatch(regexp, ua) {
+      const match = ua.match(regexp);
+      return (match && match.length > 1 && match[2]) || '';
+    }
+  
+    /**
+     * Match a regexp and return a constant or undefined
+     * @param {RegExp} regexp
+     * @param {String} ua
+     * @param {*} _const Any const that will be returned if regexp matches the string
+     * @return {*}
+     */
+    static matchAndReturnConst(regexp, ua, _const) {
+      if (regexp.test(ua)) {
+        return _const;
+      }
+      return void (0);
+    }
+  
+    static getWindowsVersionName(version) {
+      switch (version) {
+        case 'NT': return 'NT';
+        case 'XP': return 'XP';
+        case 'NT 5.0': return '2000';
+        case 'NT 5.1': return 'XP';
+        case 'NT 5.2': return '2003';
+        case 'NT 6.0': return 'Vista';
+        case 'NT 6.1': return '7';
+        case 'NT 6.2': return '8';
+        case 'NT 6.3': return '8.1';
+        case 'NT 10.0': return '10';
+        default: return undefined;
+      }
+    }
+  
+    /**
+     * Get macOS version name
+     *    10.5 - Leopard
+     *    10.6 - Snow Leopard
+     *    10.7 - Lion
+     *    10.8 - Mountain Lion
+     *    10.9 - Mavericks
+     *    10.10 - Yosemite
+     *    10.11 - El Capitan
+     *    10.12 - Sierra
+     *    10.13 - High Sierra
+     *    10.14 - Mojave
+     *    10.15 - Catalina
+     *
+     * @example
+     *   getMacOSVersionName("10.14") // 'Mojave'
+     *
+     * @param  {string} version
+     * @return {string} versionName
+     */
+    static getMacOSVersionName(version) {
+      const v = version.split('.').splice(0, 2).map(s => parseInt(s, 10) || 0);
+      v.push(0);
+      if (v[0] !== 10) {
+        switch (v[0]) {
+          case 11: return 'Big Sur';
+          case 12: return 'Monterey';
+          case 13: return 'Ventura';
+          case 14: return 'Sonoma';
+        }
+      }
+      else switch (v[1]) {
+        case 5: return 'Leopard';
+        case 6: return 'Snow Leopard';
+        case 7: return 'Lion';
+        case 8: return 'Mountain Lion';
+        case 9: return 'Mavericks';
+        case 10: return 'Yosemite';
+        case 11: return 'El Capitan';
+        case 12: return 'Sierra';
+        case 13: return 'High Sierra';
+        case 14: return 'Mojave';
+        case 15: return 'Catalina';
+        default: return undefined;
+      }
+    }
+  
+    /**
+     * Get Android version name
+     *    1.5 - Cupcake
+     *    1.6 - Donut
+     *    2.0 - Eclair
+     *    2.1 - Eclair
+     *    2.2 - Froyo
+     *    2.x - Gingerbread
+     *    3.x - Honeycomb
+     *    4.0 - Ice Cream Sandwich
+     *    4.1 - Jelly Bean
+     *    4.4 - KitKat
+     *    5.x - Lollipop
+     *    6.x - Marshmallow
+     *    7.x - Nougat
+     *    8.x - Oreo
+     *    9.x - Pie
+     *
+     * @example
+     *   getAndroidVersionName("7.0") // 'Nougat'
+     *
+     * @param  {string} version
+     * @return {string} versionName
+     */
+    static getAndroidVersionName(version) {
+      const v = version.split('.').splice(0, 2).map(s => parseInt(s, 10) || 0);
+      v.push(0);
+      if (v[0] === 1 && v[1] < 5) return undefined;
+      if (v[0] === 1 && v[1] < 6) return 'Cupcake';
+      if (v[0] === 1 && v[1] >= 6) return 'Donut';
+      if (v[0] === 2 && v[1] < 2) return 'Eclair';
+      if (v[0] === 2 && v[1] === 2) return 'Froyo';
+      if (v[0] === 2 && v[1] > 2) return 'Gingerbread';
+      if (v[0] === 3) return 'Honeycomb';
+      if (v[0] === 4 && v[1] < 1) return 'Ice Cream Sandwich';
+      if (v[0] === 4 && v[1] < 4) return 'Jelly Bean';
+      if (v[0] === 4 && v[1] >= 4) return 'KitKat';
+      if (v[0] === 5) return 'Lollipop';
+      if (v[0] === 6) return 'Marshmallow';
+      if (v[0] === 7) return 'Nougat';
+      if (v[0] === 8) return 'Oreo';
+      if (v[0] === 9) return 'Pie';
+      if (v[0] === 10) return 'Android 10';
+      if (v[0] === 11) return 'Android 11';
+      if (v[0] === 12) return 'Android 12';
+      if (v[0] === 13) return 'Android 13';
+      if (v[0] === 14) return 'Android 14';
+      if (v[0] === 15) return 'Android 15';
+      return undefined;
+    }
+  
+    /**
+     * Get version precisions count
+     *
+     * @example
+     *   getVersionPrecision("1.10.3") // 3
+     *
+     * @param  {string} version
+     * @return {number}
+     */
+    static getVersionPrecision(version) {
+      return version.split('.').length;
+    }
+  
+    /**
+     * Calculate browser version weight
+     *
+     * @example
+     *   compareVersions('1.10.2.1',  '1.8.2.1.90')    // 1
+     *   compareVersions('1.010.2.1', '1.09.2.1.90');  // 1
+     *   compareVersions('1.10.2.1',  '1.10.2.1');     // 0
+     *   compareVersions('1.10.2.1',  '1.0800.2');     // -1
+     *   compareVersions('1.10.2.1',  '1.10',  true);  // 0
+     *
+     * @param {String} versionA versions versions to compare
+     * @param {String} versionB versions versions to compare
+     * @param {boolean} [isLoose] enable loose comparison
+     * @return {Number} comparison result: -1 when versionA is lower,
+     * 1 when versionA is bigger, 0 when both equal
+     */
+    /* eslint consistent-return: 1 */
+    static compareVersions(versionA, versionB, isLoose = false) {
+      // 1) get common precision for both versions, for example for "10.0" and "9" it should be 2
+      const versionAPrecision = Utils.getVersionPrecision(versionA);
+      const versionBPrecision = Utils.getVersionPrecision(versionB);
+  
+      let precision = Math.max(versionAPrecision, versionBPrecision);
+      let lastPrecision = 0;
+  
+      const chunks = Utils.map([versionA, versionB], (version) => {
+        const delta = precision - Utils.getVersionPrecision(version);
+  
+        // 2) "9" -> "9.0" (for precision = 2)
+        const _version = version + new Array(delta + 1).join('.0');
+  
+        // 3) "9.0" -> ["000000000"", "000000009"]
+        return Utils.map(_version.split('.'), chunk => new Array(20 - chunk.length).join('0') + chunk).reverse();
+      });
+  
+      // adjust precision for loose comparison
+      if (isLoose) {
+        lastPrecision = precision - Math.min(versionAPrecision, versionBPrecision);
+      }
+  
+      // iterate in reverse order by reversed chunks array
+      precision -= 1;
+      while (precision >= lastPrecision) {
+        // 4) compare: "000000009" > "000000010" = false (but "9" > "10" = true)
+        if (chunks[0][precision] > chunks[1][precision]) {
+          return 1;
+        }
+  
+        if (chunks[0][precision] === chunks[1][precision]) {
+          if (precision === lastPrecision) {
+            // all version chunks are same
+            return 0;
+          }
+  
+          precision -= 1;
+        } else if (chunks[0][precision] < chunks[1][precision]) {
+          return -1;
+        }
+      }
+  
+      return undefined;
+    }
+  
+    /**
+     * Array::map polyfill
+     *
+     * @param  {Array} arr
+     * @param  {Function} iterator
+     * @return {Array}
+     */
+    static map(arr, iterator) {
+      const result = [];
+      let i;
+      if (Array.prototype.map) {
+        return Array.prototype.map.call(arr, iterator);
+      }
+      for (i = 0; i < arr.length; i += 1) {
+        result.push(iterator(arr[i]));
+      }
+      return result;
+    }
+  
+    /**
+     * Array::find polyfill
+     *
+     * @param  {Array} arr
+     * @param  {Function} predicate
+     * @return {Array}
+     */
+    static find(arr, predicate) {
+      let i;
+      let l;
+      if (Array.prototype.find) {
+        return Array.prototype.find.call(arr, predicate);
+      }
+      for (i = 0, l = arr.length; i < l; i += 1) {
+        const value = arr[i];
+        if (predicate(value, i)) {
+          return value;
+        }
+      }
+      return undefined;
+    }
+  
+    /**
+     * Object::assign polyfill
+     *
+     * @param  {Object} obj
+     * @param  {Object} ...objs
+     * @return {Object}
+     */
+    static assign(obj, ...assigners) {
+      const result = obj;
+      let i;
+      let l;
+      if (Object.assign) {
+        return Object.assign(obj, ...assigners);
+      }
+      for (i = 0, l = assigners.length; i < l; i += 1) {
+        const assigner = assigners[i];
+        if (typeof assigner === 'object' && assigner !== null) {
+          const keys = Object.keys(assigner);
+          keys.forEach((key) => {
+            result[key] = assigner[key];
+          });
+        }
+      }
+      return obj;
+    }
+  
+    /**
+     * Get short version/alias for a browser name
+     *
+     * @example
+     *   getBrowserAlias('Microsoft Edge') // edge
+     *
+     * @param  {string} browserName
+     * @return {string}
+     */
+    static getBrowserAlias(browserName) {
+      return BROWSER_ALIASES_MAP[browserName];
+    }
+  
+    /**
+     * Get short version/alias for a browser name
+     *
+     * @example
+     *   getBrowserAlias('edge') // Microsoft Edge
+     *
+     * @param  {string} browserAlias
+     * @return {string}
+     */
+    static getBrowserTypeByAlias(browserAlias) {
+      return BROWSER_MAP[browserAlias] || '';
+    }
+  }
+
+  const commonVersionIdentifier = /version\/(\d+(\.?_?\d+)+)/i;
+
+const browserParsersList = [
+  /* Googlebot */
+  {
+    test: [/googlebot/i],
+    describe(ua) {
+      const browser = {
+        name: 'Googlebot',
+      };
+      const version = Utils.getFirstMatch(/googlebot\/(\d+(\.\d+))/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+
+  /* Opera < 13.0 */
+  {
+    test: [/opera/i],
+    describe(ua) {
+      const browser = {
+        name: 'Opera',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:opera)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+
+  /* Opera > 13.0 */
+  {
+    test: [/opr\/|opios/i],
+    describe(ua) {
+      const browser = {
+        name: 'Opera',
+      };
+      const version = Utils.getFirstMatch(/(?:opr|opios)[\s/](\S+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/SamsungBrowser/i],
+    describe(ua) {
+      const browser = {
+        name: 'Samsung Internet for Android',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:SamsungBrowser)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/Whale/i],
+    describe(ua) {
+      const browser = {
+        name: 'NAVER Whale Browser',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:whale)[\s/](\d+(?:\.\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/MZBrowser/i],
+    describe(ua) {
+      const browser = {
+        name: 'MZ Browser',
+      };
+      const version = Utils.getFirstMatch(/(?:MZBrowser)[\s/](\d+(?:\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/focus/i],
+    describe(ua) {
+      const browser = {
+        name: 'Focus',
+      };
+      const version = Utils.getFirstMatch(/(?:focus)[\s/](\d+(?:\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/swing/i],
+    describe(ua) {
+      const browser = {
+        name: 'Swing',
+      };
+      const version = Utils.getFirstMatch(/(?:swing)[\s/](\d+(?:\.\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/coast/i],
+    describe(ua) {
+      const browser = {
+        name: 'Opera Coast',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:coast)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/opt\/\d+(?:.?_?\d+)+/i],
+    describe(ua) {
+      const browser = {
+        name: 'Opera Touch',
+      };
+      const version = Utils.getFirstMatch(/(?:opt)[\s/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/yabrowser/i],
+    describe(ua) {
+      const browser = {
+        name: 'Yandex Browser',
+      };
+      const version = Utils.getFirstMatch(/(?:yabrowser)[\s/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/ucbrowser/i],
+    describe(ua) {
+      const browser = {
+        name: 'UC Browser',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:ucbrowser)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/Maxthon|mxios/i],
+    describe(ua) {
+      const browser = {
+        name: 'Maxthon',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:Maxthon|mxios)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/epiphany/i],
+    describe(ua) {
+      const browser = {
+        name: 'Epiphany',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:epiphany)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/puffin/i],
+    describe(ua) {
+      const browser = {
+        name: 'Puffin',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:puffin)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/sleipnir/i],
+    describe(ua) {
+      const browser = {
+        name: 'Sleipnir',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:sleipnir)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/k-meleon/i],
+    describe(ua) {
+      const browser = {
+        name: 'K-Meleon',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/(?:k-meleon)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/micromessenger/i],
+    describe(ua) {
+      const browser = {
+        name: 'WeChat',
+      };
+      const version = Utils.getFirstMatch(/(?:micromessenger)[\s/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/qqbrowser/i],
+    describe(ua) {
+      const browser = {
+        name: (/qqbrowserlite/i).test(ua) ? 'QQ Browser Lite' : 'QQ Browser',
+      };
+      const version = Utils.getFirstMatch(/(?:qqbrowserlite|qqbrowser)[/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/msie|trident/i],
+    describe(ua) {
+      const browser = {
+        name: 'Internet Explorer',
+      };
+      const version = Utils.getFirstMatch(/(?:msie |rv:)(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/\sedg\//i],
+    describe(ua) {
+      const browser = {
+        name: 'Microsoft Edge',
+      };
+
+      const version = Utils.getFirstMatch(/\sedg\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/edg([ea]|ios)/i],
+    describe(ua) {
+      const browser = {
+        name: 'Microsoft Edge',
+      };
+
+      const version = Utils.getSecondMatch(/edg([ea]|ios)\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/vivaldi/i],
+    describe(ua) {
+      const browser = {
+        name: 'Vivaldi',
+      };
+      const version = Utils.getFirstMatch(/vivaldi\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/seamonkey/i],
+    describe(ua) {
+      const browser = {
+        name: 'SeaMonkey',
+      };
+      const version = Utils.getFirstMatch(/seamonkey\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/sailfish/i],
+    describe(ua) {
+      const browser = {
+        name: 'Sailfish',
+      };
+
+      const version = Utils.getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/silk/i],
+    describe(ua) {
+      const browser = {
+        name: 'Amazon Silk',
+      };
+      const version = Utils.getFirstMatch(/silk\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/phantom/i],
+    describe(ua) {
+      const browser = {
+        name: 'PhantomJS',
+      };
+      const version = Utils.getFirstMatch(/phantomjs\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/slimerjs/i],
+    describe(ua) {
+      const browser = {
+        name: 'SlimerJS',
+      };
+      const version = Utils.getFirstMatch(/slimerjs\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/blackberry|\bbb\d+/i, /rim\stablet/i],
+    describe(ua) {
+      const browser = {
+        name: 'BlackBerry',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/blackberry[\d]+\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/(web|hpw)[o0]s/i],
+    describe(ua) {
+      const browser = {
+        name: 'WebOS Browser',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua) || Utils.getFirstMatch(/w(?:eb)?[o0]sbrowser\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/bada/i],
+    describe(ua) {
+      const browser = {
+        name: 'Bada',
+      };
+      const version = Utils.getFirstMatch(/dolfin\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/tizen/i],
+    describe(ua) {
+      const browser = {
+        name: 'Tizen',
+      };
+      const version = Utils.getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/qupzilla/i],
+    describe(ua) {
+      const browser = {
+        name: 'QupZilla',
+      };
+      const version = Utils.getFirstMatch(/(?:qupzilla)[\s/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/firefox|iceweasel|fxios/i],
+    describe(ua) {
+      const browser = {
+        name: 'Firefox',
+      };
+      const version = Utils.getFirstMatch(/(?:firefox|iceweasel|fxios)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/electron/i],
+    describe(ua) {
+      const browser = {
+        name: 'Electron',
+      };
+      const version = Utils.getFirstMatch(/(?:electron)\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/MiuiBrowser/i],
+    describe(ua) {
+      const browser = {
+        name: 'Miui',
+      };
+      const version = Utils.getFirstMatch(/(?:MiuiBrowser)[\s/](\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/chromium/i],
+    describe(ua) {
+      const browser = {
+        name: 'Chromium',
+      };
+      const version = Utils.getFirstMatch(/(?:chromium)[\s/](\d+(\.?_?\d+)+)/i, ua) || Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/chrome|crios|crmo/i],
+    describe(ua) {
+      const browser = {
+        name: 'Chrome',
+      };
+      const version = Utils.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+  {
+    test: [/GSA/i],
+    describe(ua) {
+      const browser = {
+        name: 'Google Search',
+      };
+      const version = Utils.getFirstMatch(/(?:GSA)\/(\d+(\.?_?\d+)+)/i, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+
+  /* Android Browser */
+  {
+    test(parser) {
+      const notLikeAndroid = !parser.test(/like android/i);
+      const butAndroid = parser.test(/android/i);
+      return notLikeAndroid && butAndroid;
+    },
+    describe(ua) {
+      const browser = {
+        name: 'Android Browser',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+
+  /* PlayStation 4 */
+  {
+    test: [/playstation 4/i],
+    describe(ua) {
+      const browser = {
+        name: 'PlayStation 4',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+
+  /* Safari */
+  {
+    test: [/safari|applewebkit/i],
+    describe(ua) {
+      const browser = {
+        name: 'Safari',
+      };
+      const version = Utils.getFirstMatch(commonVersionIdentifier, ua);
+
+      if (version) {
+        browser.version = version;
+      }
+
+      return browser;
+    },
+  },
+
+  /* Something else */
+  {
+    test: [/.*/i],
+    describe(ua) {
+      /* Here we try to make sure that there are explicit details about the device
+       * in order to decide what regexp exactly we want to apply
+       * (as there is a specific decision based on that conclusion)
+       */
+      const regexpWithoutDeviceSpec = /^(.*)\/(.*) /;
+      const regexpWithDeviceSpec = /^(.*)\/(.*)[ \t]\((.*)/;
+      const hasDeviceSpec = ua.search('\\(') !== -1;
+      const regexp = hasDeviceSpec ? regexpWithDeviceSpec : regexpWithoutDeviceSpec;
+      return {
+        name: Utils.getFirstMatch(regexp, ua),
+        version: Utils.getSecondMatch(regexp, ua),
+      };
+    },
+  },
+];
+
+const enginesParsersList = [
+    /* EdgeHTML */
+    {
+      test(parser) {
+        return parser.getBrowserName(true) === 'microsoft edge';
+      },
+      describe(ua) {
+        const isBlinkBased = /\sedg\//i.test(ua);
+  
+        // return blink if it's blink-based one
+        if (isBlinkBased) {
+          return {
+            name: ENGINE_MAP.Blink,
+          };
+        }
+  
+        // otherwise match the version and return EdgeHTML
+        const version = Utils.getFirstMatch(/edge\/(\d+(\.?_?\d+)+)/i, ua);
+  
+        return {
+          name: ENGINE_MAP.EdgeHTML,
+          version,
+        };
+      },
+    },
+  
+    /* Trident */
+    {
+      test: [/trident/i],
+      describe(ua) {
+        const engine = {
+          name: ENGINE_MAP.Trident,
+        };
+  
+        const version = Utils.getFirstMatch(/trident\/(\d+(\.?_?\d+)+)/i, ua);
+  
+        if (version) {
+          engine.version = version;
+        }
+  
+        return engine;
+      },
+    },
+  
+    /* Presto */
+    {
+      test(parser) {
+        return parser.test(/presto/i);
+      },
+      describe(ua) {
+        const engine = {
+          name: ENGINE_MAP.Presto,
+        };
+  
+        const version = Utils.getFirstMatch(/presto\/(\d+(\.?_?\d+)+)/i, ua);
+  
+        if (version) {
+          engine.version = version;
+        }
+  
+        return engine;
+      },
+    },
+  
+    /* Gecko */
+    {
+      test(parser) {
+        const isGecko = parser.test(/gecko/i);
+        const likeGecko = parser.test(/like gecko/i);
+        return isGecko && !likeGecko;
+      },
+      describe(ua) {
+        const engine = {
+          name: ENGINE_MAP.Gecko,
+        };
+  
+        const version = Utils.getFirstMatch(/gecko\/(\d+(\.?_?\d+)+)/i, ua);
+  
+        if (version) {
+          engine.version = version;
+        }
+  
+        return engine;
+      },
+    },
+  
+    /* Blink */
+    {
+      test: [/(apple)?webkit\/537\.36/i],
+      describe() {
+        return {
+          name: ENGINE_MAP.Blink,
+        };
+      },
+    },
+  
+    /* WebKit */
+    {
+      test: [/(apple)?webkit/i],
+      describe(ua) {
+        const engine = {
+          name: ENGINE_MAP.WebKit,
+        };
+  
+        const version = Utils.getFirstMatch(/webkit\/(\d+(\.?_?\d+)+)/i, ua);
+  
+        if (version) {
+          engine.version = version;
+        }
+  
+        return engine;
+      },
+    },
+  ];
+
+  const platformParsersList = [
+    /* Googlebot */
+    {
+      test: [/googlebot/i],
+      describe() {
+        return {
+          type: 'bot',
+          vendor: 'Google',
+        };
+      },
+    },
+  
+    /* Huawei */
+    {
+      test: [/huawei/i],
+      describe(ua) {
+        const model = Utils.getFirstMatch(/(can-l01)/i, ua) && 'Nova';
+        const platform = {
+          type: PLATFORMS_MAP.mobile,
+          vendor: 'Huawei',
+        };
+        if (model) {
+          platform.model = model;
+        }
+        return platform;
+      },
+    },
+  
+    /* Nexus Tablet */
+    {
+      test: [/nexus\s*(?:7|8|9|10).*/i],
+      describe() {
+        return {
+          type: PLATFORMS_MAP.tablet,
+          vendor: 'Nexus',
+        };
+      },
+    },
+  
+    /* iPad */
+    {
+      test: [/ipad/i],
+      describe() {
+        return {
+          type: PLATFORMS_MAP.tablet,
+          vendor: 'Apple',
+          model: 'iPad',
+        };
+      },
+    },
+  
+    /* Firefox on iPad */
+    {
+      test: [/Macintosh(.*?) FxiOS(.*?)\//],
+      describe() {
+        return {
+          type: PLATFORMS_MAP.tablet,
+          vendor: 'Apple',
+          model: 'iPad',
+        };
+      },
+    },
+  
+    /* Amazon Kindle Fire */
+    {
+      test: [/kftt build/i],
+      describe() {
+        return {
+          type: PLATFORMS_MAP.tablet,
+          vendor: 'Amazon',
+          model: 'Kindle Fire HD 7',
+        };
+      },
+    },
+  
+    /* Another Amazon Tablet with Silk */
+    {
+      test: [/silk/i],
+      describe() {
+        return {
+          type: PLATFORMS_MAP.tablet,
+          vendor: 'Amazon',
+        };
+      },
+    },
+  
+    /* Tablet */
+    {
+      test: [/tablet(?! pc)/i],
+      describe() {
+        return {
+          type: PLATFORMS_MAP.tablet,
+        };
+      },
+    },
+  
+    /* iPod/iPhone */
+    {
+      test(parser) {
+        const iDevice = parser.test(/ipod|iphone/i);
+        const likeIDevice = parser.test(/like (ipod|iphone)/i);
+        return iDevice && !likeIDevice;
+      },
+      describe(ua) {
+        const model = Utils.getFirstMatch(/(ipod|iphone)/i, ua);
+        return {
+          type: PLATFORMS_MAP.mobile,
+          vendor: 'Apple',
+          model,
+        };
+      },
+    },
+  
+    /* Nexus Mobile */
+    {
+      test: [/nexus\s*[0-6].*/i, /galaxy nexus/i],
+      describe() {
+        return {
+          type: PLATFORMS_MAP.mobile,
+          vendor: 'Nexus',
+        };
+      },
+    },
+  
+    /* Mobile */
+    {
+      test: [/[^-]mobi/i],
+      describe() {
+        return {
+          type: PLATFORMS_MAP.mobile,
+        };
+      },
+    },
+  
+    /* BlackBerry */
+    {
+      test(parser) {
+        return parser.getBrowserName(true) === 'blackberry';
+      },
+      describe() {
+        return {
+          type: PLATFORMS_MAP.mobile,
+          vendor: 'BlackBerry',
+        };
+      },
+    },
+  
+    /* Bada */
+    {
+      test(parser) {
+        return parser.getBrowserName(true) === 'bada';
+      },
+      describe() {
+        return {
+          type: PLATFORMS_MAP.mobile,
+        };
+      },
+    },
+  
+    /* Windows Phone */
+    {
+      test(parser) {
+        return parser.getBrowserName() === 'windows phone';
+      },
+      describe() {
+        return {
+          type: PLATFORMS_MAP.mobile,
+          vendor: 'Microsoft',
+        };
+      },
+    },
+  
+    /* Android Tablet */
+    {
+      test(parser) {
+        const osMajorVersion = Number(String(parser.getOSVersion()).split('.')[0]);
+        return parser.getOSName(true) === 'android' && (osMajorVersion >= 3);
+      },
+      describe() {
+        return {
+          type: PLATFORMS_MAP.tablet,
+        };
+      },
+    },
+  
+    /* Android Mobile */
+    {
+      test(parser) {
+        return parser.getOSName(true) === 'android';
+      },
+      describe() {
+        return {
+          type: PLATFORMS_MAP.mobile,
+        };
+      },
+    },
+  
+    /* desktop */
+    {
+      test(parser) {
+        return parser.getOSName(true) === 'macos';
+      },
+      describe() {
+        return {
+          type: PLATFORMS_MAP.desktop,
+          vendor: 'Apple',
+        };
+      },
+    },
+  
+    /* Windows */
+    {
+      test(parser) {
+        return parser.getOSName(true) === 'windows';
+      },
+      describe() {
+        return {
+          type: PLATFORMS_MAP.desktop,
+        };
+      },
+    },
+  
+    /* Linux */
+    {
+      test(parser) {
+        return parser.getOSName(true) === 'linux';
+      },
+      describe() {
+        return {
+          type: PLATFORMS_MAP.desktop,
+        };
+      },
+    },
+  
+    /* PlayStation 4 */
+    {
+      test(parser) {
+        return parser.getOSName(true) === 'playstation 4';
+      },
+      describe() {
+        return {
+          type: PLATFORMS_MAP.tv,
+        };
+      },
+    },
+  
+    /* Roku */
+    {
+      test(parser) {
+        return parser.getOSName(true) === 'roku';
+      },
+      describe() {
+        return {
+          type: PLATFORMS_MAP.tv,
+        };
+      },
+    },
+  ];
+  
+
+  const osParsersList = [
+    /* Roku */
+    {
+      test: [/Roku\/DVP/],
+      describe(ua) {
+        const version = Utils.getFirstMatch(/Roku\/DVP-(\d+\.\d+)/i, ua);
+        return {
+          name: OS_MAP.Roku,
+          version,
+        };
+      },
+    },
+  
+    /* Windows Phone */
+    {
+      test: [/windows phone/i],
+      describe(ua) {
+        const version = Utils.getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i, ua);
+        return {
+          name: OS_MAP.WindowsPhone,
+          version,
+        };
+      },
+    },
+  
+    /* Windows */
+    {
+      test: [/windows /i],
+      describe(ua) {
+        const version = Utils.getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i, ua);
+        const versionName = Utils.getWindowsVersionName(version);
+  
+        return {
+          name: OS_MAP.Windows,
+          version,
+          versionName,
+        };
+      },
+    },
+  
+    /* Firefox on iPad */
+    {
+      test: [/Macintosh(.*?) FxiOS(.*?)\//],
+      describe(ua) {
+        const result = {
+          name: OS_MAP.iOS,
+        };
+        const version = Utils.getSecondMatch(/(Version\/)(\d[\d.]+)/, ua);
+        if (version) {
+          result.version = version;
+        }
+        return result;
+      },
+    },
+  
+    /* macOS */
+    {
+      test: [/macintosh/i],
+      describe(ua) {
+        const version = Utils.getFirstMatch(/mac os x (\d+(\.?_?\d+)+)/i, ua).replace(/[_\s]/g, '.');
+        const versionName = Utils.getMacOSVersionName(version);
+  
+        const os = {
+          name: OS_MAP.MacOS,
+          version,
+        };
+        if (versionName) {
+          os.versionName = versionName;
+        }
+        return os;
+      },
+    },
+  
+    /* iOS */
+    {
+      test: [/(ipod|iphone|ipad)/i],
+      describe(ua) {
+        const version = Utils.getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i, ua).replace(/[_\s]/g, '.');
+  
+        return {
+          name: OS_MAP.iOS,
+          version,
+        };
+      },
+    },
+  
+    /* Android */
+    {
+      test(parser) {
+        const notLikeAndroid = !parser.test(/like android/i);
+        const butAndroid = parser.test(/android/i);
+        return notLikeAndroid && butAndroid;
+      },
+      describe(ua) {
+        const version = Utils.getFirstMatch(/android[\s/-](\d+(\.\d+)*)/i, ua);
+        const versionName = Utils.getAndroidVersionName(version);
+        const os = {
+          name: OS_MAP.Android,
+          version,
+        };
+        if (versionName) {
+          os.versionName = versionName;
+        }
+        return os;
+      },
+    },
+  
+    /* WebOS */
+    {
+      test: [/(web|hpw)[o0]s/i],
+      describe(ua) {
+        const version = Utils.getFirstMatch(/(?:web|hpw)[o0]s\/(\d+(\.\d+)*)/i, ua);
+        const os = {
+          name: OS_MAP.WebOS,
+        };
+  
+        if (version && version.length) {
+          os.version = version;
+        }
+        return os;
+      },
+    },
+  
+    /* BlackBerry */
+    {
+      test: [/blackberry|\bbb\d+/i, /rim\stablet/i],
+      describe(ua) {
+        const version = Utils.getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i, ua)
+          || Utils.getFirstMatch(/blackberry\d+\/(\d+([_\s]\d+)*)/i, ua)
+          || Utils.getFirstMatch(/\bbb(\d+)/i, ua);
+  
+        return {
+          name: OS_MAP.BlackBerry,
+          version,
+        };
+      },
+    },
+  
+    /* Bada */
+    {
+      test: [/bada/i],
+      describe(ua) {
+        const version = Utils.getFirstMatch(/bada\/(\d+(\.\d+)*)/i, ua);
+  
+        return {
+          name: OS_MAP.Bada,
+          version,
+        };
+      },
+    },
+  
+    /* Tizen */
+    {
+      test: [/tizen/i],
+      describe(ua) {
+        const version = Utils.getFirstMatch(/tizen[/\s](\d+(\.\d+)*)/i, ua);
+  
+        return {
+          name: OS_MAP.Tizen,
+          version,
+        };
+      },
+    },
+  
+    /* Linux */
+    {
+      test: [/linux/i],
+      describe() {
+        return {
+          name: OS_MAP.Linux,
+        };
+      },
+    },
+  
+    /* Chrome OS */
+    {
+      test: [/CrOS/],
+      describe() {
+        return {
+          name: OS_MAP.ChromeOS,
+        };
+      },
+    },
+  
+    /* Playstation 4 */
+    {
+      test: [/PlayStation 4/],
+      describe(ua) {
+        const version = Utils.getFirstMatch(/PlayStation 4[/\s](\d+(\.\d+)*)/i, ua);
+        return {
+          name: OS_MAP.PlayStation4,
+          version,
+        };
+      },
+    },
+  ];
+
+  /**
+ * The main class that arranges the whole parsing process.
+ */
+class Parser {
+    /**
+     * Create instance of Parser
+     *
+     * @param {String} UA User-Agent string
+     * @param {Boolean} [skipParsing=false] parser can skip parsing in purpose of performance
+     * improvements if you need to make a more particular parsing
+     * like {@link Parser#parseBrowser} or {@link Parser#parsePlatform}
+     *
+     * @throw {Error} in case of empty UA String
+     *
+     * @constructor
+     */
+    constructor(UA, skipParsing = false) {
+      if (UA === void (0) || UA === null || UA === '') {
+        throw new Error("UserAgent parameter can't be empty");
+      }
+  
+      this._ua = UA;
+  
+      /**
+       * @typedef ParsedResult
+       * @property {Object} browser
+       * @property {String|undefined} [browser.name]
+       * Browser name, like `"Chrome"` or `"Internet Explorer"`
+       * @property {String|undefined} [browser.version] Browser version as a String `"12.01.45334.10"`
+       * @property {Object} os
+       * @property {String|undefined} [os.name] OS name, like `"Windows"` or `"macOS"`
+       * @property {String|undefined} [os.version] OS version, like `"NT 5.1"` or `"10.11.1"`
+       * @property {String|undefined} [os.versionName] OS name, like `"XP"` or `"High Sierra"`
+       * @property {Object} platform
+       * @property {String|undefined} [platform.type]
+       * platform type, can be either `"desktop"`, `"tablet"` or `"mobile"`
+       * @property {String|undefined} [platform.vendor] Vendor of the device,
+       * like `"Apple"` or `"Samsung"`
+       * @property {String|undefined} [platform.model] Device model,
+       * like `"iPhone"` or `"Kindle Fire HD 7"`
+       * @property {Object} engine
+       * @property {String|undefined} [engine.name]
+       * Can be any of this: `WebKit`, `Blink`, `Gecko`, `Trident`, `Presto`, `EdgeHTML`
+       * @property {String|undefined} [engine.version] String version of the engine
+       */
+      this.parsedResult = {};
+  
+      if (skipParsing !== true) {
+        this.parse();
+      }
+    }
+  
+    /**
+     * Get UserAgent string of current Parser instance
+     * @return {String} User-Agent String of the current <Parser> object
+     *
+     * @public
+     */
+    getUA() {
+      return this._ua;
+    }
+  
+    /**
+     * Test a UA string for a regexp
+     * @param {RegExp} regex
+     * @return {Boolean}
+     */
+    test(regex) {
+      return regex.test(this._ua);
+    }
+  
+    /**
+     * Get parsed browser object
+     * @return {Object}
+     */
+    parseBrowser() {
+      this.parsedResult.browser = {};
+  
+      const browserDescriptor = Utils.find(browserParsersList, (_browser) => {
+        if (typeof _browser.test === 'function') {
+          return _browser.test(this);
+        }
+  
+        if (_browser.test instanceof Array) {
+          return _browser.test.some(condition => this.test(condition));
+        }
+  
+        throw new Error("Browser's test function is not valid");
+      });
+  
+      if (browserDescriptor) {
+        this.parsedResult.browser = browserDescriptor.describe(this.getUA());
+      }
+  
+      return this.parsedResult.browser;
+    }
+  
+    /**
+     * Get parsed browser object
+     * @return {Object}
+     *
+     * @public
+     */
+    getBrowser() {
+      if (this.parsedResult.browser) {
+        return this.parsedResult.browser;
+      }
+  
+      return this.parseBrowser();
+    }
+  
+    /**
+     * Get browser's name
+     * @return {String} Browser's name or an empty string
+     *
+     * @public
+     */
+    getBrowserName(toLowerCase) {
+      if (toLowerCase) {
+        return String(this.getBrowser().name).toLowerCase() || '';
+      }
+      return this.getBrowser().name || '';
+    }
+  
+  
+    /**
+     * Get browser's version
+     * @return {String} version of browser
+     *
+     * @public
+     */
+    getBrowserVersion() {
+      return this.getBrowser().version;
+    }
+  
+    /**
+     * Get OS
+     * @return {Object}
+     *
+     * @example
+     * this.getOS();
+     * {
+     *   name: 'macOS',
+     *   version: '10.11.12'
+     * }
+     */
+    getOS() {
+      if (this.parsedResult.os) {
+        return this.parsedResult.os;
+      }
+  
+      return this.parseOS();
+    }
+  
+    /**
+     * Parse OS and save it to this.parsedResult.os
+     * @return {*|{}}
+     */
+    parseOS() {
+      this.parsedResult.os = {};
+  
+      const os = Utils.find(osParsersList, (_os) => {
+        if (typeof _os.test === 'function') {
+          return _os.test(this);
+        }
+  
+        if (_os.test instanceof Array) {
+          return _os.test.some(condition => this.test(condition));
+        }
+  
+        throw new Error("Browser's test function is not valid");
+      });
+  
+      if (os) {
+        this.parsedResult.os = os.describe(this.getUA());
+      }
+  
+      return this.parsedResult.os;
+    }
+  
+    /**
+     * Get OS name
+     * @param {Boolean} [toLowerCase] return lower-cased value
+     * @return {String} name of the OS — macOS, Windows, Linux, etc.
+     */
+    getOSName(toLowerCase) {
+      const { name } = this.getOS();
+  
+      if (toLowerCase) {
+        return String(name).toLowerCase() || '';
+      }
+  
+      return name || '';
+    }
+  
+    /**
+     * Get OS version
+     * @return {String} full version with dots ('10.11.12', '5.6', etc)
+     */
+    getOSVersion() {
+      return this.getOS().version;
+    }
+  
+    /**
+     * Get parsed platform
+     * @return {{}}
+     */
+    getPlatform() {
+      if (this.parsedResult.platform) {
+        return this.parsedResult.platform;
+      }
+  
+      return this.parsePlatform();
+    }
+  
+    /**
+     * Get platform name
+     * @param {Boolean} [toLowerCase=false]
+     * @return {*}
+     */
+    getPlatformType(toLowerCase = false) {
+      const { type } = this.getPlatform();
+  
+      if (toLowerCase) {
+        return String(type).toLowerCase() || '';
+      }
+  
+      return type || '';
+    }
+  
+    /**
+     * Get parsed platform
+     * @return {{}}
+     */
+    parsePlatform() {
+      this.parsedResult.platform = {};
+  
+      const platform = Utils.find(platformParsersList, (_platform) => {
+        if (typeof _platform.test === 'function') {
+          return _platform.test(this);
+        }
+  
+        if (_platform.test instanceof Array) {
+          return _platform.test.some(condition => this.test(condition));
+        }
+  
+        throw new Error("Browser's test function is not valid");
+      });
+  
+      if (platform) {
+        this.parsedResult.platform = platform.describe(this.getUA());
+      }
+  
+      return this.parsedResult.platform;
+    }
+  
+    /**
+     * Get parsed engine
+     * @return {{}}
+     */
+    getEngine() {
+      if (this.parsedResult.engine) {
+        return this.parsedResult.engine;
+      }
+  
+      return this.parseEngine();
+    }
+  
+    /**
+     * Get engines's name
+     * @return {String} Engines's name or an empty string
+     *
+     * @public
+     */
+    getEngineName(toLowerCase) {
+      if (toLowerCase) {
+        return String(this.getEngine().name).toLowerCase() || '';
+      }
+      return this.getEngine().name || '';
+    }
+  
+    /**
+     * Get parsed platform
+     * @return {{}}
+     */
+    parseEngine() {
+      this.parsedResult.engine = {};
+  
+      const engine = Utils.find(enginesParsersList, (_engine) => {
+        if (typeof _engine.test === 'function') {
+          return _engine.test(this);
+        }
+  
+        if (_engine.test instanceof Array) {
+          return _engine.test.some(condition => this.test(condition));
+        }
+  
+        throw new Error("Browser's test function is not valid");
+      });
+  
+      if (engine) {
+        this.parsedResult.engine = engine.describe(this.getUA());
+      }
+  
+      return this.parsedResult.engine;
+    }
+  
+    /**
+     * Parse full information about the browser
+     * @returns {Parser}
+     */
+    parse() {
+      this.parseBrowser();
+      this.parseOS();
+      this.parsePlatform();
+      this.parseEngine();
+  
+      return this;
+    }
+  
+    /**
+     * Get parsed result
+     * @return {ParsedResult}
+     */
+    getResult() {
+      return Utils.assign({}, this.parsedResult);
+    }
+  
+    /**
+     * Check if parsed browser matches certain conditions
+     *
+     * @param {Object} checkTree It's one or two layered object,
+     * which can include a platform or an OS on the first layer
+     * and should have browsers specs on the bottom-laying layer
+     *
+     * @returns {Boolean|undefined} Whether the browser satisfies the set conditions or not.
+     * Returns `undefined` when the browser is no described in the checkTree object.
+     *
+     * @example
+     * const browser = Bowser.getParser(window.navigator.userAgent);
+     * if (browser.satisfies({chrome: '>118.01.1322' }))
+     * // or with os
+     * if (browser.satisfies({windows: { chrome: '>118.01.1322' } }))
+     * // or with platforms
+     * if (browser.satisfies({desktop: { chrome: '>118.01.1322' } }))
+     */
+    satisfies(checkTree) {
+      const platformsAndOSes = {};
+      let platformsAndOSCounter = 0;
+      const browsers = {};
+      let browsersCounter = 0;
+  
+      const allDefinitions = Object.keys(checkTree);
+  
+      allDefinitions.forEach((key) => {
+        const currentDefinition = checkTree[key];
+        if (typeof currentDefinition === 'string') {
+          browsers[key] = currentDefinition;
+          browsersCounter += 1;
+        } else if (typeof currentDefinition === 'object') {
+          platformsAndOSes[key] = currentDefinition;
+          platformsAndOSCounter += 1;
+        }
+      });
+  
+      if (platformsAndOSCounter > 0) {
+        const platformsAndOSNames = Object.keys(platformsAndOSes);
+        const OSMatchingDefinition = Utils.find(platformsAndOSNames, name => (this.isOS(name)));
+  
+        if (OSMatchingDefinition) {
+          const osResult = this.satisfies(platformsAndOSes[OSMatchingDefinition]);
+  
+          if (osResult !== void 0) {
+            return osResult;
+          }
+        }
+  
+        const platformMatchingDefinition = Utils.find(
+          platformsAndOSNames,
+          name => (this.isPlatform(name)),
+        );
+        if (platformMatchingDefinition) {
+          const platformResult = this.satisfies(platformsAndOSes[platformMatchingDefinition]);
+  
+          if (platformResult !== void 0) {
+            return platformResult;
+          }
+        }
+      }
+  
+      if (browsersCounter > 0) {
+        const browserNames = Object.keys(browsers);
+        const matchingDefinition = Utils.find(browserNames, name => (this.isBrowser(name, true)));
+  
+        if (matchingDefinition !== void 0) {
+          return this.compareVersion(browsers[matchingDefinition]);
+        }
+      }
+  
+      return undefined;
+    }
+  
+    /**
+     * Check if the browser name equals the passed string
+     * @param browserName The string to compare with the browser name
+     * @param [includingAlias=false] The flag showing whether alias will be included into comparison
+     * @returns {boolean}
+     */
+    isBrowser(browserName, includingAlias = false) {
+      const defaultBrowserName = this.getBrowserName().toLowerCase();
+      let browserNameLower = browserName.toLowerCase();
+      const alias = Utils.getBrowserTypeByAlias(browserNameLower);
+  
+      if (includingAlias && alias) {
+        browserNameLower = alias.toLowerCase();
+      }
+      return browserNameLower === defaultBrowserName;
+    }
+  
+    compareVersion(version) {
+      let expectedResults = [0];
+      let comparableVersion = version;
+      let isLoose = false;
+  
+      const currentBrowserVersion = this.getBrowserVersion();
+  
+      if (typeof currentBrowserVersion !== 'string') {
+        return void 0;
+      }
+  
+      if (version[0] === '>' || version[0] === '<') {
+        comparableVersion = version.substr(1);
+        if (version[1] === '=') {
+          isLoose = true;
+          comparableVersion = version.substr(2);
+        } else {
+          expectedResults = [];
+        }
+        if (version[0] === '>') {
+          expectedResults.push(1);
+        } else {
+          expectedResults.push(-1);
+        }
+      } else if (version[0] === '=') {
+        comparableVersion = version.substr(1);
+      } else if (version[0] === '~') {
+        isLoose = true;
+        comparableVersion = version.substr(1);
+      }
+  
+      return expectedResults.indexOf(
+        Utils.compareVersions(currentBrowserVersion, comparableVersion, isLoose),
+      ) > -1;
+    }
+  
+    isOS(osName) {
+      return this.getOSName(true) === String(osName).toLowerCase();
+    }
+  
+    isPlatform(platformType) {
+      return this.getPlatformType(true) === String(platformType).toLowerCase();
+    }
+  
+    isEngine(engineName) {
+      return this.getEngineName(true) === String(engineName).toLowerCase();
+    }
+  
+    /**
+     * Is anything? Check if the browser is called "anything",
+     * the OS called "anything" or the platform called "anything"
+     * @param {String} anything
+     * @param [includingAlias=false] The flag showing whether alias will be included into comparison
+     * @returns {Boolean}
+     */
+    is(anything, includingAlias = false) {
+      return this.isBrowser(anything, includingAlias) || this.isOS(anything)
+        || this.isPlatform(anything);
+    }
+  
+    /**
+     * Check if any of the given values satisfies this.is(anything)
+     * @param {String[]} anythings
+     * @returns {Boolean}
+     */
+    some(anythings = []) {
+      return anythings.some(anything => this.is(anything));
+    }
+  }
+/**
+ * Bowser class.
+ * Keep it simple as much as it can be.
+ * It's supposed to work with collections of {@link Parser} instances
+ * rather then solve one-instance problems.
+ * All the one-instance stuff is located in Parser class.
+ *
+ * @class
+ * @classdesc Bowser is a static object, that provides an API to the Parsers
+ * @hideconstructor
+ */
+ export class Bowser {
+    /**
+     * Creates a {@link Parser} instance
+     *
+     * @param {String} UA UserAgent string
+     * @param {Boolean} [skipParsing=false] Will make the Parser postpone parsing until you ask it
+     * explicitly. Same as `skipParsing` for {@link Parser}.
+     * @returns {Parser}
+     * @throws {Error} when UA is not a String
+     *
+     * @example
+     * const parser = Bowser.getParser(window.navigator.userAgent);
+     * const result = parser.getResult();
+     */
+    static getParser(UA, skipParsing = false) {
+      if (typeof UA !== 'string') {
+        throw new Error('UserAgent should be a string');
+      }
+      return new Parser(UA, skipParsing);
+    }
+  
+    /**
+     * Creates a {@link Parser} instance and runs {@link Parser.getResult} immediately
+     *
+     * @param UA
+     * @return {ParsedResult}
+     *
+     * @example
+     * const result = Bowser.parse(window.navigator.userAgent);
+     */
+    static parse(UA) {
+      return (new Parser(UA)).getResult();
+    }
+  
+    static get BROWSER_MAP() {
+      return BROWSER_MAP;
+    }
+  
+    static get ENGINE_MAP() {
+      return ENGINE_MAP;
+    }
+  
+    static get OS_MAP() {
+      return OS_MAP;
+    }
+  
+    static get PLATFORMS_MAP() {
+      return PLATFORMS_MAP;
+    }
+  };
+  
+  
\ No newline at end of file
diff --git a/ng-sdk-js/js/browser.js b/ng-sdk-js/js/browser.js
index ed74934..c2a7500 100644
--- a/ng-sdk-js/js/browser.js
+++ b/ng-sdk-js/js/browser.js
@@ -1,3 +1,14 @@
-export function random(max) {
-    return Math.round(Math.random() * max)
-  }
\ No newline at end of file
+import {version} from '../../../package.json';
+
+export function client_details() {
+    return window.navigator.userAgent;
+}
+
+export function client_details2(obj) {
+    obj.browser.appVersion = navigator?.appVersion;
+    obj.browser.arch = navigator?.platform;
+    obj.browser.vendor = navigator?.vendor;
+    obj.browser.ua = window.navigator.userAgent;
+    obj.engine.sdk = version;
+    return JSON.stringify(obj);
+}
diff --git a/ng-sdk-js/js/node.js b/ng-sdk-js/js/node.js
index ca818c9..88f9360 100644
--- a/ng-sdk-js/js/node.js
+++ b/ng-sdk-js/js/node.js
@@ -1,4 +1,122 @@
 
-module.exports.random = function (max) {
-  return 0
+
+
+const macNameMap = new Map([
+	[23, ['Sonoma', '14']],
+	[22, ['Ventura', '13']],
+	[21, ['Monterey', '12']],
+	[20, ['Big Sur', '11']],
+	[19, ['Catalina', '10.15']],
+	[18, ['Mojave', '10.14']],
+	[17, ['High Sierra', '10.13']],
+	[16, ['Sierra', '10.12']],
+	[15, ['El Capitan', '10.11']],
+	[14, ['Yosemite', '10.10']],
+	[13, ['Mavericks', '10.9']],
+	[12, ['Mountain Lion', '10.8']],
+	[11, ['Lion', '10.7']],
+	[10, ['Snow Leopard', '10.6']],
+	[9, ['Leopard', '10.5']],
+	[8, ['Tiger', '10.4']],
+	[7, ['Panther', '10.3']],
+	[6, ['Jaguar', '10.2']],
+	[5, ['Puma', '10.1']],
+]);
+
+function macosRelease(release) {
+  let split = (release).split('.');
+	rel = Number(split[0]);
+	let [name, version] = macNameMap.get(rel) || ['Unknown', release];
+  if (name!='Unknown') {
+    if (split.length>1) version += '.'+split[1];
+    //if (split.length>2 && split[2]) version += '.'+split[2];
+  }
+	return {
+    name: "macOS",
+		versionName: name,
+		version,
+    release
+	};
+}
+
+const winNames = new Map([
+	['10.0.2', '11'], // It's unclear whether future Windows 11 versions will use this version scheme: https://github.com/sindresorhus/windows-release/pull/26/files#r744945281
+  ['10.0.22', 'Server 2022'],
+	['10.0', '10 or Server 2016/2019'],
+	['6.3', '8.1 or Server 2012 R2'],
+	['6.2', '8 or Server 2012'],
+	['6.1', '7 or Server 2008 R2'],
+	['6.0', 'Vista or Server 2008'],
+	['5.2', 'Server 2003'],
+	['5.1', 'XP'],
+	['5.0', '2000'],
+	['4.90', 'ME'],
+	['4.10', '98'],
+	['4.03', '95'],
+	['4.00', '95'],
+  ['3.00', 'NT'],
+]);
+
+function windowsRelease(release) {
+	const version = /(\d+\.\d+)(?:\.(\d+))?/.exec(release);
+
+	let ver = version[1] || '';
+	const build = version[2] || '';
+
+  if (ver.startsWith('3.')) {
+    ver = '3.00';
+  }
+  if (ver === '10.0' && build.startsWith('20348')) {
+    // Windows Server 2022
+    ver = '10.0.22';
+  } else if (ver === '10.0' && build.startsWith('2')) {
+    // Windows 11
+		ver = '10.0.2';
+	}
+
+	return {
+    name: "Windows",
+    versionName: winNames.get(ver),
+    version: build,
+    release
+  };
+}
+
+function osName(platform, release) {
+  if (platform === 'darwin') {
+    return release? macosRelease(release) : {name: "macOS"};
+  }
+
+  if (platform === 'linux') {
+    id = release ? release.replace(/^(\d+\.\d+).*/, '$1') : '';
+    return {name:'Linux', version: id || release, release};
+  }
+
+  if (platform === 'win32') {
+    return release ? windowsRelease(release) : {name: "Windows"};
+  }
+
+  return {name:platform, version:release};
+}
+module.exports.version = function () {
+  return require('../../../package.json').version;
+}
+
+module.exports.client_details = function () {
+  const process = require('process');
+  const osnode = require('os');
+  let os = osName(osnode.platform(),osnode.release());
+  if (osnode.version) os.uname = osnode.version();
+  os.type = osnode.type();
+
+  return JSON.stringify({
+    platform: { type: "server", arch: osnode.machine? osnode.machine() : process.arch },
+    os,
+    engine: {
+      name: "nodejs",
+      version: process.version,
+      arch : process.arch,
+      versions: process.versions
+    }
+  });
 };
\ No newline at end of file
diff --git a/ng-sdk-js/src/lib.rs b/ng-sdk-js/src/lib.rs
index 2755606..ee272f5 100644
--- a/ng-sdk-js/src/lib.rs
+++ b/ng-sdk-js/src/lib.rs
@@ -21,12 +21,13 @@ use ng_wallet::*;
 use p2p_client_ws::remote_ws_wasm::ConnectionWebSocket;
 use p2p_net::broker::*;
 use p2p_net::connection::{ClientConfig, StartConfig};
-use p2p_net::types::{DirectPeerId, IP};
-use p2p_net::utils::{spawn_and_log_error, Receiver, ResultSend, Sender};
+use p2p_net::types::{BootstrapContentV0, ClientInfoV0, ClientType, DirectPeerId, IP};
+use p2p_net::utils::{retrieve_local_bootstrap, spawn_and_log_error, Receiver, ResultSend, Sender};
 use p2p_net::WS_PORT;
 use p2p_repo::log::*;
 use p2p_repo::types::*;
 use p2p_repo::utils::generate_keypair;
+use serde::{Deserialize, Serialize};
 use serde_json::json;
 use std::net::IpAddr;
 use std::str::FromStr;
@@ -35,6 +36,17 @@ use wasm_bindgen::prelude::*;
 #[cfg(target_arch = "wasm32")]
 use wasm_bindgen_futures::{future_to_promise, JsFuture};
 
+#[cfg(target_arch = "wasm32")]
+#[wasm_bindgen]
+pub async fn get_local_bootstrap(location: String, invite: JsValue) -> JsValue {
+    let res = retrieve_local_bootstrap(location, invite.as_string()).await;
+    if res.is_some() {
+        serde_wasm_bindgen::to_value(&res.unwrap()).unwrap()
+    } else {
+        JsValue::FALSE
+    }
+}
+
 #[cfg(target_arch = "wasm32")]
 #[wasm_bindgen]
 pub fn wallet_gen_shuffle_for_pazzle_opening(pazzle_length: u8) -> JsValue {
@@ -88,14 +100,8 @@ pub fn test_create_wallet() -> JsValue {
         "   know     yourself  ".to_string(),
         pin,
         9,
-        None,
         false,
-        PubKey::Ed25519PubKey([
-            119, 251, 253, 29, 135, 199, 254, 50, 134, 67, 1, 208, 117, 196, 167, 107, 2, 113, 98,
-            243, 49, 90, 7, 0, 157, 58, 14, 187, 14, 3, 116, 86,
-        ]),
-        0,
-        ClientV0::dummy(),
+        false,
     );
     serde_wasm_bindgen::to_value(&r).unwrap()
 }
@@ -103,19 +109,77 @@ pub fn test_create_wallet() -> JsValue {
 #[cfg(wasmpack_target = "nodejs")]
 #[wasm_bindgen(module = "/js/node.js")]
 extern "C" {
-    fn random(max: usize) -> usize;
+    fn client_details() -> String;
+}
+
+#[cfg(wasmpack_target = "nodejs")]
+#[wasm_bindgen(module = "/js/node.js")]
+extern "C" {
+    fn version() -> String;
+}
+
+#[cfg(wasmpack_target = "nodejs")]
+#[wasm_bindgen]
+pub fn client_info() -> JsValue {
+    let res = ClientInfoV0 {
+        client_type: ClientType::NodeService,
+        details: client_details(),
+        version: version(),
+        timestamp_install: 0,
+        timestamp_updated: 0,
+    };
+    serde_wasm_bindgen::to_value(&res).unwrap()
 }
 
 #[cfg(not(wasmpack_target = "nodejs"))]
 #[wasm_bindgen(module = "/js/browser.js")]
 extern "C" {
-    fn random(max: usize) -> usize;
+    fn client_details() -> String;
+}
+
+#[cfg(not(wasmpack_target = "nodejs"))]
+#[wasm_bindgen(module = "/js/bowser.js")]
+extern "C" {
+    type Bowser;
+    #[wasm_bindgen(static_method_of = Bowser)]
+    fn parse(UA: String) -> JsValue;
+}
+
+#[cfg(not(wasmpack_target = "nodejs"))]
+#[wasm_bindgen(module = "/js/browser.js")]
+extern "C" {
+    fn client_details2(val: JsValue) -> String;
+}
+
+#[cfg(all(not(wasmpack_target = "nodejs"), target_arch = "wasm32"))]
+#[wasm_bindgen]
+pub fn client_info() -> JsValue {
+    let ua = client_details();
+
+    let bowser = Bowser::parse(ua);
+    //log_info!("{:?}", bowser);
+
+    let details_string = client_details2(bowser);
+
+    let res = ClientInfoV0 {
+        client_type: ClientType::Web,
+        details: details_string,
+        version: "".to_string(),
+        timestamp_install: 0,
+        timestamp_updated: 0,
+    };
+    serde_wasm_bindgen::to_value(&res).unwrap()
 }
 
 #[cfg(target_arch = "wasm32")]
 #[wasm_bindgen]
 pub async fn test() {
     log_info!("test is {}", BROKER.read().await.test());
+    //let client_info = client_info();
+    //log_info!("{:?}", client_info);
+
+    //let b = Bowser::parse(ua);
+    //log_info!("{:?}", b);
 }
 
 #[cfg(target_arch = "wasm32")]
@@ -220,11 +284,6 @@ pub async fn probe() {
 #[cfg(target_arch = "wasm32")]
 #[wasm_bindgen]
 pub async fn start() {
-    log_info!("random {}", random(10));
-
-    // let mut random_buf = [0u8; 32];
-    // getrandom::getrandom(&mut random_buf).unwrap();
-
     async fn inner_task() -> ResultSend<()> {
         let server_key: PubKey = "X0nh-gOTGKSx0yL0LYJviOWRNacyqIzjQW_LKdK6opU".try_into()?;
         log_debug!("server_key:{}", server_key);
diff --git a/ng-wallet/src/lib.rs b/ng-wallet/src/lib.rs
index 54d35b4..b964e27 100644
--- a/ng-wallet/src/lib.rs
+++ b/ng-wallet/src/lib.rs
@@ -356,11 +356,6 @@ pub async fn create_wallet_v0(
         return Err(NgWalletError::InvalidPazzleLength);
     }
 
-    // cannot submit wallet if we don't submit also the bootstrap
-    if params.send_bootstrap.is_none() && params.send_wallet {
-        return Err(NgWalletError::SubmissionError);
-    }
-
     // check validity of PIN
 
     // shouldn't start with 0
@@ -462,16 +457,30 @@ pub async fn create_wallet_v0(
     //.ok_or(NgWalletError::InternalError)?
     //.clone(),
 
+    let user = site.site_key.to_pub();
+
+    // Creating a new client
+    let client = ClientV0::new(user);
+
     let create_op = WalletOpCreateV0 {
         wallet_privkey: wallet_privkey.clone(),
         pazzle: pazzle.clone(),
         mnemonic,
         pin: params.pin,
         personal_site: site,
-        brokers: vec![], //TODO add the broker here
-        client: params.client.clone(),
+        save_to_ng_one: if params.send_wallet {
+            SaveToNGOne::Wallet
+        } else if params.send_bootstrap {
+            SaveToNGOne::Bootstrap
+        } else {
+            SaveToNGOne::No
+        },
+        client: client.clone(),
     };
 
+    //Creating a new peerId for this Client and User
+    let peer = generate_keypair();
+
     let wallet_log = WalletLog::new_v0(create_op);
 
     let mut master_key = [0u8; 32];
@@ -509,14 +518,7 @@ pub async fn create_wallet_v0(
 
     let timestamp = now_timestamp();
 
-    let encrypted = enc_wallet_log(
-        &wallet_log,
-        &master_key,
-        params.peer_id,
-        params.nonce,
-        timestamp,
-        wallet_id,
-    )?;
+    let encrypted = enc_wallet_log(&wallet_log, &master_key, peer.1, 0, timestamp, wallet_id)?;
     master_key.zeroize();
 
     let wallet_content = WalletContentV0 {
@@ -529,8 +531,8 @@ pub async fn create_wallet_v0(
         enc_master_key_mnemonic,
         master_nonce: 0,
         timestamp,
-        peer_id: params.peer_id,
-        nonce: params.nonce,
+        peer_id: peer.1,
+        nonce: 0,
         encrypted,
     };
 
@@ -557,9 +559,6 @@ pub async fn create_wallet_v0(
     //     sig,
     // });
 
-    // TODO send bootstrap (if)
-    // TODO send wallet (if)
-
     log_info!(
         "creating of wallet took: {} ms",
         creating_pazzle.elapsed().as_millis()
@@ -575,13 +574,18 @@ pub async fn create_wallet_v0(
         pazzle,
         mnemonic: mnemonic.clone(),
         wallet_name: base64_url::encode(&wallet_id.slice()),
+        peer_id: peer.1,
+        peer_key: peer.0,
+        nonce: 0,
+        client,
+        user,
     })
 }
 
 #[cfg(test)]
 mod test {
     use super::*;
-    use p2p_repo::utils::generate_keypair;
+    use p2p_net::types::BootstrapContentV0;
     use std::fs::File;
     use std::io::BufReader;
     use std::io::Read;
@@ -626,14 +630,8 @@ mod test {
             "   know     yourself  ".to_string(),
             pin,
             9,
-            None,
             false,
-            PubKey::Ed25519PubKey([
-                119, 251, 253, 29, 135, 199, 254, 50, 134, 67, 1, 208, 117, 196, 167, 107, 2, 113,
-                98, 243, 49, 90, 7, 0, 157, 58, 14, 187, 14, 3, 116, 86,
-            ]),
-            0,
-            ClientV0::dummy(),
+            false,
         ))
         .await
         .expect("create_wallet_v0");
diff --git a/ng-wallet/src/types.rs b/ng-wallet/src/types.rs
index 49a689f..bb90743 100644
--- a/ng-wallet/src/types.rs
+++ b/ng-wallet/src/types.rs
@@ -81,6 +81,22 @@ impl ClientV0 {
             auto_open: vec![],
         }
     }
+
+    pub fn new(user: PubKey) -> Self {
+        ClientV0 {
+            priv_key: PrivKey::random_ed(),
+            storage_master_key: SymKey::random(),
+            auto_open: vec![Identity::IndividualSite(user)],
+        }
+    }
+}
+
+/// Save to nextgraph.one
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum SaveToNGOne {
+    No,
+    Bootstrap,
+    Wallet,
 }
 
 /// EncryptedWallet block Version 0
@@ -95,6 +111,9 @@ pub struct EncryptedWalletV0 {
 
     pub pin: [u8; 4],
 
+    #[zeroize(skip)]
+    pub save_to_ng_one: SaveToNGOne,
+
     #[zeroize(skip)]
     pub personal_site: PubKey,
 
@@ -244,6 +263,11 @@ impl WalletLogV0 {
                         }
                     }
                     WalletOperationV0::RemoveBrokerServerV0(_) => {}
+                    WalletOperationV0::SetSaveToNGOneV0(o) => {
+                        if self.is_last_occurrence(op.0, &op.1) != 0 {
+                            wallet.save_to_ng_one = o.clone();
+                        }
+                    }
                     WalletOperationV0::SetBrokerCoreV0(o) => {
                         if self.is_last_occurrence(op.0, &op.1) != 0 {
                             wallet.add_brokers(vec![BrokerInfoV0::CoreV0(o.clone())]);
@@ -428,6 +452,7 @@ pub enum WalletOperationV0 {
     RemoveSiteV0(PrivKey),
     AddBrokerServerV0(BrokerServerV0),
     RemoveBrokerServerV0(BrokerServerV0),
+    SetSaveToNGOneV0(SaveToNGOne),
     SetBrokerCoreV0(BrokerCoreV0),
     SetClientV0(ClientV0),
     AddOverlayCoreOverrideV0((OverlayId, Vec<PubKey>)),
@@ -464,6 +489,7 @@ impl WalletOperationV0 {
                 t.hash(&mut s);
                 (s.finish(), "RemoveBrokerServerV0")
             }
+            Self::SetSaveToNGOneV0(t) => (0, "SetSaveToNGOneV0"),
             Self::SetBrokerCoreV0(t) => {
                 t.peer_id.hash(&mut s);
                 (s.finish(), "SetBrokerCoreV0")
@@ -537,12 +563,14 @@ pub struct WalletOpCreateV0 {
     pub pin: [u8; 4],
 
     #[zeroize(skip)]
-    pub personal_site: SiteV0,
+    pub save_to_ng_one: SaveToNGOne,
 
-    // list of brokers and their connection details
     #[zeroize(skip)]
-    pub brokers: Vec<BrokerInfoV0>,
+    pub personal_site: SiteV0,
 
+    // list of brokers and their connection details
+    //#[zeroize(skip)]
+    //pub brokers: Vec<BrokerInfoV0>,
     #[zeroize(skip)]
     pub client: ClientV0,
 }
@@ -554,6 +582,7 @@ impl From<&WalletOpCreateV0> for EncryptedWalletV0 {
             pazzle: op.pazzle.clone(),
             mnemonic: op.mnemonic.clone(),
             pin: op.pin.clone(),
+            save_to_ng_one: op.save_to_ng_one.clone(),
             personal_site: op.personal_site.site_key.to_pub(),
             sites: HashMap::new(),
             brokers: HashMap::new(),
@@ -562,7 +591,7 @@ impl From<&WalletOpCreateV0> for EncryptedWalletV0 {
             third_parties: HashMap::new(),
         };
         wallet.add_site(op.personal_site.clone());
-        wallet.add_brokers(op.brokers.clone());
+        //wallet.add_brokers(op.brokers.clone());
         wallet.add_client(op.client.clone());
         wallet
     }
@@ -620,6 +649,8 @@ impl BrokerInfoV0 {
 /// ReducedEncryptedWallet block Version 0
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct ReducedEncryptedWalletV0 {
+    pub save_to_ng_one: SaveToNGOne,
+
     // main Site (Personal)
     pub personal_site: ReducedSiteV0,
 
@@ -721,19 +752,13 @@ pub struct CreateWalletV0 {
     pub pin: [u8; 4],
     pub pazzle_length: u8,
     #[zeroize(skip)]
-    pub send_bootstrap: Option<Bootstrap>,
+    pub send_bootstrap: bool,
     #[zeroize(skip)]
     pub send_wallet: bool,
     #[zeroize(skip)]
     pub result_with_wallet_file: bool,
     #[zeroize(skip)]
     pub local_save: bool,
-    #[zeroize(skip)]
-    pub peer_id: PubKey,
-    #[zeroize(skip)]
-    pub nonce: u64,
-    #[zeroize(skip)]
-    pub client: ClientV0,
 }
 
 impl CreateWalletV0 {
@@ -742,11 +767,8 @@ impl CreateWalletV0 {
         security_txt: String,
         pin: [u8; 4],
         pazzle_length: u8,
-        send_bootstrap: Option<Bootstrap>,
+        send_bootstrap: bool,
         send_wallet: bool,
-        peer_id: PubKey,
-        nonce: u64,
-        client: ClientV0,
     ) -> Self {
         CreateWalletV0 {
             result_with_wallet_file: false,
@@ -757,9 +779,6 @@ impl CreateWalletV0 {
             pazzle_length,
             send_bootstrap,
             send_wallet,
-            peer_id,
-            nonce,
-            client,
         }
     }
 }
@@ -775,6 +794,15 @@ pub struct CreateWalletResultV0 {
     pub mnemonic: [u16; 12],
     #[zeroize(skip)]
     pub wallet_name: String,
+    #[zeroize(skip)]
+    pub peer_id: PubKey,
+    pub peer_key: PrivKey,
+    #[zeroize(skip)]
+    pub nonce: u64,
+    #[zeroize(skip)]
+    pub client: ClientV0,
+    #[zeroize(skip)]
+    pub user: PubKey,
 }
 
 #[derive(Debug, Eq, PartialEq, Clone)]
diff --git a/ngaccount/Cargo.toml b/ngaccount/Cargo.toml
new file mode 100644
index 0000000..396d10e
--- /dev/null
+++ b/ngaccount/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "ngaccount"
+version = "0.1.0"
+edition = "2021"
+license = "MIT/Apache-2.0"
+authors = ["Niko PLP <niko@nextgraph.org>"]
+description = "account manager for NextGraph broker service provider"
+repository = "https://git.nextgraph.org/NextGraph/nextgraph-rs"
+
+[dependencies]
+tokio = { version = "1.27", features = ["full"] }
+warp = "0.3"
+warp-embed = "0.4"
+rust-embed = "6"
+log = "0.4"
+env_logger = "0.10"
+stores-lmdb = { path = "../stores-lmdb" }
+p2p-repo = { path = "../p2p-repo", features = ["server_log_output"] }
+p2p-net = { path = "../p2p-net" }
+ng-wallet = { path = "../ng-wallet" }
+serde = { version = "1.0.142", features = ["derive"] }
+serde_bare = "0.5.0"
+serde_bytes = "0.11.7"
+serde-big-array = "0.5.1"
+base64-url = "2.0.0"
+slice_as_array = "1.1.0"
+serde_json = "1.0.96"
+bytes = "1.0"
\ No newline at end of file
diff --git a/ngaccount/README.md b/ngaccount/README.md
new file mode 100644
index 0000000..9114894
--- /dev/null
+++ b/ngaccount/README.md
@@ -0,0 +1,37 @@
+# broker service provider account manager (ngaccount)
+
+This server is used internally by NextGraph to handle the creation of accounts at our broker service provider servers. You probably don't need this server in your infrastructure, even if you decide to self-host a broker under your own domain name.
+
+## Install
+
+```
+cd web
+npm install -g pnpm
+pnpm --ignore-workspace install
+```
+
+## Dev
+
+```
+cd web
+pnpm run dev
+// in another terminal
+cd ../
+cargo watch -c -w src -x run
+// then open http://localhost:5173/
+```
+
+## Build
+
+```
+cd web
+pnpm run build
+cd ..
+cargo build --release
+```
+
+## run
+
+```
+../target/release/ngaccount
+```
diff --git a/ngaccount/src/main.rs b/ngaccount/src/main.rs
new file mode 100644
index 0000000..fd11021
--- /dev/null
+++ b/ngaccount/src/main.rs
@@ -0,0 +1,82 @@
+// Copyright (c) 2022-2023 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.
+#[macro_use]
+extern crate slice_as_array;
+
+mod types;
+
+use p2p_repo::store::StorageError;
+use warp::reply::Response;
+use warp::{Filter, Reply};
+
+use rust_embed::RustEmbed;
+use serde_bare::{from_slice, to_vec};
+use serde_json::json;
+use std::sync::Arc;
+use std::{env, fs};
+
+use crate::types::*;
+use ng_wallet::types::*;
+use p2p_net::types::{APP_NG_ONE_URL, NG_ONE_URL};
+use p2p_repo::log::*;
+use p2p_repo::types::*;
+use p2p_repo::utils::{generate_keypair, sign, verify};
+
+#[derive(RustEmbed)]
+#[folder = "web/dist"]
+struct Static;
+
+struct Server {}
+
+impl Server {}
+
+#[tokio::main]
+async fn main() {
+    if std::env::var("RUST_LOG").is_err() {
+        std::env::set_var("RUST_LOG", "info"); //trace
+    }
+    env_logger::init();
+
+    // let (wallet_key, wallet_id) = generate_keypair();
+    // let content = BootstrapContentV0 { servers: vec![] };
+    // let ser = serde_bare::to_vec(&content).unwrap();
+    // let sig = sign(wallet_key, wallet_id, &ser).unwrap();
+
+    // let bootstrap = Bootstrap::V0(BootstrapV0 {
+    //     id: wallet_id,
+    //     content,
+    //     sig,
+    // });
+
+    let server = Arc::new(Server {});
+
+    let static_files = warp::get().and(warp_embed::embed(&Static)).boxed();
+
+    let mut cors = warp::cors()
+        .allow_methods(vec!["GET", "POST"])
+        .allow_headers(vec!["Content-Type"]);
+
+    #[cfg(not(debug_assertions))]
+    {
+        cors = cors
+            .allow_origin(NG_ONE_URL)
+            .allow_origin(APP_NG_ONE_URL)
+            .allow_origin("https://nextgraph.eu")
+            .allow_origin("https://nextgraph.net");
+    }
+    #[cfg(debug_assertions)]
+    {
+        log_debug!("CORS: any origin");
+        cors = cors.allow_any_origin();
+    }
+    log::info!("Starting server on http://localhost:3030");
+    warp::serve(static_files.with(cors))
+        .run(([127, 0, 0, 1], 3031))
+        .await;
+}
diff --git a/ngaccount/src/types.rs b/ngaccount/src/types.rs
new file mode 100644
index 0000000..ebea555
--- /dev/null
+++ b/ngaccount/src/types.rs
@@ -0,0 +1,30 @@
+// Copyright (c) 2022-2023 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 warp::{reply::Response, Reply};
+
+pub enum NgHttpError {
+    InvalidParams,
+    NotFound,
+    AlreadyExists,
+    InternalError,
+}
+
+impl Reply for NgHttpError {
+    fn into_response(self) -> Response {
+        match (self) {
+            NgHttpError::NotFound => warp::http::StatusCode::NOT_FOUND.into_response(),
+            NgHttpError::InvalidParams => warp::http::StatusCode::BAD_REQUEST.into_response(),
+            NgHttpError::AlreadyExists => warp::http::StatusCode::CONFLICT.into_response(),
+            NgHttpError::InternalError => {
+                warp::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
+            }
+        }
+    }
+}
diff --git a/ngaccount/web/.gitignore b/ngaccount/web/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/ngaccount/web/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/ngaccount/web/.vscode/extensions.json b/ngaccount/web/.vscode/extensions.json
new file mode 100644
index 0000000..bdef820
--- /dev/null
+++ b/ngaccount/web/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+  "recommendations": ["svelte.svelte-vscode"]
+}
diff --git a/ngaccount/web/index.html b/ngaccount/web/index.html
new file mode 100644
index 0000000..a05053c
--- /dev/null
+++ b/ngaccount/web/index.html
@@ -0,0 +1,61 @@
+<!--
+// Copyright (c) 2022-2023 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.
+-->
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,
+    PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMjUg
+    MjI1Ij48Y2lyY2xlIGN4PSIxMDkuODgxIiBjeT0iMTEyLjkwNSIgcj0iMTA2Ljk4IiBzdHlsZT0i
+    ZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2U6IG5vbmU7IHN0cm9rZS13aWR0aDogMC4y
+    NjgzNzU7Ij48L2NpcmNsZT48cGF0aCBkPSJNOTguMzQzIDE5MC4yNjFjLTE3Ljk0LTIuNzI3LTMz
+    LjMzMS0xMC42ODMtNDUuNzM1LTIzLjYzOC0xNC4wMDYtMTQuNjI5LTIxLjQzLTMzLjIxLTIxLjQz
+    LTUzLjYzNSAwLTEwLjIxOCAxLjctMTkuNDQ0IDUuMjIxLTI4LjMzMiA0LjI4Ny0xMC44MiAxMC4w
+    MzgtMTkuMzkgMTguNTM1LTI3LjYyMiA0LjczLTQuNTgyIDYuNjA3LTYuMTA3IDExLjI4MS05LjE2
+    MyAxMS45LTcuNzggMjQuMTc0LTExLjg4IDM4LjA5Ni0xMi43MjUgMTkuODA1LTEuMjAxIDM5LjEx
+    MiA1LjExMyA1NC42MDMgMTcuODYgMS41MDcgMS4yNCAyLjczIDIuMzU4IDIuNzE2IDIuNDg2LS4w
+    MTMuMTI4LTMuODU4IDMuNjM1LTguNTQ0IDcuNzkzLTQuNjg2IDQuMTU3LTEwLjA0NyA4Ljk2Mi0x
+    MS45MTQgMTAuNjc3LTEuODY2IDEuNzE1LTMuNTQgMy4xMTktMy43MjEgMy4xMTktLjE4MSAwLTEu
+    NC0uNzQ2LTIuNzEtMS42NTYtNy41My01LjIzOS0xNS45OTQtNy44MjItMjUuNjI1LTcuODIyLTEy
+    LjczMiAwLTIzLjI1IDQuMzM4LTMyLjE0NCAxMy4yNTctNi4zOTYgNi40MTQtMTAuNzA0IDE0LjU1
+    Ni0xMi41IDIzLjYyNC0uNjkxIDMuNDg4LS42OSAxMy41My4wMDIgMTcuMDA5IDMuNzA1IDE4LjYy
+    NiAxOC4zMTggMzMuMTAyIDM2LjY0MiAzNi4yOTcgNC4xNjQuNzI2IDExLjk4LjcxMiAxNS45OS0u
+    MDI4IDE0LjAzMi0yLjU5NCAyNS44Ni0xMS4zNjggMzIuMjY1LTIzLjkzNi43NzQtMS41MTkgMS4y
+    Ni0yLjg4NSAxLjA4LTMuMDM2LS4xNzgtLjE1Mi02Ljg3NC0xLjE3OC0xNC44NzctMi4yODEtOS43
+    OC0xLjM0OC0xNC45MjQtMi4yMTQtMTUuNjg1LTIuNjQxLTEuNTItLjg1NC0yLjgzNi0yLjg4OC0y
+    LjgzNi00LjM4NiAwLTEuMTczIDIuMDI3LTE1Ljg2OSAyLjQ5LTE4LjA2LjI5OC0xLjQwMSAyLjQy
+    Ni0zLjQ5MyAzLjg0NC0zLjc3Ny42MjItLjEyNCA4LjgyNy44NTYgMTguMjggMi4xODQgOS40MzQg
+    MS4zMjUgMTcuMjYzIDIuMjk0IDE3LjM5OSAyLjE1NC4xMzYtLjE0IDEuMTE4LTYuNTQ4IDIuMTgz
+    LTE0LjI0IDEuMTA4LTggMi4yMDQtMTQuNjAyIDIuNTYyLTE1LjQyNi4zNDQtLjc5MyAxLjExLTEu
+    ODUgMS43MDMtMi4zNDggMi4wNjMtMS43MzYgMy4xNDMtMS43ODUgMTIuMjA0LS41NTMgOS42MzYg
+    MS4zMSAxMC43MDkgMS41NjIgMTIuMjggMi44ODUgMS42NDQgMS4zODMgMi4yNzQgMi44MSAyLjI2
+    IDUuMTIzLS4wMDcgMS4xMDItLjkyMiA4LjI5Ny0yLjAzMyAxNS45ODktMS4xMTIgNy42OTEtMS45
+    NzIgMTQuMDQtMS45MTIgMTQuMTA5LjA2MS4wNjggNy4xNjcgMS4xMTEgMTUuNzkyIDIuMzE4IDEx
+    LjEwNSAxLjU1NCAxNi4wMDggMi4zODcgMTYuODAyIDIuODU2IDEuNTMuOTA0IDIuNDggMi42NDgg
+    Mi40NSA0LjQ5OC0uMDQ2IDIuODQ0LTIuNDEzIDE4LjEyMy0yLjk3NSAxOS4yMS0uNjYyIDEuMjgt
+    Mi42MDMgMi41NDgtMy45MjEgMi41NjItLjUyLjAwNS03Ljg3NS0uOTYtMTYuMzQ0LTIuMTQ0LTgu
+    NDctMS4xODUtMTUuNDc2LTIuMDc3LTE1LjU3LTEuOTgzLS4wOTQuMDk0LTEuMTg4IDcuMzQxLTIu
+    NDMxIDE2LjEwNi0xLjQ0IDEwLjE1My0yLjQ5OCAxNi40MzYtMi45MTYgMTcuMzE2LS43MjUgMS41
+    MjgtMi43NjIgMy4wNjMtNC41MzggMy40MTgtLjk1Ny4xOTEtMTAuOS0uOTI4LTEzLjU5OC0xLjUz
+    LS41NDgtLjEyMy0xLjg5Mi42NzItNC41MSAyLjY2NS0xMS4yNjMgOC41NzYtMjQuMzQyIDEzLjkx
+    LTM4LjM1NyAxNS42NDItNC40LjU0NC0xNS43MjcuNDMzLTE5Ljg1NC0uMTk1eiIgc3R5bGU9ImZp
+    bGw6IHJnYig3MywgMTE0LCAxNjUpOyBmaWxsLW9wYWNpdHk6IDE7IHN0cm9rZTogcmdiKDczLCAx
+    MTQsIDE2NSk7IHN0cm9rZS13aWR0aDogMC4zNzc5NzY7IHN0cm9rZS1vcGFjaXR5OiAxOyI+PC9w
+    YXRoPjwvc3ZnPg==" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>NextGraph</title>
+  </head>
+
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>
\ No newline at end of file
diff --git a/ngaccount/web/jsconfig.json b/ngaccount/web/jsconfig.json
new file mode 100644
index 0000000..5696a2d
--- /dev/null
+++ b/ngaccount/web/jsconfig.json
@@ -0,0 +1,32 @@
+{
+  "compilerOptions": {
+    "moduleResolution": "bundler",
+    "target": "ESNext",
+    "module": "ESNext",
+    /**
+     * svelte-preprocess cannot figure out whether you have
+     * a value or a type, so tell TypeScript to enforce using
+     * `import type` instead of `import` for Types.
+     */
+    "verbatimModuleSyntax": true,
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    /**
+     * To have warnings / errors of the Svelte compiler at the
+     * correct position, enable source maps by default.
+     */
+    "sourceMap": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    /**
+     * Typecheck JS in `.svelte` and `.js` files by default.
+     * Disable this if you'd like to use dynamic types.
+     */
+    "checkJs": true
+  },
+  /**
+   * Use global.d.ts instead of compilerOptions.types
+   * to avoid limiting type declarations.
+   */
+  "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
+}
diff --git a/ngaccount/web/package.json b/ngaccount/web/package.json
new file mode 100644
index 0000000..55f0957
--- /dev/null
+++ b/ngaccount/web/package.json
@@ -0,0 +1,27 @@
+{
+  "name": "ng-account-web",
+  "private": true,
+  "version": "0.1.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build --base=./",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "flowbite": "^1.6.5",
+    "flowbite-svelte": "^0.37.1",
+    "svelte-spa-router": "^3.3.0"
+  },
+  "devDependencies": {
+    "@sveltejs/vite-plugin-svelte": "^2.0.4",
+    "svelte": "^3.58.0",
+    "vite": "^4.3.9",
+    "postcss": "^8.4.23",
+    "postcss-load-config": "^4.0.1",
+    "svelte-preprocess": "^5.0.3",
+    "tailwindcss": "^3.3.1",
+    "autoprefixer": "^10.4.14",
+    "vite-plugin-svelte-svg": "^2.2.1"
+  }
+}
diff --git a/ngaccount/web/pnpm-lock.yaml b/ngaccount/web/pnpm-lock.yaml
new file mode 100644
index 0000000..a71774a
--- /dev/null
+++ b/ngaccount/web/pnpm-lock.yaml
@@ -0,0 +1,1310 @@
+lockfileVersion: 5.4
+
+specifiers:
+  '@sveltejs/vite-plugin-svelte': ^2.0.4
+  autoprefixer: ^10.4.14
+  flowbite: ^1.6.5
+  flowbite-svelte: ^0.37.1
+  postcss: ^8.4.23
+  postcss-load-config: ^4.0.1
+  svelte: ^3.58.0
+  svelte-preprocess: ^5.0.3
+  svelte-spa-router: ^3.3.0
+  tailwindcss: ^3.3.1
+  vite: ^4.3.9
+  vite-plugin-svelte-svg: ^2.2.1
+
+dependencies:
+  flowbite: 1.6.6
+  flowbite-svelte: 0.37.5_svelte@3.59.1
+  svelte-spa-router: 3.3.0
+
+devDependencies:
+  '@sveltejs/vite-plugin-svelte': 2.4.1_svelte@3.59.1+vite@4.3.9
+  autoprefixer: 10.4.14_postcss@8.4.24
+  postcss: 8.4.24
+  postcss-load-config: 4.0.1_postcss@8.4.24
+  svelte: 3.59.1
+  svelte-preprocess: 5.0.4_sxhny56dlbcmwov4vk7qwrzshi
+  tailwindcss: 3.3.2
+  vite: 4.3.9
+  vite-plugin-svelte-svg: 2.2.1_svelte@3.59.1+vite@4.3.9
+
+packages:
+
+  /@alloc/quick-lru/5.2.0:
+    resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
+    engines: {node: '>=10'}
+    dev: true
+
+  /@esbuild/android-arm/0.17.19:
+    resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/android-arm64/0.17.19:
+    resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/android-x64/0.17.19:
+    resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [android]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/darwin-arm64/0.17.19:
+    resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/darwin-x64/0.17.19:
+    resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/freebsd-arm64/0.17.19:
+    resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [freebsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/freebsd-x64/0.17.19:
+    resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [freebsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-arm/0.17.19:
+    resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-arm64/0.17.19:
+    resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-ia32/0.17.19:
+    resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-loong64/0.17.19:
+    resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==}
+    engines: {node: '>=12'}
+    cpu: [loong64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-mips64el/0.17.19:
+    resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==}
+    engines: {node: '>=12'}
+    cpu: [mips64el]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-ppc64/0.17.19:
+    resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-riscv64/0.17.19:
+    resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==}
+    engines: {node: '>=12'}
+    cpu: [riscv64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-s390x/0.17.19:
+    resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==}
+    engines: {node: '>=12'}
+    cpu: [s390x]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/linux-x64/0.17.19:
+    resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/netbsd-x64/0.17.19:
+    resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [netbsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/openbsd-x64/0.17.19:
+    resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [openbsd]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/sunos-x64/0.17.19:
+    resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [sunos]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/win32-arm64/0.17.19:
+    resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/win32-ia32/0.17.19:
+    resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@esbuild/win32-x64/0.17.19:
+    resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [win32]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /@jridgewell/gen-mapping/0.3.3:
+    resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
+    engines: {node: '>=6.0.0'}
+    dependencies:
+      '@jridgewell/set-array': 1.1.2
+      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/trace-mapping': 0.3.18
+    dev: true
+
+  /@jridgewell/resolve-uri/3.1.0:
+    resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
+    engines: {node: '>=6.0.0'}
+    dev: true
+
+  /@jridgewell/set-array/1.1.2:
+    resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
+    engines: {node: '>=6.0.0'}
+    dev: true
+
+  /@jridgewell/sourcemap-codec/1.4.14:
+    resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
+    dev: true
+
+  /@jridgewell/sourcemap-codec/1.4.15:
+    resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
+    dev: true
+
+  /@jridgewell/trace-mapping/0.3.18:
+    resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==}
+    dependencies:
+      '@jridgewell/resolve-uri': 3.1.0
+      '@jridgewell/sourcemap-codec': 1.4.14
+    dev: true
+
+  /@nodelib/fs.scandir/2.1.5:
+    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+    engines: {node: '>= 8'}
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      run-parallel: 1.2.0
+    dev: true
+
+  /@nodelib/fs.stat/2.0.5:
+    resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+    engines: {node: '>= 8'}
+    dev: true
+
+  /@nodelib/fs.walk/1.2.8:
+    resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+    engines: {node: '>= 8'}
+    dependencies:
+      '@nodelib/fs.scandir': 2.1.5
+      fastq: 1.15.0
+    dev: true
+
+  /@popperjs/core/2.11.8:
+    resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
+    dev: false
+
+  /@sveltejs/vite-plugin-svelte-inspector/1.0.2_qiij5gx4uovhfqjpd2vh63pzyq:
+    resolution: {integrity: sha512-Cy1dUMcYCnDVV/hPLXa43YZJ2jGKVW5rA0xuNL9dlmYhT0yoS1g7+FOFSRlgk0BXKk/Oc7grs+8BVA5Iz2fr8A==}
+    engines: {node: ^14.18.0 || >= 16}
+    peerDependencies:
+      '@sveltejs/vite-plugin-svelte': ^2.2.0
+      svelte: ^3.54.0 || ^4.0.0-next.0
+      vite: ^4.0.0
+    dependencies:
+      '@sveltejs/vite-plugin-svelte': 2.4.1_svelte@3.59.1+vite@4.3.9
+      debug: 4.3.4
+      svelte: 3.59.1
+      vite: 4.3.9
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
+  /@sveltejs/vite-plugin-svelte/2.4.1_svelte@3.59.1+vite@4.3.9:
+    resolution: {integrity: sha512-bNNKvoRY89ptY7udeBSCmTdCVwkjmMcZ0j/z9J5MuedT8jPjq0zrknAo/jF1sToAza4NVaAgR9AkZoD9oJJmnA==}
+    engines: {node: ^14.18.0 || >= 16}
+    peerDependencies:
+      svelte: ^3.54.0 || ^4.0.0-next.0
+      vite: ^4.0.0
+    dependencies:
+      '@sveltejs/vite-plugin-svelte-inspector': 1.0.2_qiij5gx4uovhfqjpd2vh63pzyq
+      debug: 4.3.4
+      deepmerge: 4.3.1
+      kleur: 4.1.5
+      magic-string: 0.30.0
+      svelte: 3.59.1
+      svelte-hmr: 0.15.2_svelte@3.59.1
+      vite: 4.3.9
+      vitefu: 0.2.4_vite@4.3.9
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
+  /@trysound/sax/0.2.0:
+    resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
+    engines: {node: '>=10.13.0'}
+    dev: true
+
+  /@types/pug/2.0.6:
+    resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
+    dev: true
+
+  /any-promise/1.3.0:
+    resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
+    dev: true
+
+  /anymatch/3.1.3:
+    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+    engines: {node: '>= 8'}
+    dependencies:
+      normalize-path: 3.0.0
+      picomatch: 2.3.1
+    dev: true
+
+  /arg/5.0.2:
+    resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+    dev: true
+
+  /autoprefixer/10.4.14_postcss@8.4.24:
+    resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==}
+    engines: {node: ^10 || ^12 || >=14}
+    hasBin: true
+    peerDependencies:
+      postcss: ^8.1.0
+    dependencies:
+      browserslist: 4.21.8
+      caniuse-lite: 1.0.30001503
+      fraction.js: 4.2.0
+      normalize-range: 0.1.2
+      picocolors: 1.0.0
+      postcss: 8.4.24
+      postcss-value-parser: 4.2.0
+    dev: true
+
+  /balanced-match/1.0.2:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+    dev: true
+
+  /binary-extensions/2.2.0:
+    resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
+    engines: {node: '>=8'}
+    dev: true
+
+  /boolbase/1.0.0:
+    resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+    dev: true
+
+  /brace-expansion/1.1.11:
+    resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+    dependencies:
+      balanced-match: 1.0.2
+      concat-map: 0.0.1
+    dev: true
+
+  /braces/3.0.2:
+    resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
+    engines: {node: '>=8'}
+    dependencies:
+      fill-range: 7.0.1
+    dev: true
+
+  /browserslist/4.21.8:
+    resolution: {integrity: sha512-j+7xYe+v+q2Id9qbBeCI8WX5NmZSRe8es1+0xntD/+gaWXznP8tFEkv5IgSaHf5dS1YwVMbX/4W6m937mj+wQw==}
+    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+    hasBin: true
+    dependencies:
+      caniuse-lite: 1.0.30001503
+      electron-to-chromium: 1.4.430
+      node-releases: 2.0.12
+      update-browserslist-db: 1.0.11_browserslist@4.21.8
+    dev: true
+
+  /buffer-crc32/0.2.13:
+    resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+    dev: true
+
+  /camelcase-css/2.0.1:
+    resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
+    engines: {node: '>= 6'}
+    dev: true
+
+  /caniuse-lite/1.0.30001503:
+    resolution: {integrity: sha512-Sf9NiF+wZxPfzv8Z3iS0rXM1Do+iOy2Lxvib38glFX+08TCYYYGR5fRJXk4d77C4AYwhUjgYgMsMudbh2TqCKw==}
+    dev: true
+
+  /chokidar/3.5.3:
+    resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
+    engines: {node: '>= 8.10.0'}
+    dependencies:
+      anymatch: 3.1.3
+      braces: 3.0.2
+      glob-parent: 5.1.2
+      is-binary-path: 2.1.0
+      is-glob: 4.0.3
+      normalize-path: 3.0.0
+      readdirp: 3.6.0
+    optionalDependencies:
+      fsevents: 2.3.2
+    dev: true
+
+  /classnames/2.3.2:
+    resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
+    dev: false
+
+  /commander/4.1.1:
+    resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
+    engines: {node: '>= 6'}
+    dev: true
+
+  /commander/7.2.0:
+    resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+    engines: {node: '>= 10'}
+    dev: true
+
+  /concat-map/0.0.1:
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+    dev: true
+
+  /css-select/5.1.0:
+    resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
+    dependencies:
+      boolbase: 1.0.0
+      css-what: 6.1.0
+      domhandler: 5.0.3
+      domutils: 3.1.0
+      nth-check: 2.1.1
+    dev: true
+
+  /css-tree/2.2.1:
+    resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
+    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+    dependencies:
+      mdn-data: 2.0.28
+      source-map-js: 1.0.2
+    dev: true
+
+  /css-tree/2.3.1:
+    resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
+    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+    dependencies:
+      mdn-data: 2.0.30
+      source-map-js: 1.0.2
+    dev: true
+
+  /css-what/6.1.0:
+    resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
+    engines: {node: '>= 6'}
+    dev: true
+
+  /cssesc/3.0.0:
+    resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+    engines: {node: '>=4'}
+    hasBin: true
+    dev: true
+
+  /csso/5.0.5:
+    resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
+    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+    dependencies:
+      css-tree: 2.2.1
+    dev: true
+
+  /debug/4.3.4:
+    resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+    dependencies:
+      ms: 2.1.2
+    dev: true
+
+  /deepmerge/4.3.1:
+    resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
+  /detect-indent/6.1.0:
+    resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
+    engines: {node: '>=8'}
+    dev: true
+
+  /didyoumean/1.2.2:
+    resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
+    dev: true
+
+  /dlv/1.1.3:
+    resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
+    dev: true
+
+  /dom-serializer/2.0.0:
+    resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+    dependencies:
+      domelementtype: 2.3.0
+      domhandler: 5.0.3
+      entities: 4.5.0
+    dev: true
+
+  /domelementtype/2.3.0:
+    resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+    dev: true
+
+  /domhandler/5.0.3:
+    resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+    engines: {node: '>= 4'}
+    dependencies:
+      domelementtype: 2.3.0
+    dev: true
+
+  /domutils/3.1.0:
+    resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
+    dependencies:
+      dom-serializer: 2.0.0
+      domelementtype: 2.3.0
+      domhandler: 5.0.3
+    dev: true
+
+  /electron-to-chromium/1.4.430:
+    resolution: {integrity: sha512-FytjTbGwz///F+ToZ5XSeXbbSaXalsVRXsz2mHityI5gfxft7ieW3HqFLkU5V1aIrY42aflICqbmFoDxW10etg==}
+    dev: true
+
+  /entities/4.5.0:
+    resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+    engines: {node: '>=0.12'}
+    dev: true
+
+  /es6-promise/3.3.1:
+    resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==}
+    dev: true
+
+  /esbuild/0.17.19:
+    resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
+    engines: {node: '>=12'}
+    hasBin: true
+    requiresBuild: true
+    optionalDependencies:
+      '@esbuild/android-arm': 0.17.19
+      '@esbuild/android-arm64': 0.17.19
+      '@esbuild/android-x64': 0.17.19
+      '@esbuild/darwin-arm64': 0.17.19
+      '@esbuild/darwin-x64': 0.17.19
+      '@esbuild/freebsd-arm64': 0.17.19
+      '@esbuild/freebsd-x64': 0.17.19
+      '@esbuild/linux-arm': 0.17.19
+      '@esbuild/linux-arm64': 0.17.19
+      '@esbuild/linux-ia32': 0.17.19
+      '@esbuild/linux-loong64': 0.17.19
+      '@esbuild/linux-mips64el': 0.17.19
+      '@esbuild/linux-ppc64': 0.17.19
+      '@esbuild/linux-riscv64': 0.17.19
+      '@esbuild/linux-s390x': 0.17.19
+      '@esbuild/linux-x64': 0.17.19
+      '@esbuild/netbsd-x64': 0.17.19
+      '@esbuild/openbsd-x64': 0.17.19
+      '@esbuild/sunos-x64': 0.17.19
+      '@esbuild/win32-arm64': 0.17.19
+      '@esbuild/win32-ia32': 0.17.19
+      '@esbuild/win32-x64': 0.17.19
+    dev: true
+
+  /escalade/3.1.1:
+    resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
+    engines: {node: '>=6'}
+    dev: true
+
+  /fast-glob/3.2.12:
+    resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
+    engines: {node: '>=8.6.0'}
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      '@nodelib/fs.walk': 1.2.8
+      glob-parent: 5.1.2
+      merge2: 1.4.1
+      micromatch: 4.0.5
+    dev: true
+
+  /fastq/1.15.0:
+    resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
+    dependencies:
+      reusify: 1.0.4
+    dev: true
+
+  /fill-range/7.0.1:
+    resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
+    engines: {node: '>=8'}
+    dependencies:
+      to-regex-range: 5.0.1
+    dev: true
+
+  /flowbite-svelte/0.37.5_svelte@3.59.1:
+    resolution: {integrity: sha512-aAPJygrfqY1+wkcl4rG/ikTxAdnB1p4Xe2sKG3Jm+BJsMZvArgl6/weH+DimoSjHLAMLhtd4spAbzfT8eHBgnQ==}
+    engines: {node: '>=16.0.0', npm: '>=7.0.0'}
+    peerDependencies:
+      svelte: ^3.55.1 || ^4.0.0
+    dependencies:
+      '@popperjs/core': 2.11.8
+      classnames: 2.3.2
+      flowbite: 1.6.6
+      svelte: 3.59.1
+    dev: false
+
+  /flowbite/1.6.6:
+    resolution: {integrity: sha512-T+IaFikHELo1PBKfT/axDqhAmKQLfm/dxVch2r07TZ+IcKwkorZjzwkVuw3OslTETniRIUf2qQvEhxk3bQCaew==}
+    dependencies:
+      '@popperjs/core': 2.11.8
+      mini-svg-data-uri: 1.4.4
+    dev: false
+
+  /fraction.js/4.2.0:
+    resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
+    dev: true
+
+  /fs.realpath/1.0.0:
+    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+    dev: true
+
+  /fsevents/2.3.2:
+    resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+    requiresBuild: true
+    dev: true
+    optional: true
+
+  /function-bind/1.1.1:
+    resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
+    dev: true
+
+  /glob-parent/5.1.2:
+    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+    engines: {node: '>= 6'}
+    dependencies:
+      is-glob: 4.0.3
+    dev: true
+
+  /glob-parent/6.0.2:
+    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+    engines: {node: '>=10.13.0'}
+    dependencies:
+      is-glob: 4.0.3
+    dev: true
+
+  /glob/7.1.6:
+    resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==}
+    dependencies:
+      fs.realpath: 1.0.0
+      inflight: 1.0.6
+      inherits: 2.0.4
+      minimatch: 3.1.2
+      once: 1.4.0
+      path-is-absolute: 1.0.1
+    dev: true
+
+  /glob/7.2.3:
+    resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+    dependencies:
+      fs.realpath: 1.0.0
+      inflight: 1.0.6
+      inherits: 2.0.4
+      minimatch: 3.1.2
+      once: 1.4.0
+      path-is-absolute: 1.0.1
+    dev: true
+
+  /graceful-fs/4.2.11:
+    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+    dev: true
+
+  /has/1.0.3:
+    resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
+    engines: {node: '>= 0.4.0'}
+    dependencies:
+      function-bind: 1.1.1
+    dev: true
+
+  /inflight/1.0.6:
+    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+    dependencies:
+      once: 1.4.0
+      wrappy: 1.0.2
+    dev: true
+
+  /inherits/2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+    dev: true
+
+  /is-binary-path/2.1.0:
+    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+    engines: {node: '>=8'}
+    dependencies:
+      binary-extensions: 2.2.0
+    dev: true
+
+  /is-core-module/2.12.1:
+    resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==}
+    dependencies:
+      has: 1.0.3
+    dev: true
+
+  /is-extglob/2.1.1:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
+  /is-glob/4.0.3:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+    dependencies:
+      is-extglob: 2.1.1
+    dev: true
+
+  /is-number/7.0.0:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+    engines: {node: '>=0.12.0'}
+    dev: true
+
+  /jiti/1.18.2:
+    resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==}
+    hasBin: true
+    dev: true
+
+  /kleur/4.1.5:
+    resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+    engines: {node: '>=6'}
+    dev: true
+
+  /lilconfig/2.1.0:
+    resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
+    engines: {node: '>=10'}
+    dev: true
+
+  /lines-and-columns/1.2.4:
+    resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+    dev: true
+
+  /magic-string/0.27.0:
+    resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
+    engines: {node: '>=12'}
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.4.15
+    dev: true
+
+  /magic-string/0.30.0:
+    resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==}
+    engines: {node: '>=12'}
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.4.15
+    dev: true
+
+  /mdn-data/2.0.28:
+    resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
+    dev: true
+
+  /mdn-data/2.0.30:
+    resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
+    dev: true
+
+  /merge2/1.4.1:
+    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+    engines: {node: '>= 8'}
+    dev: true
+
+  /micromatch/4.0.5:
+    resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
+    engines: {node: '>=8.6'}
+    dependencies:
+      braces: 3.0.2
+      picomatch: 2.3.1
+    dev: true
+
+  /min-indent/1.0.1:
+    resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+    engines: {node: '>=4'}
+    dev: true
+
+  /mini-svg-data-uri/1.4.4:
+    resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
+    hasBin: true
+    dev: false
+
+  /minimatch/3.1.2:
+    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+    dependencies:
+      brace-expansion: 1.1.11
+    dev: true
+
+  /minimist/1.2.8:
+    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+    dev: true
+
+  /mkdirp/0.5.6:
+    resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
+    hasBin: true
+    dependencies:
+      minimist: 1.2.8
+    dev: true
+
+  /ms/2.1.2:
+    resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
+    dev: true
+
+  /mz/2.7.0:
+    resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
+    dependencies:
+      any-promise: 1.3.0
+      object-assign: 4.1.1
+      thenify-all: 1.6.0
+    dev: true
+
+  /nanoid/3.3.6:
+    resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+    dev: true
+
+  /node-releases/2.0.12:
+    resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==}
+    dev: true
+
+  /normalize-path/3.0.0:
+    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
+  /normalize-range/0.1.2:
+    resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
+  /nth-check/2.1.1:
+    resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+    dependencies:
+      boolbase: 1.0.0
+    dev: true
+
+  /object-assign/4.1.1:
+    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
+  /object-hash/3.0.0:
+    resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
+    engines: {node: '>= 6'}
+    dev: true
+
+  /once/1.4.0:
+    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+    dependencies:
+      wrappy: 1.0.2
+    dev: true
+
+  /path-is-absolute/1.0.1:
+    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
+  /path-parse/1.0.7:
+    resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+    dev: true
+
+  /picocolors/1.0.0:
+    resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
+    dev: true
+
+  /picomatch/2.3.1:
+    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+    engines: {node: '>=8.6'}
+    dev: true
+
+  /pify/2.3.0:
+    resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
+  /pirates/4.0.5:
+    resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==}
+    engines: {node: '>= 6'}
+    dev: true
+
+  /postcss-import/15.1.0_postcss@8.4.24:
+    resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
+    engines: {node: '>=14.0.0'}
+    peerDependencies:
+      postcss: ^8.0.0
+    dependencies:
+      postcss: 8.4.24
+      postcss-value-parser: 4.2.0
+      read-cache: 1.0.0
+      resolve: 1.22.2
+    dev: true
+
+  /postcss-js/4.0.1_postcss@8.4.24:
+    resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
+    engines: {node: ^12 || ^14 || >= 16}
+    peerDependencies:
+      postcss: ^8.4.21
+    dependencies:
+      camelcase-css: 2.0.1
+      postcss: 8.4.24
+    dev: true
+
+  /postcss-load-config/4.0.1_postcss@8.4.24:
+    resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==}
+    engines: {node: '>= 14'}
+    peerDependencies:
+      postcss: '>=8.0.9'
+      ts-node: '>=9.0.0'
+    peerDependenciesMeta:
+      postcss:
+        optional: true
+      ts-node:
+        optional: true
+    dependencies:
+      lilconfig: 2.1.0
+      postcss: 8.4.24
+      yaml: 2.3.1
+    dev: true
+
+  /postcss-nested/6.0.1_postcss@8.4.24:
+    resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
+    engines: {node: '>=12.0'}
+    peerDependencies:
+      postcss: ^8.2.14
+    dependencies:
+      postcss: 8.4.24
+      postcss-selector-parser: 6.0.13
+    dev: true
+
+  /postcss-selector-parser/6.0.13:
+    resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==}
+    engines: {node: '>=4'}
+    dependencies:
+      cssesc: 3.0.0
+      util-deprecate: 1.0.2
+    dev: true
+
+  /postcss-value-parser/4.2.0:
+    resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+    dev: true
+
+  /postcss/8.4.24:
+    resolution: {integrity: sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==}
+    engines: {node: ^10 || ^12 || >=14}
+    dependencies:
+      nanoid: 3.3.6
+      picocolors: 1.0.0
+      source-map-js: 1.0.2
+    dev: true
+
+  /queue-microtask/1.2.3:
+    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+    dev: true
+
+  /read-cache/1.0.0:
+    resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
+    dependencies:
+      pify: 2.3.0
+    dev: true
+
+  /readdirp/3.6.0:
+    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+    engines: {node: '>=8.10.0'}
+    dependencies:
+      picomatch: 2.3.1
+    dev: true
+
+  /regexparam/2.0.1:
+    resolution: {integrity: sha512-zRgSaYemnNYxUv+/5SeoHI0eJIgTL/A2pUtXUPLHQxUldagouJ9p+K6IbIZ/JiQuCEv2E2B1O11SjVQy3aMCkw==}
+    engines: {node: '>=8'}
+    dev: false
+
+  /resolve/1.22.2:
+    resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==}
+    hasBin: true
+    dependencies:
+      is-core-module: 2.12.1
+      path-parse: 1.0.7
+      supports-preserve-symlinks-flag: 1.0.0
+    dev: true
+
+  /reusify/1.0.4:
+    resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+    engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+    dev: true
+
+  /rimraf/2.7.1:
+    resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
+    hasBin: true
+    dependencies:
+      glob: 7.2.3
+    dev: true
+
+  /rollup/3.23.0:
+    resolution: {integrity: sha512-h31UlwEi7FHihLe1zbk+3Q7z1k/84rb9BSwmBSr/XjOCEaBJ2YyedQDuM0t/kfOS0IxM+vk1/zI9XxYj9V+NJQ==}
+    engines: {node: '>=14.18.0', npm: '>=8.0.0'}
+    hasBin: true
+    optionalDependencies:
+      fsevents: 2.3.2
+    dev: true
+
+  /run-parallel/1.2.0:
+    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+    dependencies:
+      queue-microtask: 1.2.3
+    dev: true
+
+  /sander/0.5.1:
+    resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==}
+    dependencies:
+      es6-promise: 3.3.1
+      graceful-fs: 4.2.11
+      mkdirp: 0.5.6
+      rimraf: 2.7.1
+    dev: true
+
+  /sorcery/0.11.0:
+    resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==}
+    hasBin: true
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.4.15
+      buffer-crc32: 0.2.13
+      minimist: 1.2.8
+      sander: 0.5.1
+    dev: true
+
+  /source-map-js/1.0.2:
+    resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
+    engines: {node: '>=0.10.0'}
+    dev: true
+
+  /strip-indent/3.0.0:
+    resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+    engines: {node: '>=8'}
+    dependencies:
+      min-indent: 1.0.1
+    dev: true
+
+  /sucrase/3.32.0:
+    resolution: {integrity: sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==}
+    engines: {node: '>=8'}
+    hasBin: true
+    dependencies:
+      '@jridgewell/gen-mapping': 0.3.3
+      commander: 4.1.1
+      glob: 7.1.6
+      lines-and-columns: 1.2.4
+      mz: 2.7.0
+      pirates: 4.0.5
+      ts-interface-checker: 0.1.13
+    dev: true
+
+  /supports-preserve-symlinks-flag/1.0.0:
+    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+    engines: {node: '>= 0.4'}
+    dev: true
+
+  /svelte-hmr/0.15.2_svelte@3.59.1:
+    resolution: {integrity: sha512-q/bAruCvFLwvNbeE1x3n37TYFb3mTBJ6TrCq6p2CoFbSTNhDE9oAtEfpy+wmc9So8AG0Tja+X0/mJzX9tSfvIg==}
+    engines: {node: ^12.20 || ^14.13.1 || >= 16}
+    peerDependencies:
+      svelte: ^3.19.0 || ^4.0.0-next.0
+    dependencies:
+      svelte: 3.59.1
+    dev: true
+
+  /svelte-preprocess/5.0.4_sxhny56dlbcmwov4vk7qwrzshi:
+    resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==}
+    engines: {node: '>= 14.10.0'}
+    requiresBuild: true
+    peerDependencies:
+      '@babel/core': ^7.10.2
+      coffeescript: ^2.5.1
+      less: ^3.11.3 || ^4.0.0
+      postcss: ^7 || ^8
+      postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0
+      pug: ^3.0.0
+      sass: ^1.26.8
+      stylus: ^0.55.0
+      sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0
+      svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0
+      typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0'
+    peerDependenciesMeta:
+      '@babel/core':
+        optional: true
+      coffeescript:
+        optional: true
+      less:
+        optional: true
+      postcss:
+        optional: true
+      postcss-load-config:
+        optional: true
+      pug:
+        optional: true
+      sass:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      typescript:
+        optional: true
+    dependencies:
+      '@types/pug': 2.0.6
+      detect-indent: 6.1.0
+      magic-string: 0.27.0
+      postcss: 8.4.24
+      postcss-load-config: 4.0.1_postcss@8.4.24
+      sorcery: 0.11.0
+      strip-indent: 3.0.0
+      svelte: 3.59.1
+    dev: true
+
+  /svelte-spa-router/3.3.0:
+    resolution: {integrity: sha512-cwRNe7cxD43sCvSfEeaKiNZg3FCizGxeMcf7CPiWRP3jKXjEma3vxyyuDtPOam6nWbVxl9TNM3hlE/i87ZlqcQ==}
+    dependencies:
+      regexparam: 2.0.1
+    dev: false
+
+  /svelte/3.59.1:
+    resolution: {integrity: sha512-pKj8fEBmqf6mq3/NfrB9SLtcJcUvjYSWyePlfCqN9gujLB25RitWK8PvFzlwim6hD/We35KbPlRteuA6rnPGcQ==}
+    engines: {node: '>= 8'}
+
+  /svgo/3.0.2:
+    resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==}
+    engines: {node: '>=14.0.0'}
+    hasBin: true
+    dependencies:
+      '@trysound/sax': 0.2.0
+      commander: 7.2.0
+      css-select: 5.1.0
+      css-tree: 2.3.1
+      csso: 5.0.5
+      picocolors: 1.0.0
+    dev: true
+
+  /tailwindcss/3.3.2:
+    resolution: {integrity: sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==}
+    engines: {node: '>=14.0.0'}
+    hasBin: true
+    dependencies:
+      '@alloc/quick-lru': 5.2.0
+      arg: 5.0.2
+      chokidar: 3.5.3
+      didyoumean: 1.2.2
+      dlv: 1.1.3
+      fast-glob: 3.2.12
+      glob-parent: 6.0.2
+      is-glob: 4.0.3
+      jiti: 1.18.2
+      lilconfig: 2.1.0
+      micromatch: 4.0.5
+      normalize-path: 3.0.0
+      object-hash: 3.0.0
+      picocolors: 1.0.0
+      postcss: 8.4.24
+      postcss-import: 15.1.0_postcss@8.4.24
+      postcss-js: 4.0.1_postcss@8.4.24
+      postcss-load-config: 4.0.1_postcss@8.4.24
+      postcss-nested: 6.0.1_postcss@8.4.24
+      postcss-selector-parser: 6.0.13
+      postcss-value-parser: 4.2.0
+      resolve: 1.22.2
+      sucrase: 3.32.0
+    transitivePeerDependencies:
+      - ts-node
+    dev: true
+
+  /thenify-all/1.6.0:
+    resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
+    engines: {node: '>=0.8'}
+    dependencies:
+      thenify: 3.3.1
+    dev: true
+
+  /thenify/3.3.1:
+    resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+    dependencies:
+      any-promise: 1.3.0
+    dev: true
+
+  /to-regex-range/5.0.1:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+    engines: {node: '>=8.0'}
+    dependencies:
+      is-number: 7.0.0
+    dev: true
+
+  /ts-interface-checker/0.1.13:
+    resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+    dev: true
+
+  /update-browserslist-db/1.0.11_browserslist@4.21.8:
+    resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
+    hasBin: true
+    peerDependencies:
+      browserslist: '>= 4.21.0'
+    dependencies:
+      browserslist: 4.21.8
+      escalade: 3.1.1
+      picocolors: 1.0.0
+    dev: true
+
+  /util-deprecate/1.0.2:
+    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+    dev: true
+
+  /vite-plugin-svelte-svg/2.2.1_svelte@3.59.1+vite@4.3.9:
+    resolution: {integrity: sha512-CoGzvoAY02u4Haek6whVkvTPus2fIPU37dkKvlk2wBI8bB+A7UKasR8mDIbq4rH5fxvZee8UrEzLcZxNbm4bTQ==}
+    peerDependencies:
+      svelte: ^3.55.0
+      vite: < 5.0.0
+    dependencies:
+      svelte: 3.59.1
+      svgo: 3.0.2
+      vite: 4.3.9
+    dev: true
+
+  /vite/4.3.9:
+    resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==}
+    engines: {node: ^14.18.0 || >=16.0.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': '>= 14'
+      less: '*'
+      sass: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.4.0
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      less:
+        optional: true
+      sass:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+    dependencies:
+      esbuild: 0.17.19
+      postcss: 8.4.24
+      rollup: 3.23.0
+    optionalDependencies:
+      fsevents: 2.3.2
+    dev: true
+
+  /vitefu/0.2.4_vite@4.3.9:
+    resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
+    peerDependencies:
+      vite: ^3.0.0 || ^4.0.0
+    peerDependenciesMeta:
+      vite:
+        optional: true
+    dependencies:
+      vite: 4.3.9
+    dev: true
+
+  /wrappy/1.0.2:
+    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+    dev: true
+
+  /yaml/2.3.1:
+    resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==}
+    engines: {node: '>= 14'}
+    dev: true
diff --git a/ngaccount/web/postcss.config.cjs b/ngaccount/web/postcss.config.cjs
new file mode 100644
index 0000000..e48cff5
--- /dev/null
+++ b/ngaccount/web/postcss.config.cjs
@@ -0,0 +1,13 @@
+const tailwindcss = require("tailwindcss");
+const autoprefixer = require("autoprefixer");
+
+const config = {
+  plugins: [
+    //Some plugins, like tailwindcss/nesting, need to run before Tailwind,
+    tailwindcss(),
+    //But others, like autoprefixer, need to run after,
+    autoprefixer,
+  ],
+};
+
+module.exports = config;
diff --git a/ngaccount/web/public/robots.txt b/ngaccount/web/public/robots.txt
new file mode 100644
index 0000000..77470cb
--- /dev/null
+++ b/ngaccount/web/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
\ No newline at end of file
diff --git a/ngaccount/web/public/vite.svg b/ngaccount/web/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/ngaccount/web/public/vite.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
diff --git a/ngaccount/web/src/App.svelte b/ngaccount/web/src/App.svelte
new file mode 100644
index 0000000..18fb37d
--- /dev/null
+++ b/ngaccount/web/src/App.svelte
@@ -0,0 +1,26 @@
+<!--
+// Copyright (c) 2022-2023 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.
+-->
+<script>
+  import Router from "svelte-spa-router";
+  import { onMount, tick } from "svelte";
+
+  import Home from "./routes/Home.svelte";
+
+  import NotFound from "./routes/NotFound.svelte";
+
+  const routes = new Map();
+  routes.set("/", Home);
+  routes.set("*", NotFound);
+</script>
+
+<main class="">
+  <Router {routes} />
+</main>
diff --git a/ngaccount/web/src/app.postcss b/ngaccount/web/src/app.postcss
new file mode 100644
index 0000000..1a7b7cf
--- /dev/null
+++ b/ngaccount/web/src/app.postcss
@@ -0,0 +1,4 @@
+/* Write your global styles here, in PostCSS syntax */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/ngaccount/web/src/assets/EU.svg b/ngaccount/web/src/assets/EU.svg
new file mode 100755
index 0000000..a9a71aa
--- /dev/null
+++ b/ngaccount/web/src/assets/EU.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg viewBox="0 0 810 540" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ><desc>European flag</desc>
+<defs><g id="s"><g id="c"><path id="t" d="M0,0v1h0.5z" transform="translate(0,-1)rotate(18)"/><use xlink:href="#t" transform="scale(-1,1)"/></g><g id="a"><use xlink:href="#c" transform="rotate(72)"/><use xlink:href="#c" transform="rotate(144)"/></g><use xlink:href="#a" transform="scale(-1,1)"/></g></defs>
+<rect fill="#039" width="810" height="540"/><g fill="#fc0" transform="scale(30)translate(13.5,9)"><use xlink:href="#s" y="-6"/><use xlink:href="#s" y="6"/><g id="l"><use xlink:href="#s" x="-6"/><use xlink:href="#s" transform="rotate(150)translate(0,6)rotate(66)"/><use xlink:href="#s" transform="rotate(120)translate(0,6)rotate(24)"/><use xlink:href="#s" transform="rotate(60)translate(0,6)rotate(12)"/><use xlink:href="#s" transform="rotate(30)translate(0,6)rotate(42)"/></g><use xlink:href="#l" transform="scale(-1,1)"/></g>
+</svg>
diff --git a/ngaccount/web/src/assets/nextgraph.svg b/ngaccount/web/src/assets/nextgraph.svg
new file mode 100644
index 0000000..585d488
--- /dev/null
+++ b/ngaccount/web/src/assets/nextgraph.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns="http://www.w3.org/2000/svg"
+   viewBox="0 0 225 225"
+>
+  <g>
+    <circle
+       r="106.98013"
+       cy="112.90476"
+       cx="109.88096"
+       style="fill:#ffffff;stroke:none;stroke-width:0.268375" />
+    <path
+       d="M 98.343352,190.26108 C 80.403778,187.53354 65.011938,179.57839 52.608228,166.62327 38.602093,151.99448 31.178059,133.41381 31.178059,112.98841 c 0,-10.21889 1.700058,-19.44396 5.221234,-28.332119 4.28678,-10.820699 10.037295,-19.39063 18.535095,-27.62263 4.72982,-4.58187 6.60687,-6.10643 11.28099,-9.16256 11.89869,-7.779841 24.173884,-11.879991 38.095802,-12.724761 19.80437,-1.2017 39.11165,5.11306 54.60284,17.858751 1.50718,1.24006 2.72951,2.35934 2.71628,2.48729 -0.0132,0.12795 -3.85821,3.63443 -8.54442,7.79217 -4.6862,4.157729 -10.04724,8.96276 -11.91342,10.677819 -1.86617,1.715071 -3.54094,3.11831 -3.7217,3.11831 -0.18075,0 -1.39985,-0.745188 -2.70911,-1.655969 -7.53011,-5.23834 -15.99428,-7.82188 -25.62597,-7.82188 -12.731628,0 -23.249192,4.3379 -32.143882,13.257541 -6.39594,6.413868 -10.70387,14.555268 -12.50018,23.623578 -0.69099,3.48832 -0.68968,13.53072 0.002,17.00893 3.70508,18.62577 18.31886,33.10194 36.642322,36.29729 4.16439,0.72621 11.98099,0.71223 15.98975,-0.0286 14.03187,-2.59311 25.86047,-11.36806 32.26533,-23.93578 0.77379,-1.51834 1.26018,-2.88461 1.08086,-3.03616 -0.17934,-0.15156 -6.87448,-1.1779 -14.87813,-2.28078 -9.7795,-1.34758 -14.92353,-2.21379 -15.68471,-2.64117 -1.52067,-0.85379 -2.83611,-2.88806 -2.83611,-4.3859 0,-1.1732 2.02687,-15.86876 2.49085,-18.05962 0.29676,-1.40127 2.42559,-3.4934 3.84317,-3.77691 0.62227,-0.12445 8.82712,0.85555 18.28065,2.18348 9.43343,1.32511 17.26269,2.29453 17.39833,2.15427 0.13566,-0.14026 1.11808,-6.54833 2.18313,-14.24014 1.10778,-8.000208 2.20407,-14.60184 2.56177,-15.426229 0.34392,-0.792599 1.11019,-1.849131 1.70287,-2.34782 2.06321,-1.736079 3.1433,-1.785011 12.20439,-0.55291 9.63637,1.310309 10.70873,1.56224 12.28077,2.88503 1.64359,1.382979 2.2732,2.810909 2.25906,5.123309 -0.007,1.10173 -0.92172,8.29645 -2.03332,15.98826 -1.11158,7.69182 -1.97159,14.04091 -1.91113,14.1091 0.0605,0.0682 7.16644,1.11143 15.79109,2.31832 11.10566,1.55407 16.00827,2.38757 16.80223,2.85657 1.53015,0.90389 2.48023,2.64785 2.45017,4.49756 -0.0462,2.84349 -2.41252,18.12279 -2.97521,19.21089 -0.66164,1.27949 -2.60244,2.54696 -3.92109,2.56074 -0.51973,0.005 -7.87449,-0.95937 -16.34391,-2.144 -8.46944,-1.18464 -15.47588,-2.077 -15.56986,-1.98301 -0.094,0.094 -1.18792,7.34163 -2.43097,16.10589 -1.44004,10.15311 -2.49792,16.43621 -2.91556,17.31631 -0.72531,1.52848 -2.76261,3.06291 -4.53817,3.41802 -0.95688,0.19138 -10.90014,-0.92798 -13.59859,-1.53084 -0.5471,-0.12223 -1.89146,0.67252 -4.50941,2.66588 -11.2627,8.57562 -24.34195,13.90917 -38.35741,15.64164 -4.40038,0.54395 -15.72658,0.43298 -19.853658,-0.19451 z"
+       style="fill:#4972a5;fill-opacity:1;stroke:#4972a5;stroke-width:0.377976;stroke-opacity:1" />
+  </g>
+</svg>
\ No newline at end of file
diff --git a/ngaccount/web/src/main.js b/ngaccount/web/src/main.js
new file mode 100644
index 0000000..4472c64
--- /dev/null
+++ b/ngaccount/web/src/main.js
@@ -0,0 +1,9 @@
+import './app.postcss'
+import "../../../ng-app/src/styles.css";
+import App from './App.svelte'
+
+const app = new App({
+  target: document.getElementById('app'),
+})
+
+export default app
diff --git a/ngaccount/web/src/routes/Home.svelte b/ngaccount/web/src/routes/Home.svelte
new file mode 100644
index 0000000..3828c22
--- /dev/null
+++ b/ngaccount/web/src/routes/Home.svelte
@@ -0,0 +1,27 @@
+<!--
+// Copyright (c) 2022-2023 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.
+-->
+
+<script>
+  import { Button } from "flowbite-svelte";
+  import { link } from "svelte-spa-router";
+
+  import { onMount } from "svelte";
+
+  const api_url = import.meta.env.PROD
+    ? "api/v1/"
+    : "http://localhost:3030/api/v1/";
+
+  async function bootstrap() {}
+
+  onMount(() => bootstrap());
+</script>
+
+<div />
diff --git a/ngaccount/web/src/routes/NotFound.svelte b/ngaccount/web/src/routes/NotFound.svelte
new file mode 100644
index 0000000..59c0f9b
--- /dev/null
+++ b/ngaccount/web/src/routes/NotFound.svelte
@@ -0,0 +1,20 @@
+<!--
+// Copyright (c) 2022-2023 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.
+-->
+
+<script>
+  import { Alert } from "flowbite-svelte";
+</script>
+
+<div class="p-8">
+  <Alert color="red">
+    <span class="font-medium">404</span> Page not found.
+  </Alert>
+</div>
diff --git a/ngaccount/web/src/vite-env.d.ts b/ngaccount/web/src/vite-env.d.ts
new file mode 100644
index 0000000..4078e74
--- /dev/null
+++ b/ngaccount/web/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+/// <reference types="svelte" />
+/// <reference types="vite/client" />
diff --git a/ngaccount/web/svelte.config.js b/ngaccount/web/svelte.config.js
new file mode 100644
index 0000000..b0683fd
--- /dev/null
+++ b/ngaccount/web/svelte.config.js
@@ -0,0 +1,7 @@
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
+
+export default {
+  // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
+  // for more information about preprocessors
+  preprocess: vitePreprocess(),
+}
diff --git a/ngaccount/web/tailwind.config.cjs b/ngaccount/web/tailwind.config.cjs
new file mode 100644
index 0000000..e338bf8
--- /dev/null
+++ b/ngaccount/web/tailwind.config.cjs
@@ -0,0 +1,23 @@
+/** @type {import('tailwindcss').Config}*/
+const config = {
+  content: [
+    "./src/**/*.{html,js,svelte,ts}",
+    "../../ng-app/src/**/*.{html,js,svelte,ts}",
+    "./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",
+  ],
+
+  theme: {
+    extend: {
+      colors: {
+        primary: { "50": "#eff6ff", "100": "#dbeafe", "200": "#bfdbfe", "300": "#93c5fd", "400": "#60a5fa", "500": "#3b82f6", "600": "#1E88E5", "700": "#4972A5", "800": "#1e40af", "900": "#1e3a8a" }
+      }
+    },
+  },
+
+  plugins: [
+    require('flowbite/plugin')
+  ],
+  darkMode: 'class',
+};
+
+module.exports = config;
diff --git a/ngaccount/web/vite.config.js b/ngaccount/web/vite.config.js
new file mode 100644
index 0000000..cb3bff8
--- /dev/null
+++ b/ngaccount/web/vite.config.js
@@ -0,0 +1,36 @@
+import { defineConfig } from 'vite'
+import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'
+import sveltePreprocess from "svelte-preprocess";
+import svelteSVG from "vite-plugin-svelte-svg";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [svelte({
+    preprocess: [
+      vitePreprocess(),
+      sveltePreprocess({
+        typescript: false,
+        postcss: true,
+      }),
+    ],
+  }),
+  svelteSVG({
+    svgoConfig: {
+      plugins: [
+          {
+              name: 'preset-default',
+              params: {
+                overrides: {
+                  // disable plugins
+                  removeViewBox: false,
+                },
+              },
+          },
+          {
+            name: 'prefixIds',
+          }
+      ],
+    }, // See https://github.com/svg/svgo#configuration
+    requireSuffix: true, // Set false to accept '.svg' without the '?component'
+  }),],
+})
diff --git a/ngd/src/cli.rs b/ngd/src/cli.rs
index 088e906..4edb285 100644
--- a/ngd/src/cli.rs
+++ b/ngd/src/cli.rs
@@ -104,6 +104,14 @@ pub(crate) struct Cli {
     #[arg(long)]
     pub no_ipv6: bool,
 
+    /// Registration of new users is off. default is invitation-only registration
+    #[arg(long)]
+    pub registration_off: bool,
+
+    /// Registration of new users is open to anybody without restriction. default is invitation-only registration
+    #[arg(long, conflicts_with("registration_off"))]
+    pub registration_open: bool,
+
     /// Saves the quick config into a file on disk, that can then be modified for advanced configs
     #[arg(long)]
     pub save_config: bool,
diff --git a/ngd/src/main.rs b/ngd/src/main.rs
index 97cae0f..97f75d8 100644
--- a/ngd/src/main.rs
+++ b/ngd/src/main.rs
@@ -908,9 +908,18 @@ async fn main_inner() -> Result<(), ()> {
             }];
         }
 
+        let registration = if args.registration_off {
+            RegistrationConfig::Closed
+        } else if args.registration_open {
+            RegistrationConfig::Open
+        } else {
+            RegistrationConfig::Invitation
+        };
+
         config = Some(DaemonConfig::V0(DaemonConfigV0 {
             listeners,
             overlays_configs: vec![overlays_config],
+            registration,
         }));
 
         if args.print_config {
diff --git a/ngone/README.md b/ngone/README.md
index 45e24fe..0ad39e1 100644
--- a/ngone/README.md
+++ b/ngone/README.md
@@ -1,6 +1,6 @@
 # nextgraph.one server (ngone)
 
-This server is used internally by NextGraph to redirect users to the right app server from web clients. You probably don't need this server in your infrastructure, even if you decide to self-host a broker under a domain name.
+This server is used internally by NextGraph to redirect users to the right app server from web clients. You probably don't need this server in your infrastructure, even if you decide to self-host a broker under your own domain name.
 
 ## Install
 
@@ -23,17 +23,6 @@ cargo watch -c -w src -x run
 
 ## Build
 
-First you will need to build the single-file release of ng-app.
-
-```
-// uncomment line 14 of src/App.svelte: import * as api from "ng-sdk-js";
-cd ../ng-app
-pnpm filebuild
-cd ../ngone
-```
-
-then, in ngone:
-
 ```
 cd web
 pnpm run build
diff --git a/ngone/src/main.rs b/ngone/src/main.rs
index 3ca6626..7651633 100644
--- a/ngone/src/main.rs
+++ b/ngone/src/main.rs
@@ -25,6 +25,7 @@ use std::{env, fs};
 use crate::store::wallet_record::*;
 use crate::types::*;
 use ng_wallet::types::*;
+use p2p_net::types::{APP_NG_ONE_URL, NG_ONE_URL};
 use p2p_repo::log::*;
 use p2p_repo::types::*;
 use p2p_repo::utils::{generate_keypair, sign, verify};
@@ -202,8 +203,8 @@ async fn main() {
     #[cfg(not(debug_assertions))]
     {
         cors = cors
-            .allow_origin("https://nextgraph.one")
-            .allow_origin("https://app.nextgraph.one")
+            .allow_origin(NG_ONE_URL)
+            .allow_origin(APP_NG_ONE_URL)
             .allow_origin("https://nextgraph.eu")
             .allow_origin("https://nextgraph.net");
     }
diff --git a/ngone/web/src/routes/WalletCreate.svelte b/ngone/web/src/routes/WalletCreate.svelte
index 19aa4bd..d5ca013 100644
--- a/ngone/web/src/routes/WalletCreate.svelte
+++ b/ngone/web/src/routes/WalletCreate.svelte
@@ -46,7 +46,7 @@
   </div>
 
   <p class="max-w-sm">
-    A <b>NextGraph Wallet</b> is unique to each individual.<br /> It stores your
+    A <b>NextGraph Wallet</b> is unique to each person.<br /> It stores your
     credentials to access documents. <br />If you already have a wallet, you
     should not create a new one, instead,
     {#if display_note_on_local_wallets}
@@ -190,7 +190,7 @@
     </a>
   </div>
   <div class="row mt-5">
-    <a href="https://docs.nextgraph.org/en/self-hosted">
+    <a href="https://nextgraph.org/self-host">
       <button
         tabindex="-1"
         class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mr-2 mb-2"
@@ -215,7 +215,7 @@
     </a>
   </div>
   <div class="row mt-5 mb-12">
-    <a href="#">
+    <a href="https://nextgraph.org/ng-box/">
       <button
         tabindex="-1"
         class="text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:outline-none focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mr-2 mb-2"
diff --git a/p2p-broker/src/broker_store/account.rs b/p2p-broker/src/broker_store/account.rs
index 3e928ca..ae7dc18 100644
--- a/p2p-broker/src/broker_store/account.rs
+++ b/p2p-broker/src/broker_store/account.rs
@@ -9,10 +9,15 @@
 
 //! User account
 
+use std::collections::hash_map::DefaultHasher;
+use std::hash::Hash;
+use std::hash::Hasher;
+use std::time::SystemTime;
+
 use p2p_net::types::*;
 use p2p_repo::kcv_store::KCVStore;
 use p2p_repo::store::*;
-use p2p_repo::types::*;
+use p2p_repo::types::Timestamp;
 use serde_bare::to_vec;
 
 pub struct Account<'a> {
@@ -23,13 +28,20 @@ pub struct Account<'a> {
 
 impl<'a> Account<'a> {
     const PREFIX: u8 = b"u"[0];
+    const PREFIX_CLIENT: u8 = b"d"[0];
 
     // propertie's suffixes
     const CLIENT: u8 = b"c"[0];
     const ADMIN: u8 = b"a"[0];
-    const OVERLAY: u8 = b"o"[0];
+    //const OVERLAY: u8 = b"o"[0];
+
+    // propertie's client suffixes
+    const INFO: u8 = b"i"[0];
+    const LAST_SEEN: u8 = b"l"[0];
+
+    const ALL_PROPERTIES: [u8; 2] = [Self::CLIENT, Self::ADMIN];
 
-    const ALL_PROPERTIES: [u8; 3] = [Self::CLIENT, Self::ADMIN, Self::OVERLAY];
+    const ALL_CLIENT_PROPERTIES: [u8; 2] = [Self::INFO, Self::LAST_SEEN];
 
     const SUFFIX_FOR_EXIST_CHECK: u8 = Self::ADMIN;
 
@@ -75,64 +87,105 @@ impl<'a> Account<'a> {
     pub fn id(&self) -> UserId {
         self.id
     }
-    pub fn add_client(&self, client: &ClientId) -> Result<(), StorageError> {
+    pub fn add_client(&self, client: &ClientId, info: &ClientInfo) -> Result<(), StorageError> {
         if !self.exists() {
             return Err(StorageError::BackendError);
         }
-        self.store.put(
-            Self::PREFIX,
-            &to_vec(&self.id)?,
-            Some(Self::CLIENT),
-            to_vec(client)?,
-        )
-    }
-    pub fn remove_client(&self, client: &ClientId) -> Result<(), StorageError> {
-        self.store.del_property_value(
-            Self::PREFIX,
-            &to_vec(&self.id)?,
-            Some(Self::CLIENT),
-            to_vec(client)?,
-        )
-    }
 
-    pub fn has_client(&self, client: &ClientId) -> Result<(), StorageError> {
-        self.store.has_property_value(
-            Self::PREFIX,
-            &to_vec(&self.id)?,
-            Some(Self::CLIENT),
-            to_vec(client)?,
-        )
-    }
+        let mut s = DefaultHasher::new();
+        info.hash(&mut s);
+        let hash = s.finish();
 
-    pub fn add_overlay(&self, overlay: &OverlayId) -> Result<(), StorageError> {
-        if !self.exists() {
-            return Err(StorageError::BackendError);
-        }
-        self.store.put(
-            Self::PREFIX,
-            &to_vec(&self.id)?,
-            Some(Self::OVERLAY),
-            to_vec(overlay)?,
-        )
-    }
-    pub fn remove_overlay(&self, overlay: &OverlayId) -> Result<(), StorageError> {
-        self.store.del_property_value(
-            Self::PREFIX,
-            &to_vec(&self.id)?,
-            Some(Self::OVERLAY),
-            to_vec(overlay)?,
-        )
-    }
+        let client_key = (client.clone(), hash);
+        let client_key_ser = to_vec(&client_key)?;
 
-    pub fn has_overlay(&self, overlay: &OverlayId) -> Result<(), StorageError> {
-        self.store.has_property_value(
-            Self::PREFIX,
-            &to_vec(&self.id)?,
-            Some(Self::OVERLAY),
-            to_vec(overlay)?,
-        )
+        let info_ser = to_vec(info)?;
+
+        self.store.write_transaction(&|tx| {
+            if tx
+                .has_property_value(
+                    Self::PREFIX,
+                    &to_vec(&self.id)?,
+                    Some(Self::CLIENT),
+                    &client_key_ser,
+                )
+                .is_err()
+            {
+                tx.put(
+                    Self::PREFIX,
+                    &to_vec(&self.id)?,
+                    Some(Self::CLIENT),
+                    &client_key_ser,
+                )?;
+            }
+            if tx
+                .has_property_value(
+                    Self::PREFIX_CLIENT,
+                    &client_key_ser,
+                    Some(Self::INFO),
+                    &info_ser,
+                )
+                .is_err()
+            {
+                tx.put(
+                    Self::PREFIX_CLIENT,
+                    &client_key_ser,
+                    Some(Self::INFO),
+                    &info_ser,
+                )?;
+            }
+            let now = SystemTime::now()
+                .duration_since(SystemTime::UNIX_EPOCH)
+                .unwrap()
+                .as_secs();
+            tx.replace(
+                Self::PREFIX_CLIENT,
+                &client_key_ser,
+                Some(Self::LAST_SEEN),
+                &to_vec(&now)?,
+            )?;
+            Ok(())
+        })
     }
 
+    // pub fn has_client(&self, client: &ClientId) -> Result<(), StorageError> {
+    //     self.store.has_property_value(
+    //         Self::PREFIX,
+    //         &to_vec(&self.id)?,
+    //         Some(Self::CLIENT),
+    //         to_vec(client)?,
+    //     )
+    // }
+
+    // pub fn add_overlay(&self, overlay: &OverlayId) -> Result<(), StorageError> {
+    //     if !self.exists() {
+    //         return Err(StorageError::BackendError);
+    //     }
+    //     self.store.put(
+    //         Self::PREFIX,
+    //         &to_vec(&self.id)?,
+    //         Some(Self::OVERLAY),
+    //         to_vec(overlay)?,
+    //     )
+    // }
+    // pub fn remove_overlay(&self, overlay: &OverlayId) -> Result<(), StorageError> {
+    //     self.store.del_property_value(
+    //         Self::PREFIX,
+    //         &to_vec(&self.id)?,
+    //         Some(Self::OVERLAY),
+    //         to_vec(overlay)?,
+    //     )
+    // }
+
+    // pub fn has_overlay(&self, overlay: &OverlayId) -> Result<(), StorageError> {
+    //     self.store.has_property_value(
+    //         Self::PREFIX,
+    //         &to_vec(&self.id)?,
+    //         Some(Self::OVERLAY),
+    //         to_vec(overlay)?,
+    //     )
+    // }
+
     pub fn is_admin(&self) -> Result<bool, StorageError> {
         if self
             .store
@@ -140,7 +193,7 @@ impl<'a> Account<'a> {
                 Self::PREFIX,
                 &to_vec(&self.id)?,
                 Some(Self::ADMIN),
-                to_vec(&true)?,
+                &to_vec(&true)?,
             )
             .is_ok()
         {
@@ -150,8 +203,15 @@ impl<'a> Account<'a> {
     }
 
     pub fn del(&self) -> Result<(), StorageError> {
-        self.store
-            .del_all(Self::PREFIX, &to_vec(&self.id)?, &Self::ALL_PROPERTIES)
+        self.store.write_transaction(&|tx| {
+            if let Ok(clients) = tx.get_all(Self::PREFIX, &to_vec(&self.id)?, Some(Self::CLIENT)) {
+                for client in clients {
+                    tx.del_all(Self::PREFIX_CLIENT, &client, &Self::ALL_CLIENT_PROPERTIES)?;
+                }
+            }
+            tx.del_all(Self::PREFIX, &to_vec(&self.id)?, &Self::ALL_PROPERTIES)?;
+            Ok(())
+        })
     }
 }
 
@@ -161,8 +221,8 @@ mod test {
     use p2p_repo::store::*;
     use p2p_repo::types::*;
     use p2p_repo::utils::*;
-    use stores_lmdb::kcv_store::LmdbKCVStore;
     use std::fs;
+    use stores_lmdb::kcv_store::LmdbKCVStore;
     use tempfile::Builder;
 
     use crate::broker_store::account::Account;
@@ -184,14 +244,14 @@ mod test {
         let account2 = Account::open(&user_id, &store).unwrap();
         println!("account opened {}", account2.id());
 
-        let client_id = PubKey::Ed25519PubKey([56; 32]);
-        let client_id_not_added = PubKey::Ed25519PubKey([57; 32]);
+        // let client_id = PubKey::Ed25519PubKey([56; 32]);
+        // let client_id_not_added = PubKey::Ed25519PubKey([57; 32]);
 
-        account2.add_client(&client_id).unwrap();
+        // account2.add_client(&client_id).unwrap();
 
-        assert!(account2.is_admin().unwrap());
+        // assert!(account2.is_admin().unwrap());
 
-        account.has_client(&client_id).unwrap();
-        assert!(account.has_client(&client_id_not_added).is_err());
+        // account.has_client(&client_id).unwrap();
+        // assert!(account.has_client(&client_id_not_added).is_err());
     }
 }
diff --git a/p2p-broker/src/broker_store/invitation.rs b/p2p-broker/src/broker_store/invitation.rs
new file mode 100644
index 0000000..f66b5bf
--- /dev/null
+++ b/p2p-broker/src/broker_store/invitation.rs
@@ -0,0 +1,157 @@
+// Copyright (c) 2022-2023 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.
+
+//! User account
+
+use std::collections::hash_map::DefaultHasher;
+use std::hash::Hash;
+use std::hash::Hasher;
+use std::time::SystemTime;
+
+use p2p_net::types::*;
+use p2p_repo::kcv_store::KCVStore;
+use p2p_repo::store::*;
+use p2p_repo::types::Timestamp;
+use p2p_repo::utils::now_timestamp;
+use serde_bare::from_slice;
+use serde_bare::to_vec;
+
+pub struct Invitation<'a> {
+    /// User ID
+    id: [u8; 32],
+    store: &'a dyn KCVStore,
+}
+
+impl<'a> Invitation<'a> {
+    const PREFIX: u8 = b"i"[0];
+
+    // propertie's invitation suffixes
+    const TYPE: u8 = b"t"[0];
+    const EXPIRE: u8 = b"e"[0];
+
+    const ALL_PROPERTIES: [u8; 2] = [Self::TYPE, Self::EXPIRE];
+
+    const SUFFIX_FOR_EXIST_CHECK: u8 = Self::TYPE;
+
+    pub fn open(id: &[u8; 32], store: &'a dyn KCVStore) -> Result<Invitation<'a>, StorageError> {
+        let opening = Invitation {
+            id: id.clone(),
+            store,
+        };
+        if !opening.exists() {
+            return Err(StorageError::NotFound);
+        }
+        Ok(opening)
+    }
+    pub fn create(
+        id: &InvitationCode,
+        expiry: u32,
+        store: &'a dyn KCVStore,
+    ) -> Result<Invitation<'a>, StorageError> {
+        let (code_type, code) = match id {
+            InvitationCode::Unique(c) => (0, c.slice()),
+            InvitationCode::Multi(c) => (1, c.slice()),
+            InvitationCode::Admin(c) => (2, c.slice()),
+        };
+        let acc = Invitation {
+            id: code.clone(),
+            store,
+        };
+        if acc.exists() {
+            return Err(StorageError::BackendError);
+        }
+        store.write_transaction(&|tx| {
+            tx.put(
+                Self::PREFIX,
+                &to_vec(code)?,
+                Some(Self::TYPE),
+                &to_vec(&code_type)?,
+            )?;
+            tx.put(
+                Self::PREFIX,
+                &to_vec(code)?,
+                Some(Self::EXPIRE),
+                &to_vec(&expiry)?,
+            )?;
+            Ok(())
+        })?;
+        Ok(acc)
+    }
+    pub fn exists(&self) -> bool {
+        self.store
+            .get(
+                Self::PREFIX,
+                &to_vec(&self.id).unwrap(),
+                Some(Self::SUFFIX_FOR_EXIST_CHECK),
+            )
+            .is_ok()
+    }
+    pub fn id(&self) -> [u8; 32] {
+        self.id
+    }
+
+    pub fn is_expired(&self) -> Result<bool, StorageError> {
+        let expire_ser = self
+            .store
+            .get(Self::PREFIX, &to_vec(&self.id)?, Some(Self::EXPIRE))?;
+        let expire: u32 = from_slice(&expire_ser)?;
+        if expire < now_timestamp() {
+            return Ok(true);
+        }
+        Ok(false)
+    }
+
+    pub fn del(&self) -> Result<(), StorageError> {
+        self.store.write_transaction(&|tx| {
+            tx.del_all(Self::PREFIX, &to_vec(&self.id)?, &Self::ALL_PROPERTIES)?;
+            Ok(())
+        })
+    }
+}
+
+#[cfg(test)]
+mod test {
+
+    use p2p_repo::store::*;
+    use p2p_repo::types::*;
+    use p2p_repo::utils::*;
+    use std::fs;
+    use stores_lmdb::kcv_store::LmdbKCVStore;
+    use tempfile::Builder;
+
+    use crate::broker_store::account::Account;
+
+    #[test]
+    pub fn test_account() {
+        let path_str = "test-env";
+        let root = Builder::new().prefix(path_str).tempdir().unwrap();
+        let key: [u8; 32] = [0; 32];
+        fs::create_dir_all(root.path()).unwrap();
+        println!("{}", root.path().to_str().unwrap());
+        let mut store = LmdbKCVStore::open(root.path(), key);
+
+        let user_id = PubKey::Ed25519PubKey([1; 32]);
+
+        let account = Account::create(&user_id, true, &store).unwrap();
+        println!("account created {}", account.id());
+
+        let account2 = Account::open(&user_id, &store).unwrap();
+        println!("account opened {}", account2.id());
+
+        // let client_id = PubKey::Ed25519PubKey([56; 32]);
+        // let client_id_not_added = PubKey::Ed25519PubKey([57; 32]);
+
+        // account2.add_client(&client_id).unwrap();
+
+        // assert!(account2.is_admin().unwrap());
+
+        // account.has_client(&client_id).unwrap();
+        // assert!(account.has_client(&client_id_not_added).is_err());
+    }
+}
diff --git a/p2p-broker/src/broker_store/mod.rs b/p2p-broker/src/broker_store/mod.rs
index 9897e67..8fcb113 100644
--- a/p2p-broker/src/broker_store/mod.rs
+++ b/p2p-broker/src/broker_store/mod.rs
@@ -8,4 +8,6 @@ pub mod peer;
 
 pub mod repostoreinfo;
 
-pub mod topic;
\ No newline at end of file
+pub mod topic;
+
+pub mod invitation;
diff --git a/p2p-broker/src/broker_store/overlay.rs b/p2p-broker/src/broker_store/overlay.rs
index b7cde3e..3347688 100644
--- a/p2p-broker/src/broker_store/overlay.rs
+++ b/p2p-broker/src/broker_store/overlay.rs
@@ -139,7 +139,7 @@ impl<'a> Overlay<'a> {
             Self::PREFIX,
             &to_vec(&self.id)?,
             Some(Self::PEER),
-            to_vec(peer)?,
+            &to_vec(peer)?,
         )
     }
 
@@ -168,7 +168,7 @@ impl<'a> Overlay<'a> {
             Self::PREFIX,
             &to_vec(&self.id)?,
             Some(Self::TOPIC),
-            to_vec(topic)?,
+            &to_vec(topic)?,
         )
     }
 
diff --git a/p2p-broker/src/broker_store/topic.rs b/p2p-broker/src/broker_store/topic.rs
index 3fdb1eb..1136224 100644
--- a/p2p-broker/src/broker_store/topic.rs
+++ b/p2p-broker/src/broker_store/topic.rs
@@ -104,7 +104,7 @@ impl<'a> Topic<'a> {
             Self::PREFIX,
             &to_vec(&self.id)?,
             Some(Self::HEAD),
-            to_vec(head)?,
+            &to_vec(head)?,
         )
     }
 
diff --git a/p2p-broker/src/lib.rs b/p2p-broker/src/lib.rs
index e1eb639..91f431b 100644
--- a/p2p-broker/src/lib.rs
+++ b/p2p-broker/src/lib.rs
@@ -7,3 +7,5 @@ pub mod types;
 pub mod utils;
 
 pub mod interfaces;
+
+pub mod storage;
diff --git a/p2p-broker/src/server_ws.rs b/p2p-broker/src/server_ws.rs
index 40e0e76..328f020 100644
--- a/p2p-broker/src/server_ws.rs
+++ b/p2p-broker/src/server_ws.rs
@@ -12,6 +12,7 @@
 //! WebSocket implementation of the Broker
 
 use crate::interfaces::*;
+use crate::storage::LmdbBrokerStorage;
 use crate::types::*;
 use async_std::io::ReadExt;
 use async_std::net::{TcpListener, TcpStream};
@@ -38,6 +39,7 @@ use p2p_net::types::*;
 use p2p_net::utils::get_domain_without_port;
 use p2p_net::utils::is_private_ip;
 use p2p_net::utils::is_public_ip;
+use p2p_net::NG_BOOTSTRAP_LOCAL_URL;
 use p2p_repo::log::*;
 use p2p_repo::types::SymKey;
 use p2p_repo::types::{PrivKey, PubKey};
@@ -54,8 +56,7 @@ use std::ops::Deref;
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use std::{thread, time};
-use stores_lmdb::kcv_store::LmdbKCVStore;
-use stores_lmdb::repo_store::LmdbRepoStore;
+
 use tempfile::Builder;
 
 static LISTENERS_INFO: OnceCell<(HashMap<String, ListenerInfo>, HashMap<BindAddress, String>)> =
@@ -90,7 +91,7 @@ fn check_no_origin(origin: Option<&HeaderValue>) -> Result<(), ErrorResponse> {
 
 fn check_origin_is_url(
     origin: Option<&HeaderValue>,
-    domains: Vec<String>,
+    domains: &Vec<String>,
 ) -> Result<(), ErrorResponse> {
     match origin {
         None => Ok(()),
@@ -213,6 +214,7 @@ fn upgrade_ws_or_serve_app(
     serve_app: bool,
     uri: &Uri,
     last_etag: Option<&HeaderValue>,
+    cors: Option<&str>,
 ) -> Result<(), ErrorResponse> {
     if connection.is_some()
         && connection
@@ -253,13 +255,16 @@ fn upgrade_ws_or_serve_app(
                 .body(Some(file.data.to_vec()))
                 .unwrap();
             return Err(res);
-        } else if uri == "/.ng_bootstrap" {
+        } else if uri == NG_BOOTSTRAP_LOCAL_URL {
             log_debug!("Serving bootstrap");
 
-            let res = Response::builder()
-                .status(StatusCode::OK)
+            let mut builder = Response::builder().status(StatusCode::OK);
+            if cors.is_some() {
+                builder = builder.header("Access-Control-Allow-Origin", cors.unwrap());
+            }
+            let res = builder
                 .header("Content-Type", "text/json")
-                .header("Cache-Control", "max-age=3600, must-revalidate")
+                .header("Cache-Control", "max-age=0, must-revalidate")
                 .body(Some(BOOTSTRAP_STRING.get().unwrap().as_bytes().to_vec()))
                 .unwrap();
             return Err(res);
@@ -354,9 +359,10 @@ impl Callback for SecurityCallback {
                 return upgrade_ws_or_serve_app(
                     connection,
                     remote,
-                    listener.config.serve_app,
+                    listener.config.serve_app && !listener.config.refuse_clients,
                     uri,
                     last_etag,
+                    None,
                 );
             }
             InterfaceType::Loopback => {
@@ -372,7 +378,7 @@ impl Callback for SecurityCallback {
                         // TODO local_urls might need a trailing :port, but it is ok for now as we do starts_with
                         urls_str = [urls_str, local_urls].concat();
                     }
-                    check_origin_is_url(origin, urls_str)?;
+                    check_origin_is_url(origin, &urls_str)?;
                     check_host(host, hosts_str)?;
                     check_xff_is_public_or_private(xff, listener.config.accept_direct, true)?;
                     log_debug!(
@@ -385,11 +391,18 @@ impl Callback for SecurityCallback {
                         listener.config.serve_app,
                         uri,
                         last_etag,
+                        origin.map(|or| or.to_str().unwrap()).and_then(|val| {
+                            if listener.config.refuse_clients {
+                                None
+                            } else {
+                                Some(val)
+                            }
+                        }),
                     );
                 } else if listener.config.accept_forward_for.is_private_domain() {
                     let (hosts_str, urls_str) =
                         prepare_domain_url_and_host(&listener.config.accept_forward_for);
-                    check_origin_is_url(origin, urls_str)?;
+                    check_origin_is_url(origin, &urls_str)?;
                     check_host(host, hosts_str)?;
                     check_xff_is_public_or_private(xff, false, false)?;
                     log_debug!("accepted loopback PRIVATE_DOMAIN");
@@ -399,12 +412,13 @@ impl Callback for SecurityCallback {
                         listener.config.serve_app,
                         uri,
                         last_etag,
+                        origin.map(|or| or.to_str().unwrap()),
                     );
                 } else if listener.config.accept_forward_for == AcceptForwardForV0::No {
                     check_host(host, local_hosts)?;
                     check_no_xff(xff)?;
                     // TODO local_urls might need a trailing :port, but it is ok for now as we do starts_with
-                    check_origin_is_url(origin, local_urls)?;
+                    check_origin_is_url(origin, &local_urls)?;
                     log_debug!("accepted loopback DIRECT");
                     return upgrade_ws_or_serve_app(
                         connection,
@@ -412,6 +426,7 @@ impl Callback for SecurityCallback {
                         listener.config.serve_app,
                         uri,
                         last_etag,
+                        origin.map(|or| or.to_str().unwrap()),
                     );
                 }
             }
@@ -444,7 +459,7 @@ impl Callback for SecurityCallback {
                         ]
                         .concat();
                     }
-                    check_origin_is_url(origin, urls_str)?;
+                    check_origin_is_url(origin, &urls_str)?;
                     check_host_in_addrs(host, &addrs)?;
                     log_debug!("accepted private PUBLIC_STATIC or PUBLIC_DYN with direct {} with refuse_clients {}",listener.config.accept_direct, listener.config.refuse_clients);
                     return upgrade_ws_or_serve_app(
@@ -453,6 +468,7 @@ impl Callback for SecurityCallback {
                         listener.config.serve_app,
                         uri,
                         last_etag,
+                        origin.map(|or| or.to_str().unwrap()),
                     );
                 } else if listener.config.accept_forward_for.is_public_domain() {
                     if !remote.is_private() {
@@ -473,7 +489,7 @@ impl Callback for SecurityCallback {
                         ]
                         .concat();
                     }
-                    check_origin_is_url(origin, urls_str)?;
+                    check_origin_is_url(origin, &urls_str)?;
                     check_host(host, hosts_str)?;
                     log_debug!(
                         "accepted private PUBLIC_DOMAIN with direct {}",
@@ -485,6 +501,13 @@ impl Callback for SecurityCallback {
                         listener.config.serve_app,
                         uri,
                         last_etag,
+                        origin.map(|or| or.to_str().unwrap()).and_then(|val| {
+                            if listener.config.refuse_clients {
+                                None
+                            } else {
+                                Some(val)
+                            }
+                        }),
                     );
                 } else if listener.config.accept_forward_for == AcceptForwardForV0::No {
                     if !remote.is_private() {
@@ -494,10 +517,9 @@ impl Callback for SecurityCallback {
                     check_no_xff(xff)?;
 
                     check_host_in_addrs(host, &listener.addrs)?;
-                    check_origin_is_url(
-                        origin,
-                        prepare_urls_from_private_addrs(&listener.addrs, listener.config.port),
-                    )?;
+                    let urls_str =
+                        prepare_urls_from_private_addrs(&listener.addrs, listener.config.port);
+                    check_origin_is_url(origin, &urls_str)?;
                     log_debug!("accepted private DIRECT");
                     return upgrade_ws_or_serve_app(
                         connection,
@@ -505,6 +527,7 @@ impl Callback for SecurityCallback {
                         listener.config.serve_app,
                         uri,
                         last_etag,
+                        origin.map(|or| or.to_str().unwrap()),
                     );
                 }
             }
@@ -560,10 +583,10 @@ pub async fn run_server_accept_one(
 ) -> std::io::Result<()> {
     let addrs = format!("{}:{}", addr, port);
     let root = tempfile::Builder::new().prefix("ngd").tempdir().unwrap();
-    let master_key: [u8; 32] = [0; 32];
-    std::fs::create_dir_all(root.path()).unwrap();
-    log_debug!("data directory: {}", root.path().to_str().unwrap());
-    let store = LmdbKCVStore::open(root.path(), master_key);
+    // let master_key: [u8; 32] = [0; 32];
+    // std::fs::create_dir_all(root.path()).unwrap();
+    // log_debug!("data directory: {}", root.path().to_str().unwrap());
+    // let store = LmdbKCVStore::open(root.path(), master_key);
 
     let socket = TcpListener::bind(addrs.as_str()).await?;
     log_debug!("Listening on {}", addrs.as_str());
@@ -619,16 +642,6 @@ pub async fn run_server_v0(
             return Err(());
         }
     }
-    //let root = tempfile::Builder::new().prefix("ngd").tempdir().unwrap();
-
-    path.push("storage");
-    std::fs::create_dir_all(path.clone()).unwrap();
-    //log::info!("Home directory is {}");
-
-    // TODO: open wallet
-    let master_key: [u8; 32] = [0; 32];
-
-    let store = LmdbKCVStore::open(&path, master_key);
 
     let interfaces = get_interface();
     let mut listener_infos: HashMap<String, ListenerInfo> = HashMap::new();
@@ -748,8 +761,16 @@ pub async fn run_server_v0(
     // saving the infos in the broker. This needs to happen before we start listening, as new incoming connections can happen anytime after that.
     // and we need those infos for permission checking.
     {
+        //let root = tempfile::Builder::new().prefix("ngd").tempdir().unwrap();
+        path.push("storage");
+        std::fs::create_dir_all(path.clone()).unwrap();
+
+        // opening the server storage (that contains the encryption keys for each store/overlay )
+        let broker_storage = LmdbBrokerStorage::open(&mut path, wallet_master_key);
+
         let mut broker = BROKER.write().await;
         broker.set_my_peer_id(peer_id);
+        broker.set_storage(broker_storage);
         LISTENERS_INFO
             .set(broker.set_listeners(listener_infos))
             .unwrap();
diff --git a/p2p-broker/src/storage.rs b/p2p-broker/src/storage.rs
new file mode 100644
index 0000000..38649c4
--- /dev/null
+++ b/p2p-broker/src/storage.rs
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022-2023 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::path::PathBuf;
+
+use crate::types::*;
+use p2p_net::broker_storage::*;
+use p2p_repo::kcv_store::KCVStore;
+use p2p_repo::types::SymKey;
+use stores_lmdb::kcv_store::LmdbKCVStore;
+use stores_lmdb::repo_store::LmdbRepoStore;
+
+pub struct LmdbBrokerStorage {
+    wallet_storage: LmdbKCVStore,
+}
+
+impl LmdbBrokerStorage {
+    pub fn open(path: &mut PathBuf, master_key: SymKey) -> Self {
+        path.push("wallet");
+        std::fs::create_dir_all(path.clone()).unwrap();
+        //TODO redo the whole key passing mechanism so it uses zeroize all the way
+        let wallet_storage = LmdbKCVStore::open(&path, master_key.slice().clone());
+        LmdbBrokerStorage { wallet_storage }
+    }
+}
+
+impl BrokerStorage for LmdbBrokerStorage {
+    fn get_user(&self) {}
+}
diff --git a/p2p-broker/src/types.rs b/p2p-broker/src/types.rs
index b216294..d2e70da 100644
--- a/p2p-broker/src/types.rs
+++ b/p2p-broker/src/types.rs
@@ -10,6 +10,14 @@ use p2p_net::types::{BrokerOverlayConfigV0, ListenerV0};
 use p2p_repo::types::PrivKey;
 use serde::{Deserialize, Serialize};
 
+/// Registration config
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum RegistrationConfig {
+    Closed,
+    Invitation,
+    Open,
+}
+
 /// DaemonConfig Version 0
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct DaemonConfigV0 {
@@ -17,6 +25,8 @@ pub struct DaemonConfigV0 {
     pub listeners: Vec<ListenerV0>,
 
     pub overlays_configs: Vec<BrokerOverlayConfigV0>,
+
+    pub registration: RegistrationConfig,
 }
 
 /// Daemon config
diff --git a/p2p-net/Cargo.toml b/p2p-net/Cargo.toml
index 5b9bb9d..8e0f1d2 100644
--- a/p2p-net/Cargo.toml
+++ b/p2p-net/Cargo.toml
@@ -25,9 +25,10 @@ noise-protocol = "0.2.0-rc1"
 noise-rust-crypto = "0.6.0-rc.1"
 ed25519-dalek = "1.0.1"
 either = "1.8.1"
-reqwest = { version = "0.11.18", features = ["json"] }
+reqwest = { version = "0.11.18", features = ["json","native-tls-vendored"] }
 url = "2.4.0"
 base64-url = "2.0.0"
+web-time = "0.2.0"
 
 [target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]
 version = "0.2.7"
diff --git a/p2p-net/src/broker.rs b/p2p-net/src/broker.rs
index c836316..b9069a0 100644
--- a/p2p-net/src/broker.rs
+++ b/p2p-net/src/broker.rs
@@ -10,6 +10,7 @@
 */
 
 use crate::actor::*;
+use crate::broker_storage::BrokerStorage;
 use crate::connection::*;
 use crate::errors::*;
 use crate::types::*;
@@ -75,6 +76,7 @@ pub struct Broker {
     shutdown_sender: Sender<ProtocolError>,
     closing: bool,
     my_peer_id: Option<PubKey>,
+    storage: Option<Box<dyn BrokerStorage + Send + Sync>>,
 
     test: u32,
     tauri_streams: HashMap<String, Sender<Commit>>,
@@ -102,6 +104,10 @@ impl Broker {
         }
     }
 
+    pub fn set_storage(&mut self, storage: impl BrokerStorage + 'static) {
+        self.storage = Some(Box::new(storage));
+    }
+
     #[cfg(not(target_arch = "wasm32"))]
     pub fn set_listeners(
         &mut self,
@@ -314,6 +320,7 @@ impl Broker {
             closing: false,
             test: u32::from_be_bytes(random_buf),
             my_peer_id: None,
+            storage: None,
         }
     }
 
diff --git a/p2p-net/src/broker_storage.rs b/p2p-net/src/broker_storage.rs
new file mode 100644
index 0000000..76b6b43
--- /dev/null
+++ b/p2p-net/src/broker_storage.rs
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2022-2023 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 crate::types::*;
+use p2p_repo::kcv_store::KCVStore;
+
+pub trait BrokerStorage: Send + Sync {
+    fn get_user(&self);
+}
diff --git a/p2p-net/src/lib.rs b/p2p-net/src/lib.rs
index 9f20fef..83a0b0d 100644
--- a/p2p-net/src/lib.rs
+++ b/p2p-net/src/lib.rs
@@ -19,6 +19,8 @@ pub mod errors;
 
 pub mod broker;
 
+pub mod broker_storage;
+
 pub mod connection;
 
 pub mod actor;
@@ -31,6 +33,8 @@ pub mod tests;
 
 pub mod site;
 
+pub static NG_BOOTSTRAP_LOCAL_URL: &str = "/.ng_bootstrap";
+
 #[cfg(debug_assertions)]
 pub static WS_PORT: u16 = 14400;
 
diff --git a/p2p-net/src/types.rs b/p2p-net/src/types.rs
index 95995ee..83a8cb0 100644
--- a/p2p-net/src/types.rs
+++ b/p2p-net/src/types.rs
@@ -19,6 +19,7 @@ use crate::utils::{
 };
 use crate::{actor::EActor, actors::*, errors::ProtocolError};
 use core::fmt;
+use p2p_repo::errors::NgError;
 use p2p_repo::types::*;
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
@@ -26,6 +27,7 @@ use std::{
     any::{Any, TypeId},
     net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
 };
+use web_time::SystemTime;
 
 //
 //  Broker common types
@@ -228,6 +230,8 @@ pub struct BrokerServerV0 {
     pub peer_id: PubKey,
 }
 
+pub const NG_ONE_URL: &str = "https://nextgraph.one";
+
 pub const APP_NG_ONE_URL: &str = "https://app.nextgraph.one";
 
 pub const APP_NG_ONE_WS_URL: &str = "wss://app.nextgraph.one";
@@ -242,6 +246,10 @@ fn local_ws_url(port: &u16) -> String {
     format!("ws://localhost:{}", if *port == 0 { 80 } else { *port })
 }
 
+fn local_http_url(port: &u16) -> String {
+    format!("http://localhost:{}", if *port == 0 { 80 } else { *port })
+}
+
 pub const LOCAL_URLS: [&str; 3] = ["http://localhost", "http://127.0.0.1", "http://[::1]"];
 use url::{Host, Url};
 
@@ -316,7 +324,7 @@ impl BrokerServerV0 {
         }
         Some(format!(
             "{}?b={}",
-            APP_NG_ONE_WS_URL,
+            APP_NG_ONE_URL,
             base64_url::encode(&payload_ser.unwrap())
         ))
     }
@@ -369,16 +377,20 @@ impl BrokerServerV0 {
                 }
             }
             BrokerServerTypeV0::Domain(domain) => Some(format!("https://{}", domain)),
-            BrokerServerTypeV0::Localhost(port) => Some(local_ws_url(&port)),
+            BrokerServerTypeV0::Localhost(port) => Some(local_http_url(&port)),
             BrokerServerTypeV0::BoxPrivate(_) => {
                 if ipv6 {
-                    let v6 = self.first_ipv6().map(|v| v.0);
+                    let v6 = self.server_type.find_first_ipv6().map_or(None, |bindaddr| {
+                        Some(format!("http://{}:{}", bindaddr.ip, bindaddr.port))
+                    });
                     if v6.is_some() {
                         return v6;
                     }
                 }
                 if ipv4 {
-                    self.first_ipv4().map(|v| v.0)
+                    self.server_type.find_first_ipv4().map_or(None, |bindaddr| {
+                        Some(format!("http://{}:{}", bindaddr.ip, bindaddr.port))
+                    })
                 } else {
                     None
                 }
@@ -387,11 +399,21 @@ impl BrokerServerV0 {
         }
     }
 
+    pub async fn is_public_broker(&self) -> bool {
+        match &self.server_type {
+            BrokerServerTypeV0::Localhost(_) => false,
+            BrokerServerTypeV0::BoxPrivate(_) => false,
+            BrokerServerTypeV0::BoxPublic(_) => true,
+            BrokerServerTypeV0::BoxPublicDyn(_) => true,
+            BrokerServerTypeV0::Domain(_) => true,
+        }
+    }
+
     /// on web browser, returns the connection URL and an optional list of BindAddress if a relay is needed
     /// filtered by the current location url of the webpage
-    /// on native apps, returns or the connection URL without optional BindAddress or an empty string with
+    /// on native apps (do not pass a location), returns or the connection URL without optional BindAddress or an empty string with
     /// several BindAddresses to try to connect to with .to_ws_url()
-    pub async fn get_url(&self, location: Option<String>) -> Option<(String, Vec<BindAddress>)> {
+    pub async fn get_ws_url(&self, location: Option<String>) -> Option<(String, Vec<BindAddress>)> {
         if location.is_some() {
             let location = location.unwrap();
             if location.starts_with(APP_NG_ONE_URL) {
@@ -498,6 +520,173 @@ pub enum BootstrapContent {
     V0(BootstrapContentV0),
 }
 
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum InvitationCode {
+    Unique(SymKey),
+    Admin(SymKey),
+    Multi(SymKey),
+}
+
+/// Invitation to create an account at a broker. Version 0
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct InvitationV0 {
+    /// list of servers, in order of preference
+    pub bootstrap: BootstrapContentV0,
+
+    pub code: Option<SymKey>,
+
+    /// an optional name to display to the invitee
+    pub name: Option<String>,
+
+    // an optional url to redirect the user to, for accepting ToS and making payment, if any.
+    pub url: Option<String>,
+}
+
+impl Invitation {
+    pub fn new_v0(
+        bootstrap: BootstrapContentV0,
+        name: Option<String>,
+        url: Option<String>,
+    ) -> Self {
+        Invitation::V0(InvitationV0 {
+            bootstrap,
+            code: Some(SymKey::random()),
+            name,
+            url,
+        })
+    }
+
+    pub fn new_v0_free(
+        bootstrap: BootstrapContentV0,
+        name: Option<String>,
+        url: Option<String>,
+    ) -> Self {
+        Invitation::V0(InvitationV0 {
+            bootstrap,
+            code: None,
+            name,
+            url,
+        })
+    }
+
+    pub fn intersects(&self, invite2: Invitation) -> Invitation {
+        let Invitation::V0(v0) = self;
+        let mut new_invite = InvitationV0 {
+            bootstrap: BootstrapContentV0 { servers: vec![] },
+            code: v0.code.clone(),
+            name: v0.name.clone(),
+            url: v0.url.clone(),
+        };
+        for server2 in invite2.get_servers() {
+            for server1 in &v0.bootstrap.servers {
+                if *server1 == *server2 {
+                    new_invite.bootstrap.servers.push(server2.clone());
+                    break;
+                }
+            }
+        }
+        Invitation::V0(new_invite)
+    }
+
+    pub fn get_servers(&self) -> &Vec<BrokerServerV0> {
+        match self {
+            Invitation::V0(v0) => &v0.bootstrap.servers,
+        }
+    }
+
+    /// first URL in the list is the ngone one
+    pub fn get_urls(&self) -> Vec<String> {
+        match self {
+            Invitation::V0(v0) => {
+                let mut res = vec![];
+                let ser = serde_bare::to_vec(&self).unwrap();
+                let url_param = base64_url::encode(&ser);
+                res.push(format!("{}/#/i/{}", NG_ONE_URL, url_param));
+                for server in &v0.bootstrap.servers {
+                    match &server.server_type {
+                        BrokerServerTypeV0::Domain(domain) => {
+                            res.push(format!("https://{}/#/i/{}", domain, url_param));
+                        }
+                        BrokerServerTypeV0::BoxPrivate(addrs) => {
+                            for bindaddr in addrs {
+                                res.push(format!(
+                                    "http://{}:{}/#/i/{}",
+                                    bindaddr.ip, bindaddr.port, url_param
+                                ));
+                            }
+                        }
+                        BrokerServerTypeV0::Localhost(port) => {
+                            res.push(format!("{}/#/i/{}", local_http_url(&port), url_param));
+                        }
+                        _ => {}
+                    }
+                }
+                res
+            }
+        }
+    }
+}
+
+impl TryFrom<String> for Invitation {
+    type Error = NgError;
+    fn try_from(value: String) -> Result<Self, NgError> {
+        let ser = base64_url::decode(&value).map_err(|_| NgError::InvalidInvitation)?;
+        let invite: Invitation =
+            serde_bare::from_slice(&ser).map_err(|_| NgError::InvalidInvitation)?;
+        Ok(invite)
+    }
+}
+
+/// Invitation to create an account at a broker.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum Invitation {
+    V0(InvitationV0),
+}
+
+impl From<BootstrapContent> for Invitation {
+    fn from(value: BootstrapContent) -> Self {
+        let BootstrapContent::V0(boot) = value;
+
+        Invitation::V0(InvitationV0 {
+            bootstrap: boot,
+            code: None,
+            name: None,
+            url: None,
+        })
+    }
+}
+
+/// Create an account at a Broker Service Provider (BSP).
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum CreateAccountBSP {
+    V0(CreateAccountBSPV0),
+}
+
+impl TryFrom<String> for CreateAccountBSP {
+    type Error = NgError;
+    fn try_from(value: String) -> Result<Self, NgError> {
+        let ser = base64_url::decode(&value).map_err(|_| NgError::InvalidCreateAccount)?;
+        let invite: CreateAccountBSP =
+            serde_bare::from_slice(&ser).map_err(|_| NgError::InvalidCreateAccount)?;
+        Ok(invite)
+    }
+}
+
+/// Create an account at a Broker Service Provider (BSP). Version 0
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct CreateAccountBSPV0 {
+    pub invitation_code: Option<SymKey>,
+
+    /// the user asking to create an account
+    pub user: PubKey,
+
+    /// signature over serialized invitation, with user key
+    pub sig: Sig,
+
+    /// for web access, will redirect after successful signup. if left empty, it means user is on native app.
+    pub redirect_url: Option<String>,
+}
+
 /// ListenerInfo
 #[cfg(not(target_arch = "wasm32"))]
 #[derive(Clone, Debug, Serialize, Deserialize)]
@@ -940,6 +1129,66 @@ pub enum NetAddr {
     IPTransport(IPTransportAddr),
 }
 
+/**
+* info : {
+     type : WEB | NATIVE-IOS | NATIVE-ANDROID | NATIVE-MACOS | NATIVE-LINUX | NATIVE-WIN
+            NATIVE-SERVICE | NODE-SERVICE | VERIFIER | CLIENT-BROKER | CLI
+     vendor : (UA, node version, tauri webview, rust version)
+     os : operating system string
+     version : version of client
+     date_install
+     date_updated : last update
+   }
+*/
+
+/// Client Type
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub enum ClientType {
+    Web,
+    NativeIos,
+    NativeAndroid,
+    NativeMacOS,
+    NativeLinux,
+    NativeWin,
+    NativeService,
+    NodeService,
+    Verifier,
+    ClientBroker,
+    Cli,
+}
+
+/// IP transport address
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub struct ClientInfoV0 {
+    pub client_type: ClientType,
+    pub details: String,
+    pub version: String,
+    pub timestamp_install: u64,
+    pub timestamp_updated: u64,
+}
+
+/// Client Info
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+pub enum ClientInfo {
+    V0(ClientInfoV0),
+}
+
+impl ClientInfo {
+    pub fn new(client_type: ClientType, details: String, version: String) -> ClientInfo {
+        let timestamp_install = SystemTime::now()
+            .duration_since(SystemTime::UNIX_EPOCH)
+            .unwrap()
+            .as_secs();
+        ClientInfo::V0(ClientInfoV0 {
+            details,
+            version,
+            client_type,
+            timestamp_install,
+            timestamp_updated: timestamp_install,
+        })
+    }
+}
+
 //
 // OVERLAY MESSAGES
 //
@@ -2829,3 +3078,29 @@ pub struct RepoKeysV0 {
 pub enum RepoKeys {
     V0(RepoKeysV0),
 }
+
+#[cfg(test)]
+mod test {
+
+    use crate::types::{BootstrapContentV0, BrokerServerTypeV0, BrokerServerV0, Invitation};
+    use p2p_repo::types::PubKey;
+
+    #[test]
+    pub fn invitation() {
+        let inv = Invitation::new_v0(
+            BootstrapContentV0 {
+                servers: vec![BrokerServerV0 {
+                    server_type: BrokerServerTypeV0::Localhost(14400),
+                    peer_id: PubKey::Ed25519PubKey([
+                        95, 73, 225, 250, 3, 147, 24, 164, 177, 211, 34, 244, 45, 130, 111, 136,
+                        229, 145, 53, 167, 50, 168, 140, 227, 65, 111, 203, 41, 210, 186, 162, 149,
+                    ]),
+                }],
+            },
+            Some("test invitation".to_string()),
+            None,
+        );
+
+        println!("{:?}", inv.get_urls());
+    }
+}
diff --git a/p2p-net/src/utils.rs b/p2p-net/src/utils.rs
index 1f298ff..8543f32 100644
--- a/p2p-net/src/utils.rs
+++ b/p2p-net/src/utils.rs
@@ -9,12 +9,16 @@
  * according to those terms.
 */
 
+use crate::types::BootstrapContent;
+use crate::types::Invitation;
+use crate::NG_BOOTSTRAP_LOCAL_URL;
 use async_std::task;
 use ed25519_dalek::*;
 use futures::{channel::mpsc, select, Future, FutureExt, SinkExt};
 use noise_protocol::U8Array;
 use noise_protocol::DH;
 use noise_rust_crypto::sensitive::Sensitive;
+use p2p_repo::errors::NgError;
 use p2p_repo::types::PubKey;
 use p2p_repo::{log::*, types::PrivKey};
 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
@@ -48,6 +52,58 @@ where
     })
 }
 
+#[cfg(debug_assertions)]
+const APP_PREFIX: &str = "http://localhost:14400";
+
+#[cfg(not(debug_assertions))]
+const APP_PREFIX: &str = "";
+
+pub async fn retrieve_local_bootstrap(
+    location_string: String,
+    invite_string: Option<String>,
+) -> Option<Invitation> {
+    let invite1: Option<Invitation> = if invite_string.is_some() {
+        let invitation: Result<Invitation, NgError> = invite_string.clone().unwrap().try_into();
+        invitation.ok()
+    } else {
+        None
+    };
+    log_debug!("{}", location_string);
+    log_debug!("invite_String {:?} invite1{:?}", invite_string, invite1);
+
+    let invite2: Option<Invitation> = {
+        let resp = reqwest::get(format!("{}{}", APP_PREFIX, NG_BOOTSTRAP_LOCAL_URL)).await;
+        if resp.is_ok() {
+            let resp = resp.unwrap().json::<BootstrapContent>().await;
+            resp.ok().map(|v| v.into())
+        } else {
+            None
+        }
+    };
+
+    let res = if invite1.is_none() {
+        invite2
+    } else if invite2.is_none() {
+        invite1
+    } else {
+        invite1.map(|i| i.intersects(invite2.unwrap()))
+    };
+
+    if res.is_some() {
+        for server in res.as_ref().unwrap().get_servers() {
+            if server
+                .get_ws_url(Some(location_string.clone()))
+                .await
+                .is_some()
+            {
+                return res;
+            }
+        }
+        return None;
+    }
+    res
+}
+
 pub fn sensitive_from_privkey(privkey: PrivKey) -> Sensitive<[u8; 32]> {
     // we copy the key here, because otherwise the 2 zeroize would conflict. as the drop of the PrivKey might be called before the one of Sensitive
     let mut bits: [u8; 32] = [0u8; 32];
diff --git a/p2p-repo/src/errors.rs b/p2p-repo/src/errors.rs
index 8d4a8ca..fddc8bd 100644
--- a/p2p-repo/src/errors.rs
+++ b/p2p-repo/src/errors.rs
@@ -20,6 +20,8 @@ pub enum NgError {
     InvalidSignature,
     SerializationError,
     InvalidKey,
+    InvalidInvitation,
+    InvalidCreateAccount,
 }
 
 impl Error for NgError {}
diff --git a/p2p-repo/src/kcv_store.rs b/p2p-repo/src/kcv_store.rs
index 610fb91..b1d2bd9 100644
--- a/p2p-repo/src/kcv_store.rs
+++ b/p2p-repo/src/kcv_store.rs
@@ -66,7 +66,7 @@ pub trait ReadTransaction {
         prefix: u8,
         key: &Vec<u8>,
         suffix: Option<u8>,
-        value: Vec<u8>,
+        value: &Vec<u8>,
     ) -> Result<(), StorageError>;
 }
 
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b77d24c..439d64b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,7 @@ importers:
       '@sveltejs/vite-plugin-svelte': ^2.0.0
       '@tauri-apps/api': 2.0.0-alpha.4
       '@tauri-apps/cli': 2.0.0-alpha.9
+      '@tauri-apps/plugin-window': 2.0.0-alpha.0
       '@tsconfig/svelte': ^3.0.0
       '@types/node': ^18.7.10
       async-proxy: ^0.4.1
@@ -39,6 +40,7 @@ importers:
     dependencies:
       '@popperjs/core': 2.11.8
       '@tauri-apps/api': 2.0.0-alpha.4
+      '@tauri-apps/plugin-window': 2.0.0-alpha.0
       async-proxy: 0.4.1
       classnames: 2.3.2
       flowbite: 1.6.5
@@ -602,6 +604,12 @@ packages:
       '@tauri-apps/cli-win32-x64-msvc': 2.0.0-alpha.9
     dev: true
 
+  /@tauri-apps/plugin-window/2.0.0-alpha.0:
+    resolution: {integrity: sha512-ZXFXOu9m8QiDB8d8LFFgwcfxIAbr0bhzj06YvmZDB3isuVtlFP9EyU4D+zmumWEWvNN2XP7xgpn68ivOVhmNNQ==}
+    dependencies:
+      '@tauri-apps/api': 2.0.0-alpha.4
+    dev: false
+
   /@trysound/sax/0.2.0:
     resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
     engines: {node: '>=10.13.0'}
diff --git a/stores-lmdb/src/kcv_store.rs b/stores-lmdb/src/kcv_store.rs
index a7ae4f0..ee157a9 100644
--- a/stores-lmdb/src/kcv_store.rs
+++ b/stores-lmdb/src/kcv_store.rs
@@ -90,7 +90,7 @@ impl<'a> ReadTransaction for LmdbTransaction<'a> {
         prefix: u8,
         key: &Vec<u8>,
         suffix: Option<u8>,
-        value: Vec<u8>,
+        value: &Vec<u8>,
     ) -> Result<(), StorageError> {
         let property = LmdbKCVStore::compute_property(prefix, key, suffix);
 
@@ -269,7 +269,7 @@ impl ReadTransaction for LmdbKCVStore {
         prefix: u8,
         key: &Vec<u8>,
         suffix: Option<u8>,
-        value: Vec<u8>,
+        value: &Vec<u8>,
     ) -> Result<(), StorageError> {
         let property = Self::compute_property(prefix, key, suffix);
         let lock = self.environment.read().unwrap();
@@ -378,6 +378,7 @@ impl LmdbKCVStore {
         let shared_rkv = manager
             .get_or_create(path, |path| {
                 //Rkv::new::<Lmdb>(path) // use this instead to disable encryption
+                // TODO: fix memory management of the key. it should be zeroized all the way to the LMDB C FFI
                 Rkv::with_encryption_key_and_mapsize::<Lmdb>(path, key, 2 * 1024 * 1024 * 1024)
             })
             .unwrap();