diff --git a/.gitignore b/.gitignore index 5fef10c..c4cd092 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +apps/mobile-core/target package-lock.json next-env.d.ts node_modules @@ -5,4 +6,4 @@ node_modules .DS_Store .next dist -.env \ No newline at end of file +.env diff --git a/apps/desktop/.gitkeep b/apps/desktop/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/desktop/index.html b/apps/desktop/index.html new file mode 100644 index 0000000..9fb2c72 --- /dev/null +++ b/apps/desktop/index.html @@ -0,0 +1,30 @@ + + + + + + noro + + + +
+ + + diff --git a/apps/desktop/package.json b/apps/desktop/package.json index fd57089..ecc5e50 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,5 +1,28 @@ { - "name": "@noro/desktop", - "version": "0.0.0", - "private": true + "name": "@noro/desktop", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@cloudworxx/tauri-plugin-mac-rounded-corners": "^1.1.1", + "@tauri-apps/api": "^2.9.1", + "@tauri-apps/plugin-clipboard-manager": "^2.3.2", + "@tauri-apps/plugin-window-state": "~2", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.9.6", + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "typescript": "^5.9.3", + "vite": "^7.3.1" + } } diff --git a/apps/desktop/src-tauri/.gitignore b/apps/desktop/src-tauri/.gitignore new file mode 100644 index 0000000..35e201c --- /dev/null +++ b/apps/desktop/src-tauri/.gitignore @@ -0,0 +1,2 @@ +/gen/schemas +/target/ \ No newline at end of file diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..d35f3cf --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.lock @@ -0,0 +1,5690 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "app" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "argon2", + "base64 0.22.1", + "block", + "cocoa", + "directories", + "keyring", + "log", + "objc", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-log", + "tauri-plugin-window-state", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "byte-unit" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +dependencies = [ + "rust_decimal", + "schemars 1.2.0", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.11+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cocoa" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" +dependencies = [ + "bitflags 2.10.0", + "block", + "cocoa-foundation", + "core-foundation 0.10.1", + "core-graphics", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.10.0", + "block", + "core-foundation 0.10.1", + "core-graphics-types", + "objc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.11+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.0", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.10.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "log", + "zeroize", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.13.0", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.17.16", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.114", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.10.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "image", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.11+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.114", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.11+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.10.0", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-runtime" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.11+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.11+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.17.16", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..4522979 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "app" +version = "0.1.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2021" +rust-version = "1.77.2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.5.3", features = [] } + +[dependencies] +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +log = "0.4" +tauri = { version = "2.9.5", features = ["macos-private-api", "tray-icon", "image-png"] } +tauri-plugin-log = "2" +keyring = "3" +aes-gcm = "0.10" +argon2 = "0.5" +rand = "0.8" +base64 = "0.22" +thiserror = "2" +directories = "6" +reqwest = { version = "0.12", features = ["json", "cookies"] } +tokio = { version = "1", features = ["full"] } + +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.26" +objc = "0.2" +block = "0.1" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-window-state = "2" diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/apps/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..fc82f9c --- /dev/null +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,18 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "default permissions", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:default", + "core:window:allow-close", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-toggle-maximize", + "core:window:allow-start-dragging", + "core:window:allow-set-focus", + "core:window:allow-is-maximized", + "core:window:allow-is-fullscreen" + ] +} diff --git a/apps/desktop/src-tauri/capabilities/desktop.json b/apps/desktop/src-tauri/capabilities/desktop.json new file mode 100644 index 0000000..9be5102 --- /dev/null +++ b/apps/desktop/src-tauri/capabilities/desktop.json @@ -0,0 +1,14 @@ +{ + "identifier": "desktop-capability", + "platforms": [ + "macOS", + "windows", + "linux" + ], + "windows": [ + "main" + ], + "permissions": [ + "window-state:default" + ] +} \ No newline at end of file diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000..b82e5d4 Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128.png differ diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..20fb55d Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop/src-tauri/icons/32x32.png new file mode 100644 index 0000000..4990ecf Binary files /dev/null and b/apps/desktop/src-tauri/icons/32x32.png differ diff --git a/apps/desktop/src-tauri/icons/64x64.png b/apps/desktop/src-tauri/icons/64x64.png new file mode 100644 index 0000000..9bf0b61 Binary files /dev/null and b/apps/desktop/src-tauri/icons/64x64.png differ diff --git a/apps/desktop/src-tauri/icons/Square107x107Logo.png b/apps/desktop/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..64ee232 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square107x107Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square142x142Logo.png b/apps/desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..1bb5203 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square142x142Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square150x150Logo.png b/apps/desktop/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..cda6736 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square150x150Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square284x284Logo.png b/apps/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..fd8379b Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square284x284Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square30x30Logo.png b/apps/desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..43afe0e Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square30x30Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square310x310Logo.png b/apps/desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..513ba5e Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square310x310Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square44x44Logo.png b/apps/desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..e53e966 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square44x44Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square71x71Logo.png b/apps/desktop/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..362128e Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square71x71Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square89x89Logo.png b/apps/desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..e107c0e Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square89x89Logo.png differ diff --git a/apps/desktop/src-tauri/icons/StoreLogo.png b/apps/desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..fd5685e Binary files /dev/null and b/apps/desktop/src-tauri/icons/StoreLogo.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/apps/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/apps/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..4d8290d Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..43e3b43 Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..6e9eac6 Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..be0b447 Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a74ff5d Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..0e827e3 Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..809cb49 Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8b3fd16 Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..d69189c Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..155f014 Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..e19125e Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..66d7ba9 Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..e2c76f6 Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..e9d6c4c Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..fbd42e2 Binary files /dev/null and b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/apps/desktop/src-tauri/icons/android/values/ic_launcher_background.xml b/apps/desktop/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/apps/desktop/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000..6c58c18 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.icns differ diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop/src-tauri/icons/icon.ico new file mode 100644 index 0000000..c9f1b8b Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.ico differ diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000..746d265 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.png differ diff --git a/apps/desktop/src-tauri/icons/icon.svg b/apps/desktop/src-tauri/icons/icon.svg new file mode 100644 index 0000000..12e8f7e --- /dev/null +++ b/apps/desktop/src-tauri/icons/icon.svg @@ -0,0 +1,8 @@ + + + + diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..faf335a Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..984bcb5 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..984bcb5 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..3577206 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..559a037 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..201e0ef Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..201e0ef Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..d3d62c5 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..984bcb5 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..1d736d8 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..1d736d8 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..4f062c5 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..47b2340 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..4f062c5 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..895c87f Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..603c042 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..a65d498 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..ec7a331 Binary files /dev/null and b/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/apps/desktop/src-tauri/src/auth.rs b/apps/desktop/src-tauri/src/auth.rs new file mode 100644 index 0000000..83bed61 --- /dev/null +++ b/apps/desktop/src-tauri/src/auth.rs @@ -0,0 +1,189 @@ +use keyring::Entry; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AuthError { + #[error("http error: {0}")] + Http(String), + #[error("keyring error: {0}")] + Keyring(String), + #[error("auth failed: {0}")] + Failed(String), +} + +impl Serialize for AuthError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + pub token: String, + pub email: String, +} + +const SERVICE: &str = "sh.noro.app"; +const TOKEN_KEY: &str = "session_token"; +const EMAIL_KEY: &str = "session_email"; + +fn get_entry(key: &str) -> Result { + Entry::new(SERVICE, key).map_err(|e| AuthError::Keyring(e.to_string())) +} + +#[tauri::command] +pub async fn login( + base_url: String, + email: String, + password: String, +) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/api/auth/sign-in/email", base_url); + + #[derive(Serialize)] + struct LoginRequest { + email: String, + password: String, + } + + let body = LoginRequest { + email: email.clone(), + password, + }; + + let res = client + .post(&url) + .json(&body) + .send() + .await + .map_err(|e| AuthError::Http(e.to_string()))?; + + if !res.status().is_success() { + #[derive(Deserialize)] + struct ErrorResponse { + message: Option, + } + let err: ErrorResponse = res.json().await.unwrap_or(ErrorResponse { + message: Some("login failed".into()), + }); + return Err(AuthError::Failed( + err.message.unwrap_or("login failed".into()), + )); + } + + let token = res + .cookies() + .find(|c| c.name() == "better-auth.session_token") + .map(|c| c.value().to_string()) + .ok_or_else(|| AuthError::Failed("no session token".into()))?; + + let token_entry = get_entry(TOKEN_KEY)?; + token_entry + .set_password(&token) + .map_err(|e| AuthError::Keyring(e.to_string()))?; + + let email_entry = get_entry(EMAIL_KEY)?; + email_entry + .set_password(&email) + .map_err(|e| AuthError::Keyring(e.to_string()))?; + + Ok(Session { token, email }) +} + +#[tauri::command] +pub async fn register( + base_url: String, + email: String, + password: String, + name: Option, +) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/api/auth/sign-up/email", base_url); + + #[derive(Serialize)] + struct RegisterRequest { + email: String, + password: String, + name: String, + } + + let display_name = + name.unwrap_or_else(|| email.split('@').next().unwrap_or("user").to_string()); + + let body = RegisterRequest { + email: email.clone(), + password, + name: display_name, + }; + + let res = client + .post(&url) + .json(&body) + .send() + .await + .map_err(|e| AuthError::Http(e.to_string()))?; + + if !res.status().is_success() { + #[derive(Deserialize)] + struct ErrorResponse { + message: Option, + } + let err: ErrorResponse = res.json().await.unwrap_or(ErrorResponse { + message: Some("registration failed".into()), + }); + return Err(AuthError::Failed( + err.message.unwrap_or("registration failed".into()), + )); + } + + let token = res + .cookies() + .find(|c| c.name() == "better-auth.session_token") + .map(|c| c.value().to_string()) + .ok_or_else(|| AuthError::Failed("no session token".into()))?; + + let token_entry = get_entry(TOKEN_KEY)?; + token_entry + .set_password(&token) + .map_err(|e| AuthError::Keyring(e.to_string()))?; + + let email_entry = get_entry(EMAIL_KEY)?; + email_entry + .set_password(&email) + .map_err(|e| AuthError::Keyring(e.to_string()))?; + + Ok(Session { token, email }) +} + +#[tauri::command] +pub fn auth_get_session() -> Result, AuthError> { + let token_entry = get_entry(TOKEN_KEY)?; + let email_entry = get_entry(EMAIL_KEY)?; + + let token = match token_entry.get_password() { + Ok(t) => t, + Err(_) => return Ok(None), + }; + + let email = match email_entry.get_password() { + Ok(e) => e, + Err(_) => return Ok(None), + }; + + Ok(Some(Session { token, email })) +} + +#[tauri::command] +pub fn auth_logout() -> Result { + let token_entry = get_entry(TOKEN_KEY)?; + let email_entry = get_entry(EMAIL_KEY)?; + + let _ = token_entry.delete_credential(); + let _ = email_entry.delete_credential(); + + Ok(true) +} diff --git a/apps/desktop/src-tauri/src/biometric.rs b/apps/desktop/src-tauri/src/biometric.rs new file mode 100644 index 0000000..cc3fbc8 --- /dev/null +++ b/apps/desktop/src-tauri/src/biometric.rs @@ -0,0 +1,278 @@ +use keyring::Entry; +use serde::Serialize; +use thiserror::Error; + +const SERVICE: &str = "sh.noro.app"; +const BIOMETRIC_KEY: &str = "biometric_vault_key"; +const BIOMETRIC_ENABLED: &str = "biometric_enabled"; + +#[derive(Error, Debug)] +pub enum BiometricError { + #[error("biometrics unavailable")] + Unavailable, + #[error("authentication failed: {0}")] + AuthFailed(String), + #[error("authentication cancelled")] + Cancelled, + #[error("keyring error: {0}")] + Keyring(String), + #[error("platform not supported")] + NotSupported, +} + +impl Serialize for BiometricError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(target_os = "macos")] +mod macos { + use super::*; + use objc::runtime::{Class, Object, BOOL, YES}; + use objc::{msg_send, sel, sel_impl}; + use std::ffi::CStr; + use std::ptr; + use std::sync::mpsc; + + const LA_POLICY_BIOMETRICS: i64 = 1; + const LA_ERROR_USER_CANCEL: i64 = -2; + const LA_ERROR_USER_FALLBACK: i64 = -3; + const LA_ERROR_BIOMETRY_NOT_AVAILABLE: i64 = -6; + const LA_ERROR_BIOMETRY_NOT_ENROLLED: i64 = -7; + + pub fn check_available() -> bool { + unsafe { + let la_context_class = match Class::get("LAContext") { + Some(c) => c, + None => return false, + }; + let context: *mut Object = msg_send![la_context_class, new]; + if context.is_null() { + return false; + } + let error: *mut Object = ptr::null_mut(); + let can_evaluate: BOOL = msg_send![ + context, + canEvaluatePolicy: LA_POLICY_BIOMETRICS + error: &error + ]; + let _: () = msg_send![context, release]; + can_evaluate == YES + } + } + + pub fn authenticate(reason: &str) -> Result { + if !check_available() { + return Err(BiometricError::Unavailable); + } + + let (tx, rx) = mpsc::channel(); + + unsafe { + let la_context_class = Class::get("LAContext").ok_or(BiometricError::Unavailable)?; + let context: *mut Object = msg_send![la_context_class, new]; + if context.is_null() { + return Err(BiometricError::Unavailable); + } + + let ns_string_class = Class::get("NSString").ok_or(BiometricError::Unavailable)?; + let reason_cstring = std::ffi::CString::new(reason).unwrap(); + let reason_ns: *mut Object = msg_send![ + ns_string_class, + stringWithUTF8String: reason_cstring.as_ptr() + ]; + + let tx_clone = tx.clone(); + + let block = block::ConcreteBlock::new(move |success: BOOL, error: *mut Object| { + if success == YES { + let _ = tx_clone.send(Ok(true)); + } else if !error.is_null() { + let code: i64 = msg_send![error, code]; + let result = match code { + LA_ERROR_USER_CANCEL | LA_ERROR_USER_FALLBACK => { + Err(BiometricError::Cancelled) + } + LA_ERROR_BIOMETRY_NOT_AVAILABLE | LA_ERROR_BIOMETRY_NOT_ENROLLED => { + Err(BiometricError::Unavailable) + } + _ => { + let desc: *mut Object = msg_send![error, localizedDescription]; + let desc_utf8: *const i8 = msg_send![desc, UTF8String]; + let msg = if !desc_utf8.is_null() { + CStr::from_ptr(desc_utf8).to_string_lossy().to_string() + } else { + format!("error code {}", code) + }; + Err(BiometricError::AuthFailed(msg)) + } + }; + let _ = tx_clone.send(result); + } else { + let _ = tx_clone.send(Err(BiometricError::AuthFailed("unknown error".into()))); + } + }); + let block = block.copy(); + + let _: () = msg_send![ + context, + evaluatePolicy: LA_POLICY_BIOMETRICS + localizedReason: reason_ns + reply: &*block + ]; + + rx.recv() + .unwrap_or(Err(BiometricError::AuthFailed("timeout".into()))) + } + } +} + +#[cfg(target_os = "windows")] +mod windows { + use super::*; + + pub fn check_available() -> bool { + false + } + + pub fn authenticate(_reason: &str) -> Result { + Err(BiometricError::NotSupported) + } +} + +#[cfg(target_os = "linux")] +mod linux { + use super::*; + + pub fn check_available() -> bool { + false + } + + pub fn authenticate(_reason: &str) -> Result { + Err(BiometricError::NotSupported) + } +} + +#[cfg(target_os = "macos")] +use macos as platform; + +#[cfg(target_os = "windows")] +use windows as platform; + +#[cfg(target_os = "linux")] +use linux as platform; + +#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] +mod platform { + use super::*; + + pub fn check_available() -> bool { + false + } + + pub fn authenticate(_reason: &str) -> Result { + Err(BiometricError::NotSupported) + } +} + +fn get_entry(key: &str) -> Result { + Entry::new(SERVICE, key).map_err(|e| BiometricError::Keyring(e.to_string())) +} + +#[tauri::command] +pub fn biometric_available() -> bool { + platform::check_available() +} + +#[tauri::command] +pub fn biometric_authenticate(reason: String) -> Result { + platform::authenticate(&reason) +} + +#[tauri::command] +pub fn biometric_enabled() -> bool { + get_entry(BIOMETRIC_ENABLED) + .and_then(|e| { + e.get_password() + .map_err(|err| BiometricError::Keyring(err.to_string())) + }) + .map(|v| v == "true") + .unwrap_or(false) +} + +#[tauri::command] +pub fn biometric_enable() -> Result<(), BiometricError> { + if !platform::check_available() { + return Err(BiometricError::Unavailable); + } + + platform::authenticate("enable biometric unlock")?; + + let master_key = + crate::storage::get_master_key().map_err(|e| BiometricError::Keyring(e.to_string()))?; + let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &master_key); + + let key_entry = get_entry(BIOMETRIC_KEY)?; + key_entry + .set_password(&encoded) + .map_err(|e| BiometricError::Keyring(e.to_string()))?; + + let enabled_entry = get_entry(BIOMETRIC_ENABLED)?; + enabled_entry + .set_password("true") + .map_err(|e| BiometricError::Keyring(e.to_string()))?; + + Ok(()) +} + +#[tauri::command] +pub fn biometric_disable() -> Result<(), BiometricError> { + let key_entry = get_entry(BIOMETRIC_KEY)?; + let _ = key_entry.delete_credential(); + + let enabled_entry = get_entry(BIOMETRIC_ENABLED)?; + let _ = enabled_entry.delete_credential(); + + Ok(()) +} + +#[tauri::command] +pub fn biometric_unlock() -> Result { + if !biometric_enabled() { + return Err(BiometricError::Unavailable); + } + + platform::authenticate("unlock vault")?; + + let key_entry = get_entry(BIOMETRIC_KEY)?; + let encoded = key_entry + .get_password() + .map_err(|e| BiometricError::Keyring(e.to_string()))?; + + let key_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &encoded) + .map_err(|e| BiometricError::Keyring(e.to_string()))?; + + let master_entry = + Entry::new(SERVICE, "master_key").map_err(|e| BiometricError::Keyring(e.to_string()))?; + + let existing = master_entry.get_password().ok(); + if let Some(combined) = existing { + let parts: Vec<&str> = combined.split(':').collect(); + if parts.len() == 2 { + let new_combined = format!( + "{}:{}", + parts[0], + base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &key_bytes) + ); + master_entry + .set_password(&new_combined) + .map_err(|e| BiometricError::Keyring(e.to_string()))?; + } + } + + Ok(true) +} diff --git a/apps/desktop/src-tauri/src/commands.rs b/apps/desktop/src-tauri/src/commands.rs new file mode 100644 index 0000000..1d4f22e --- /dev/null +++ b/apps/desktop/src-tauri/src/commands.rs @@ -0,0 +1,94 @@ +use crate::storage::{self, VaultData, VaultEntry}; + +#[tauri::command] +pub fn store_session(token: String) -> Result<(), String> { + storage::store_session(&token).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn get_session() -> Result { + storage::get_session().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn delete_session() -> Result<(), String> { + storage::delete_session().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn store_master_key(password: String) -> Result<(), String> { + storage::store_master_key(&password).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn verify_password(password: String) -> Result { + storage::verify_password(&password).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn delete_master_key() -> Result<(), String> { + storage::delete_master_key().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn has_master_key() -> bool { + storage::get_master_key().is_ok() +} + +#[tauri::command] +pub fn store_vault(data: VaultData) -> Result<(), String> { + storage::store_vault(&data).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn get_vault() -> Result { + storage::get_vault().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn delete_vault() -> Result<(), String> { + storage::delete_vault().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn add_vault_entry(entry: VaultEntry) -> Result<(), String> { + let mut vault = storage::get_vault().unwrap_or(VaultData { + entries: vec![], + updated: now(), + }); + vault.entries.push(entry); + vault.updated = now(); + storage::store_vault(&vault).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn update_vault_entry(entry: VaultEntry) -> Result<(), String> { + let mut vault = storage::get_vault().map_err(|e| e.to_string())?; + if let Some(existing) = vault.entries.iter_mut().find(|e| e.id == entry.id) { + *existing = entry; + vault.updated = now(); + storage::store_vault(&vault).map_err(|e| e.to_string()) + } else { + Err("entry not found".into()) + } +} + +#[tauri::command] +pub fn delete_vault_entry(id: String) -> Result<(), String> { + let mut vault = storage::get_vault().map_err(|e| e.to_string())?; + vault.entries.retain(|e| e.id != id); + vault.updated = now(); + storage::store_vault(&vault).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn clear_all() -> Result<(), String> { + storage::clear_all().map_err(|e| e.to_string()) +} + +fn now() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() +} diff --git a/apps/desktop/src-tauri/src/crypto.rs b/apps/desktop/src-tauri/src/crypto.rs new file mode 100644 index 0000000..5a900ee --- /dev/null +++ b/apps/desktop/src-tauri/src/crypto.rs @@ -0,0 +1,161 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +use keyring::Entry; +use serde::{Deserialize, Serialize}; +use std::sync::RwLock; +use thiserror::Error; + +use crate::twoskd; + +const SERVICE: &str = "sh.noro.app"; +const VAULT_KEY_ENTRY: &str = "vault_key"; +const SECRET_KEY_ENTRY: &str = "secret_key"; +const SALT_ENTRY: &str = "vault_salt"; + +#[derive(Error, Debug)] +pub enum CryptoError { + #[error("vault locked")] + Locked, + #[error("invalid password")] + InvalidPassword, + #[error("keyring error: {0}")] + Keyring(String), + #[error("encryption error")] + Encryption, + #[error("not setup")] + NotSetup, +} + +impl Serialize for CryptoError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetupResult { + pub secretkey: String, +} + +static VAULT_KEY: RwLock> = RwLock::new(None); + +fn entry(key: &str) -> Result { + Entry::new(SERVICE, key).map_err(|e| CryptoError::Keyring(e.to_string())) +} + +pub fn islocked() -> bool { + VAULT_KEY.read().unwrap().is_none() +} + +pub fn issetup() -> Result { + let e = entry(VAULT_KEY_ENTRY)?; + Ok(e.get_password().is_ok()) +} + +#[tauri::command] +pub fn crypto_is_locked() -> bool { + islocked() +} + +#[tauri::command] +pub fn crypto_is_setup() -> Result { + issetup() +} + +#[tauri::command] +pub fn crypto_setup(password: String) -> Result { + let secretkey = twoskd::generatesecretkey(); + let salt = twoskd::generatesalt(); + let vaultkey = twoskd::generatevaultkey(); + + let auk = + twoskd::deriveauk(&password, &secretkey, &salt).map_err(|_| CryptoError::Encryption)?; + let wrapped = twoskd::wrapvaultkey(&vaultkey, &auk).map_err(|_| CryptoError::Encryption)?; + + entry(VAULT_KEY_ENTRY)? + .set_password(&STANDARD.encode(&wrapped)) + .map_err(|e| CryptoError::Keyring(e.to_string()))?; + + entry(SECRET_KEY_ENTRY)? + .set_password(&secretkey) + .map_err(|e| CryptoError::Keyring(e.to_string()))?; + + entry(SALT_ENTRY)? + .set_password(&STANDARD.encode(&salt)) + .map_err(|e| CryptoError::Keyring(e.to_string()))?; + + *VAULT_KEY.write().unwrap() = Some(vaultkey); + + Ok(SetupResult { secretkey }) +} + +#[tauri::command] +pub fn crypto_unlock(password: String, secretkey: String) -> Result { + let wrapped_b64 = entry(VAULT_KEY_ENTRY)? + .get_password() + .map_err(|_| CryptoError::NotSetup)?; + let wrapped = STANDARD + .decode(&wrapped_b64) + .map_err(|_| CryptoError::Encryption)?; + + let salt_b64 = entry(SALT_ENTRY)? + .get_password() + .map_err(|_| CryptoError::NotSetup)?; + let salt = STANDARD + .decode(&salt_b64) + .map_err(|_| CryptoError::Encryption)?; + + let auk = twoskd::deriveauk(&password, &secretkey, &salt) + .map_err(|_| CryptoError::InvalidPassword)?; + + let vaultkey = + twoskd::unwrapvaultkey(&wrapped, &auk).map_err(|_| CryptoError::InvalidPassword)?; + + *VAULT_KEY.write().unwrap() = Some(vaultkey); + Ok(true) +} + +#[tauri::command] +pub fn crypto_lock() { + *VAULT_KEY.write().unwrap() = None; +} + +#[tauri::command] +pub fn crypto_get_secret_key() -> Result, CryptoError> { + match entry(SECRET_KEY_ENTRY)?.get_password() { + Ok(key) => Ok(Some(key)), + Err(_) => Ok(None), + } +} + +pub fn encryptfield(itemid: &str, plaintext: &str) -> Result { + let vaultkey = VAULT_KEY.read().unwrap(); + let vaultkey = vaultkey.as_ref().ok_or(CryptoError::Locked)?; + let itemkey = twoskd::deriveitemkey(vaultkey, itemid).map_err(|_| CryptoError::Encryption)?; + let encrypted = + twoskd::encryptitem(plaintext.as_bytes(), &itemkey).map_err(|_| CryptoError::Encryption)?; + Ok(STANDARD.encode(&encrypted)) +} + +pub fn decryptfield(itemid: &str, ciphertext: &str) -> Result { + let vaultkey = VAULT_KEY.read().unwrap(); + let vaultkey = vaultkey.as_ref().ok_or(CryptoError::Locked)?; + let itemkey = twoskd::deriveitemkey(vaultkey, itemid).map_err(|_| CryptoError::Encryption)?; + let encrypted = STANDARD + .decode(ciphertext) + .map_err(|_| CryptoError::Encryption)?; + let decrypted = + twoskd::decryptitem(&encrypted, &itemkey).map_err(|_| CryptoError::Encryption)?; + String::from_utf8(decrypted).map_err(|_| CryptoError::Encryption) +} + +#[tauri::command] +pub fn crypto_clear() -> Result<(), CryptoError> { + crypto_lock(); + let _ = entry(VAULT_KEY_ENTRY)?.delete_credential(); + let _ = entry(SECRET_KEY_ENTRY)?.delete_credential(); + let _ = entry(SALT_ENTRY)?.delete_credential(); + Ok(()) +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..8414a44 --- /dev/null +++ b/apps/desktop/src-tauri/src/lib.rs @@ -0,0 +1,145 @@ +mod auth; +mod biometric; +mod commands; +mod crypto; +mod plugins; +mod storage; +mod sync; +mod tray; +mod twoskd; + +use rand::Rng; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +pub struct GeneratorOptions { + length: usize, + uppercase: bool, + lowercase: bool, + numbers: bool, + symbols: bool, +} + +#[derive(Debug, Serialize)] +pub struct GeneratorResult { + password: String, + strength: String, +} + +const UPPERCASE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const LOWERCASE: &[u8] = b"abcdefghijklmnopqrstuvwxyz"; +const NUMBERS: &[u8] = b"0123456789"; +const SYMBOLS: &[u8] = b"!@#$%^&*()_+-=[]{}|;:,.<>?"; + +fn calculate_strength(length: usize, charset_size: usize) -> String { + let entropy = (length as f64) * (charset_size as f64).log2(); + if entropy >= 128.0 { + "excellent".to_string() + } else if entropy >= 80.0 { + "strong".to_string() + } else if entropy >= 60.0 { + "good".to_string() + } else if entropy >= 40.0 { + "fair".to_string() + } else { + "weak".to_string() + } +} + +#[tauri::command] +fn generate_password(options: GeneratorOptions) -> Result { + let mut charset = Vec::new(); + + if options.uppercase { + charset.extend_from_slice(UPPERCASE); + } + if options.lowercase { + charset.extend_from_slice(LOWERCASE); + } + if options.numbers { + charset.extend_from_slice(NUMBERS); + } + if options.symbols { + charset.extend_from_slice(SYMBOLS); + } + + if charset.is_empty() { + return Err("at least one character type required".to_string()); + } + + let length = options.length.clamp(4, 128); + let mut rng = rand::thread_rng(); + + let password: String = (0..length) + .map(|_| { + let idx = rng.gen_range(0..charset.len()); + charset[idx] as char + }) + .collect(); + + let strength = calculate_strength(length, charset.len()); + + Ok(GeneratorResult { password, strength }) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_window_state::Builder::new().build()) + .setup(|app| { + if cfg!(debug_assertions) { + app.handle().plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + )?; + } + tray::create(app.handle())?; + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + generate_password, + auth::login, + auth::register, + auth::auth_get_session, + auth::auth_logout, + commands::store_session, + commands::get_session, + commands::delete_session, + commands::store_master_key, + commands::verify_password, + commands::delete_master_key, + commands::has_master_key, + commands::store_vault, + commands::get_vault, + commands::delete_vault, + commands::add_vault_entry, + commands::update_vault_entry, + commands::delete_vault_entry, + commands::clear_all, + sync::sync_fetch, + sync::sync_create, + sync::sync_update, + sync::sync_delete, + sync::sync_login, + plugins::rounded::enable_rounded_corners, + biometric::biometric_available, + biometric::biometric_authenticate, + biometric::biometric_enabled, + biometric::biometric_enable, + biometric::biometric_disable, + biometric::biometric_unlock, + tray::tray_show_notification, + tray::tray_set_autostart, + tray::tray_get_autostart, + crypto::crypto_is_locked, + crypto::crypto_is_setup, + crypto::crypto_setup, + crypto::crypto_unlock, + crypto::crypto_lock, + crypto::crypto_get_secret_key, + crypto::crypto_clear, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..c672c6a --- /dev/null +++ b/apps/desktop/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + app_lib::run(); +} diff --git a/apps/desktop/src-tauri/src/plugins/mod.rs b/apps/desktop/src-tauri/src/plugins/mod.rs new file mode 100644 index 0000000..983d0de --- /dev/null +++ b/apps/desktop/src-tauri/src/plugins/mod.rs @@ -0,0 +1 @@ +pub mod rounded; diff --git a/apps/desktop/src-tauri/src/plugins/rounded.rs b/apps/desktop/src-tauri/src/plugins/rounded.rs new file mode 100644 index 0000000..f1383eb --- /dev/null +++ b/apps/desktop/src-tauri/src/plugins/rounded.rs @@ -0,0 +1,53 @@ +#![allow(unexpected_cfgs)] +#![allow(deprecated)] + +use tauri::{AppHandle, Runtime, WebviewWindow}; + +#[cfg(target_os = "macos")] +use cocoa::{ + appkit::{NSColor, NSView, NSWindow}, + base::{id, nil}, +}; + +#[cfg(target_os = "macos")] +use objc::{msg_send, sel, sel_impl}; + +#[tauri::command] +pub fn enable_rounded_corners( + _app: AppHandle, + window: WebviewWindow, + corner_radius: Option, +) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + let radius = corner_radius.unwrap_or(12.0); + + window + .with_webview(move |webview| { + #[cfg(target_os = "macos")] + unsafe { + let ns_window = webview.ns_window() as id; + + ns_window.setOpaque_(cocoa::base::NO); + ns_window.setBackgroundColor_(NSColor::clearColor(nil)); + + let content_view = ns_window.contentView(); + content_view.setWantsLayer(cocoa::base::YES); + + let layer: id = msg_send![content_view, layer]; + if !layer.is_null() { + let _: () = msg_send![layer, setCornerRadius: radius]; + let _: () = msg_send![layer, setMasksToBounds: cocoa::base::YES]; + } + } + }) + .map_err(|e| e.to_string())?; + + Ok(()) + } + + #[cfg(not(target_os = "macos"))] + { + Ok(()) + } +} diff --git a/apps/desktop/src-tauri/src/storage.rs b/apps/desktop/src-tauri/src/storage.rs new file mode 100644 index 0000000..9156a41 --- /dev/null +++ b/apps/desktop/src-tauri/src/storage.rs @@ -0,0 +1,218 @@ +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Nonce, +}; +use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; +use base64::{engine::general_purpose::STANDARD, Engine}; +use keyring::Entry; +use rand::{rngs::OsRng, RngCore}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::PathBuf}; +use thiserror::Error; + +const SERVICE: &str = "sh.noro.app"; +const NONCE_SIZE: usize = 12; +const KEY_SIZE: usize = 32; + +#[derive(Error, Debug)] +pub enum StorageError { + #[error("keyring error: {0}")] + Keyring(String), + #[error("encryption error: {0}")] + Encryption(String), + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("json error: {0}")] + Json(#[from] serde_json::Error), + #[error("base64 error: {0}")] + Base64(#[from] base64::DecodeError), + #[error("not found")] + NotFound, +} + +impl From for String { + fn from(e: StorageError) -> Self { + e.to_string() + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct VaultData { + pub entries: Vec, + pub updated: u64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct VaultEntry { + pub id: String, + pub title: String, + pub username: Option, + pub password: Option, + pub url: Option, + pub notes: Option, + pub created: u64, + pub updated: u64, +} + +fn get_data_dir() -> Result { + let dir = directories::ProjectDirs::from("sh", "noro", "app") + .ok_or_else(|| StorageError::Io(std::io::Error::other("no data dir")))?; + let path = dir.data_dir().to_path_buf(); + fs::create_dir_all(&path)?; + Ok(path) +} + +fn derive_key(password: &str, salt: &[u8]) -> Result<[u8; KEY_SIZE], StorageError> { + let salt_str = + SaltString::encode_b64(salt).map_err(|e| StorageError::Encryption(e.to_string()))?; + let argon2 = Argon2::default(); + let hash = argon2 + .hash_password(password.as_bytes(), &salt_str) + .map_err(|e| StorageError::Encryption(e.to_string()))?; + let hash_bytes = hash + .hash + .ok_or_else(|| StorageError::Encryption("no hash".into()))?; + let mut key = [0u8; KEY_SIZE]; + key.copy_from_slice(&hash_bytes.as_bytes()[..KEY_SIZE]); + Ok(key) +} + +fn encrypt(data: &[u8], key: &[u8; KEY_SIZE]) -> Result, StorageError> { + let cipher = + Aes256Gcm::new_from_slice(key).map_err(|e| StorageError::Encryption(e.to_string()))?; + let mut nonce_bytes = [0u8; NONCE_SIZE]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, data) + .map_err(|e| StorageError::Encryption(e.to_string()))?; + let mut result = nonce_bytes.to_vec(); + result.extend(ciphertext); + Ok(result) +} + +fn decrypt(data: &[u8], key: &[u8; KEY_SIZE]) -> Result, StorageError> { + if data.len() < NONCE_SIZE { + return Err(StorageError::Encryption("data too short".into())); + } + let cipher = + Aes256Gcm::new_from_slice(key).map_err(|e| StorageError::Encryption(e.to_string()))?; + let nonce = Nonce::from_slice(&data[..NONCE_SIZE]); + let plaintext = cipher + .decrypt(nonce, &data[NONCE_SIZE..]) + .map_err(|e| StorageError::Encryption(e.to_string()))?; + Ok(plaintext) +} + +pub fn store_session(token: &str) -> Result<(), StorageError> { + let entry = Entry::new(SERVICE, "session").map_err(|e| StorageError::Keyring(e.to_string()))?; + entry + .set_password(token) + .map_err(|e| StorageError::Keyring(e.to_string()))?; + Ok(()) +} + +pub fn get_session() -> Result { + let entry = Entry::new(SERVICE, "session").map_err(|e| StorageError::Keyring(e.to_string()))?; + entry + .get_password() + .map_err(|e| StorageError::Keyring(e.to_string())) +} + +pub fn delete_session() -> Result<(), StorageError> { + let entry = Entry::new(SERVICE, "session").map_err(|e| StorageError::Keyring(e.to_string()))?; + entry + .delete_credential() + .map_err(|e| StorageError::Keyring(e.to_string()))?; + Ok(()) +} + +pub fn store_master_key(password: &str) -> Result<(), StorageError> { + let mut salt = [0u8; 16]; + OsRng.fill_bytes(&mut salt); + let key = derive_key(password, &salt)?; + let entry = + Entry::new(SERVICE, "master_key").map_err(|e| StorageError::Keyring(e.to_string()))?; + let combined = format!("{}:{}", STANDARD.encode(salt), STANDARD.encode(key)); + entry + .set_password(&combined) + .map_err(|e| StorageError::Keyring(e.to_string()))?; + Ok(()) +} + +pub fn get_master_key() -> Result, StorageError> { + let entry = + Entry::new(SERVICE, "master_key").map_err(|e| StorageError::Keyring(e.to_string()))?; + let combined = entry + .get_password() + .map_err(|e| StorageError::Keyring(e.to_string()))?; + let parts: Vec<&str> = combined.split(':').collect(); + if parts.len() != 2 { + return Err(StorageError::Encryption("invalid key format".into())); + } + Ok(STANDARD.decode(parts[1])?) +} + +pub fn verify_password(password: &str) -> Result { + let entry = + Entry::new(SERVICE, "master_key").map_err(|e| StorageError::Keyring(e.to_string()))?; + let combined = entry + .get_password() + .map_err(|e| StorageError::Keyring(e.to_string()))?; + let parts: Vec<&str> = combined.split(':').collect(); + if parts.len() != 2 { + return Err(StorageError::Encryption("invalid key format".into())); + } + let salt = STANDARD.decode(parts[0])?; + let stored_key = STANDARD.decode(parts[1])?; + let derived_key = derive_key(password, &salt)?; + Ok(derived_key.as_slice() == stored_key.as_slice()) +} + +pub fn delete_master_key() -> Result<(), StorageError> { + let entry = + Entry::new(SERVICE, "master_key").map_err(|e| StorageError::Keyring(e.to_string()))?; + entry + .delete_credential() + .map_err(|e| StorageError::Keyring(e.to_string()))?; + Ok(()) +} + +pub fn store_vault(data: &VaultData) -> Result<(), StorageError> { + let key_bytes = get_master_key()?; + let mut key = [0u8; KEY_SIZE]; + key.copy_from_slice(&key_bytes[..KEY_SIZE]); + let json = serde_json::to_vec(data)?; + let encrypted = encrypt(&json, &key)?; + let path = get_data_dir()?.join("vault.enc"); + fs::write(path, encrypted)?; + Ok(()) +} + +pub fn get_vault() -> Result { + let key_bytes = get_master_key()?; + let mut key = [0u8; KEY_SIZE]; + key.copy_from_slice(&key_bytes[..KEY_SIZE]); + let path = get_data_dir()?.join("vault.enc"); + if !path.exists() { + return Err(StorageError::NotFound); + } + let encrypted = fs::read(path)?; + let decrypted = decrypt(&encrypted, &key)?; + Ok(serde_json::from_slice(&decrypted)?) +} + +pub fn delete_vault() -> Result<(), StorageError> { + let path = get_data_dir()?.join("vault.enc"); + if path.exists() { + fs::remove_file(path)?; + } + Ok(()) +} + +pub fn clear_all() -> Result<(), StorageError> { + let _ = delete_session(); + let _ = delete_master_key(); + let _ = delete_vault(); + Ok(()) +} diff --git a/apps/desktop/src-tauri/src/sync.rs b/apps/desktop/src-tauri/src/sync.rs new file mode 100644 index 0000000..ffe388c --- /dev/null +++ b/apps/desktop/src-tauri/src/sync.rs @@ -0,0 +1,309 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::crypto; + +#[derive(Error, Debug)] +pub enum SyncError { + #[error("http error: {0}")] + Http(String), + #[error("auth error: {0}")] + Auth(String), + #[error("conflict: server revision {0}")] + Conflict(i32), + #[error("crypto error: {0}")] + Crypto(String), +} + +impl Serialize for SyncError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RemoteItem { + pub id: String, + #[serde(rename = "type")] + pub item_type: String, + pub title: String, + pub data: String, + pub revision: i32, + pub favorite: bool, + pub deleted: bool, + pub tags: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RemoteTag { + pub id: String, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ItemsResponse { + pub items: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ItemResponse { + pub item: RemoteItem, +} + +fn auth_header(token: &str) -> String { + format!("better-auth.session_token={}", token) +} + +fn encryptitem(id: &str, title: &str, data: &str) -> Result<(String, String), SyncError> { + let enctitle = crypto::encryptfield(id, title).map_err(|e| SyncError::Crypto(e.to_string()))?; + let encdata = crypto::encryptfield(id, data).map_err(|e| SyncError::Crypto(e.to_string()))?; + Ok((enctitle, encdata)) +} + +fn decryptitem(item: &mut RemoteItem) -> Result<(), SyncError> { + item.title = crypto::decryptfield(&item.id, &item.title) + .map_err(|e| SyncError::Crypto(e.to_string()))?; + item.data = + crypto::decryptfield(&item.id, &item.data).map_err(|e| SyncError::Crypto(e.to_string()))?; + Ok(()) +} + +#[tauri::command] +pub async fn sync_fetch(base_url: String, token: String) -> Result, SyncError> { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/vault/items", base_url); + + let res = client + .get(&url) + .header("cookie", auth_header(&token)) + .send() + .await + .map_err(|e| SyncError::Http(e.to_string()))?; + + if res.status() == 401 { + return Err(SyncError::Auth("session expired".into())); + } + + if !res.status().is_success() { + let status = res.status(); + let body = res.text().await.unwrap_or_default(); + return Err(SyncError::Http(format!("{}: {}", status, body))); + } + + let data: ItemsResponse = res + .json() + .await + .map_err(|e| SyncError::Http(e.to_string()))?; + + let mut items = data.items; + for item in &mut items { + decryptitem(item)?; + } + + Ok(items) +} + +#[tauri::command] +pub async fn sync_create( + base_url: String, + token: String, + id: String, + item_type: String, + title: String, + data: String, + tags: Vec, + favorite: bool, +) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/vault/items", base_url); + + let (enctitle, encdata) = encryptitem(&id, &title, &data)?; + + #[derive(Serialize)] + struct Body { + id: String, + #[serde(rename = "type")] + item_type: String, + title: String, + data: String, + tags: Vec, + favorite: bool, + } + + let body = Body { + id, + item_type, + title: enctitle, + data: encdata, + tags, + favorite, + }; + + let res = client + .post(&url) + .header("cookie", auth_header(&token)) + .json(&body) + .send() + .await + .map_err(|e| SyncError::Http(e.to_string()))?; + + if res.status() == 401 { + return Err(SyncError::Auth("session expired".into())); + } + + if !res.status().is_success() { + let status = res.status(); + let body = res.text().await.unwrap_or_default(); + return Err(SyncError::Http(format!("{}: {}", status, body))); + } + + let resp: ItemResponse = res + .json() + .await + .map_err(|e| SyncError::Http(e.to_string()))?; + + let mut item = resp.item; + decryptitem(&mut item)?; + + Ok(item) +} + +#[tauri::command] +pub async fn sync_update( + base_url: String, + token: String, + id: String, + title: Option, + data: Option, + tags: Option>, + favorite: Option, +) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/vault/items/{}", base_url, id); + + let enctitle = match &title { + Some(t) => { + Some(crypto::encryptfield(&id, t).map_err(|e| SyncError::Crypto(e.to_string()))?) + } + None => None, + }; + + let encdata = match &data { + Some(d) => { + Some(crypto::encryptfield(&id, d).map_err(|e| SyncError::Crypto(e.to_string()))?) + } + None => None, + }; + + #[derive(Serialize)] + struct Body { + #[serde(skip_serializing_if = "Option::is_none")] + title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + favorite: Option, + } + + let body = Body { + title: enctitle, + data: encdata, + tags, + favorite, + }; + + let res = client + .put(&url) + .header("cookie", auth_header(&token)) + .json(&body) + .send() + .await + .map_err(|e| SyncError::Http(e.to_string()))?; + + if res.status() == 401 { + return Err(SyncError::Auth("session expired".into())); + } + + if !res.status().is_success() { + let status = res.status(); + let body = res.text().await.unwrap_or_default(); + return Err(SyncError::Http(format!("{}: {}", status, body))); + } + + let resp: ItemResponse = res + .json() + .await + .map_err(|e| SyncError::Http(e.to_string()))?; + + let mut item = resp.item; + decryptitem(&mut item)?; + + Ok(item) +} + +#[tauri::command] +pub async fn sync_delete(base_url: String, token: String, id: String) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/vault/items/{}", base_url, id); + + let res = client + .delete(&url) + .header("cookie", auth_header(&token)) + .send() + .await + .map_err(|e| SyncError::Http(e.to_string()))?; + + if res.status() == 401 { + return Err(SyncError::Auth("session expired".into())); + } + + if !res.status().is_success() { + let status = res.status(); + let body = res.text().await.unwrap_or_default(); + return Err(SyncError::Http(format!("{}: {}", status, body))); + } + + Ok(true) +} + +#[tauri::command] +pub async fn sync_login( + base_url: String, + email: String, + password: String, +) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/api/auth/sign-in/email", base_url); + + #[derive(Serialize)] + struct Body { + email: String, + password: String, + } + + let body = Body { email, password }; + + let res = client + .post(&url) + .json(&body) + .send() + .await + .map_err(|e| SyncError::Http(e.to_string()))?; + + if !res.status().is_success() { + return Err(SyncError::Auth("invalid credentials".into())); + } + + let token = res + .cookies() + .find(|c| c.name() == "better-auth.session_token") + .map(|c| c.value().to_string()) + .ok_or_else(|| SyncError::Auth("no session token".into()))?; + + Ok(token) +} diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs new file mode 100644 index 0000000..e0a5625 --- /dev/null +++ b/apps/desktop/src-tauri/src/tray.rs @@ -0,0 +1,243 @@ +use rand::Rng; +use tauri::{ + menu::{Menu, MenuItem, PredefinedMenuItem}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, + AppHandle, Emitter, Manager, Runtime, +}; + +fn quickpassword() -> String { + let charset: Vec = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?" + .to_vec(); + let mut rng = rand::thread_rng(); + (0..20) + .map(|_| charset[rng.gen_range(0..charset.len())] as char) + .collect() +} + +fn copytoclipboard(text: &str) { + #[cfg(target_os = "macos")] + { + use std::io::Write; + use std::process::{Command, Stdio}; + if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() { + if let Some(stdin) = child.stdin.as_mut() { + let _ = stdin.write_all(text.as_bytes()); + } + let _ = child.wait(); + } + } + #[cfg(target_os = "windows")] + { + use std::process::Command; + let _ = Command::new("cmd") + .args(["/C", &format!("echo {} | clip", text)]) + .spawn(); + } + #[cfg(target_os = "linux")] + { + use std::io::Write; + use std::process::{Command, Stdio}; + if let Ok(mut child) = Command::new("xclip") + .args(["-selection", "clipboard"]) + .stdin(Stdio::piped()) + .spawn() + { + if let Some(stdin) = child.stdin.as_mut() { + let _ = stdin.write_all(text.as_bytes()); + } + let _ = child.wait(); + } + } +} + +pub fn create(app: &AppHandle) -> Result<(), Box> { + let open = MenuItem::with_id(app, "open", "Open", true, None::<&str>)?; + let lock = MenuItem::with_id(app, "lock", "Lock", true, None::<&str>)?; + let generate = MenuItem::with_id(app, "generate", "Generate Password", true, None::<&str>)?; + let separator = PredefinedMenuItem::separator(app)?; + let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&open, &lock, &generate, &separator, &quit])?; + + let _tray = TrayIconBuilder::new() + .icon(tauri::image::Image::from_bytes(include_bytes!( + "../icons/32x32.png" + ))?) + .menu(&menu) + .show_menu_on_left_click(false) + .on_menu_event(|app, event| match event.id.as_ref() { + "open" => { + if let Some(w) = app.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } + } + "lock" => { + if let Some(w) = app.get_webview_window("main") { + let _ = w.emit("vault_lock", ()); + } + } + "generate" => { + let password = quickpassword(); + copytoclipboard(&password); + if let Some(w) = app.get_webview_window("main") { + let _ = w.emit("password_copied", password); + } + } + "quit" => app.exit(0), + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + if let Some(w) = tray.app_handle().get_webview_window("main") { + if w.is_visible().unwrap_or(false) { + let _ = w.hide(); + } else { + let _ = w.show(); + let _ = w.set_focus(); + } + } + } + }) + .build(app)?; + + Ok(()) +} + +#[tauri::command] +pub fn tray_show_notification(title: String, body: String) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + use std::process::Command; + let script = format!( + r#"display notification "{}" with title "{}""#, + body.replace('"', r#"\""#), + title.replace('"', r#"\""#) + ); + Command::new("osascript") + .args(["-e", &script]) + .spawn() + .map_err(|e| e.to_string())?; + } + #[cfg(target_os = "windows")] + log::info!("notification: {} - {}", title, body); + #[cfg(target_os = "linux")] + { + use std::process::Command; + Command::new("notify-send") + .args([&title, &body]) + .spawn() + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[tauri::command] +pub fn tray_set_autostart(enabled: bool) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + let home = std::env::var("HOME").map_err(|e| e.to_string())?; + let path = format!("{}/Library/LaunchAgents/sh.noro.app.plist", home); + if enabled { + let plist = r#" + + + + Label + sh.noro.app + ProgramArguments + + /Applications/noro.app/Contents/MacOS/noro + + RunAtLoad + + +"#; + std::fs::write(&path, plist).map_err(|e| e.to_string())?; + } else { + let _ = std::fs::remove_file(&path); + } + } + #[cfg(target_os = "windows")] + { + use std::process::Command; + let exe = std::env::current_exe().map_err(|e| e.to_string())?; + let key = r#"HKCU\Software\Microsoft\Windows\CurrentVersion\Run"#; + if enabled { + Command::new("reg") + .args([ + "add", + key, + "/v", + "noro", + "/t", + "REG_SZ", + "/d", + exe.to_str().unwrap_or(""), + "/f", + ]) + .spawn() + .map_err(|e| e.to_string())?; + } else { + Command::new("reg") + .args(["delete", key, "/v", "noro", "/f"]) + .spawn() + .map_err(|e| e.to_string())?; + } + } + #[cfg(target_os = "linux")] + { + use directories::BaseDirs; + let base = BaseDirs::new().ok_or("could not find base directories")?; + let dir = base.config_dir().join("autostart"); + let file = dir.join("noro.desktop"); + if enabled { + std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + std::fs::write(&file, "[Desktop Entry]\nType=Application\nName=noro\nExec=noro\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true").map_err(|e| e.to_string())?; + } else { + let _ = std::fs::remove_file(&file); + } + } + Ok(()) +} + +#[tauri::command] +pub fn tray_get_autostart() -> Result { + #[cfg(target_os = "macos")] + { + let home = std::env::var("HOME").map_err(|e| e.to_string())?; + Ok( + std::path::Path::new(&format!("{}/Library/LaunchAgents/sh.noro.app.plist", home)) + .exists(), + ) + } + #[cfg(target_os = "windows")] + { + use std::process::Command; + let out = Command::new("reg") + .args([ + "query", + r#"HKCU\Software\Microsoft\Windows\CurrentVersion\Run"#, + "/v", + "noro", + ]) + .output() + .map_err(|e| e.to_string())?; + Ok(out.status.success()) + } + #[cfg(target_os = "linux")] + { + use directories::BaseDirs; + let base = BaseDirs::new().ok_or("could not find base directories")?; + Ok(base + .config_dir() + .join("autostart") + .join("noro.desktop") + .exists()) + } +} diff --git a/apps/desktop/src-tauri/src/twoskd.rs b/apps/desktop/src-tauri/src/twoskd.rs new file mode 100644 index 0000000..6973ccc --- /dev/null +++ b/apps/desktop/src-tauri/src/twoskd.rs @@ -0,0 +1,215 @@ +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Nonce, +}; +use argon2::{Algorithm, Argon2, Params, Version}; +use rand::Rng; +use thiserror::Error; + +const ARGON_MEMORY: u32 = 65536; +const ARGON_ITERATIONS: u32 = 3; +const ARGON_PARALLELISM: u32 = 4; +const ARGON_OUTPUT_LEN: usize = 32; +const SECRET_KEY_BYTES: usize = 20; +const NONCE_LEN: usize = 12; +const BASE32_ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + +#[derive(Error, Debug)] +pub enum TwoskdError { + #[error("argon2 error")] + Argon2, + #[error("encryption error")] + Encryption, + #[error("decryption error")] + Decryption, + #[error("invalid secret key format")] + InvalidSecretKey, +} + +impl From for TwoskdError { + fn from(_: argon2::Error) -> Self { + TwoskdError::Argon2 + } +} + +pub type Result = std::result::Result; + +fn base32encode(bytes: &[u8]) -> String { + let mut result = String::new(); + let mut bits = 0u32; + let mut bitcount = 0; + for &byte in bytes { + bits = (bits << 8) | byte as u32; + bitcount += 8; + while bitcount >= 5 { + bitcount -= 5; + let index = ((bits >> bitcount) & 0x1F) as usize; + result.push(BASE32_ALPHABET[index] as char); + } + } + if bitcount > 0 { + let index = ((bits << (5 - bitcount)) & 0x1F) as usize; + result.push(BASE32_ALPHABET[index] as char); + } + result +} + +fn base32decode(encoded: &str) -> Option> { + let mut result = Vec::new(); + let mut bits = 0u32; + let mut bitcount = 0; + for c in encoded.chars() { + let index = BASE32_ALPHABET.iter().position(|&x| x as char == c)?; + bits = (bits << 5) | index as u32; + bitcount += 5; + if bitcount >= 8 { + bitcount -= 8; + result.push((bits >> bitcount) as u8); + } + } + Some(result) +} + +pub fn generatesecretkey() -> String { + let mut rng = rand::thread_rng(); + let bytes: Vec = (0..SECRET_KEY_BYTES).map(|_| rng.gen()).collect(); + let encoded = base32encode(&bytes); + format!( + "A3-{}-{}-{}-{}-{}-{}", + &encoded[0..6], + &encoded[6..12], + &encoded[12..17], + &encoded[17..22], + &encoded[22..27], + &encoded[27..] + ) +} + +fn parsesecretkey(secretkey: &str) -> Result> { + if !secretkey.starts_with("A3-") { + return Err(TwoskdError::InvalidSecretKey); + } + let parts: Vec<&str> = secretkey[3..].split('-').collect(); + if parts.len() != 6 { + return Err(TwoskdError::InvalidSecretKey); + } + let encoded: String = parts.join(""); + base32decode(&encoded).ok_or(TwoskdError::InvalidSecretKey) +} + +pub fn deriveauk(password: &str, secretkey: &str, salt: &[u8]) -> Result<[u8; 32]> { + let keybytes = parsesecretkey(secretkey)?; + let mut combined = Vec::with_capacity(password.len() + keybytes.len()); + combined.extend_from_slice(password.as_bytes()); + combined.extend_from_slice(&keybytes); + + let params = Params::new( + ARGON_MEMORY, + ARGON_ITERATIONS, + ARGON_PARALLELISM, + Some(ARGON_OUTPUT_LEN), + )?; + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + + let mut auk = [0u8; 32]; + argon2.hash_password_into(&combined, salt, &mut auk)?; + Ok(auk) +} + +pub fn wrapvaultkey(vaultkey: &[u8; 32], auk: &[u8; 32]) -> Result> { + let cipher = Aes256Gcm::new_from_slice(auk).map_err(|_| TwoskdError::Encryption)?; + let mut rng = rand::thread_rng(); + let noncebytes: [u8; NONCE_LEN] = rng.gen(); + let nonce = Nonce::from_slice(&noncebytes); + let ciphertext = cipher + .encrypt(nonce, vaultkey.as_ref()) + .map_err(|_| TwoskdError::Encryption)?; + let mut wrapped = Vec::with_capacity(NONCE_LEN + ciphertext.len()); + wrapped.extend_from_slice(&noncebytes); + wrapped.extend_from_slice(&ciphertext); + Ok(wrapped) +} + +pub fn unwrapvaultkey(wrapped: &[u8], auk: &[u8; 32]) -> Result<[u8; 32]> { + if wrapped.len() < NONCE_LEN + 32 + 16 { + return Err(TwoskdError::Decryption); + } + let cipher = Aes256Gcm::new_from_slice(auk).map_err(|_| TwoskdError::Decryption)?; + let nonce = Nonce::from_slice(&wrapped[..NONCE_LEN]); + let ciphertext = &wrapped[NONCE_LEN..]; + let plaintext = cipher + .decrypt(nonce, ciphertext) + .map_err(|_| TwoskdError::Decryption)?; + let mut vaultkey = [0u8; 32]; + vaultkey.copy_from_slice(&plaintext); + Ok(vaultkey) +} + +pub fn deriveitemkey(vaultkey: &[u8; 32], itemid: &str) -> Result<[u8; 32]> { + let params = Params::new(4096, 1, 1, Some(ARGON_OUTPUT_LEN))?; + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + let mut itemkey = [0u8; 32]; + argon2.hash_password_into(vaultkey, itemid.as_bytes(), &mut itemkey)?; + Ok(itemkey) +} + +pub fn encryptitem(data: &[u8], itemkey: &[u8; 32]) -> Result> { + let cipher = Aes256Gcm::new_from_slice(itemkey).map_err(|_| TwoskdError::Encryption)?; + let mut rng = rand::thread_rng(); + let noncebytes: [u8; NONCE_LEN] = rng.gen(); + let nonce = Nonce::from_slice(&noncebytes); + let ciphertext = cipher + .encrypt(nonce, data) + .map_err(|_| TwoskdError::Encryption)?; + let mut result = Vec::with_capacity(NONCE_LEN + ciphertext.len()); + result.extend_from_slice(&noncebytes); + result.extend_from_slice(&ciphertext); + Ok(result) +} + +pub fn decryptitem(encrypted: &[u8], itemkey: &[u8; 32]) -> Result> { + if encrypted.len() < NONCE_LEN + 16 { + return Err(TwoskdError::Decryption); + } + let cipher = Aes256Gcm::new_from_slice(itemkey).map_err(|_| TwoskdError::Decryption)?; + let nonce = Nonce::from_slice(&encrypted[..NONCE_LEN]); + let ciphertext = &encrypted[NONCE_LEN..]; + cipher + .decrypt(nonce, ciphertext) + .map_err(|_| TwoskdError::Decryption) +} + +pub fn generatevaultkey() -> [u8; 32] { + let mut rng = rand::thread_rng(); + rng.gen() +} + +pub fn generatesalt() -> [u8; 16] { + let mut rng = rand::thread_rng(); + rng.gen() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_secretkey_format() { + let key = generatesecretkey(); + assert!(key.starts_with("A3-")); + let parts: Vec<&str> = key[3..].split('-').collect(); + assert_eq!(parts.len(), 6); + } + + #[test] + fn test_roundtrip() { + let password = "testpassword123"; + let secretkey = generatesecretkey(); + let salt = [0u8; 16]; + let auk = deriveauk(password, &secretkey, &salt).unwrap(); + let vaultkey = [42u8; 32]; + let wrapped = wrapvaultkey(&vaultkey, &auk).unwrap(); + let unwrapped = unwrapvaultkey(&wrapped, &auk).unwrap(); + assert_eq!(vaultkey, unwrapped); + } +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..69a2665 --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,49 @@ +{ + "$schema": "../../../node_modules/@tauri-apps/cli/config.schema.json", + "productName": "noro", + "version": "0.1.0", + "identifier": "sh.noro.app", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:1420", + "beforeDevCommand": "bun run dev", + "beforeBuildCommand": "bun run build" + }, + "app": { + "macOSPrivateApi": true, + "windows": [ + { + "title": "noro", + "width": 1024, + "height": 768, + "minWidth": 800, + "minHeight": 600, + "resizable": true, + "fullscreen": false, + "center": true, + "visible": false, + "decorations": true, + "titleBarStyle": "Overlay", + "hiddenTitle": true, + "trafficLightPosition": { "x": 16, "y": 16 } + } + ], + "security": { + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "category": "Utility", + "shortDescription": "password manager", + "longDescription": "noro is a secure, zero-knowledge password manager." + } +} diff --git a/apps/desktop/src/app.tsx b/apps/desktop/src/app.tsx new file mode 100644 index 0000000..077f10b --- /dev/null +++ b/apps/desktop/src/app.tsx @@ -0,0 +1,122 @@ +import { useEffect, useState } from "react"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { getSession, logout, type Session } from "./auth"; +import { Login } from "./pages/login"; +import { Vault } from "./pages/vault"; +import { Generator } from "./pages/generator"; +import { enableRoundedCorners } from "./rounded"; + +type View = "login" | "vault" | "generator"; + +const appWindow = getCurrentWindow(); + +enableRoundedCorners(12); + +function Titlebar({ showLogo = true }: { showLogo?: boolean }) { + async function handleMouseDown(e: React.MouseEvent) { + const target = e.target as HTMLElement; + if (target.closest("button")) return; + if (e.buttons === 1) { + if (e.detail === 2) { + await appWindow.toggleMaximize(); + } else { + await appWindow.startDragging(); + } + } + } + + return ( +
+
+ {showLogo && ( +
+ + noro +
+ )} +
+
+ ); +} + +export { Titlebar }; + +export function App() { + const [view, setView] = useState("login"); + const [session, setSession] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + getSession() + .then((s) => { + if (s) { + setSession(s); + setView("vault"); + } + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + + function handleLogin(s: Session) { + setSession(s); + setView("vault"); + } + + async function handleLogout() { + await logout(); + setSession(null); + setView("login"); + } + + function navigate(to: View) { + setView(to); + } + + if (loading) { + return ( +
+ +
+ +
+
+ ); + } + + if (view === "login") { + return ; + } + + if (view === "generator") { + return ( + navigate("vault")} + user={{ email: session!.email }} + onLogout={handleLogout} + /> + ); + } + + return ( + + ); +} diff --git a/apps/desktop/src/auth.ts b/apps/desktop/src/auth.ts new file mode 100644 index 0000000..5d01042 --- /dev/null +++ b/apps/desktop/src/auth.ts @@ -0,0 +1,27 @@ +import { invoke } from "@tauri-apps/api/core"; +import { BASE_URL } from "./config"; + +export interface Session { + token: string; + email: string; +} + +export async function login(email: string, password: string): Promise { + return invoke("login", { baseUrl: BASE_URL, email, password }); +} + +export async function register( + email: string, + password: string, + name?: string, +): Promise { + return invoke("register", { baseUrl: BASE_URL, email, password, name }); +} + +export async function getSession(): Promise { + return invoke("auth_get_session"); +} + +export async function logout(): Promise { + return invoke("auth_logout"); +} diff --git a/apps/desktop/src/components/item.tsx b/apps/desktop/src/components/item.tsx new file mode 100644 index 0000000..cc72eb1 --- /dev/null +++ b/apps/desktop/src/components/item.tsx @@ -0,0 +1,372 @@ +import { useState, type ReactNode } from "react"; +import { writeText } from "@tauri-apps/plugin-clipboard-manager"; +import type { RemoteItem } from "../hooks/sync"; + +interface Props { + item: RemoteItem; + onEdit: () => void; + onDelete: () => void; + onFavorite: () => void; +} + +const icons: Record = { + login: ( + + ), + note: ( + + ), + card: ( + + ), + identity: ( + + ), + ssh: ( + + ), + api: ( + + ), + otp: ( + + ), + passkey: ( + + ), +}; + +function parsedata(data: string): Record { + try { + return JSON.parse(data); + } catch { + return {}; + } +} + +export function Item({ item, onEdit, onDelete, onFavorite }: Props) { + const [revealed, setRevealed] = useState>({}); + const [copied, setCopied] = useState(null); + const data = parsedata(item.data); + + async function copy(key: string, value: string) { + await writeText(value); + setCopied(key); + setTimeout(() => setCopied(null), 1500); + } + + function toggle(key: string) { + setRevealed((prev) => ({ ...prev, [key]: !prev[key] })); + } + + function mask(value: string) { + return "•".repeat(Math.min(value.length, 24)); + } + + function sensitive(key: string) { + const lower = key.toLowerCase(); + return ( + lower.includes("password") || + lower.includes("secret") || + lower.includes("key") || + lower.includes("token") || + lower.includes("pin") || + lower.includes("cvv") || + lower.includes("private") + ); + } + + function renderfield(key: string, value: string) { + const isSensitive = sensitive(key); + const isRevealed = revealed[key]; + const display = isSensitive && !isRevealed ? mask(value) : value; + + return ( +
+
{key}
+
+ {display} +
+ {isSensitive && ( + + )} + +
+
+
+ ); + } + + return ( +
+
+
{icons[item.type] || icons.note}
+
+

{item.title}

+ {item.type} +
+
+ + + +
+
+ + {item.tags.length > 0 && ( +
+ {item.tags.map((tag) => ( + {tag.name} + ))} +
+ )} + +
+ {Object.entries(data).map(([key, value]) => renderfield(key, String(value)))} +
+ + +
+ ); +} + +export { icons as typeicons }; diff --git a/apps/desktop/src/components/itemform.tsx b/apps/desktop/src/components/itemform.tsx new file mode 100644 index 0000000..b814689 --- /dev/null +++ b/apps/desktop/src/components/itemform.tsx @@ -0,0 +1,427 @@ +import { useState, useEffect } from "react"; +import type { RemoteItem } from "../hooks/sync"; + +type ItemType = "login" | "note" | "card" | "identity" | "ssh" | "api" | "otp" | "passkey"; + +interface Props { + item?: RemoteItem; + onSave: (data: { + type: ItemType; + title: string; + data: string; + tags: string[]; + favorite: boolean; + }) => void; + onCancel: () => void; +} + +const schemas: Record = { + login: [ + { key: "username", label: "username", type: "text" }, + { key: "password", label: "password", type: "password", sensitive: true }, + { key: "url", label: "url", type: "url" }, + { key: "notes", label: "notes", type: "textarea" }, + ], + note: [ + { key: "content", label: "content", type: "textarea" }, + ], + card: [ + { key: "cardholder", label: "cardholder name", type: "text" }, + { key: "number", label: "card number", type: "text", sensitive: true }, + { key: "expiry", label: "expiry date", type: "text" }, + { key: "cvv", label: "cvv", type: "password", sensitive: true }, + { key: "pin", label: "pin", type: "password", sensitive: true }, + ], + identity: [ + { key: "firstname", label: "first name", type: "text" }, + { key: "lastname", label: "last name", type: "text" }, + { key: "email", label: "email", type: "email" }, + { key: "phone", label: "phone", type: "tel" }, + { key: "address", label: "address", type: "textarea" }, + ], + ssh: [ + { key: "host", label: "host", type: "text" }, + { key: "port", label: "port", type: "text" }, + { key: "username", label: "username", type: "text" }, + { key: "privatekey", label: "private key", type: "textarea", sensitive: true }, + { key: "passphrase", label: "passphrase", type: "password", sensitive: true }, + ], + api: [ + { key: "service", label: "service", type: "text" }, + { key: "apikey", label: "api key", type: "password", sensitive: true }, + { key: "secret", label: "secret", type: "password", sensitive: true }, + { key: "endpoint", label: "endpoint", type: "url" }, + ], + otp: [ + { key: "issuer", label: "issuer", type: "text" }, + { key: "account", label: "account", type: "text" }, + { key: "secret", label: "secret key", type: "password", sensitive: true }, + { key: "digits", label: "digits", type: "text" }, + { key: "period", label: "period", type: "text" }, + ], + passkey: [ + { key: "rpid", label: "relying party id", type: "text" }, + { key: "rpname", label: "relying party name", type: "text" }, + { key: "userid", label: "user id", type: "text" }, + { key: "username", label: "username", type: "text" }, + { key: "credentialid", label: "credential id", type: "text", sensitive: true }, + ], +}; + +const typelabels: Record = { + login: "login", + note: "secure note", + card: "payment card", + identity: "identity", + ssh: "ssh key", + api: "api credential", + otp: "one-time password", + passkey: "passkey", +}; + +function parsedata(data: string): Record { + try { + return JSON.parse(data); + } catch { + return {}; + } +} + +export function Itemform({ item, onSave, onCancel }: Props) { + const isEdit = !!item; + const [itemtype, setItemtype] = useState(isEdit ? item.type as ItemType : "login"); + const [title, setTitle] = useState(isEdit ? item.title : ""); + const [fields, setFields] = useState>({}); + const [tags, setTags] = useState(isEdit ? item.tags.map((t) => t.name).join(", ") : ""); + const [favorite, setFavorite] = useState(isEdit ? item.favorite : false); + const [revealed, setRevealed] = useState>({}); + + useEffect(() => { + if (isEdit) { + setFields(parsedata(item.data)); + } else { + setFields({}); + } + }, [isEdit, item]); + + function handlefieldchange(key: string, value: string) { + setFields((prev) => ({ ...prev, [key]: value })); + } + + function togglereveal(key: string) { + setRevealed((prev) => ({ ...prev, [key]: !prev[key] })); + } + + function handlesubmit(e: React.FormEvent) { + e.preventDefault(); + const taglist = tags + .split(",") + .map((t) => t.trim()) + .filter(Boolean); + onSave({ + type: itemtype, + title, + data: JSON.stringify(fields), + tags: taglist, + favorite, + }); + } + + const schema = schemas[itemtype]; + + return ( +
+
+

{isEdit ? "edit item" : "new item"}

+ +
+ +
+
+ + +
+ +
+ + setTitle(e.target.value)} + placeholder="e.g. github, gmail..." + required + /> +
+ +
+ + {schema.map((field) => ( +
+ + {field.type === "textarea" ? ( + -
- - -
- +
+ + +

+ + create account
-
- -
- - +
+ +
    +
    -
    -

    recent

    -
      +
      + +
      + +
      +

      +

      +
      +
      +
      diff --git a/apps/extension/src/api.ts b/apps/extension/src/api.ts index 1f86ed2..d1d638f 100644 --- a/apps/extension/src/api.ts +++ b/apps/extension/src/api.ts @@ -1,4 +1,6 @@ -const baseurl = "https://noro.sh/api"; +import { baseurl } from "./constants"; + +export { baseurl }; export interface StoreResponse { id: string; diff --git a/apps/extension/src/autofill.ts b/apps/extension/src/autofill.ts new file mode 100644 index 0000000..71e59c7 --- /dev/null +++ b/apps/extension/src/autofill.ts @@ -0,0 +1,355 @@ +import type { Credential, VaultItem } from "./types"; +import type { LoginForm } from "./detector"; +import { setvalue } from "./detector"; +import { generatepassword } from "./crypto"; + +type CredentialLike = Credential | VaultItem; + +let popup: HTMLDivElement | null = null; + +export function createpopup( + credentials: CredentialLike[], + anchor: HTMLInputElement, + onselect: (cred: CredentialLike) => void, + onsave: () => void, + ongenerate?: (password: string) => void +): void { + removepopup(); + + popup = document.createElement("div"); + popup.id = "noro-autofill"; + + const style = document.createElement("style"); + style.textContent = ` + #noro-autofill { + position: fixed; + z-index: 2147483647; + background: #1a1a1a; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + font-family: ui-monospace, monospace; + font-size: 13px; + color: #fff; + min-width: 240px; + max-width: 320px; + overflow: hidden; + } + #noro-autofill .noro-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + color: #d4b08c; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + } + #noro-autofill .noro-list { + max-height: 200px; + overflow-y: auto; + } + #noro-autofill .noro-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + cursor: pointer; + transition: background 0.1s; + border: none; + background: transparent; + width: 100%; + text-align: left; + } + #noro-autofill .noro-item:hover { + background: rgba(255, 255, 255, 0.05); + } + #noro-autofill .noro-icon { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(212, 176, 140, 0.15); + border-radius: 6px; + color: #d4b08c; + font-size: 12px; + flex-shrink: 0; + } + #noro-autofill .noro-info { + flex: 1; + min-width: 0; + } + #noro-autofill .noro-title { + font-size: 13px; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + #noro-autofill .noro-subtitle { + font-size: 11px; + color: rgba(255, 255, 255, 0.4); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + #noro-autofill .noro-actions { + display: flex; + gap: 4px; + } + #noro-autofill .noro-btn { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.08); + border: none; + border-radius: 4px; + color: rgba(255, 255, 255, 0.5); + cursor: pointer; + transition: all 0.1s; + } + #noro-autofill .noro-btn:hover { + background: rgba(212, 176, 140, 0.2); + color: #d4b08c; + } + #noro-autofill .noro-empty { + padding: 20px 12px; + text-align: center; + color: rgba(255, 255, 255, 0.4); + font-size: 12px; + } + #noro-autofill .noro-generate { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + cursor: pointer; + transition: background 0.1s; + } + #noro-autofill .noro-generate:hover { + background: rgba(255, 255, 255, 0.05); + } + #noro-autofill .noro-save { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + cursor: pointer; + transition: background 0.1s; + } + #noro-autofill .noro-save:hover { + background: rgba(255, 255, 255, 0.05); + } + `; + popup.appendChild(style); + + const header = document.createElement("div"); + header.className = "noro-header"; + header.innerHTML = `noro`; + popup.appendChild(header); + + const list = document.createElement("div"); + list.className = "noro-list"; + + if (credentials.length === 0) { + const empty = document.createElement("div"); + empty.className = "noro-empty"; + empty.textContent = "no saved logins for this site"; + list.appendChild(empty); + } else { + for (const cred of credentials) { + const item = document.createElement("button"); + item.className = "noro-item"; + + const icon = document.createElement("div"); + icon.className = "noro-icon"; + icon.textContent = "K"; + + const info = document.createElement("div"); + info.className = "noro-info"; + + const title = document.createElement("div"); + title.className = "noro-title"; + title.textContent = "title" in cred ? cred.title : cred.site; + + const subtitle = document.createElement("div"); + subtitle.className = "noro-subtitle"; + subtitle.textContent = cred.username || ""; + + info.appendChild(title); + info.appendChild(subtitle); + + const actions = document.createElement("div"); + actions.className = "noro-actions"; + + const copybtn = document.createElement("button"); + copybtn.className = "noro-btn"; + copybtn.innerHTML = ``; + copybtn.addEventListener("click", async (e) => { + e.stopPropagation(); + if (cred.password) { + await navigator.clipboard.writeText(cred.password); + } + }); + + actions.appendChild(copybtn); + + item.appendChild(icon); + item.appendChild(info); + item.appendChild(actions); + + item.addEventListener("click", () => { + onselect(cred); + removepopup(); + }); + + list.appendChild(item); + } + } + + popup.appendChild(list); + + if (credentials.length === 0) { + const save = document.createElement("div"); + save.className = "noro-save"; + save.innerHTML = ` +
      + +
      +
      +
      save after login
      +
      capture credentials on submit
      +
      + `; + save.addEventListener("click", () => { + onsave(); + removepopup(); + }); + popup.appendChild(save); + } + + if (ongenerate) { + const generate = document.createElement("div"); + generate.className = "noro-generate"; + generate.innerHTML = ` +
      + +
      +
      +
      generate password
      +
      create strong password
      +
      + `; + generate.addEventListener("click", () => { + const password = generatepassword(); + ongenerate(password); + removepopup(); + }); + popup.appendChild(generate); + } + + document.body.appendChild(popup); + positionpopup(anchor); + + const close = (e: MouseEvent) => { + if (popup && !popup.contains(e.target as Node)) { + removepopup(); + document.removeEventListener("click", close); + } + }; + setTimeout(() => document.addEventListener("click", close), 0); + + document.addEventListener("keydown", handleescape); +} + +function positionpopup(anchor: HTMLInputElement): void { + if (!popup) return; + + const rect = anchor.getBoundingClientRect(); + popup.style.top = `${rect.bottom + 4}px`; + popup.style.left = `${rect.left}px`; + + const popuprect = popup.getBoundingClientRect(); + + if (popuprect.bottom > window.innerHeight) { + popup.style.top = `${rect.top - popuprect.height - 4}px`; + } + + if (popuprect.right > window.innerWidth) { + popup.style.left = `${window.innerWidth - popuprect.width - 8}px`; + } +} + +function handleescape(e: KeyboardEvent): void { + if (e.key === "Escape") { + removepopup(); + } +} + +export function removepopup(): void { + if (popup) { + popup.remove(); + popup = null; + } + document.removeEventListener("keydown", handleescape); +} + +export function createicon(onclick: () => void): HTMLDivElement { + const icon = document.createElement("div"); + icon.style.cssText = ` + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(212, 176, 140, 0.15); + border-radius: 4px; + cursor: pointer; + z-index: 10000; + transition: background 0.1s; + `; + icon.innerHTML = ``; + + icon.addEventListener("mouseenter", () => { + icon.style.background = "rgba(212, 176, 140, 0.25)"; + }); + icon.addEventListener("mouseleave", () => { + icon.style.background = "rgba(212, 176, 140, 0.15)"; + }); + icon.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + onclick(); + }); + + return icon; +} + +export function fill(form: LoginForm, cred: { username?: string; password?: string }): void { + if (cred.username) { + setvalue(form.username, cred.username); + } + if (cred.password) { + setvalue(form.password, cred.password); + } +} + +export function fillpassword(form: LoginForm, password: string): void { + setvalue(form.password, password); +} diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts index 6ddf55a..f566bd9 100644 --- a/apps/extension/src/background.ts +++ b/apps/extension/src/background.ts @@ -1,5 +1,12 @@ import { encrypt, generatekey } from "./crypto"; import { store } from "./api"; +import { baseurl } from "./constants"; +import { getcredentials, savecredential } from "./credentials"; +import { matchurl, fetchitems } from "./vault"; +import type { VaultItem, RecentSecret } from "./types"; + +let cachedvault: VaultItem[] = []; +let vaultexpiry = 0; chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ @@ -37,19 +44,32 @@ chrome.contextMenus.onClicked.addListener(async (info) => { } }); -interface RecentSecret { - id: string; - url: string; - preview: string; - created: number; -} - async function saverecent(secret: RecentSecret) { const { recents = [] } = await chrome.storage.local.get("recents"); const updated = [secret, ...recents.slice(0, 9)]; await chrome.storage.local.set({ recents: updated }); } +async function fetchvaultitems(): Promise { + const now = Date.now(); + if (cachedvault.length > 0 && now < vaultexpiry) { + return cachedvault; + } + + const result = await fetchitems(); + if (result.success && result.items) { + cachedvault = result.items; + vaultexpiry = now + 60000; + return cachedvault; + } + return []; +} + +function clearvaultcache() { + cachedvault = []; + vaultexpiry = 0; +} + chrome.runtime.onMessage.addListener((message, _, respond) => { if (message.type === "share") { (async () => { @@ -73,4 +93,97 @@ chrome.runtime.onMessage.addListener((message, _, respond) => { }); return true; } + + if (message.type === "login") { + (async () => { + try { + const res = await fetch(`${baseurl}/auth/sign-in/email`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + email: message.email, + password: message.password, + }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + respond({ success: false, error: data.message || "invalid credentials" }); + return; + } + + const data = await res.json(); + clearvaultcache(); + respond({ success: true, token: data.token || "session" }); + } catch { + respond({ success: false, error: "connection failed" }); + } + })(); + return true; + } + + if (message.type === "items") { + (async () => { + try { + const items = await fetchvaultitems(); + respond({ success: true, items }); + } catch { + respond({ success: false, error: "connection failed" }); + } + })(); + return true; + } + + if (message.type === "vaultmatch") { + (async () => { + try { + const items = await fetchvaultitems(); + const matched = items.filter((item) => item.type === "login" && matchurl(item.url, message.url)); + respond(matched); + } catch { + respond([]); + } + })(); + return true; + } + + if (message.type === "credentials") { + getcredentials(message.site).then(respond); + return true; + } + + if (message.type === "savecredential") { + savecredential(message.site, message.username, message.password).then(() => { + chrome.notifications.create({ + type: "basic", + iconUrl: "icons/128.png", + title: "noro", + message: "login saved", + }); + respond({ success: true }); + }); + return true; + } + + if (message.type === "autofill") { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (tabs[0]?.id) { + chrome.tabs.sendMessage(tabs[0].id, { type: "autofill", item: message.item }); + } + }); + respond({ success: true }); + return true; + } + + if (message.type === "clearcache") { + clearvaultcache(); + respond({ success: true }); + return true; + } +}); + +chrome.commands.onCommand.addListener((command) => { + if (command === "open-popup") { + chrome.action.openPopup(); + } }); diff --git a/apps/extension/src/constants.ts b/apps/extension/src/constants.ts new file mode 100644 index 0000000..37960c8 --- /dev/null +++ b/apps/extension/src/constants.ts @@ -0,0 +1 @@ +export const baseurl = "https://noro.sh/api"; diff --git a/apps/extension/src/content.ts b/apps/extension/src/content.ts new file mode 100644 index 0000000..efd6154 --- /dev/null +++ b/apps/extension/src/content.ts @@ -0,0 +1,125 @@ +import { createpopup, createicon, fill, fillpassword } from "./autofill"; +import { findloginforms, getsite, observeforms } from "./detector"; +import type { LoginForm } from "./detector"; +import type { Credential, VaultItem } from "./types"; + +type CredentialOrVaultItem = Credential | VaultItem; + +let currentform: LoginForm | null = null; + +async function getcredentials(): Promise { + return new Promise((resolve) => { + chrome.runtime.sendMessage({ type: "credentials", site: getsite() }, (response) => { + resolve(response || []); + }); + }); +} + +async function getvaultitems(): Promise { + return new Promise((resolve) => { + chrome.runtime.sendMessage({ type: "vaultmatch", url: window.location.href }, (response) => { + resolve(response || []); + }); + }); +} + +function enablesaveonsubmit(): void { + if (!currentform) return; + + const { form, username, password } = currentform; + + const save = () => { + const user = username.value.trim(); + const pass = password.value; + + if (user && pass) { + chrome.runtime.sendMessage({ + type: "savecredential", + site: getsite(), + username: user, + password: pass, + }); + } + }; + + if (form) { + form.addEventListener("submit", save, { once: true }); + } + + const buttons = document.querySelectorAll( + 'button[type="submit"], input[type="submit"], button:not([type])' + ); + for (const button of buttons) { + button.addEventListener("click", () => setTimeout(save, 100), { once: true }); + } +} + +function handleselect(cred: CredentialOrVaultItem): void { + if (!currentform) return; + fill(currentform, cred); +} + +function handlegenerate(password: string): void { + if (!currentform) return; + fillpassword(currentform, password); +} + +function addicons(): void { + const forms = findloginforms(); + + for (const loginform of forms) { + if (loginform.username.dataset.noroicon) continue; + + loginform.username.dataset.noroicon = "true"; + loginform.password.dataset.noroicon = "true"; + + const wrapper = loginform.username.parentElement; + if (wrapper) { + const computed = getComputedStyle(wrapper); + if (computed.position === "static") { + wrapper.style.position = "relative"; + } + + const icon = createicon(async () => { + currentform = loginform; + const [credentials, vaultitems] = await Promise.all([ + getcredentials(), + getvaultitems(), + ]); + const combined: CredentialOrVaultItem[] = [...vaultitems, ...credentials]; + createpopup(combined, loginform.username, handleselect, enablesaveonsubmit, handlegenerate); + }); + + wrapper.appendChild(icon); + } + } +} + +function init(): void { + addicons(); + observeforms(addicons); +} + +chrome.runtime.onMessage.addListener((message, _, respond) => { + if (message.type === "autofill") { + const forms = findloginforms(); + if (forms.length > 0 && message.item) { + currentform = forms[0]; + fill(currentform, message.item); + } + respond({ success: true }); + return true; + } + + if (message.type === "getforms") { + const forms = findloginforms(); + respond({ hasforms: forms.length > 0 }); + return true; + } +}); + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); +} else { + init(); +} diff --git a/apps/extension/src/credentials.ts b/apps/extension/src/credentials.ts new file mode 100644 index 0000000..4dbcfe9 --- /dev/null +++ b/apps/extension/src/credentials.ts @@ -0,0 +1,33 @@ +import type { Credential } from "./types"; + +export type { Credential }; + +export { getsession, setsession } from "./session"; + +export async function getcredentials(site: string): Promise { + const { credentials = [] } = await chrome.storage.local.get("credentials"); + return credentials.filter((c: Credential) => c.site === site || site.endsWith("." + c.site)); +} + +export async function savecredential(site: string, username: string, password: string): Promise { + const { credentials = [] } = await chrome.storage.local.get("credentials"); + const existing = credentials.findIndex( + (c: Credential) => c.site === site && c.username === username + ); + + const cred: Credential = { + id: crypto.randomUUID(), + site, + username, + password, + created: Date.now(), + }; + + if (existing >= 0) { + credentials[existing] = cred; + } else { + credentials.unshift(cred); + } + + await chrome.storage.local.set({ credentials }); +} diff --git a/apps/extension/src/crypto.ts b/apps/extension/src/crypto.ts index 31053ae..45299bc 100644 --- a/apps/extension/src/crypto.ts +++ b/apps/extension/src/crypto.ts @@ -1,40 +1,88 @@ -export function generatekey(): string { - const bytes = crypto.getRandomValues(new Uint8Array(24)); - let key = ""; - const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 24; i++) { - key += chars[bytes[i] % chars.length]; - } - return key; +async function derivekey(key: string, usage: "encrypt" | "decrypt") { + const keydata = new TextEncoder().encode(key.padEnd(32, "0").slice(0, 32)); + return crypto.subtle.importKey("raw", keydata, "AES-GCM", false, [usage]); +} + +function tobase64url(bytes: Uint8Array): string { + let binary = ""; + bytes.forEach((byte) => (binary += String.fromCharCode(byte))); + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); +} + +function frombase64url(encoded: string): Uint8Array { + const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/"); + const padding = (4 - (base64.length % 4)) % 4; + const padded = base64 + "=".repeat(padding); + const binary = atob(padded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; } export async function encrypt(text: string, key: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(text); - const keydata = encoder.encode(key.padEnd(32, "0").slice(0, 32)); - const iv = crypto.getRandomValues(new Uint8Array(12)); - const cryptokey = await crypto.subtle.importKey("raw", keydata, "AES-GCM", false, ["encrypt"]); - const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, cryptokey, data); - const combined = new Uint8Array(iv.length + encrypted.byteLength); - combined.set(iv); - combined.set(new Uint8Array(encrypted), iv.length); - return btoa(String.fromCharCode(...combined)) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/, ""); + const data = new TextEncoder().encode(text); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const cryptokey = await derivekey(key, "encrypt"); + const encrypted = await crypto.subtle.encrypt( + { name: "AES-GCM", iv }, + cryptokey, + data + ); + const combined = new Uint8Array(iv.length + encrypted.byteLength); + combined.set(iv); + combined.set(new Uint8Array(encrypted), iv.length); + return tobase64url(combined); } export async function decrypt(encoded: string, key: string): Promise { - const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/"); - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - const iv = bytes.slice(0, 12); - const data = bytes.slice(12); - const keydata = new TextEncoder().encode(key.padEnd(32, "0").slice(0, 32)); - const cryptokey = await crypto.subtle.importKey("raw", keydata, "AES-GCM", false, ["decrypt"]); - const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, cryptokey, data); - return new TextDecoder().decode(decrypted); + const bytes = frombase64url(encoded); + const iv = bytes.slice(0, 12); + const data = bytes.slice(12); + const cryptokey = await derivekey(key, "decrypt"); + const decrypted = await crypto.subtle.decrypt( + { name: "AES-GCM", iv }, + cryptokey, + data + ); + return new TextDecoder().decode(decrypted); +} + +export function generatekey(): string { + const bytes = crypto.getRandomValues(new Uint8Array(24)); + let key = ""; + const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 24; i++) { + key += chars[bytes[i] % chars.length]; + } + return key; +} + +export function generatepassword(length = 20): string { + const lower = "abcdefghijklmnopqrstuvwxyz"; + const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const digits = "0123456789"; + const symbols = "!@#$%^&*"; + const all = lower + upper + digits + symbols; + + const bytes = crypto.getRandomValues(new Uint8Array(length)); + let password = ""; + + password += lower[bytes[0] % lower.length]; + password += upper[bytes[1] % upper.length]; + password += digits[bytes[2] % digits.length]; + password += symbols[bytes[3] % symbols.length]; + + for (let i = 4; i < length; i++) { + password += all[bytes[i] % all.length]; + } + + const shuffled = password.split(""); + for (let i = shuffled.length - 1; i > 0; i--) { + const j = bytes[i % bytes.length] % (i + 1); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + + return shuffled.join(""); } diff --git a/apps/extension/src/detector.ts b/apps/extension/src/detector.ts new file mode 100644 index 0000000..033f78a --- /dev/null +++ b/apps/extension/src/detector.ts @@ -0,0 +1,77 @@ +export interface LoginForm { + form: HTMLFormElement | null; + username: HTMLInputElement; + password: HTMLInputElement; +} + +export function findloginforms(): LoginForm[] { + const forms: LoginForm[] = []; + const passwords = document.querySelectorAll('input[type="password"]'); + + for (const password of passwords) { + if (!password.offsetParent) continue; + + const form = password.closest("form"); + const container = form || password.parentElement; + if (!container) continue; + + const inputs = container.querySelectorAll("input"); + let username: HTMLInputElement | null = null; + + for (const input of inputs) { + if (input === password) continue; + if (input.type === "hidden" || input.type === "submit") continue; + + const type = input.type.toLowerCase(); + const name = (input.name || "").toLowerCase(); + const id = (input.id || "").toLowerCase(); + const autocomplete = (input.autocomplete || "").toLowerCase(); + + if ( + type === "email" || + type === "text" || + name.includes("user") || + name.includes("email") || + name.includes("login") || + id.includes("user") || + id.includes("email") || + id.includes("login") || + autocomplete === "username" || + autocomplete === "email" + ) { + username = input; + break; + } + } + + if (username) { + forms.push({ form, username, password }); + } + } + + return forms; +} + +export function getsite(): string { + return window.location.hostname.replace(/^www\./, ""); +} + +export function setvalue(input: HTMLInputElement, value: string): void { + input.focus(); + input.value = value; + input.dispatchEvent(new Event("input", { bubbles: true })); + input.dispatchEvent(new Event("change", { bubbles: true })); +} + +export function observeforms(callback: () => void): MutationObserver { + const observer = new MutationObserver(() => { + callback(); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + return observer; +} diff --git a/apps/extension/src/popup.ts b/apps/extension/src/popup.ts index 5618df0..6634ae4 100644 --- a/apps/extension/src/popup.ts +++ b/apps/extension/src/popup.ts @@ -1,97 +1,307 @@ -const secret = document.getElementById("secret") as HTMLTextAreaElement; -const ttl = document.getElementById("ttl") as HTMLSelectElement; -const views = document.getElementById("views") as HTMLSelectElement; -const share = document.getElementById("share") as HTMLButtonElement; -const url = document.getElementById("url") as HTMLInputElement; -const copy = document.getElementById("copy") as HTMLButtonElement; -const newbtn = document.getElementById("new") as HTMLButtonElement; -const list = document.getElementById("list") as HTMLUListElement; -const createview = document.getElementById("create") as HTMLDivElement; -const resultview = document.getElementById("result") as HTMLDivElement; - -function show(view: "create" | "result") { - createview.classList.toggle("active", view === "create"); - resultview.classList.toggle("active", view === "result"); -} +import type { VaultItem } from "./types"; +import { getsession, setsession } from "./session"; -share.addEventListener("click", async () => { - const text = secret.value.trim(); - if (!text) return; +const loginview = document.getElementById("login") as HTMLDivElement; +const vaultview = document.getElementById("vault") as HTMLDivElement; +const detailview = document.getElementById("detail") as HTMLDivElement; - share.disabled = true; - share.textContent = "sharing..."; +const emailinput = document.getElementById("email") as HTMLInputElement; +const passwordinput = document.getElementById("password") as HTMLInputElement; +const loginerror = document.getElementById("loginerror") as HTMLParagraphElement; +const signinbtn = document.getElementById("signin") as HTMLButtonElement; - const response = await chrome.runtime.sendMessage({ - type: "share", - text, - ttl: ttl.value, - views: parseInt(views.value), - }); +const searchinput = document.getElementById("search") as HTMLInputElement; +const itemslist = document.getElementById("items") as HTMLUListElement; +const useremail = document.getElementById("useremail") as HTMLSpanElement; +const signoutbtn = document.getElementById("signout") as HTMLButtonElement; - if (response.success) { - url.value = response.url; - show("result"); - loadrecents(); +const backbtn = document.getElementById("back") as HTMLButtonElement; +const detailicon = document.getElementById("detailicon") as HTMLSpanElement; +const detailtitle = document.getElementById("detailtitle") as HTMLHeadingElement; +const detailtype = document.getElementById("detailtype") as HTMLParagraphElement; +const fieldscontainer = document.getElementById("fields") as HTMLDivElement; + +let items: VaultItem[] = []; +let currentitem: VaultItem | null = null; + +const icons: Record = { + login: "K", + note: "N", + card: "C", + identity: "I", + ssh: "S", + api: "A", +}; + +function show(view: "login" | "vault" | "detail") { + loginview.classList.toggle("active", view === "login"); + vaultview.classList.toggle("active", view === "vault"); + detailview.classList.toggle("active", view === "detail"); +} + +async function init() { + const session = await getsession(); + if (session) { + useremail.textContent = session.email; + await loaditems(); + show("vault"); } else { - share.textContent = "failed"; - setTimeout(() => { - share.textContent = "share"; - share.disabled = false; - }, 2000); + show("login"); } -}); +} + +signinbtn.addEventListener("click", async () => { + const email = emailinput.value.trim(); + const password = passwordinput.value; + + if (!email || !password) { + loginerror.textContent = "all fields required"; + return; + } + + loginerror.textContent = ""; + signinbtn.disabled = true; + signinbtn.textContent = "signing in..."; + + try { + const response = await chrome.runtime.sendMessage({ + type: "login", + email, + password, + }); -copy.addEventListener("click", async () => { - await navigator.clipboard.writeText(url.value); - copy.textContent = "copied!"; - setTimeout(() => { - copy.textContent = "copy"; - }, 2000); + if (response.success) { + await setsession({ email, token: response.token }); + useremail.textContent = email; + await loaditems(); + show("vault"); + emailinput.value = ""; + passwordinput.value = ""; + } else { + loginerror.textContent = response.error || "login failed"; + } + } catch { + loginerror.textContent = "connection error"; + } finally { + signinbtn.disabled = false; + signinbtn.textContent = "sign in"; + } }); -newbtn.addEventListener("click", () => { - secret.value = ""; - share.textContent = "share"; - share.disabled = false; - show("create"); +signoutbtn.addEventListener("click", async () => { + await setsession(null); + await chrome.runtime.sendMessage({ type: "clearcache" }); + items = []; + searchinput.value = ""; + show("login"); }); -interface RecentSecret { - id: string; - url: string; - preview: string; - created: number; +async function loaditems() { + const response = await chrome.runtime.sendMessage({ type: "items" }); + if (response.success) { + items = response.items; + renderitems(); + } } -async function loadrecents() { - const recents: RecentSecret[] = await chrome.runtime.sendMessage({ type: "recents" }); - list.innerHTML = ""; +function renderitems() { + const query = searchinput.value.toLowerCase(); + const filtered = items.filter( + (item) => + item.title.toLowerCase().includes(query) || + (item.username && item.username.toLowerCase().includes(query)) || + (item.url && item.url.toLowerCase().includes(query)), + ); - if (recents.length === 0) { - list.innerHTML = "
    • no recent secrets
    • "; + itemslist.innerHTML = ""; + + if (filtered.length === 0) { + const empty = document.createElement("li"); + empty.className = "empty"; + const icon = document.createElement("div"); + icon.className = "emptyicon"; + icon.innerHTML = items.length === 0 + ? `` + : ``; + const text = document.createElement("span"); + text.textContent = items.length === 0 ? "your vault is empty" : "no matches found"; + empty.appendChild(icon); + empty.appendChild(text); + itemslist.appendChild(empty); return; } - for (const item of recents) { + const sorted = [...filtered].sort((a, b) => { + if (a.favorite !== b.favorite) return a.favorite ? -1 : 1; + return a.title.localeCompare(b.title); + }); + + for (const item of sorted) { const li = document.createElement("li"); - const preview = document.createElement("span"); - preview.className = "preview"; - preview.textContent = item.preview + (item.preview.length >= 50 ? "..." : ""); - - const copybtn = document.createElement("button"); - copybtn.textContent = "copy"; - copybtn.addEventListener("click", async () => { - await navigator.clipboard.writeText(item.url); - copybtn.textContent = "copied!"; - setTimeout(() => { - copybtn.textContent = "copy"; - }, 2000); + li.addEventListener("click", () => showdetail(item)); + + const icon = document.createElement("span"); + icon.className = "icon"; + icon.textContent = icons[item.type] || "?"; + + const info = document.createElement("div"); + info.className = "info"; + + const title = document.createElement("div"); + title.className = "title"; + title.textContent = item.title; + + const subtitle = document.createElement("div"); + subtitle.className = "subtitle"; + subtitle.textContent = item.username || item.type; + + info.appendChild(title); + info.appendChild(subtitle); + + li.appendChild(icon); + li.appendChild(info); + + if (item.favorite) { + const star = document.createElement("span"); + star.className = "star"; + star.textContent = "*"; + li.appendChild(star); + } + + itemslist.appendChild(li); + } +} + +searchinput.addEventListener("input", renderitems); + +function showdetail(item: VaultItem) { + currentitem = item; + detailicon.textContent = icons[item.type] || "?"; + detailtitle.textContent = item.title; + detailtype.textContent = item.type; + + fieldscontainer.innerHTML = ""; + + if (item.username) { + fieldscontainer.appendChild(createfield("username", item.username, false)); + } + + if (item.password) { + fieldscontainer.appendChild(createfield("password", item.password, true)); + } + + if (item.url) { + fieldscontainer.appendChild(createfield("url", item.url, false, true)); + } + + if (item.notes) { + fieldscontainer.appendChild(createfield("notes", item.notes, false)); + } + + if (item.type === "login" && (item.username || item.password)) { + fieldscontainer.appendChild(createautofillbtn(item)); + } + + show("detail"); +} + +function createfield( + label: string, + value: string, + ispassword: boolean, + isurl = false, +): HTMLDivElement { + const field = document.createElement("div"); + field.className = "field"; + + const labelel = document.createElement("div"); + labelel.className = "label"; + labelel.textContent = label; + + const valueel = document.createElement("div"); + valueel.className = "value"; + + const textel = document.createElement("span"); + textel.className = ispassword ? "text password" : "text"; + + let hidden = ispassword; + const displayvalue = () => { + textel.textContent = hidden ? "••••••••••" : value; + }; + displayvalue(); + + const actions = document.createElement("div"); + actions.className = "actions"; + + const copybtn = document.createElement("button"); + copybtn.type = "button"; + copybtn.innerHTML = ``; + copybtn.addEventListener("click", async () => { + await navigator.clipboard.writeText(value); + copybtn.innerHTML = ``; + setTimeout(() => { + copybtn.innerHTML = ``; + }, 1500); + }); + + actions.appendChild(copybtn); + + if (ispassword) { + const togglebtn = document.createElement("button"); + togglebtn.type = "button"; + togglebtn.innerHTML = ``; + togglebtn.addEventListener("click", () => { + hidden = !hidden; + displayvalue(); + togglebtn.innerHTML = hidden + ? `` + : ``; }); + actions.appendChild(togglebtn); + } - li.appendChild(preview); - li.appendChild(copybtn); - list.appendChild(li); + if (isurl) { + const openbtn = document.createElement("button"); + openbtn.type = "button"; + openbtn.innerHTML = ``; + openbtn.addEventListener("click", () => { + let url = value; + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url; + } + chrome.tabs.create({ url }); + }); + actions.appendChild(openbtn); } + + valueel.appendChild(textel); + valueel.appendChild(actions); + field.appendChild(labelel); + field.appendChild(valueel); + + return field; +} + +function createautofillbtn(item: VaultItem): HTMLDivElement { + const wrapper = document.createElement("div"); + wrapper.className = "field autofill"; + + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "autofillbtn"; + btn.innerHTML = `autofill on page`; + btn.addEventListener("click", async () => { + await chrome.runtime.sendMessage({ type: "autofill", item }); + window.close(); + }); + + wrapper.appendChild(btn); + return wrapper; } -loadrecents(); +backbtn.addEventListener("click", () => { + currentitem = null; + show("vault"); +}); + +init(); diff --git a/apps/extension/src/session.ts b/apps/extension/src/session.ts new file mode 100644 index 0000000..2c2f30e --- /dev/null +++ b/apps/extension/src/session.ts @@ -0,0 +1,14 @@ +import type { Session } from "./types"; + +export async function getsession(): Promise { + const { session } = await chrome.storage.local.get("session"); + return session || null; +} + +export async function setsession(session: Session | null): Promise { + if (session) { + await chrome.storage.local.set({ session }); + } else { + await chrome.storage.local.remove("session"); + } +} diff --git a/apps/extension/src/types.ts b/apps/extension/src/types.ts new file mode 100644 index 0000000..9d75378 --- /dev/null +++ b/apps/extension/src/types.ts @@ -0,0 +1,30 @@ +export interface Credential { + id: string; + site: string; + username: string; + password: string; + created: number; +} + +export interface Session { + email: string; + token: string; +} + +export interface VaultItem { + id: string; + type: string; + title: string; + username?: string; + password?: string; + url?: string; + notes?: string; + favorite: boolean; +} + +export interface RecentSecret { + id: string; + url: string; + preview: string; + created: number; +} diff --git a/apps/extension/src/vault.ts b/apps/extension/src/vault.ts new file mode 100644 index 0000000..9d19a53 --- /dev/null +++ b/apps/extension/src/vault.ts @@ -0,0 +1,64 @@ +import type { VaultItem } from "./types"; +import { getsession } from "./session"; +import { baseurl } from "./constants"; + +export type { VaultItem }; + +export async function fetchitems(): Promise<{ success: boolean; items?: VaultItem[]; error?: string }> { + const session = await getsession(); + if (!session) { + return { success: false, error: "not authenticated" }; + } + + try { + const res = await fetch(`${baseurl}/v1/vault/items`, { + headers: { authorization: `Bearer ${session.token}` }, + }); + + if (!res.ok) { + if (res.status === 401) { + await chrome.storage.local.remove("session"); + return { success: false, error: "session expired" }; + } + return { success: false, error: "failed to load items" }; + } + + const data = await res.json(); + const items = (data.items || []).map((item: Record) => { + const parsed = typeof item.data === "string" ? JSON.parse(item.data as string) : item.data; + return { + id: item.id, + type: item.type, + title: item.title, + username: parsed?.username, + password: parsed?.password, + url: parsed?.url, + notes: parsed?.notes, + favorite: item.favorite || false, + }; + }); + + return { success: true, items }; + } catch { + return { success: false, error: "connection failed" }; + } +} + +export function matchurl(itemurl: string | undefined, pageurl: string): boolean { + if (!itemurl) return false; + + try { + let normalized = itemurl; + if (!normalized.startsWith("http://") && !normalized.startsWith("https://")) { + normalized = "https://" + normalized; + } + + const itemhost = new URL(normalized).hostname.replace(/^www\./, ""); + const pagehost = new URL(pageurl).hostname.replace(/^www\./, ""); + + return itemhost === pagehost || pagehost.endsWith("." + itemhost); + } catch { + return false; + } +} + diff --git a/apps/extension/styles.css b/apps/extension/styles.css index dade2ea..1e62348 100644 --- a/apps/extension/styles.css +++ b/apps/extension/styles.css @@ -13,225 +13,536 @@ } ::selection { - background: #ff6b00; - color: #000; + background: #d4b08c; + color: #0a0a0a; +} + +::-webkit-scrollbar { + width: 4px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.08); + border-radius: 2px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.12); } body { - width: 320px; - font-family: "JetBrains Mono", ui-monospace, monospace; + width: 340px; + min-height: 420px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 13px; - background: oklch(0.145 0 0); - color: oklch(0.985 0 0); + background: #0a0a0a; + color: #ededed; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } main { - padding: 16px; + padding: 24px; + display: flex; + flex-direction: column; + min-height: 420px; + position: relative; +} + +main::before { + content: ""; + position: absolute; + top: 0; + right: 0; + width: 200px; + height: 200px; + background: radial-gradient(circle at top right, rgba(212, 176, 140, 0.04) 0%, transparent 70%); + pointer-events: none; } header { display: flex; align-items: center; - gap: 8px; - margin-bottom: 16px; + gap: 10px; + margin-bottom: 28px; + position: relative; + z-index: 1; } header svg { - color: #ff6b00; + color: #d4b08c; + opacity: 0.9; } header span { - font-size: 11px; - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; - color: rgba(255, 255, 255, 0.6); + font-family: Georgia, "Times New Roman", serif; + font-style: italic; + font-size: 15px; + letter-spacing: 0.02em; + color: #ededed; } .view { display: none; + flex-direction: column; + flex: 1; + position: relative; + z-index: 1; } .view.active { - display: block; + display: flex; } -textarea { +input { width: 100%; - padding: 12px 16px; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 10px; - background: transparent; - color: oklch(0.985 0 0); + padding: 14px 16px; + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 12px; + background: #161616; + color: #ededed; font-family: inherit; font-size: 13px; - resize: none; - transition: border-color 0.15s; + transition: all 0.2s ease; +} + +input:hover { + border-color: rgba(255, 255, 255, 0.08); } -textarea:focus { +input:focus { outline: none; - border-color: #ff6b00; + border-color: rgba(212, 176, 140, 0.4); + background: #161616; + box-shadow: 0 0 0 3px rgba(212, 176, 140, 0.08); } -textarea::placeholder { - color: rgba(255, 255, 255, 0.2); +input::placeholder { + color: rgba(255, 255, 255, 0.25); } -.options { - display: flex; - gap: 8px; - margin-top: 12px; +input.mono { + font-family: "JetBrains Mono", ui-monospace, monospace; + font-size: 11px; + letter-spacing: 0.02em; } -select { - flex: 1; - padding: 10px 12px; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 10px; - background: transparent; - color: oklch(0.985 0 0); - font-family: inherit; - font-size: 12px; - cursor: pointer; - transition: border-color 0.15s; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='rgba(255,255,255,0.4)' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 12px center; +#login { + justify-content: center; + padding-top: 16px; } -select:focus { - outline: none; - border-color: #ff6b00; +#login input { + margin-bottom: 12px; } -select option { - background: oklch(0.145 0 0); - color: oklch(0.985 0 0); +.error { + color: #ef4444; + font-size: 11px; + min-height: 18px; + margin-bottom: 8px; + text-align: center; + font-weight: 500; } button { width: 100%; - padding: 12px 16px; - margin-top: 12px; + padding: 14px 20px; border: none; - border-radius: 10px; - background: #ff6b00; - color: #000; + border-radius: 12px; + background: linear-gradient(135deg, #d4b08c 0%, #c9a27b 100%); + color: #0a0a0a; font-family: inherit; - font-weight: 700; - font-size: 11px; - letter-spacing: 0.1em; - text-transform: uppercase; + font-weight: 600; + font-size: 13px; + letter-spacing: 0.01em; cursor: pointer; - transition: opacity 0.15s; + transition: all 0.25s ease; + box-shadow: 0 2px 12px rgba(212, 176, 140, 0.15); } button:hover { - opacity: 0.8; + background: linear-gradient(135deg, #dfc09e 0%, #d4b08c 100%); + box-shadow: 0 4px 20px rgba(212, 176, 140, 0.25); + transform: translateY(-1px); +} + +button:active { + transform: translateY(0); + box-shadow: 0 2px 8px rgba(212, 176, 140, 0.2); } button:disabled { - opacity: 0.3; + opacity: 0.5; cursor: not-allowed; + transform: none; + box-shadow: none; } -input[type="text"] { - width: 100%; - padding: 12px 16px; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 10px; +button.ghost { background: transparent; - color: oklch(0.985 0 0); - font-family: inherit; + border: 1px solid rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.5); + padding: 8px 14px; + width: auto; + box-shadow: none; + font-weight: 500; +} + +button.ghost:hover { + border-color: rgba(255, 255, 255, 0.12); + color: rgba(255, 255, 255, 0.8); + background: rgba(255, 255, 255, 0.03); + transform: none; +} + +.link { + display: block; + text-align: center; + margin-top: 20px; + color: rgba(255, 255, 255, 0.4); font-size: 12px; + text-decoration: none; + transition: color 0.2s ease; } -input[type="text"]:focus { - outline: none; - border-color: #ff6b00; +.link:hover { + color: #d4b08c; } -.actions { - display: flex; - gap: 8px; +.search { + position: relative; + margin-bottom: 20px; +} + +.search svg { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: rgba(255, 255, 255, 0.2); + pointer-events: none; + transition: color 0.2s ease; +} + +.search:focus-within svg { + color: rgba(212, 176, 140, 0.6); +} + +.search input { + padding-left: 40px; + background: rgba(255, 255, 255, 0.03); + border-color: rgba(255, 255, 255, 0.04); +} + +.search input:hover { + background: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.06); +} + +.search input:focus { + background: rgba(255, 255, 255, 0.04); } -.actions button { +#items { + list-style: none; flex: 1; + overflow-y: auto; + max-height: 260px; + margin: 0 -12px; + padding: 0 12px; } -.actions button:last-child { +#items li { + display: flex; + align-items: center; + gap: 14px; + padding: 14px; + margin-bottom: 6px; + border-radius: 14px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid transparent; background: transparent; - border: 1px solid rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.4); } -.actions button:last-child:hover { - border-color: rgba(255, 255, 255, 0.3); - color: rgba(255, 255, 255, 0.6); +#items li:hover { + background: rgba(255, 255, 255, 0.03); + border-color: rgba(255, 255, 255, 0.05); } -#recents { - margin-top: 20px; - padding-top: 16px; - border-top: 1px solid rgba(255, 255, 255, 0.1); - display: block !important; +#items li:active { + background: rgba(255, 255, 255, 0.05); + transform: scale(0.99); } -#recents h3 { - font-size: 10px; - font-weight: 700; +#items li .icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(212, 176, 140, 0.12) 0%, rgba(212, 176, 140, 0.06) 100%); + border: 1px solid rgba(212, 176, 140, 0.1); + border-radius: 12px; + font-size: 14px; + font-weight: 600; + color: #d4b08c; + flex-shrink: 0; + font-family: "JetBrains Mono", ui-monospace, monospace; +} + +#items li .info { + flex: 1; + min-width: 0; +} + +#items li .title { + font-size: 13px; + font-weight: 500; + color: #ededed; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 3px; +} + +#items li .subtitle { + font-size: 11px; + color: rgba(255, 255, 255, 0.35); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#items li .star { + color: #d4b08c; + font-size: 12px; + opacity: 0.7; +} + +#items .empty { + padding: 56px 24px; + text-align: center; color: rgba(255, 255, 255, 0.4); - text-transform: uppercase; - letter-spacing: 0.1em; - margin-bottom: 12px; + font-size: 13px; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + cursor: default; } -#list { - list-style: none; +#items .empty .emptyicon { + width: 56px; + height: 56px; + background: linear-gradient(135deg, rgba(212, 176, 140, 0.08) 0%, rgba(212, 176, 140, 0.03) 100%); + border: 1px solid rgba(212, 176, 140, 0.08); + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + color: rgba(212, 176, 140, 0.5); } -#list li { +.footer { display: flex; align-items: center; justify-content: space-between; - padding: 10px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding-top: 20px; + margin-top: auto; + border-top: 1px solid rgba(255, 255, 255, 0.05); } -#list li:last-child { - border-bottom: none; +.email { + font-size: 11px; + color: rgba(255, 255, 255, 0.35); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; } -#list li.empty { - color: rgba(255, 255, 255, 0.3); +.back { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 24px; font-size: 12px; } -#list .preview { +.back svg { + transition: transform 0.2s ease; +} + +.back:hover svg { + transform: translateX(-3px); +} + +.detailheader { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 28px; + padding-bottom: 24px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.detailheader .icon { + width: 52px; + height: 52px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(212, 176, 140, 0.15) 0%, rgba(212, 176, 140, 0.08) 100%); + border: 1px solid rgba(212, 176, 140, 0.15); + border-radius: 14px; + font-size: 18px; + font-weight: 600; + color: #d4b08c; + font-family: "JetBrains Mono", ui-monospace, monospace; +} + +.detailheader h2 { + font-size: 16px; + font-weight: 600; + margin-bottom: 4px; + color: #ededed; +} + +.detailheader .type { + font-size: 11px; + color: rgba(255, 255, 255, 0.4); + text-transform: lowercase; + font-weight: 500; +} + +.fields { + display: flex; + flex-direction: column; + gap: 10px; +} + +.field { + background: #161616; + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 14px; + padding: 14px 16px; + transition: all 0.2s ease; +} + +.field:hover { + border-color: rgba(255, 255, 255, 0.08); + background: #181818; +} + +.field .label { + font-size: 10px; + color: rgba(255, 255, 255, 0.35); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 8px; + font-weight: 500; +} + +.field .value { + font-size: 13px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.field .text { flex: 1; - font-size: 12px; - color: rgba(255, 255, 255, 0.5); + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - margin-right: 12px; + color: #ededed; } -#list button { - width: auto; - margin: 0; - padding: 6px 12px; - font-size: 10px; - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.1); +.field .text.password { + font-family: "JetBrains Mono", ui-monospace, monospace; + letter-spacing: 0.2em; + color: rgba(255, 255, 255, 0.5); + font-size: 12px; +} + +.field .actions { + display: flex; + gap: 6px; +} + +.field button { + width: 32px; + height: 32px; + padding: 0; + border-radius: 10px; + background: rgba(255, 255, 255, 0.04); color: rgba(255, 255, 255, 0.4); + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid transparent; + box-shadow: none; +} + +.field button:hover { + background: rgba(212, 176, 140, 0.12); + border-color: rgba(212, 176, 140, 0.15); + color: #d4b08c; + transform: none; + box-shadow: none; +} + +.field button svg { + width: 14px; + height: 14px; +} + +.field.autofill { + padding: 0; + background: transparent; + border: none; +} + +.field.autofill:hover { + border-color: transparent; + background: transparent; +} + +.autofillbtn { + width: 100%; + padding: 16px 20px; + border: none; + border-radius: 14px; + background: linear-gradient(135deg, #d4b08c 0%, #c9a27b 100%); + color: #0a0a0a; + font-family: inherit; + font-weight: 600; + font-size: 13px; + letter-spacing: 0.01em; + cursor: pointer; + transition: all 0.25s ease; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 16px rgba(212, 176, 140, 0.2); +} + +.autofillbtn:hover { + background: linear-gradient(135deg, #dfc09e 0%, #d4b08c 100%); + box-shadow: 0 6px 24px rgba(212, 176, 140, 0.3); + transform: translateY(-1px); } -#list button:hover { - border-color: #ff6b00; - color: #ff6b00; - opacity: 1; +.autofillbtn:active { + transform: translateY(0); + box-shadow: 0 2px 12px rgba(212, 176, 140, 0.2); } diff --git a/apps/mobile-core/Cargo.lock b/apps/mobile-core/Cargo.lock new file mode 100644 index 0000000..03ca03b --- /dev/null +++ b/apps/mobile-core/Cargo.lock @@ -0,0 +1,1588 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "cc" +version = "1.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noro-mobile-core" +version = "0.1.0" +dependencies = [ + "argon2", + "base64", + "chacha20poly1305", + "rand", + "serde", + "serde_json", + "thiserror 2.0.18", + "uniffi", + "ureq", + "uuid", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "uniffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb08c58c7ed7033150132febe696bef553f891b1ede57424b40d87a89e3c170" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cade167af943e189a55020eda2c314681e223f1e42aca7c4e52614c2b627698f" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "once_cell", + "paste", + "serde", + "textwrap", + "toml", + "uniffi_meta", + "uniffi_udl", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "uniffi_core" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f" +dependencies = [ + "anyhow", + "bytes", + "log", + "once_cell", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a74ed96c26882dac1ca9b93ca23c827e284bacbd7ec23c6f0b0372f747d59e4" +dependencies = [ + "anyhow", + "bytes", + "siphasher", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6f984f0781f892cc864a62c3a5c60361b1ccbd68e538e6c9fbced5d82268ac" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037820a4cfc4422db1eaa82f291a3863c92c7d1789dc513489c36223f9b4cdfc" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/apps/mobile-core/Cargo.toml b/apps/mobile-core/Cargo.toml new file mode 100644 index 0000000..bfefaf6 --- /dev/null +++ b/apps/mobile-core/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "noro-mobile-core" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib", "lib"] +name = "noro_mobile_core" + +[dependencies] +uniffi = { version = "0.28", features = ["cli"] } +chacha20poly1305 = "0.10" +argon2 = "0.5" +rand = "0.8" +thiserror = "2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +base64 = "0.22" +uuid = { version = "1", features = ["v4"] } +ureq = { version = "2", features = ["json"] } + +[[bin]] +name = "uniffi-bindgen" +path = "src/bindgen.rs" diff --git a/apps/mobile-core/bindings/NoroCore.swift b/apps/mobile-core/bindings/NoroCore.swift new file mode 100644 index 0000000..018cead --- /dev/null +++ b/apps/mobile-core/bindings/NoroCore.swift @@ -0,0 +1,1657 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +// swiftlint:disable all +import Foundation + +// Depending on the consumer's build setup, the low-level FFI code +// might be in a separate module, or it might be compiled inline into +// this module. This is a bit of light hackery to work with both. +#if canImport(NoroCoreFFI) +import NoroCoreFFI +#endif + +fileprivate extension RustBuffer { + // Allocate a new buffer, copying the contents of a `UInt8` array. + init(bytes: [UInt8]) { + let rbuf = bytes.withUnsafeBufferPointer { ptr in + RustBuffer.from(ptr) + } + self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data) + } + + static func empty() -> RustBuffer { + RustBuffer(capacity: 0, len:0, data: nil) + } + + static func from(_ ptr: UnsafeBufferPointer) -> RustBuffer { + try! rustCall { ffi_noro_mobile_core_rustbuffer_from_bytes(ForeignBytes(bufferPointer: ptr), $0) } + } + + // Frees the buffer in place. + // The buffer must not be used after this is called. + func deallocate() { + try! rustCall { ffi_noro_mobile_core_rustbuffer_free(self, $0) } + } +} + +fileprivate extension ForeignBytes { + init(bufferPointer: UnsafeBufferPointer) { + self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) + } +} + +// For every type used in the interface, we provide helper methods for conveniently +// lifting and lowering that type from C-compatible data, and for reading and writing +// values of that type in a buffer. + +// Helper classes/extensions that don't change. +// Someday, this will be in a library of its own. + +fileprivate extension Data { + init(rustBuffer: RustBuffer) { + self.init( + bytesNoCopy: rustBuffer.data!, + count: Int(rustBuffer.len), + deallocator: .none + ) + } +} + +// Define reader functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. +// +// With external types, one swift source file needs to be able to call the read +// method on another source file's FfiConverter, but then what visibility +// should Reader have? +// - If Reader is fileprivate, then this means the read() must also +// be fileprivate, which doesn't work with external types. +// - If Reader is internal/public, we'll get compile errors since both source +// files will try define the same type. +// +// Instead, the read() method and these helper functions input a tuple of data + +fileprivate func createReader(data: Data) -> (data: Data, offset: Data.Index) { + (data: data, offset: 0) +} + +// Reads an integer at the current offset, in big-endian order, and advances +// the offset on success. Throws if reading the integer would move the +// offset past the end of the buffer. +fileprivate func readInt(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { + let range = reader.offset...size + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + if T.self == UInt8.self { + let value = reader.data[reader.offset] + reader.offset += 1 + return value as! T + } + var value: T = 0 + let _ = withUnsafeMutableBytes(of: &value, { reader.data.copyBytes(to: $0, from: range)}) + reader.offset = range.upperBound + return value.bigEndian +} + +// Reads an arbitrary number of bytes, to be used to read +// raw bytes, this is useful when lifting strings +fileprivate func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> Array { + let range = reader.offset..<(reader.offset+count) + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + var value = [UInt8](repeating: 0, count: count) + value.withUnsafeMutableBufferPointer({ buffer in + reader.data.copyBytes(to: buffer, from: range) + }) + reader.offset = range.upperBound + return value +} + +// Reads a float at the current offset. +fileprivate func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { + return Float(bitPattern: try readInt(&reader)) +} + +// Reads a float at the current offset. +fileprivate func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { + return Double(bitPattern: try readInt(&reader)) +} + +// Indicates if the offset has reached the end of the buffer. +fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { + return reader.offset < reader.data.count +} + +// Define writer functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. See the above discussion on Readers for details. + +fileprivate func createWriter() -> [UInt8] { + return [] +} + +fileprivate func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { + writer.append(contentsOf: byteArr) +} + +// Writes an integer in big-endian order. +// +// Warning: make sure what you are trying to write +// is in the correct type! +fileprivate func writeInt(_ writer: inout [UInt8], _ value: T) { + var value = value.bigEndian + withUnsafeBytes(of: &value) { writer.append(contentsOf: $0) } +} + +fileprivate func writeFloat(_ writer: inout [UInt8], _ value: Float) { + writeInt(&writer, value.bitPattern) +} + +fileprivate func writeDouble(_ writer: inout [UInt8], _ value: Double) { + writeInt(&writer, value.bitPattern) +} + +// Protocol for types that transfer other types across the FFI. This is +// analogous to the Rust trait of the same name. +fileprivate protocol FfiConverter { + associatedtype FfiType + associatedtype SwiftType + + static func lift(_ value: FfiType) throws -> SwiftType + static func lower(_ value: SwiftType) -> FfiType + static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType + static func write(_ value: SwiftType, into buf: inout [UInt8]) +} + +// Types conforming to `Primitive` pass themselves directly over the FFI. +fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { } + +extension FfiConverterPrimitive { +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lift(_ value: FfiType) throws -> SwiftType { + return value + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lower(_ value: SwiftType) -> FfiType { + return value + } +} + +// Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`. +// Used for complex types where it's hard to write a custom lift/lower. +fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} + +extension FfiConverterRustBuffer { +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lift(_ buf: RustBuffer) throws -> SwiftType { + var reader = createReader(data: Data(rustBuffer: buf)) + let value = try read(from: &reader) + if hasRemaining(reader) { + throw UniffiInternalError.incompleteData + } + buf.deallocate() + return value + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lower(_ value: SwiftType) -> RustBuffer { + var writer = createWriter() + write(value, into: &writer) + return RustBuffer(bytes: writer) + } +} +// An error type for FFI errors. These errors occur at the UniFFI level, not +// the library level. +fileprivate enum UniffiInternalError: LocalizedError { + case bufferOverflow + case incompleteData + case unexpectedOptionalTag + case unexpectedEnumCase + case unexpectedNullPointer + case unexpectedRustCallStatusCode + case unexpectedRustCallError + case unexpectedStaleHandle + case rustPanic(_ message: String) + + public var errorDescription: String? { + switch self { + case .bufferOverflow: return "Reading the requested value would read past the end of the buffer" + case .incompleteData: return "The buffer still has data after lifting its containing value" + case .unexpectedOptionalTag: return "Unexpected optional tag; should be 0 or 1" + case .unexpectedEnumCase: return "Raw enum value doesn't match any cases" + case .unexpectedNullPointer: return "Raw pointer value was null" + case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" + case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" + case .unexpectedStaleHandle: return "The object in the handle map has been dropped already" + case let .rustPanic(message): return message + } + } +} + +fileprivate extension NSLock { + func withLock(f: () throws -> T) rethrows -> T { + self.lock() + defer { self.unlock() } + return try f() + } +} + +fileprivate let CALL_SUCCESS: Int8 = 0 +fileprivate let CALL_ERROR: Int8 = 1 +fileprivate let CALL_UNEXPECTED_ERROR: Int8 = 2 +fileprivate let CALL_CANCELLED: Int8 = 3 + +fileprivate extension RustCallStatus { + init() { + self.init( + code: CALL_SUCCESS, + errorBuf: RustBuffer.init( + capacity: 0, + len: 0, + data: nil + ) + ) + } +} + +private func rustCall(_ callback: (UnsafeMutablePointer) -> T) throws -> T { + let neverThrow: ((RustBuffer) throws -> Never)? = nil + return try makeRustCall(callback, errorHandler: neverThrow) +} + +private func rustCallWithError( + _ errorHandler: @escaping (RustBuffer) throws -> E, + _ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: errorHandler) +} + +private func makeRustCall( + _ callback: (UnsafeMutablePointer) -> T, + errorHandler: ((RustBuffer) throws -> E)? +) throws -> T { + uniffiEnsureInitialized() + var callStatus = RustCallStatus.init() + let returnedVal = callback(&callStatus) + try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: errorHandler) + return returnedVal +} + +private func uniffiCheckCallStatus( + callStatus: RustCallStatus, + errorHandler: ((RustBuffer) throws -> E)? +) throws { + switch callStatus.code { + case CALL_SUCCESS: + return + + case CALL_ERROR: + if let errorHandler = errorHandler { + throw try errorHandler(callStatus.errorBuf) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.unexpectedRustCallError + } + + case CALL_UNEXPECTED_ERROR: + // When the rust code sees a panic, it tries to construct a RustBuffer + // with the message. But if that code panics, then it just sends back + // an empty buffer. + if callStatus.errorBuf.len > 0 { + throw UniffiInternalError.rustPanic(try FfiConverterString.lift(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } + + case CALL_CANCELLED: + fatalError("Cancellation not supported yet") + + default: + throw UniffiInternalError.unexpectedRustCallStatusCode + } +} + +private func uniffiTraitInterfaceCall( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + writeReturn: (T) -> () +) { + do { + try writeReturn(makeCall()) + } catch let error { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) + } +} + +private func uniffiTraitInterfaceCallWithError( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + writeReturn: (T) -> (), + lowerError: (E) -> RustBuffer +) { + do { + try writeReturn(makeCall()) + } catch let error as E { + callStatus.pointee.code = CALL_ERROR + callStatus.pointee.errorBuf = lowerError(error) + } catch { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) + } +} +fileprivate class UniffiHandleMap { + private var map: [UInt64: T] = [:] + private let lock = NSLock() + private var currentHandle: UInt64 = 1 + + func insert(obj: T) -> UInt64 { + lock.withLock { + let handle = currentHandle + currentHandle += 1 + map[handle] = obj + return handle + } + } + + func get(handle: UInt64) throws -> T { + try lock.withLock { + guard let obj = map[handle] else { + throw UniffiInternalError.unexpectedStaleHandle + } + return obj + } + } + + @discardableResult + func remove(handle: UInt64) throws -> T { + try lock.withLock { + guard let obj = map.removeValue(forKey: handle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return obj + } + } + + var count: Int { + get { + map.count + } + } +} + + +// Public interface members begin here. + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterInt32: FfiConverterPrimitive { + typealias FfiType = Int32 + typealias SwiftType = Int32 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int32 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int32, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterUInt64: FfiConverterPrimitive { + typealias FfiType = UInt64 + typealias SwiftType = UInt64 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt64 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterBool : FfiConverter { + typealias FfiType = Int8 + typealias SwiftType = Bool + + public static func lift(_ value: Int8) throws -> Bool { + return value != 0 + } + + public static func lower(_ value: Bool) -> Int8 { + return value ? 1 : 0 + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bool { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Bool, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterString: FfiConverter { + typealias SwiftType = String + typealias FfiType = RustBuffer + + public static func lift(_ value: RustBuffer) throws -> String { + defer { + value.deallocate() + } + if value.data == nil { + return String() + } + let bytes = UnsafeBufferPointer(start: value.data!, count: Int(value.len)) + return String(bytes: bytes, encoding: String.Encoding.utf8)! + } + + public static func lower(_ value: String) -> RustBuffer { + return value.utf8CString.withUnsafeBufferPointer { ptr in + // The swift string gives us int8_t, we want uint8_t. + ptr.withMemoryRebound(to: UInt8.self) { ptr in + // The swift string gives us a trailing null byte, we don't want it. + let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1)) + return RustBuffer.from(buf) + } + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String { + let len: Int32 = try readInt(&buf) + return String(bytes: try readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! + } + + public static func write(_ value: String, into buf: inout [UInt8]) { + let len = Int32(value.utf8.count) + writeInt(&buf, len) + writeBytes(&buf, value.utf8) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterData: FfiConverterRustBuffer { + typealias SwiftType = Data + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Data { + let len: Int32 = try readInt(&buf) + return Data(try readBytes(&buf, count: Int(len))) + } + + public static func write(_ value: Data, into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + writeBytes(&buf, value) + } +} + + + + +public protocol SyncClientProtocol : AnyObject { + + func createItem(itemType: String, title: String, data: Data, tags: [String], favorite: Bool) throws -> VaultItem + + func deleteItem(id: String) throws + + func fetchItems() throws -> [VaultItem] + + func login(email: String, password: String) throws -> String + + func setToken(token: String) + + func updateItem(id: String, title: String?, data: Data?, tags: [String]?, favorite: Bool?) throws -> VaultItem + +} + +open class SyncClient: + SyncClientProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + // This constructor can be used to instantiate a fake object. + // - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + // + // - Warning: + // Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public init(noPointer: NoPointer) { + self.pointer = nil + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_noro_mobile_core_fn_clone_syncclient(self.pointer, $0) } + } +public convenience init(baseUrl: String) { + let pointer = + try! rustCall() { + uniffi_noro_mobile_core_fn_constructor_syncclient_new( + FfiConverterString.lower(baseUrl),$0 + ) +} + self.init(unsafeFromRawPointer: pointer) +} + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_noro_mobile_core_fn_free_syncclient(pointer, $0) } + } + + + + +open func createItem(itemType: String, title: String, data: Data, tags: [String], favorite: Bool)throws -> VaultItem { + return try FfiConverterTypeVaultItem.lift(try rustCallWithError(FfiConverterTypeSyncError.lift) { + uniffi_noro_mobile_core_fn_method_syncclient_create_item(self.uniffiClonePointer(), + FfiConverterString.lower(itemType), + FfiConverterString.lower(title), + FfiConverterData.lower(data), + FfiConverterSequenceString.lower(tags), + FfiConverterBool.lower(favorite),$0 + ) +}) +} + +open func deleteItem(id: String)throws {try rustCallWithError(FfiConverterTypeSyncError.lift) { + uniffi_noro_mobile_core_fn_method_syncclient_delete_item(self.uniffiClonePointer(), + FfiConverterString.lower(id),$0 + ) +} +} + +open func fetchItems()throws -> [VaultItem] { + return try FfiConverterSequenceTypeVaultItem.lift(try rustCallWithError(FfiConverterTypeSyncError.lift) { + uniffi_noro_mobile_core_fn_method_syncclient_fetch_items(self.uniffiClonePointer(),$0 + ) +}) +} + +open func login(email: String, password: String)throws -> String { + return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeSyncError.lift) { + uniffi_noro_mobile_core_fn_method_syncclient_login(self.uniffiClonePointer(), + FfiConverterString.lower(email), + FfiConverterString.lower(password),$0 + ) +}) +} + +open func setToken(token: String) {try! rustCall() { + uniffi_noro_mobile_core_fn_method_syncclient_set_token(self.uniffiClonePointer(), + FfiConverterString.lower(token),$0 + ) +} +} + +open func updateItem(id: String, title: String?, data: Data?, tags: [String]?, favorite: Bool?)throws -> VaultItem { + return try FfiConverterTypeVaultItem.lift(try rustCallWithError(FfiConverterTypeSyncError.lift) { + uniffi_noro_mobile_core_fn_method_syncclient_update_item(self.uniffiClonePointer(), + FfiConverterString.lower(id), + FfiConverterOptionString.lower(title), + FfiConverterOptionData.lower(data), + FfiConverterOptionSequenceString.lower(tags), + FfiConverterOptionBool.lower(favorite),$0 + ) +}) +} + + +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeSyncClient: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = SyncClient + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> SyncClient { + return SyncClient(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: SyncClient) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SyncClient { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: SyncClient, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeSyncClient_lift(_ pointer: UnsafeMutableRawPointer) throws -> SyncClient { + return try FfiConverterTypeSyncClient.lift(pointer) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeSyncClient_lower(_ value: SyncClient) -> UnsafeMutableRawPointer { + return FfiConverterTypeSyncClient.lower(value) +} + + + + +public protocol VaultProtocol : AnyObject { + + func createItem(itemType: String, title: String, data: Data, tags: [String], favorite: Bool) throws -> VaultItem + + func deleteItem(id: String) throws + + func getItem(id: String) throws -> VaultItem? + + func listItems() -> [VaultItem] + + func load(encrypted: Data, key: Data) throws + + func save(key: Data) throws -> Data + + func searchItems(query: String) -> [VaultItem] + + func updateItem(id: String, title: String?, data: Data?, tags: [String]?, favorite: Bool?) throws -> VaultItem + +} + +open class Vault: + VaultProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + // This constructor can be used to instantiate a fake object. + // - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + // + // - Warning: + // Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public init(noPointer: NoPointer) { + self.pointer = nil + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_noro_mobile_core_fn_clone_vault(self.pointer, $0) } + } +public convenience init() { + let pointer = + try! rustCall() { + uniffi_noro_mobile_core_fn_constructor_vault_new($0 + ) +} + self.init(unsafeFromRawPointer: pointer) +} + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_noro_mobile_core_fn_free_vault(pointer, $0) } + } + + + + +open func createItem(itemType: String, title: String, data: Data, tags: [String], favorite: Bool)throws -> VaultItem { + return try FfiConverterTypeVaultItem.lift(try rustCallWithError(FfiConverterTypeVaultError.lift) { + uniffi_noro_mobile_core_fn_method_vault_create_item(self.uniffiClonePointer(), + FfiConverterString.lower(itemType), + FfiConverterString.lower(title), + FfiConverterData.lower(data), + FfiConverterSequenceString.lower(tags), + FfiConverterBool.lower(favorite),$0 + ) +}) +} + +open func deleteItem(id: String)throws {try rustCallWithError(FfiConverterTypeVaultError.lift) { + uniffi_noro_mobile_core_fn_method_vault_delete_item(self.uniffiClonePointer(), + FfiConverterString.lower(id),$0 + ) +} +} + +open func getItem(id: String)throws -> VaultItem? { + return try FfiConverterOptionTypeVaultItem.lift(try rustCallWithError(FfiConverterTypeVaultError.lift) { + uniffi_noro_mobile_core_fn_method_vault_get_item(self.uniffiClonePointer(), + FfiConverterString.lower(id),$0 + ) +}) +} + +open func listItems() -> [VaultItem] { + return try! FfiConverterSequenceTypeVaultItem.lift(try! rustCall() { + uniffi_noro_mobile_core_fn_method_vault_list_items(self.uniffiClonePointer(),$0 + ) +}) +} + +open func load(encrypted: Data, key: Data)throws {try rustCallWithError(FfiConverterTypeVaultError.lift) { + uniffi_noro_mobile_core_fn_method_vault_load(self.uniffiClonePointer(), + FfiConverterData.lower(encrypted), + FfiConverterData.lower(key),$0 + ) +} +} + +open func save(key: Data)throws -> Data { + return try FfiConverterData.lift(try rustCallWithError(FfiConverterTypeVaultError.lift) { + uniffi_noro_mobile_core_fn_method_vault_save(self.uniffiClonePointer(), + FfiConverterData.lower(key),$0 + ) +}) +} + +open func searchItems(query: String) -> [VaultItem] { + return try! FfiConverterSequenceTypeVaultItem.lift(try! rustCall() { + uniffi_noro_mobile_core_fn_method_vault_search_items(self.uniffiClonePointer(), + FfiConverterString.lower(query),$0 + ) +}) +} + +open func updateItem(id: String, title: String?, data: Data?, tags: [String]?, favorite: Bool?)throws -> VaultItem { + return try FfiConverterTypeVaultItem.lift(try rustCallWithError(FfiConverterTypeVaultError.lift) { + uniffi_noro_mobile_core_fn_method_vault_update_item(self.uniffiClonePointer(), + FfiConverterString.lower(id), + FfiConverterOptionString.lower(title), + FfiConverterOptionData.lower(data), + FfiConverterOptionSequenceString.lower(tags), + FfiConverterOptionBool.lower(favorite),$0 + ) +}) +} + + +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeVault: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = Vault + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Vault { + return Vault(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: Vault) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Vault { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: Vault, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeVault_lift(_ pointer: UnsafeMutableRawPointer) throws -> Vault { + return try FfiConverterTypeVault.lift(pointer) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeVault_lower(_ value: Vault) -> UnsafeMutableRawPointer { + return FfiConverterTypeVault.lower(value) +} + + +public struct VaultData { + public var items: [VaultItem] + public var updated: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(items: [VaultItem], updated: UInt64) { + self.items = items + self.updated = updated + } +} + + + +extension VaultData: Equatable, Hashable { + public static func ==(lhs: VaultData, rhs: VaultData) -> Bool { + if lhs.items != rhs.items { + return false + } + if lhs.updated != rhs.updated { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(items) + hasher.combine(updated) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeVaultData: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> VaultData { + return + try VaultData( + items: FfiConverterSequenceTypeVaultItem.read(from: &buf), + updated: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: VaultData, into buf: inout [UInt8]) { + FfiConverterSequenceTypeVaultItem.write(value.items, into: &buf) + FfiConverterUInt64.write(value.updated, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeVaultData_lift(_ buf: RustBuffer) throws -> VaultData { + return try FfiConverterTypeVaultData.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeVaultData_lower(_ value: VaultData) -> RustBuffer { + return FfiConverterTypeVaultData.lower(value) +} + + +public struct VaultItem { + public var id: String + public var itemType: String + public var title: String + public var data: Data + public var revision: Int32 + public var favorite: Bool + public var deleted: Bool + public var tags: [String] + public var created: UInt64 + public var updated: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(id: String, itemType: String, title: String, data: Data, revision: Int32, favorite: Bool, deleted: Bool, tags: [String], created: UInt64, updated: UInt64) { + self.id = id + self.itemType = itemType + self.title = title + self.data = data + self.revision = revision + self.favorite = favorite + self.deleted = deleted + self.tags = tags + self.created = created + self.updated = updated + } +} + + + +extension VaultItem: Equatable, Hashable { + public static func ==(lhs: VaultItem, rhs: VaultItem) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.itemType != rhs.itemType { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.data != rhs.data { + return false + } + if lhs.revision != rhs.revision { + return false + } + if lhs.favorite != rhs.favorite { + return false + } + if lhs.deleted != rhs.deleted { + return false + } + if lhs.tags != rhs.tags { + return false + } + if lhs.created != rhs.created { + return false + } + if lhs.updated != rhs.updated { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(itemType) + hasher.combine(title) + hasher.combine(data) + hasher.combine(revision) + hasher.combine(favorite) + hasher.combine(deleted) + hasher.combine(tags) + hasher.combine(created) + hasher.combine(updated) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeVaultItem: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> VaultItem { + return + try VaultItem( + id: FfiConverterString.read(from: &buf), + itemType: FfiConverterString.read(from: &buf), + title: FfiConverterString.read(from: &buf), + data: FfiConverterData.read(from: &buf), + revision: FfiConverterInt32.read(from: &buf), + favorite: FfiConverterBool.read(from: &buf), + deleted: FfiConverterBool.read(from: &buf), + tags: FfiConverterSequenceString.read(from: &buf), + created: FfiConverterUInt64.read(from: &buf), + updated: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: VaultItem, into buf: inout [UInt8]) { + FfiConverterString.write(value.id, into: &buf) + FfiConverterString.write(value.itemType, into: &buf) + FfiConverterString.write(value.title, into: &buf) + FfiConverterData.write(value.data, into: &buf) + FfiConverterInt32.write(value.revision, into: &buf) + FfiConverterBool.write(value.favorite, into: &buf) + FfiConverterBool.write(value.deleted, into: &buf) + FfiConverterSequenceString.write(value.tags, into: &buf) + FfiConverterUInt64.write(value.created, into: &buf) + FfiConverterUInt64.write(value.updated, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeVaultItem_lift(_ buf: RustBuffer) throws -> VaultItem { + return try FfiConverterTypeVaultItem.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeVaultItem_lower(_ value: VaultItem) -> RustBuffer { + return FfiConverterTypeVaultItem.lower(value) +} + + +public enum CryptoError { + + + + case Argon2 + case Encryption + case Decryption + case InvalidSecretKey + case InvalidKeyLength +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeCryptoError: FfiConverterRustBuffer { + typealias SwiftType = CryptoError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> CryptoError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .Argon2 + case 2: return .Encryption + case 3: return .Decryption + case 4: return .InvalidSecretKey + case 5: return .InvalidKeyLength + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: CryptoError, into buf: inout [UInt8]) { + switch value { + + + + + + case .Argon2: + writeInt(&buf, Int32(1)) + + + case .Encryption: + writeInt(&buf, Int32(2)) + + + case .Decryption: + writeInt(&buf, Int32(3)) + + + case .InvalidSecretKey: + writeInt(&buf, Int32(4)) + + + case .InvalidKeyLength: + writeInt(&buf, Int32(5)) + + } + } +} + + +extension CryptoError: Equatable, Hashable {} + +extension CryptoError: Foundation.LocalizedError { + public var errorDescription: String? { + String(reflecting: self) + } +} + + +public enum SyncError { + + + + case Http + case Auth + case Conflict + case Parse +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeSyncError: FfiConverterRustBuffer { + typealias SwiftType = SyncError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SyncError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .Http + case 2: return .Auth + case 3: return .Conflict + case 4: return .Parse + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: SyncError, into buf: inout [UInt8]) { + switch value { + + + + + + case .Http: + writeInt(&buf, Int32(1)) + + + case .Auth: + writeInt(&buf, Int32(2)) + + + case .Conflict: + writeInt(&buf, Int32(3)) + + + case .Parse: + writeInt(&buf, Int32(4)) + + } + } +} + + +extension SyncError: Equatable, Hashable {} + +extension SyncError: Foundation.LocalizedError { + public var errorDescription: String? { + String(reflecting: self) + } +} + + +public enum VaultError { + + + + case NotFound + case Serialization + case Crypto +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeVaultError: FfiConverterRustBuffer { + typealias SwiftType = VaultError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> VaultError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .NotFound + case 2: return .Serialization + case 3: return .Crypto + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: VaultError, into buf: inout [UInt8]) { + switch value { + + + + + + case .NotFound: + writeInt(&buf, Int32(1)) + + + case .Serialization: + writeInt(&buf, Int32(2)) + + + case .Crypto: + writeInt(&buf, Int32(3)) + + } + } +} + + +extension VaultError: Equatable, Hashable {} + +extension VaultError: Foundation.LocalizedError { + public var errorDescription: String? { + String(reflecting: self) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionBool: FfiConverterRustBuffer { + typealias SwiftType = Bool? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterBool.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterBool.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer { + typealias SwiftType = String? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterString.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterString.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionData: FfiConverterRustBuffer { + typealias SwiftType = Data? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterData.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterData.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionTypeVaultItem: FfiConverterRustBuffer { + typealias SwiftType = VaultItem? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeVaultItem.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeVaultItem.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionSequenceString: FfiConverterRustBuffer { + typealias SwiftType = [String]? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterSequenceString.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterSequenceString.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceString: FfiConverterRustBuffer { + typealias SwiftType = [String] + + public static func write(_ value: [String], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterString.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [String] { + let len: Int32 = try readInt(&buf) + var seq = [String]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterString.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeVaultItem: FfiConverterRustBuffer { + typealias SwiftType = [VaultItem] + + public static func write(_ value: [VaultItem], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeVaultItem.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [VaultItem] { + let len: Int32 = try readInt(&buf) + var seq = [VaultItem]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeVaultItem.read(from: &buf)) + } + return seq + } +} +public func decrypt(ciphertext: Data, key: Data)throws -> Data { + return try FfiConverterData.lift(try rustCallWithError(FfiConverterTypeCryptoError.lift) { + uniffi_noro_mobile_core_fn_func_decrypt( + FfiConverterData.lower(ciphertext), + FfiConverterData.lower(key),$0 + ) +}) +} +public func deriveAuk(password: String, secretKey: String, salt: Data)throws -> Data { + return try FfiConverterData.lift(try rustCallWithError(FfiConverterTypeCryptoError.lift) { + uniffi_noro_mobile_core_fn_func_derive_auk( + FfiConverterString.lower(password), + FfiConverterString.lower(secretKey), + FfiConverterData.lower(salt),$0 + ) +}) +} +public func deriveItemKey(vaultKey: Data, itemId: String)throws -> Data { + return try FfiConverterData.lift(try rustCallWithError(FfiConverterTypeCryptoError.lift) { + uniffi_noro_mobile_core_fn_func_derive_item_key( + FfiConverterData.lower(vaultKey), + FfiConverterString.lower(itemId),$0 + ) +}) +} +public func encrypt(plaintext: Data, key: Data)throws -> Data { + return try FfiConverterData.lift(try rustCallWithError(FfiConverterTypeCryptoError.lift) { + uniffi_noro_mobile_core_fn_func_encrypt( + FfiConverterData.lower(plaintext), + FfiConverterData.lower(key),$0 + ) +}) +} +public func generateItemId() -> String { + return try! FfiConverterString.lift(try! rustCall() { + uniffi_noro_mobile_core_fn_func_generate_item_id($0 + ) +}) +} +public func generateSalt() -> Data { + return try! FfiConverterData.lift(try! rustCall() { + uniffi_noro_mobile_core_fn_func_generate_salt($0 + ) +}) +} +public func generateSecretKey() -> String { + return try! FfiConverterString.lift(try! rustCall() { + uniffi_noro_mobile_core_fn_func_generate_secret_key($0 + ) +}) +} +public func generateVaultKey() -> Data { + return try! FfiConverterData.lift(try! rustCall() { + uniffi_noro_mobile_core_fn_func_generate_vault_key($0 + ) +}) +} +public func unwrapVaultKey(wrapped: Data, auk: Data)throws -> Data { + return try FfiConverterData.lift(try rustCallWithError(FfiConverterTypeCryptoError.lift) { + uniffi_noro_mobile_core_fn_func_unwrap_vault_key( + FfiConverterData.lower(wrapped), + FfiConverterData.lower(auk),$0 + ) +}) +} +public func wrapVaultKey(vaultKey: Data, auk: Data)throws -> Data { + return try FfiConverterData.lift(try rustCallWithError(FfiConverterTypeCryptoError.lift) { + uniffi_noro_mobile_core_fn_func_wrap_vault_key( + FfiConverterData.lower(vaultKey), + FfiConverterData.lower(auk),$0 + ) +}) +} + +private enum InitializationResult { + case ok + case contractVersionMismatch + case apiChecksumMismatch +} +// Use a global variable to perform the versioning checks. Swift ensures that +// the code inside is only computed once. +private var initializationResult: InitializationResult = { + // Get the bindings contract version from our ComponentInterface + let bindings_contract_version = 26 + // Get the scaffolding contract version by calling the into the dylib + let scaffolding_contract_version = ffi_noro_mobile_core_uniffi_contract_version() + if bindings_contract_version != scaffolding_contract_version { + return InitializationResult.contractVersionMismatch + } + if (uniffi_noro_mobile_core_checksum_func_decrypt() != 32839) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_func_derive_auk() != 37186) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_func_derive_item_key() != 51281) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_func_encrypt() != 46133) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_func_generate_item_id() != 22417) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_func_generate_salt() != 31865) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_func_generate_secret_key() != 53137) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_func_generate_vault_key() != 50386) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_func_unwrap_vault_key() != 38585) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_func_wrap_vault_key() != 17122) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_syncclient_create_item() != 51847) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_syncclient_delete_item() != 22816) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_syncclient_fetch_items() != 53777) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_syncclient_login() != 19049) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_syncclient_set_token() != 22416) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_syncclient_update_item() != 45106) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_vault_create_item() != 60480) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_vault_delete_item() != 63195) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_vault_get_item() != 55932) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_vault_list_items() != 61447) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_vault_load() != 6499) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_vault_save() != 50962) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_vault_search_items() != 42826) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_method_vault_update_item() != 55533) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_constructor_syncclient_new() != 38165) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_noro_mobile_core_checksum_constructor_vault_new() != 9762) { + return InitializationResult.apiChecksumMismatch + } + + return InitializationResult.ok +}() + +private func uniffiEnsureInitialized() { + switch initializationResult { + case .ok: + break + case .contractVersionMismatch: + fatalError("UniFFI contract version mismatch: try cleaning and rebuilding your project") + case .apiChecksumMismatch: + fatalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} + +// swiftlint:enable all \ No newline at end of file diff --git a/apps/mobile-core/bindings/NoroCoreFFI.h b/apps/mobile-core/bindings/NoroCoreFFI.h new file mode 100644 index 0000000..8be282c --- /dev/null +++ b/apps/mobile-core/bindings/NoroCoreFFI.h @@ -0,0 +1,851 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +#pragma once + +#include +#include +#include + +// The following structs are used to implement the lowest level +// of the FFI, and thus useful to multiple uniffied crates. +// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H. +#ifdef UNIFFI_SHARED_H + // We also try to prevent mixing versions of shared uniffi header structs. + // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4 + #ifndef UNIFFI_SHARED_HEADER_V4 + #error Combining helper code from multiple versions of uniffi is not supported + #endif // ndef UNIFFI_SHARED_HEADER_V4 +#else +#define UNIFFI_SHARED_H +#define UNIFFI_SHARED_HEADER_V4 +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ + +typedef struct RustBuffer +{ + uint64_t capacity; + uint64_t len; + uint8_t *_Nullable data; +} RustBuffer; + +typedef struct ForeignBytes +{ + int32_t len; + const uint8_t *_Nullable data; +} ForeignBytes; + +// Error definitions +typedef struct RustCallStatus { + int8_t code; + RustBuffer errorBuf; +} RustCallStatus; + +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ +#endif // def UNIFFI_SHARED_H +#ifndef UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK +#define UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK +typedef void (*UniffiRustFutureContinuationCallback)(uint64_t, int8_t + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_FREE +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_FREE +typedef void (*UniffiForeignFutureFree)(uint64_t + ); + +#endif +#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE +#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE +typedef void (*UniffiCallbackInterfaceFree)(uint64_t + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE +#define UNIFFI_FFIDEF_FOREIGN_FUTURE +typedef struct UniffiForeignFuture { + uint64_t handle; + UniffiForeignFutureFree _Nonnull free; +} UniffiForeignFuture; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_U8 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_U8 +typedef struct UniffiForeignFutureStructU8 { + uint8_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructU8; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8 +typedef void (*UniffiForeignFutureCompleteU8)(uint64_t, UniffiForeignFutureStructU8 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_I8 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_I8 +typedef struct UniffiForeignFutureStructI8 { + int8_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructI8; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8 +typedef void (*UniffiForeignFutureCompleteI8)(uint64_t, UniffiForeignFutureStructI8 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_U16 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_U16 +typedef struct UniffiForeignFutureStructU16 { + uint16_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructU16; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16 +typedef void (*UniffiForeignFutureCompleteU16)(uint64_t, UniffiForeignFutureStructU16 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_I16 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_I16 +typedef struct UniffiForeignFutureStructI16 { + int16_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructI16; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16 +typedef void (*UniffiForeignFutureCompleteI16)(uint64_t, UniffiForeignFutureStructI16 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_U32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_U32 +typedef struct UniffiForeignFutureStructU32 { + uint32_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructU32; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32 +typedef void (*UniffiForeignFutureCompleteU32)(uint64_t, UniffiForeignFutureStructU32 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_I32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_I32 +typedef struct UniffiForeignFutureStructI32 { + int32_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructI32; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32 +typedef void (*UniffiForeignFutureCompleteI32)(uint64_t, UniffiForeignFutureStructI32 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_U64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_U64 +typedef struct UniffiForeignFutureStructU64 { + uint64_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructU64; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64 +typedef void (*UniffiForeignFutureCompleteU64)(uint64_t, UniffiForeignFutureStructU64 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_I64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_I64 +typedef struct UniffiForeignFutureStructI64 { + int64_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructI64; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64 +typedef void (*UniffiForeignFutureCompleteI64)(uint64_t, UniffiForeignFutureStructI64 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_F32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_F32 +typedef struct UniffiForeignFutureStructF32 { + float returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructF32; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32 +typedef void (*UniffiForeignFutureCompleteF32)(uint64_t, UniffiForeignFutureStructF32 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_F64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_F64 +typedef struct UniffiForeignFutureStructF64 { + double returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructF64; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64 +typedef void (*UniffiForeignFutureCompleteF64)(uint64_t, UniffiForeignFutureStructF64 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_POINTER +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_POINTER +typedef struct UniffiForeignFutureStructPointer { + void*_Nonnull returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructPointer; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_POINTER +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_POINTER +typedef void (*UniffiForeignFutureCompletePointer)(uint64_t, UniffiForeignFutureStructPointer + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_RUST_BUFFER +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_RUST_BUFFER +typedef struct UniffiForeignFutureStructRustBuffer { + RustBuffer returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureStructRustBuffer; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER +typedef void (*UniffiForeignFutureCompleteRustBuffer)(uint64_t, UniffiForeignFutureStructRustBuffer + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_VOID +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_STRUCT_VOID +typedef struct UniffiForeignFutureStructVoid { + RustCallStatus callStatus; +} UniffiForeignFutureStructVoid; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID +typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureStructVoid + ); + +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_CLONE_SYNCCLIENT +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_CLONE_SYNCCLIENT +void*_Nonnull uniffi_noro_mobile_core_fn_clone_syncclient(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FREE_SYNCCLIENT +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FREE_SYNCCLIENT +void uniffi_noro_mobile_core_fn_free_syncclient(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_CONSTRUCTOR_SYNCCLIENT_NEW +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_CONSTRUCTOR_SYNCCLIENT_NEW +void*_Nonnull uniffi_noro_mobile_core_fn_constructor_syncclient_new(RustBuffer base_url, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_CREATE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_CREATE_ITEM +RustBuffer uniffi_noro_mobile_core_fn_method_syncclient_create_item(void*_Nonnull ptr, RustBuffer item_type, RustBuffer title, RustBuffer data, RustBuffer tags, int8_t favorite, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_DELETE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_DELETE_ITEM +void uniffi_noro_mobile_core_fn_method_syncclient_delete_item(void*_Nonnull ptr, RustBuffer id, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_FETCH_ITEMS +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_FETCH_ITEMS +RustBuffer uniffi_noro_mobile_core_fn_method_syncclient_fetch_items(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_LOGIN +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_LOGIN +RustBuffer uniffi_noro_mobile_core_fn_method_syncclient_login(void*_Nonnull ptr, RustBuffer email, RustBuffer password, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_SET_TOKEN +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_SET_TOKEN +void uniffi_noro_mobile_core_fn_method_syncclient_set_token(void*_Nonnull ptr, RustBuffer token, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_UPDATE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_SYNCCLIENT_UPDATE_ITEM +RustBuffer uniffi_noro_mobile_core_fn_method_syncclient_update_item(void*_Nonnull ptr, RustBuffer id, RustBuffer title, RustBuffer data, RustBuffer tags, RustBuffer favorite, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_CLONE_VAULT +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_CLONE_VAULT +void*_Nonnull uniffi_noro_mobile_core_fn_clone_vault(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FREE_VAULT +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FREE_VAULT +void uniffi_noro_mobile_core_fn_free_vault(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_CONSTRUCTOR_VAULT_NEW +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_CONSTRUCTOR_VAULT_NEW +void*_Nonnull uniffi_noro_mobile_core_fn_constructor_vault_new(RustCallStatus *_Nonnull out_status + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_CREATE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_CREATE_ITEM +RustBuffer uniffi_noro_mobile_core_fn_method_vault_create_item(void*_Nonnull ptr, RustBuffer item_type, RustBuffer title, RustBuffer data, RustBuffer tags, int8_t favorite, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_DELETE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_DELETE_ITEM +void uniffi_noro_mobile_core_fn_method_vault_delete_item(void*_Nonnull ptr, RustBuffer id, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_GET_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_GET_ITEM +RustBuffer uniffi_noro_mobile_core_fn_method_vault_get_item(void*_Nonnull ptr, RustBuffer id, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_LIST_ITEMS +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_LIST_ITEMS +RustBuffer uniffi_noro_mobile_core_fn_method_vault_list_items(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_LOAD +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_LOAD +void uniffi_noro_mobile_core_fn_method_vault_load(void*_Nonnull ptr, RustBuffer encrypted, RustBuffer key, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_SAVE +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_SAVE +RustBuffer uniffi_noro_mobile_core_fn_method_vault_save(void*_Nonnull ptr, RustBuffer key, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_SEARCH_ITEMS +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_SEARCH_ITEMS +RustBuffer uniffi_noro_mobile_core_fn_method_vault_search_items(void*_Nonnull ptr, RustBuffer query, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_UPDATE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_METHOD_VAULT_UPDATE_ITEM +RustBuffer uniffi_noro_mobile_core_fn_method_vault_update_item(void*_Nonnull ptr, RustBuffer id, RustBuffer title, RustBuffer data, RustBuffer tags, RustBuffer favorite, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_DECRYPT +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_DECRYPT +RustBuffer uniffi_noro_mobile_core_fn_func_decrypt(RustBuffer ciphertext, RustBuffer key, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_DERIVE_AUK +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_DERIVE_AUK +RustBuffer uniffi_noro_mobile_core_fn_func_derive_auk(RustBuffer password, RustBuffer secret_key, RustBuffer salt, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_DERIVE_ITEM_KEY +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_DERIVE_ITEM_KEY +RustBuffer uniffi_noro_mobile_core_fn_func_derive_item_key(RustBuffer vault_key, RustBuffer item_id, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_ENCRYPT +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_ENCRYPT +RustBuffer uniffi_noro_mobile_core_fn_func_encrypt(RustBuffer plaintext, RustBuffer key, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_GENERATE_ITEM_ID +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_GENERATE_ITEM_ID +RustBuffer uniffi_noro_mobile_core_fn_func_generate_item_id(RustCallStatus *_Nonnull out_status + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_GENERATE_SALT +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_GENERATE_SALT +RustBuffer uniffi_noro_mobile_core_fn_func_generate_salt(RustCallStatus *_Nonnull out_status + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_GENERATE_SECRET_KEY +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_GENERATE_SECRET_KEY +RustBuffer uniffi_noro_mobile_core_fn_func_generate_secret_key(RustCallStatus *_Nonnull out_status + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_GENERATE_VAULT_KEY +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_GENERATE_VAULT_KEY +RustBuffer uniffi_noro_mobile_core_fn_func_generate_vault_key(RustCallStatus *_Nonnull out_status + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_UNWRAP_VAULT_KEY +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_UNWRAP_VAULT_KEY +RustBuffer uniffi_noro_mobile_core_fn_func_unwrap_vault_key(RustBuffer wrapped, RustBuffer auk, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_WRAP_VAULT_KEY +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_FN_FUNC_WRAP_VAULT_KEY +RustBuffer uniffi_noro_mobile_core_fn_func_wrap_vault_key(RustBuffer vault_key, RustBuffer auk, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUSTBUFFER_ALLOC +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUSTBUFFER_ALLOC +RustBuffer ffi_noro_mobile_core_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUSTBUFFER_FROM_BYTES +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUSTBUFFER_FROM_BYTES +RustBuffer ffi_noro_mobile_core_rustbuffer_from_bytes(ForeignBytes bytes, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUSTBUFFER_FREE +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUSTBUFFER_FREE +void ffi_noro_mobile_core_rustbuffer_free(RustBuffer buf, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUSTBUFFER_RESERVE +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUSTBUFFER_RESERVE +RustBuffer ffi_noro_mobile_core_rustbuffer_reserve(RustBuffer buf, uint64_t additional, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_U8 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_U8 +void ffi_noro_mobile_core_rust_future_poll_u8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_U8 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_U8 +void ffi_noro_mobile_core_rust_future_cancel_u8(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_U8 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_U8 +void ffi_noro_mobile_core_rust_future_free_u8(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_U8 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_U8 +uint8_t ffi_noro_mobile_core_rust_future_complete_u8(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_I8 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_I8 +void ffi_noro_mobile_core_rust_future_poll_i8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_I8 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_I8 +void ffi_noro_mobile_core_rust_future_cancel_i8(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_I8 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_I8 +void ffi_noro_mobile_core_rust_future_free_i8(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_I8 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_I8 +int8_t ffi_noro_mobile_core_rust_future_complete_i8(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_U16 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_U16 +void ffi_noro_mobile_core_rust_future_poll_u16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_U16 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_U16 +void ffi_noro_mobile_core_rust_future_cancel_u16(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_U16 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_U16 +void ffi_noro_mobile_core_rust_future_free_u16(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_U16 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_U16 +uint16_t ffi_noro_mobile_core_rust_future_complete_u16(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_I16 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_I16 +void ffi_noro_mobile_core_rust_future_poll_i16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_I16 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_I16 +void ffi_noro_mobile_core_rust_future_cancel_i16(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_I16 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_I16 +void ffi_noro_mobile_core_rust_future_free_i16(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_I16 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_I16 +int16_t ffi_noro_mobile_core_rust_future_complete_i16(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_U32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_U32 +void ffi_noro_mobile_core_rust_future_poll_u32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_U32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_U32 +void ffi_noro_mobile_core_rust_future_cancel_u32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_U32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_U32 +void ffi_noro_mobile_core_rust_future_free_u32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_U32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_U32 +uint32_t ffi_noro_mobile_core_rust_future_complete_u32(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_I32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_I32 +void ffi_noro_mobile_core_rust_future_poll_i32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_I32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_I32 +void ffi_noro_mobile_core_rust_future_cancel_i32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_I32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_I32 +void ffi_noro_mobile_core_rust_future_free_i32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_I32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_I32 +int32_t ffi_noro_mobile_core_rust_future_complete_i32(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_U64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_U64 +void ffi_noro_mobile_core_rust_future_poll_u64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_U64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_U64 +void ffi_noro_mobile_core_rust_future_cancel_u64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_U64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_U64 +void ffi_noro_mobile_core_rust_future_free_u64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_U64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_U64 +uint64_t ffi_noro_mobile_core_rust_future_complete_u64(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_I64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_I64 +void ffi_noro_mobile_core_rust_future_poll_i64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_I64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_I64 +void ffi_noro_mobile_core_rust_future_cancel_i64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_I64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_I64 +void ffi_noro_mobile_core_rust_future_free_i64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_I64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_I64 +int64_t ffi_noro_mobile_core_rust_future_complete_i64(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_F32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_F32 +void ffi_noro_mobile_core_rust_future_poll_f32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_F32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_F32 +void ffi_noro_mobile_core_rust_future_cancel_f32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_F32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_F32 +void ffi_noro_mobile_core_rust_future_free_f32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_F32 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_F32 +float ffi_noro_mobile_core_rust_future_complete_f32(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_F64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_F64 +void ffi_noro_mobile_core_rust_future_poll_f64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_F64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_F64 +void ffi_noro_mobile_core_rust_future_cancel_f64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_F64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_F64 +void ffi_noro_mobile_core_rust_future_free_f64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_F64 +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_F64 +double ffi_noro_mobile_core_rust_future_complete_f64(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_POINTER +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_POINTER +void ffi_noro_mobile_core_rust_future_poll_pointer(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_POINTER +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_POINTER +void ffi_noro_mobile_core_rust_future_cancel_pointer(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_POINTER +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_POINTER +void ffi_noro_mobile_core_rust_future_free_pointer(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_POINTER +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_POINTER +void*_Nonnull ffi_noro_mobile_core_rust_future_complete_pointer(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_RUST_BUFFER +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_RUST_BUFFER +void ffi_noro_mobile_core_rust_future_poll_rust_buffer(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_RUST_BUFFER +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_RUST_BUFFER +void ffi_noro_mobile_core_rust_future_cancel_rust_buffer(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_RUST_BUFFER +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_RUST_BUFFER +void ffi_noro_mobile_core_rust_future_free_rust_buffer(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_RUST_BUFFER +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_RUST_BUFFER +RustBuffer ffi_noro_mobile_core_rust_future_complete_rust_buffer(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_VOID +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_POLL_VOID +void ffi_noro_mobile_core_rust_future_poll_void(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_VOID +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_CANCEL_VOID +void ffi_noro_mobile_core_rust_future_cancel_void(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_VOID +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_FREE_VOID +void ffi_noro_mobile_core_rust_future_free_void(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_VOID +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_RUST_FUTURE_COMPLETE_VOID +void ffi_noro_mobile_core_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_DECRYPT +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_DECRYPT +uint16_t uniffi_noro_mobile_core_checksum_func_decrypt(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_DERIVE_AUK +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_DERIVE_AUK +uint16_t uniffi_noro_mobile_core_checksum_func_derive_auk(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_DERIVE_ITEM_KEY +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_DERIVE_ITEM_KEY +uint16_t uniffi_noro_mobile_core_checksum_func_derive_item_key(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_ENCRYPT +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_ENCRYPT +uint16_t uniffi_noro_mobile_core_checksum_func_encrypt(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_GENERATE_ITEM_ID +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_GENERATE_ITEM_ID +uint16_t uniffi_noro_mobile_core_checksum_func_generate_item_id(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_GENERATE_SALT +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_GENERATE_SALT +uint16_t uniffi_noro_mobile_core_checksum_func_generate_salt(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_GENERATE_SECRET_KEY +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_GENERATE_SECRET_KEY +uint16_t uniffi_noro_mobile_core_checksum_func_generate_secret_key(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_GENERATE_VAULT_KEY +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_GENERATE_VAULT_KEY +uint16_t uniffi_noro_mobile_core_checksum_func_generate_vault_key(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_UNWRAP_VAULT_KEY +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_UNWRAP_VAULT_KEY +uint16_t uniffi_noro_mobile_core_checksum_func_unwrap_vault_key(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_WRAP_VAULT_KEY +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_FUNC_WRAP_VAULT_KEY +uint16_t uniffi_noro_mobile_core_checksum_func_wrap_vault_key(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_CREATE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_CREATE_ITEM +uint16_t uniffi_noro_mobile_core_checksum_method_syncclient_create_item(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_DELETE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_DELETE_ITEM +uint16_t uniffi_noro_mobile_core_checksum_method_syncclient_delete_item(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_FETCH_ITEMS +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_FETCH_ITEMS +uint16_t uniffi_noro_mobile_core_checksum_method_syncclient_fetch_items(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_LOGIN +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_LOGIN +uint16_t uniffi_noro_mobile_core_checksum_method_syncclient_login(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_SET_TOKEN +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_SET_TOKEN +uint16_t uniffi_noro_mobile_core_checksum_method_syncclient_set_token(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_UPDATE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_SYNCCLIENT_UPDATE_ITEM +uint16_t uniffi_noro_mobile_core_checksum_method_syncclient_update_item(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_CREATE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_CREATE_ITEM +uint16_t uniffi_noro_mobile_core_checksum_method_vault_create_item(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_DELETE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_DELETE_ITEM +uint16_t uniffi_noro_mobile_core_checksum_method_vault_delete_item(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_GET_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_GET_ITEM +uint16_t uniffi_noro_mobile_core_checksum_method_vault_get_item(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_LIST_ITEMS +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_LIST_ITEMS +uint16_t uniffi_noro_mobile_core_checksum_method_vault_list_items(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_LOAD +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_LOAD +uint16_t uniffi_noro_mobile_core_checksum_method_vault_load(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_SAVE +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_SAVE +uint16_t uniffi_noro_mobile_core_checksum_method_vault_save(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_SEARCH_ITEMS +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_SEARCH_ITEMS +uint16_t uniffi_noro_mobile_core_checksum_method_vault_search_items(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_UPDATE_ITEM +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_METHOD_VAULT_UPDATE_ITEM +uint16_t uniffi_noro_mobile_core_checksum_method_vault_update_item(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_CONSTRUCTOR_SYNCCLIENT_NEW +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_CONSTRUCTOR_SYNCCLIENT_NEW +uint16_t uniffi_noro_mobile_core_checksum_constructor_syncclient_new(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_CONSTRUCTOR_VAULT_NEW +#define UNIFFI_FFIDEF_UNIFFI_NORO_MOBILE_CORE_CHECKSUM_CONSTRUCTOR_VAULT_NEW +uint16_t uniffi_noro_mobile_core_checksum_constructor_vault_new(void + +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_UNIFFI_CONTRACT_VERSION +#define UNIFFI_FFIDEF_FFI_NORO_MOBILE_CORE_UNIFFI_CONTRACT_VERSION +uint32_t ffi_noro_mobile_core_uniffi_contract_version(void + +); +#endif + diff --git a/apps/mobile-core/bindings/NoroCoreFFI.modulemap b/apps/mobile-core/bindings/NoroCoreFFI.modulemap new file mode 100644 index 0000000..fdea99e --- /dev/null +++ b/apps/mobile-core/bindings/NoroCoreFFI.modulemap @@ -0,0 +1,4 @@ +module NoroCoreFFI { + header "NoroCoreFFI.h" + export * +} \ No newline at end of file diff --git a/apps/mobile-core/bindings/sh/noro/core/noro_mobile_core.kt b/apps/mobile-core/bindings/sh/noro/core/noro_mobile_core.kt new file mode 100644 index 0000000..75a65b5 --- /dev/null +++ b/apps/mobile-core/bindings/sh/noro/core/noro_mobile_core.kt @@ -0,0 +1,2724 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +@file:Suppress("NAME_SHADOWING") + +package sh.noro.core + +// Common helper code. +// +// Ideally this would live in a separate .kt file where it can be unittested etc +// in isolation, and perhaps even published as a re-useable package. +// +// However, it's important that the details of how this helper code works (e.g. the +// way that different builtin types are passed across the FFI) exactly match what's +// expected by the Rust code on the other side of the interface. In practice right +// now that means coming from the exact some version of `uniffi` that was used to +// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin +// helpers directly inline like we're doing here. + +import com.sun.jna.Library +import com.sun.jna.IntegerType +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.Structure +import com.sun.jna.Callback +import com.sun.jna.ptr.* +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.CharBuffer +import java.nio.charset.CodingErrorAction +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean + +// This is a helper for safely working with byte buffers returned from the Rust code. +// A rust-owned buffer is represented by its capacity, its current length, and a +// pointer to the underlying data. + +/** + * @suppress + */ +@Structure.FieldOrder("capacity", "len", "data") +open class RustBuffer : Structure() { + // Note: `capacity` and `len` are actually `ULong` values, but JVM only supports signed values. + // When dealing with these fields, make sure to call `toULong()`. + @JvmField var capacity: Long = 0 + @JvmField var len: Long = 0 + @JvmField var data: Pointer? = null + + class ByValue: RustBuffer(), Structure.ByValue + class ByReference: RustBuffer(), Structure.ByReference + + internal fun setValue(other: RustBuffer) { + capacity = other.capacity + len = other.len + data = other.data + } + + companion object { + internal fun alloc(size: ULong = 0UL) = uniffiRustCall() { status -> + // Note: need to convert the size to a `Long` value to make this work with JVM. + UniffiLib.INSTANCE.ffi_noro_mobile_core_rustbuffer_alloc(size.toLong(), status) + }.also { + if(it.data == null) { + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") + } + } + + internal fun create(capacity: ULong, len: ULong, data: Pointer?): RustBuffer.ByValue { + var buf = RustBuffer.ByValue() + buf.capacity = capacity.toLong() + buf.len = len.toLong() + buf.data = data + return buf + } + + internal fun free(buf: RustBuffer.ByValue) = uniffiRustCall() { status -> + UniffiLib.INSTANCE.ffi_noro_mobile_core_rustbuffer_free(buf, status) + } + } + + @Suppress("TooGenericExceptionThrown") + fun asByteBuffer() = + this.data?.getByteBuffer(0, this.len.toLong())?.also { + it.order(ByteOrder.BIG_ENDIAN) + } +} + +/** + * The equivalent of the `*mut RustBuffer` type. + * Required for callbacks taking in an out pointer. + * + * Size is the sum of all values in the struct. + * + * @suppress + */ +class RustBufferByReference : ByReference(16) { + /** + * Set the pointed-to `RustBuffer` to the given value. + */ + fun setValue(value: RustBuffer.ByValue) { + // NOTE: The offsets are as they are in the C-like struct. + val pointer = getPointer() + pointer.setLong(0, value.capacity) + pointer.setLong(8, value.len) + pointer.setPointer(16, value.data) + } + + /** + * Get a `RustBuffer.ByValue` from this reference. + */ + fun getValue(): RustBuffer.ByValue { + val pointer = getPointer() + val value = RustBuffer.ByValue() + value.writeField("capacity", pointer.getLong(0)) + value.writeField("len", pointer.getLong(8)) + value.writeField("data", pointer.getLong(16)) + + return value + } +} + +// This is a helper for safely passing byte references into the rust code. +// It's not actually used at the moment, because there aren't many things that you +// can take a direct pointer to in the JVM, and if we're going to copy something +// then we might as well copy it into a `RustBuffer`. But it's here for API +// completeness. + +@Structure.FieldOrder("len", "data") +internal open class ForeignBytes : Structure() { + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue : ForeignBytes(), Structure.ByValue +} +/** + * The FfiConverter interface handles converter types to and from the FFI + * + * All implementing objects should be public to support external types. When a + * type is external we need to import it's FfiConverter. + * + * @suppress + */ +public interface FfiConverter { + // Convert an FFI type to a Kotlin type + fun lift(value: FfiType): KotlinType + + // Convert an Kotlin type to an FFI type + fun lower(value: KotlinType): FfiType + + // Read a Kotlin type from a `ByteBuffer` + fun read(buf: ByteBuffer): KotlinType + + // Calculate bytes to allocate when creating a `RustBuffer` + // + // This must return at least as many bytes as the write() function will + // write. It can return more bytes than needed, for example when writing + // Strings we can't know the exact bytes needed until we the UTF-8 + // encoding, so we pessimistically allocate the largest size possible (3 + // bytes per codepoint). Allocating extra bytes is not really a big deal + // because the `RustBuffer` is short-lived. + fun allocationSize(value: KotlinType): ULong + + // Write a Kotlin type to a `ByteBuffer` + fun write(value: KotlinType, buf: ByteBuffer) + + // Lower a value into a `RustBuffer` + // + // This method lowers a value into a `RustBuffer` rather than the normal + // FfiType. It's used by the callback interface code. Callback interface + // returns are always serialized into a `RustBuffer` regardless of their + // normal FFI type. + fun lowerIntoRustBuffer(value: KotlinType): RustBuffer.ByValue { + val rbuf = RustBuffer.alloc(allocationSize(value)) + try { + val bbuf = rbuf.data!!.getByteBuffer(0, rbuf.capacity).also { + it.order(ByteOrder.BIG_ENDIAN) + } + write(value, bbuf) + rbuf.writeField("len", bbuf.position().toLong()) + return rbuf + } catch (e: Throwable) { + RustBuffer.free(rbuf) + throw e + } + } + + // Lift a value from a `RustBuffer`. + // + // This here mostly because of the symmetry with `lowerIntoRustBuffer()`. + // It's currently only used by the `FfiConverterRustBuffer` class below. + fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType { + val byteBuf = rbuf.asByteBuffer()!! + try { + val item = read(byteBuf) + if (byteBuf.hasRemaining()) { + throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") + } + return item + } finally { + RustBuffer.free(rbuf) + } + } +} + +/** + * FfiConverter that uses `RustBuffer` as the FfiType + * + * @suppress + */ +public interface FfiConverterRustBuffer: FfiConverter { + override fun lift(value: RustBuffer.ByValue) = liftFromRustBuffer(value) + override fun lower(value: KotlinType) = lowerIntoRustBuffer(value) +} +// A handful of classes and functions to support the generated data structures. +// This would be a good candidate for isolating in its own ffi-support lib. + +internal const val UNIFFI_CALL_SUCCESS = 0.toByte() +internal const val UNIFFI_CALL_ERROR = 1.toByte() +internal const val UNIFFI_CALL_UNEXPECTED_ERROR = 2.toByte() + +@Structure.FieldOrder("code", "error_buf") +internal open class UniffiRustCallStatus : Structure() { + @JvmField var code: Byte = 0 + @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() + + class ByValue: UniffiRustCallStatus(), Structure.ByValue + + fun isSuccess(): Boolean { + return code == UNIFFI_CALL_SUCCESS + } + + fun isError(): Boolean { + return code == UNIFFI_CALL_ERROR + } + + fun isPanic(): Boolean { + return code == UNIFFI_CALL_UNEXPECTED_ERROR + } + + companion object { + fun create(code: Byte, errorBuf: RustBuffer.ByValue): UniffiRustCallStatus.ByValue { + val callStatus = UniffiRustCallStatus.ByValue() + callStatus.code = code + callStatus.error_buf = errorBuf + return callStatus + } + } +} + +class InternalException(message: String) : kotlin.Exception(message) + +/** + * Each top-level error class has a companion object that can lift the error from the call status's rust buffer + * + * @suppress + */ +interface UniffiRustCallStatusErrorHandler { + fun lift(error_buf: RustBuffer.ByValue): E; +} + +// Helpers for calling Rust +// In practice we usually need to be synchronized to call this safely, so it doesn't +// synchronize itself + +// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err +private inline fun uniffiRustCallWithError(errorHandler: UniffiRustCallStatusErrorHandler, callback: (UniffiRustCallStatus) -> U): U { + var status = UniffiRustCallStatus() + val return_value = callback(status) + uniffiCheckCallStatus(errorHandler, status) + return return_value +} + +// Check UniffiRustCallStatus and throw an error if the call wasn't successful +private fun uniffiCheckCallStatus(errorHandler: UniffiRustCallStatusErrorHandler, status: UniffiRustCallStatus) { + if (status.isSuccess()) { + return + } else if (status.isError()) { + throw errorHandler.lift(status.error_buf) + } else if (status.isPanic()) { + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if (status.error_buf.len > 0) { + throw InternalException(FfiConverterString.lift(status.error_buf)) + } else { + throw InternalException("Rust panic") + } + } else { + throw InternalException("Unknown rust call status: $status.code") + } +} + +/** + * UniffiRustCallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR + * + * @suppress + */ +object UniffiNullRustCallStatusErrorHandler: UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): InternalException { + RustBuffer.free(error_buf) + return InternalException("Unexpected CALL_ERROR") + } +} + +// Call a rust function that returns a plain value +private inline fun uniffiRustCall(callback: (UniffiRustCallStatus) -> U): U { + return uniffiRustCallWithError(UniffiNullRustCallStatusErrorHandler, callback) +} + +internal inline fun uniffiTraitInterfaceCall( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, +) { + try { + writeReturn(makeCall()) + } catch(e: kotlin.Exception) { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(e.toString()) + } +} + +internal inline fun uniffiTraitInterfaceCallWithError( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, + lowerError: (E) -> RustBuffer.ByValue +) { + try { + writeReturn(makeCall()) + } catch(e: kotlin.Exception) { + if (e is E) { + callStatus.code = UNIFFI_CALL_ERROR + callStatus.error_buf = lowerError(e) + } else { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(e.toString()) + } + } +} +// Map handles to objects +// +// This is used pass an opaque 64-bit handle representing a foreign object to the Rust code. +internal class UniffiHandleMap { + private val map = ConcurrentHashMap() + private val counter = java.util.concurrent.atomic.AtomicLong(0) + + val size: Int + get() = map.size + + // Insert a new object into the handle map and get a handle for it + fun insert(obj: T): Long { + val handle = counter.getAndAdd(1) + map.put(handle, obj) + return handle + } + + // Get an object from the handle map + fun get(handle: Long): T { + return map.get(handle) ?: throw InternalException("UniffiHandleMap.get: Invalid handle") + } + + // Remove an entry from the handlemap and get the Kotlin object back + fun remove(handle: Long): T { + return map.remove(handle) ?: throw InternalException("UniffiHandleMap: Invalid handle") + } +} + +// Contains loading, initialization code, +// and the FFI Function declarations in a com.sun.jna.Library. +@Synchronized +private fun findLibraryName(componentName: String): String { + val libOverride = System.getProperty("uniffi.component.$componentName.libraryOverride") + if (libOverride != null) { + return libOverride + } + return "noro_mobile_core" +} + +private inline fun loadIndirect( + componentName: String +): Lib { + return Native.load(findLibraryName(componentName), Lib::class.java) +} + +// Define FFI callback types +internal interface UniffiRustFutureContinuationCallback : com.sun.jna.Callback { + fun callback(`data`: Long,`pollResult`: Byte,) +} +internal interface UniffiForeignFutureFree : com.sun.jna.Callback { + fun callback(`handle`: Long,) +} +internal interface UniffiCallbackInterfaceFree : com.sun.jna.Callback { + fun callback(`handle`: Long,) +} +@Structure.FieldOrder("handle", "free") +internal open class UniffiForeignFuture( + @JvmField internal var `handle`: Long = 0.toLong(), + @JvmField internal var `free`: UniffiForeignFutureFree? = null, +) : Structure() { + class UniffiByValue( + `handle`: Long = 0.toLong(), + `free`: UniffiForeignFutureFree? = null, + ): UniffiForeignFuture(`handle`,`free`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFuture) { + `handle` = other.`handle` + `free` = other.`free` + } + +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU8(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU8 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU8.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI8(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI8 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI8.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU16(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU16 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU16.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI16(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI16 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI16.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructF32( + @JvmField internal var `returnValue`: Float = 0.0f, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Float = 0.0f, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructF32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructF32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteF32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructF32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructF64( + @JvmField internal var `returnValue`: Double = 0.0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Double = 0.0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructF64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructF64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteF64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructF64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructPointer( + @JvmField internal var `returnValue`: Pointer = Pointer.NULL, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Pointer = Pointer.NULL, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructPointer(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructPointer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompletePointer : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructPointer.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructRustBuffer( + @JvmField internal var `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructRustBuffer(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructRustBuffer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteRustBuffer : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructRustBuffer.UniffiByValue,) +} +@Structure.FieldOrder("callStatus") +internal open class UniffiForeignFutureStructVoid( + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructVoid(`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructVoid) { + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructVoid.UniffiByValue,) +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +// A JNA Library to expose the extern-C FFI definitions. +// This is an implementation detail which will be called internally by the public API. + +internal interface UniffiLib : Library { + companion object { + internal val INSTANCE: UniffiLib by lazy { + loadIndirect(componentName = "noro_mobile_core") + .also { lib: UniffiLib -> + uniffiCheckContractApiVersion(lib) + uniffiCheckApiChecksums(lib) + } + } + + // The Cleaner for the whole library + internal val CLEANER: UniffiCleaner by lazy { + UniffiCleaner.create() + } + } + + fun uniffi_noro_mobile_core_fn_clone_syncclient(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_noro_mobile_core_fn_free_syncclient(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_noro_mobile_core_fn_constructor_syncclient_new(`baseUrl`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_noro_mobile_core_fn_method_syncclient_create_item(`ptr`: Pointer,`itemType`: RustBuffer.ByValue,`title`: RustBuffer.ByValue,`data`: RustBuffer.ByValue,`tags`: RustBuffer.ByValue,`favorite`: Byte,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_method_syncclient_delete_item(`ptr`: Pointer,`id`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_noro_mobile_core_fn_method_syncclient_fetch_items(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_method_syncclient_login(`ptr`: Pointer,`email`: RustBuffer.ByValue,`password`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_method_syncclient_set_token(`ptr`: Pointer,`token`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_noro_mobile_core_fn_method_syncclient_update_item(`ptr`: Pointer,`id`: RustBuffer.ByValue,`title`: RustBuffer.ByValue,`data`: RustBuffer.ByValue,`tags`: RustBuffer.ByValue,`favorite`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_clone_vault(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_noro_mobile_core_fn_free_vault(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_noro_mobile_core_fn_constructor_vault_new(uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_noro_mobile_core_fn_method_vault_create_item(`ptr`: Pointer,`itemType`: RustBuffer.ByValue,`title`: RustBuffer.ByValue,`data`: RustBuffer.ByValue,`tags`: RustBuffer.ByValue,`favorite`: Byte,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_method_vault_delete_item(`ptr`: Pointer,`id`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_noro_mobile_core_fn_method_vault_get_item(`ptr`: Pointer,`id`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_method_vault_list_items(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_method_vault_load(`ptr`: Pointer,`encrypted`: RustBuffer.ByValue,`key`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_noro_mobile_core_fn_method_vault_save(`ptr`: Pointer,`key`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_method_vault_search_items(`ptr`: Pointer,`query`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_method_vault_update_item(`ptr`: Pointer,`id`: RustBuffer.ByValue,`title`: RustBuffer.ByValue,`data`: RustBuffer.ByValue,`tags`: RustBuffer.ByValue,`favorite`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_func_decrypt(`ciphertext`: RustBuffer.ByValue,`key`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_func_derive_auk(`password`: RustBuffer.ByValue,`secretKey`: RustBuffer.ByValue,`salt`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_func_derive_item_key(`vaultKey`: RustBuffer.ByValue,`itemId`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_func_encrypt(`plaintext`: RustBuffer.ByValue,`key`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_func_generate_item_id(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_func_generate_salt(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_func_generate_secret_key(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_func_generate_vault_key(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_func_unwrap_vault_key(`wrapped`: RustBuffer.ByValue,`auk`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_noro_mobile_core_fn_func_wrap_vault_key(`vaultKey`: RustBuffer.ByValue,`auk`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_noro_mobile_core_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_noro_mobile_core_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_noro_mobile_core_rustbuffer_free(`buf`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun ffi_noro_mobile_core_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_noro_mobile_core_rust_future_poll_u8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_u8(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_u8(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_u8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun ffi_noro_mobile_core_rust_future_poll_i8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_i8(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_i8(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_i8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun ffi_noro_mobile_core_rust_future_poll_u16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_u16(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_u16(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_u16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Short + fun ffi_noro_mobile_core_rust_future_poll_i16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_i16(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_i16(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_i16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Short + fun ffi_noro_mobile_core_rust_future_poll_u32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_u32(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_u32(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_u32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun ffi_noro_mobile_core_rust_future_poll_i32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_i32(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_i32(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_i32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun ffi_noro_mobile_core_rust_future_poll_u64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_u64(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_u64(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_u64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + fun ffi_noro_mobile_core_rust_future_poll_i64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_i64(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_i64(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_i64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + fun ffi_noro_mobile_core_rust_future_poll_f32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_f32(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_f32(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_f32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Float + fun ffi_noro_mobile_core_rust_future_poll_f64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_f64(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_f64(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_f64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Double + fun ffi_noro_mobile_core_rust_future_poll_pointer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_pointer(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_pointer(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_pointer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun ffi_noro_mobile_core_rust_future_poll_rust_buffer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_rust_buffer(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_rust_buffer(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_rust_buffer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_noro_mobile_core_rust_future_poll_void(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_cancel_void(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_free_void(`handle`: Long, + ): Unit + fun ffi_noro_mobile_core_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_noro_mobile_core_checksum_func_decrypt( + ): Short + fun uniffi_noro_mobile_core_checksum_func_derive_auk( + ): Short + fun uniffi_noro_mobile_core_checksum_func_derive_item_key( + ): Short + fun uniffi_noro_mobile_core_checksum_func_encrypt( + ): Short + fun uniffi_noro_mobile_core_checksum_func_generate_item_id( + ): Short + fun uniffi_noro_mobile_core_checksum_func_generate_salt( + ): Short + fun uniffi_noro_mobile_core_checksum_func_generate_secret_key( + ): Short + fun uniffi_noro_mobile_core_checksum_func_generate_vault_key( + ): Short + fun uniffi_noro_mobile_core_checksum_func_unwrap_vault_key( + ): Short + fun uniffi_noro_mobile_core_checksum_func_wrap_vault_key( + ): Short + fun uniffi_noro_mobile_core_checksum_method_syncclient_create_item( + ): Short + fun uniffi_noro_mobile_core_checksum_method_syncclient_delete_item( + ): Short + fun uniffi_noro_mobile_core_checksum_method_syncclient_fetch_items( + ): Short + fun uniffi_noro_mobile_core_checksum_method_syncclient_login( + ): Short + fun uniffi_noro_mobile_core_checksum_method_syncclient_set_token( + ): Short + fun uniffi_noro_mobile_core_checksum_method_syncclient_update_item( + ): Short + fun uniffi_noro_mobile_core_checksum_method_vault_create_item( + ): Short + fun uniffi_noro_mobile_core_checksum_method_vault_delete_item( + ): Short + fun uniffi_noro_mobile_core_checksum_method_vault_get_item( + ): Short + fun uniffi_noro_mobile_core_checksum_method_vault_list_items( + ): Short + fun uniffi_noro_mobile_core_checksum_method_vault_load( + ): Short + fun uniffi_noro_mobile_core_checksum_method_vault_save( + ): Short + fun uniffi_noro_mobile_core_checksum_method_vault_search_items( + ): Short + fun uniffi_noro_mobile_core_checksum_method_vault_update_item( + ): Short + fun uniffi_noro_mobile_core_checksum_constructor_syncclient_new( + ): Short + fun uniffi_noro_mobile_core_checksum_constructor_vault_new( + ): Short + fun ffi_noro_mobile_core_uniffi_contract_version( + ): Int + +} + +private fun uniffiCheckContractApiVersion(lib: UniffiLib) { + // Get the bindings contract version from our ComponentInterface + val bindings_contract_version = 26 + // Get the scaffolding contract version by calling the into the dylib + val scaffolding_contract_version = lib.ffi_noro_mobile_core_uniffi_contract_version() + if (bindings_contract_version != scaffolding_contract_version) { + throw RuntimeException("UniFFI contract version mismatch: try cleaning and rebuilding your project") + } +} + +@Suppress("UNUSED_PARAMETER") +private fun uniffiCheckApiChecksums(lib: UniffiLib) { + if (lib.uniffi_noro_mobile_core_checksum_func_decrypt() != 32839.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_func_derive_auk() != 37186.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_func_derive_item_key() != 51281.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_func_encrypt() != 46133.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_func_generate_item_id() != 22417.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_func_generate_salt() != 31865.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_func_generate_secret_key() != 53137.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_func_generate_vault_key() != 50386.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_func_unwrap_vault_key() != 38585.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_func_wrap_vault_key() != 17122.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_syncclient_create_item() != 51847.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_syncclient_delete_item() != 22816.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_syncclient_fetch_items() != 53777.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_syncclient_login() != 19049.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_syncclient_set_token() != 22416.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_syncclient_update_item() != 45106.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_vault_create_item() != 60480.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_vault_delete_item() != 63195.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_vault_get_item() != 55932.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_vault_list_items() != 61447.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_vault_load() != 6499.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_vault_save() != 50962.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_vault_search_items() != 42826.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_method_vault_update_item() != 55533.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_constructor_syncclient_new() != 38165.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_noro_mobile_core_checksum_constructor_vault_new() != 9762.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} + +// Async support + +// Public interface members begin here. + + +// Interface implemented by anything that can contain an object reference. +// +// Such types expose a `destroy()` method that must be called to cleanly +// dispose of the contained objects. Failure to call this method may result +// in memory leaks. +// +// The easiest way to ensure this method is called is to use the `.use` +// helper method to execute a block and destroy the object at the end. +interface Disposable { + fun destroy() + companion object { + fun destroy(vararg args: Any?) { + args.filterIsInstance() + .forEach(Disposable::destroy) + } + } +} + +/** + * @suppress + */ +inline fun T.use(block: (T) -> R) = + try { + block(this) + } finally { + try { + // N.B. our implementation is on the nullable type `Disposable?`. + this?.destroy() + } catch (e: Throwable) { + // swallow + } + } + +/** + * Used to instantiate an interface without an actual pointer, for fakes in tests, mostly. + * + * @suppress + * */ +object NoPointer + +/** + * @suppress + */ +public object FfiConverterInt: FfiConverter { + override fun lift(value: Int): Int { + return value + } + + override fun read(buf: ByteBuffer): Int { + return buf.getInt() + } + + override fun lower(value: Int): Int { + return value + } + + override fun allocationSize(value: Int) = 4UL + + override fun write(value: Int, buf: ByteBuffer) { + buf.putInt(value) + } +} + +/** + * @suppress + */ +public object FfiConverterULong: FfiConverter { + override fun lift(value: Long): ULong { + return value.toULong() + } + + override fun read(buf: ByteBuffer): ULong { + return lift(buf.getLong()) + } + + override fun lower(value: ULong): Long { + return value.toLong() + } + + override fun allocationSize(value: ULong) = 8UL + + override fun write(value: ULong, buf: ByteBuffer) { + buf.putLong(value.toLong()) + } +} + +/** + * @suppress + */ +public object FfiConverterBoolean: FfiConverter { + override fun lift(value: Byte): Boolean { + return value.toInt() != 0 + } + + override fun read(buf: ByteBuffer): Boolean { + return lift(buf.get()) + } + + override fun lower(value: Boolean): Byte { + return if (value) 1.toByte() else 0.toByte() + } + + override fun allocationSize(value: Boolean) = 1UL + + override fun write(value: Boolean, buf: ByteBuffer) { + buf.put(lower(value)) + } +} + +/** + * @suppress + */ +public object FfiConverterString: FfiConverter { + // Note: we don't inherit from FfiConverterRustBuffer, because we use a + // special encoding when lowering/lifting. We can use `RustBuffer.len` to + // store our length and avoid writing it out to the buffer. + override fun lift(value: RustBuffer.ByValue): String { + try { + val byteArr = ByteArray(value.len.toInt()) + value.asByteBuffer()!!.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } finally { + RustBuffer.free(value) + } + } + + override fun read(buf: ByteBuffer): String { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } + + fun toUtf8(value: String): ByteBuffer { + // Make sure we don't have invalid UTF-16, check for lone surrogates. + return Charsets.UTF_8.newEncoder().run { + onMalformedInput(CodingErrorAction.REPORT) + encode(CharBuffer.wrap(value)) + } + } + + override fun lower(value: String): RustBuffer.ByValue { + val byteBuf = toUtf8(value) + // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us + // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. + val rbuf = RustBuffer.alloc(byteBuf.limit().toULong()) + rbuf.asByteBuffer()!!.put(byteBuf) + return rbuf + } + + // We aren't sure exactly how many bytes our string will be once it's UTF-8 + // encoded. Allocate 3 bytes per UTF-16 code unit which will always be + // enough. + override fun allocationSize(value: String): ULong { + val sizeForLength = 4UL + val sizeForString = value.length.toULong() * 3UL + return sizeForLength + sizeForString + } + + override fun write(value: String, buf: ByteBuffer) { + val byteBuf = toUtf8(value) + buf.putInt(byteBuf.limit()) + buf.put(byteBuf) + } +} + +/** + * @suppress + */ +public object FfiConverterByteArray: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ByteArray { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr + } + override fun allocationSize(value: ByteArray): ULong { + return 4UL + value.size.toULong() + } + override fun write(value: ByteArray, buf: ByteBuffer) { + buf.putInt(value.size) + buf.put(value) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + +/** + * The cleaner interface for Object finalization code to run. + * This is the entry point to any implementation that we're using. + * + * The cleaner registers objects and returns cleanables, so now we are + * defining a `UniffiCleaner` with a `UniffiClenaer.Cleanable` to abstract the + * different implmentations available at compile time. + * + * @suppress + */ +interface UniffiCleaner { + interface Cleanable { + fun clean() + } + + fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable + + companion object +} + +// The fallback Jna cleaner, which is available for both Android, and the JVM. +private class UniffiJnaCleaner : UniffiCleaner { + private val cleaner = com.sun.jna.internal.Cleaner.getCleaner() + + override fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable = + UniffiJnaCleanable(cleaner.register(value, cleanUpTask)) +} + +private class UniffiJnaCleanable( + private val cleanable: com.sun.jna.internal.Cleaner.Cleanable, +) : UniffiCleaner.Cleanable { + override fun clean() = cleanable.clean() +} + +// We decide at uniffi binding generation time whether we were +// using Android or not. +// There are further runtime checks to chose the correct implementation +// of the cleaner. +private fun UniffiCleaner.Companion.create(): UniffiCleaner = + try { + // For safety's sake: if the library hasn't been run in android_cleaner = true + // mode, but is being run on Android, then we still need to think about + // Android API versions. + // So we check if java.lang.ref.Cleaner is there, and use that… + java.lang.Class.forName("java.lang.ref.Cleaner") + JavaLangRefCleaner() + } catch (e: ClassNotFoundException) { + // … otherwise, fallback to the JNA cleaner. + UniffiJnaCleaner() + } + +private class JavaLangRefCleaner : UniffiCleaner { + val cleaner = java.lang.ref.Cleaner.create() + + override fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable = + JavaLangRefCleanable(cleaner.register(value, cleanUpTask)) +} + +private class JavaLangRefCleanable( + val cleanable: java.lang.ref.Cleaner.Cleanable +) : UniffiCleaner.Cleanable { + override fun clean() = cleanable.clean() +} +public interface SyncClientInterface { + + fun `createItem`(`itemType`: kotlin.String, `title`: kotlin.String, `data`: kotlin.ByteArray, `tags`: List, `favorite`: kotlin.Boolean): VaultItem + + fun `deleteItem`(`id`: kotlin.String) + + fun `fetchItems`(): List + + fun `login`(`email`: kotlin.String, `password`: kotlin.String): kotlin.String + + fun `setToken`(`token`: kotlin.String) + + fun `updateItem`(`id`: kotlin.String, `title`: kotlin.String?, `data`: kotlin.ByteArray?, `tags`: List?, `favorite`: kotlin.Boolean?): VaultItem + + companion object +} + +open class SyncClient: Disposable, AutoCloseable, SyncClientInterface { + + constructor(pointer: Pointer) { + this.pointer = pointer + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + /** + * This constructor can be used to instantiate a fake object. Only used for tests. Any + * attempt to actually use an object constructed this way will fail as there is no + * connected Rust object. + */ + @Suppress("UNUSED_PARAMETER") + constructor(noPointer: NoPointer) { + this.pointer = null + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + constructor(`baseUrl`: kotlin.String) : + this( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_constructor_syncclient_new( + FfiConverterString.lower(`baseUrl`),_status) +} + ) + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_free_syncclient(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_clone_syncclient(pointer!!, status) + } + } + + + @Throws(SyncException::class)override fun `createItem`(`itemType`: kotlin.String, `title`: kotlin.String, `data`: kotlin.ByteArray, `tags`: List, `favorite`: kotlin.Boolean): VaultItem { + return FfiConverterTypeVaultItem.lift( + callWithPointer { + uniffiRustCallWithError(SyncException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_syncclient_create_item( + it, FfiConverterString.lower(`itemType`),FfiConverterString.lower(`title`),FfiConverterByteArray.lower(`data`),FfiConverterSequenceString.lower(`tags`),FfiConverterBoolean.lower(`favorite`),_status) +} + } + ) + } + + + + @Throws(SyncException::class)override fun `deleteItem`(`id`: kotlin.String) + = + callWithPointer { + uniffiRustCallWithError(SyncException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_syncclient_delete_item( + it, FfiConverterString.lower(`id`),_status) +} + } + + + + + @Throws(SyncException::class)override fun `fetchItems`(): List { + return FfiConverterSequenceTypeVaultItem.lift( + callWithPointer { + uniffiRustCallWithError(SyncException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_syncclient_fetch_items( + it, _status) +} + } + ) + } + + + + @Throws(SyncException::class)override fun `login`(`email`: kotlin.String, `password`: kotlin.String): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCallWithError(SyncException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_syncclient_login( + it, FfiConverterString.lower(`email`),FfiConverterString.lower(`password`),_status) +} + } + ) + } + + + override fun `setToken`(`token`: kotlin.String) + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_syncclient_set_token( + it, FfiConverterString.lower(`token`),_status) +} + } + + + + + @Throws(SyncException::class)override fun `updateItem`(`id`: kotlin.String, `title`: kotlin.String?, `data`: kotlin.ByteArray?, `tags`: List?, `favorite`: kotlin.Boolean?): VaultItem { + return FfiConverterTypeVaultItem.lift( + callWithPointer { + uniffiRustCallWithError(SyncException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_syncclient_update_item( + it, FfiConverterString.lower(`id`),FfiConverterOptionalString.lower(`title`),FfiConverterOptionalByteArray.lower(`data`),FfiConverterOptionalSequenceString.lower(`tags`),FfiConverterOptionalBoolean.lower(`favorite`),_status) +} + } + ) + } + + + + + + + companion object + +} + +/** + * @suppress + */ +public object FfiConverterTypeSyncClient: FfiConverter { + + override fun lower(value: SyncClient): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): SyncClient { + return SyncClient(value) + } + + override fun read(buf: ByteBuffer): SyncClient { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: SyncClient) = 8UL + + override fun write(value: SyncClient, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + +public interface VaultInterface { + + fun `createItem`(`itemType`: kotlin.String, `title`: kotlin.String, `data`: kotlin.ByteArray, `tags`: List, `favorite`: kotlin.Boolean): VaultItem + + fun `deleteItem`(`id`: kotlin.String) + + fun `getItem`(`id`: kotlin.String): VaultItem? + + fun `listItems`(): List + + fun `load`(`encrypted`: kotlin.ByteArray, `key`: kotlin.ByteArray) + + fun `save`(`key`: kotlin.ByteArray): kotlin.ByteArray + + fun `searchItems`(`query`: kotlin.String): List + + fun `updateItem`(`id`: kotlin.String, `title`: kotlin.String?, `data`: kotlin.ByteArray?, `tags`: List?, `favorite`: kotlin.Boolean?): VaultItem + + companion object +} + +open class Vault: Disposable, AutoCloseable, VaultInterface { + + constructor(pointer: Pointer) { + this.pointer = pointer + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + /** + * This constructor can be used to instantiate a fake object. Only used for tests. Any + * attempt to actually use an object constructed this way will fail as there is no + * connected Rust object. + */ + @Suppress("UNUSED_PARAMETER") + constructor(noPointer: NoPointer) { + this.pointer = null + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + constructor() : + this( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_constructor_vault_new( + _status) +} + ) + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_free_vault(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_clone_vault(pointer!!, status) + } + } + + + @Throws(VaultException::class)override fun `createItem`(`itemType`: kotlin.String, `title`: kotlin.String, `data`: kotlin.ByteArray, `tags`: List, `favorite`: kotlin.Boolean): VaultItem { + return FfiConverterTypeVaultItem.lift( + callWithPointer { + uniffiRustCallWithError(VaultException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_vault_create_item( + it, FfiConverterString.lower(`itemType`),FfiConverterString.lower(`title`),FfiConverterByteArray.lower(`data`),FfiConverterSequenceString.lower(`tags`),FfiConverterBoolean.lower(`favorite`),_status) +} + } + ) + } + + + + @Throws(VaultException::class)override fun `deleteItem`(`id`: kotlin.String) + = + callWithPointer { + uniffiRustCallWithError(VaultException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_vault_delete_item( + it, FfiConverterString.lower(`id`),_status) +} + } + + + + + @Throws(VaultException::class)override fun `getItem`(`id`: kotlin.String): VaultItem? { + return FfiConverterOptionalTypeVaultItem.lift( + callWithPointer { + uniffiRustCallWithError(VaultException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_vault_get_item( + it, FfiConverterString.lower(`id`),_status) +} + } + ) + } + + + override fun `listItems`(): List { + return FfiConverterSequenceTypeVaultItem.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_vault_list_items( + it, _status) +} + } + ) + } + + + + @Throws(VaultException::class)override fun `load`(`encrypted`: kotlin.ByteArray, `key`: kotlin.ByteArray) + = + callWithPointer { + uniffiRustCallWithError(VaultException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_vault_load( + it, FfiConverterByteArray.lower(`encrypted`),FfiConverterByteArray.lower(`key`),_status) +} + } + + + + + @Throws(VaultException::class)override fun `save`(`key`: kotlin.ByteArray): kotlin.ByteArray { + return FfiConverterByteArray.lift( + callWithPointer { + uniffiRustCallWithError(VaultException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_vault_save( + it, FfiConverterByteArray.lower(`key`),_status) +} + } + ) + } + + + override fun `searchItems`(`query`: kotlin.String): List { + return FfiConverterSequenceTypeVaultItem.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_vault_search_items( + it, FfiConverterString.lower(`query`),_status) +} + } + ) + } + + + + @Throws(VaultException::class)override fun `updateItem`(`id`: kotlin.String, `title`: kotlin.String?, `data`: kotlin.ByteArray?, `tags`: List?, `favorite`: kotlin.Boolean?): VaultItem { + return FfiConverterTypeVaultItem.lift( + callWithPointer { + uniffiRustCallWithError(VaultException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_method_vault_update_item( + it, FfiConverterString.lower(`id`),FfiConverterOptionalString.lower(`title`),FfiConverterOptionalByteArray.lower(`data`),FfiConverterOptionalSequenceString.lower(`tags`),FfiConverterOptionalBoolean.lower(`favorite`),_status) +} + } + ) + } + + + + + + + companion object + +} + +/** + * @suppress + */ +public object FfiConverterTypeVault: FfiConverter { + + override fun lower(value: Vault): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): Vault { + return Vault(value) + } + + override fun read(buf: ByteBuffer): Vault { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: Vault) = 8UL + + override fun write(value: Vault, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + + +data class VaultData ( + var `items`: List, + var `updated`: kotlin.ULong +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeVaultData: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): VaultData { + return VaultData( + FfiConverterSequenceTypeVaultItem.read(buf), + FfiConverterULong.read(buf), + ) + } + + override fun allocationSize(value: VaultData) = ( + FfiConverterSequenceTypeVaultItem.allocationSize(value.`items`) + + FfiConverterULong.allocationSize(value.`updated`) + ) + + override fun write(value: VaultData, buf: ByteBuffer) { + FfiConverterSequenceTypeVaultItem.write(value.`items`, buf) + FfiConverterULong.write(value.`updated`, buf) + } +} + + + +data class VaultItem ( + var `id`: kotlin.String, + var `itemType`: kotlin.String, + var `title`: kotlin.String, + var `data`: kotlin.ByteArray, + var `revision`: kotlin.Int, + var `favorite`: kotlin.Boolean, + var `deleted`: kotlin.Boolean, + var `tags`: List, + var `created`: kotlin.ULong, + var `updated`: kotlin.ULong +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeVaultItem: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): VaultItem { + return VaultItem( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterString.read(buf), + FfiConverterByteArray.read(buf), + FfiConverterInt.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterSequenceString.read(buf), + FfiConverterULong.read(buf), + FfiConverterULong.read(buf), + ) + } + + override fun allocationSize(value: VaultItem) = ( + FfiConverterString.allocationSize(value.`id`) + + FfiConverterString.allocationSize(value.`itemType`) + + FfiConverterString.allocationSize(value.`title`) + + FfiConverterByteArray.allocationSize(value.`data`) + + FfiConverterInt.allocationSize(value.`revision`) + + FfiConverterBoolean.allocationSize(value.`favorite`) + + FfiConverterBoolean.allocationSize(value.`deleted`) + + FfiConverterSequenceString.allocationSize(value.`tags`) + + FfiConverterULong.allocationSize(value.`created`) + + FfiConverterULong.allocationSize(value.`updated`) + ) + + override fun write(value: VaultItem, buf: ByteBuffer) { + FfiConverterString.write(value.`id`, buf) + FfiConverterString.write(value.`itemType`, buf) + FfiConverterString.write(value.`title`, buf) + FfiConverterByteArray.write(value.`data`, buf) + FfiConverterInt.write(value.`revision`, buf) + FfiConverterBoolean.write(value.`favorite`, buf) + FfiConverterBoolean.write(value.`deleted`, buf) + FfiConverterSequenceString.write(value.`tags`, buf) + FfiConverterULong.write(value.`created`, buf) + FfiConverterULong.write(value.`updated`, buf) + } +} + + + + + +sealed class CryptoException: kotlin.Exception() { + + class Argon2( + ) : CryptoException() { + override val message + get() = "" + } + + class Encryption( + ) : CryptoException() { + override val message + get() = "" + } + + class Decryption( + ) : CryptoException() { + override val message + get() = "" + } + + class InvalidSecretKey( + ) : CryptoException() { + override val message + get() = "" + } + + class InvalidKeyLength( + ) : CryptoException() { + override val message + get() = "" + } + + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): CryptoException = FfiConverterTypeCryptoError.lift(error_buf) + } + + +} + +/** + * @suppress + */ +public object FfiConverterTypeCryptoError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): CryptoException { + + + return when(buf.getInt()) { + 1 -> CryptoException.Argon2() + 2 -> CryptoException.Encryption() + 3 -> CryptoException.Decryption() + 4 -> CryptoException.InvalidSecretKey() + 5 -> CryptoException.InvalidKeyLength() + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: CryptoException): ULong { + return when(value) { + is CryptoException.Argon2 -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is CryptoException.Encryption -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is CryptoException.Decryption -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is CryptoException.InvalidSecretKey -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is CryptoException.InvalidKeyLength -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + } + } + + override fun write(value: CryptoException, buf: ByteBuffer) { + when(value) { + is CryptoException.Argon2 -> { + buf.putInt(1) + Unit + } + is CryptoException.Encryption -> { + buf.putInt(2) + Unit + } + is CryptoException.Decryption -> { + buf.putInt(3) + Unit + } + is CryptoException.InvalidSecretKey -> { + buf.putInt(4) + Unit + } + is CryptoException.InvalidKeyLength -> { + buf.putInt(5) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} + + + + + +sealed class SyncException: kotlin.Exception() { + + class Http( + ) : SyncException() { + override val message + get() = "" + } + + class Auth( + ) : SyncException() { + override val message + get() = "" + } + + class Conflict( + ) : SyncException() { + override val message + get() = "" + } + + class Parse( + ) : SyncException() { + override val message + get() = "" + } + + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): SyncException = FfiConverterTypeSyncError.lift(error_buf) + } + + +} + +/** + * @suppress + */ +public object FfiConverterTypeSyncError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): SyncException { + + + return when(buf.getInt()) { + 1 -> SyncException.Http() + 2 -> SyncException.Auth() + 3 -> SyncException.Conflict() + 4 -> SyncException.Parse() + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: SyncException): ULong { + return when(value) { + is SyncException.Http -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is SyncException.Auth -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is SyncException.Conflict -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is SyncException.Parse -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + } + } + + override fun write(value: SyncException, buf: ByteBuffer) { + when(value) { + is SyncException.Http -> { + buf.putInt(1) + Unit + } + is SyncException.Auth -> { + buf.putInt(2) + Unit + } + is SyncException.Conflict -> { + buf.putInt(3) + Unit + } + is SyncException.Parse -> { + buf.putInt(4) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} + + + + + +sealed class VaultException: kotlin.Exception() { + + class NotFound( + ) : VaultException() { + override val message + get() = "" + } + + class Serialization( + ) : VaultException() { + override val message + get() = "" + } + + class Crypto( + ) : VaultException() { + override val message + get() = "" + } + + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): VaultException = FfiConverterTypeVaultError.lift(error_buf) + } + + +} + +/** + * @suppress + */ +public object FfiConverterTypeVaultError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): VaultException { + + + return when(buf.getInt()) { + 1 -> VaultException.NotFound() + 2 -> VaultException.Serialization() + 3 -> VaultException.Crypto() + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: VaultException): ULong { + return when(value) { + is VaultException.NotFound -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is VaultException.Serialization -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is VaultException.Crypto -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + } + } + + override fun write(value: VaultException, buf: ByteBuffer) { + when(value) { + is VaultException.NotFound -> { + buf.putInt(1) + Unit + } + is VaultException.Serialization -> { + buf.putInt(2) + Unit + } + is VaultException.Crypto -> { + buf.putInt(3) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalBoolean: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.Boolean? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterBoolean.read(buf) + } + + override fun allocationSize(value: kotlin.Boolean?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterBoolean.allocationSize(value) + } + } + + override fun write(value: kotlin.Boolean?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterBoolean.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalString: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.String? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterString.read(buf) + } + + override fun allocationSize(value: kotlin.String?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterString.allocationSize(value) + } + } + + override fun write(value: kotlin.String?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterString.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalByteArray: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.ByteArray? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterByteArray.read(buf) + } + + override fun allocationSize(value: kotlin.ByteArray?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterByteArray.allocationSize(value) + } + } + + override fun write(value: kotlin.ByteArray?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterByteArray.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalTypeVaultItem: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): VaultItem? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterTypeVaultItem.read(buf) + } + + override fun allocationSize(value: VaultItem?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterTypeVaultItem.allocationSize(value) + } + } + + override fun write(value: VaultItem?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterTypeVaultItem.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalSequenceString: FfiConverterRustBuffer?> { + override fun read(buf: ByteBuffer): List? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterSequenceString.read(buf) + } + + override fun allocationSize(value: List?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterSequenceString.allocationSize(value) + } + } + + override fun write(value: List?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterSequenceString.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceString: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterString.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterString.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterString.write(it, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceTypeVaultItem: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeVaultItem.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeVaultItem.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeVaultItem.write(it, buf) + } + } +} + @Throws(CryptoException::class) fun `decrypt`(`ciphertext`: kotlin.ByteArray, `key`: kotlin.ByteArray): kotlin.ByteArray { + return FfiConverterByteArray.lift( + uniffiRustCallWithError(CryptoException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_func_decrypt( + FfiConverterByteArray.lower(`ciphertext`),FfiConverterByteArray.lower(`key`),_status) +} + ) + } + + + @Throws(CryptoException::class) fun `deriveAuk`(`password`: kotlin.String, `secretKey`: kotlin.String, `salt`: kotlin.ByteArray): kotlin.ByteArray { + return FfiConverterByteArray.lift( + uniffiRustCallWithError(CryptoException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_func_derive_auk( + FfiConverterString.lower(`password`),FfiConverterString.lower(`secretKey`),FfiConverterByteArray.lower(`salt`),_status) +} + ) + } + + + @Throws(CryptoException::class) fun `deriveItemKey`(`vaultKey`: kotlin.ByteArray, `itemId`: kotlin.String): kotlin.ByteArray { + return FfiConverterByteArray.lift( + uniffiRustCallWithError(CryptoException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_func_derive_item_key( + FfiConverterByteArray.lower(`vaultKey`),FfiConverterString.lower(`itemId`),_status) +} + ) + } + + + @Throws(CryptoException::class) fun `encrypt`(`plaintext`: kotlin.ByteArray, `key`: kotlin.ByteArray): kotlin.ByteArray { + return FfiConverterByteArray.lift( + uniffiRustCallWithError(CryptoException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_func_encrypt( + FfiConverterByteArray.lower(`plaintext`),FfiConverterByteArray.lower(`key`),_status) +} + ) + } + + fun `generateItemId`(): kotlin.String { + return FfiConverterString.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_func_generate_item_id( + _status) +} + ) + } + + fun `generateSalt`(): kotlin.ByteArray { + return FfiConverterByteArray.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_func_generate_salt( + _status) +} + ) + } + + fun `generateSecretKey`(): kotlin.String { + return FfiConverterString.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_func_generate_secret_key( + _status) +} + ) + } + + fun `generateVaultKey`(): kotlin.ByteArray { + return FfiConverterByteArray.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_func_generate_vault_key( + _status) +} + ) + } + + + @Throws(CryptoException::class) fun `unwrapVaultKey`(`wrapped`: kotlin.ByteArray, `auk`: kotlin.ByteArray): kotlin.ByteArray { + return FfiConverterByteArray.lift( + uniffiRustCallWithError(CryptoException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_func_unwrap_vault_key( + FfiConverterByteArray.lower(`wrapped`),FfiConverterByteArray.lower(`auk`),_status) +} + ) + } + + + @Throws(CryptoException::class) fun `wrapVaultKey`(`vaultKey`: kotlin.ByteArray, `auk`: kotlin.ByteArray): kotlin.ByteArray { + return FfiConverterByteArray.lift( + uniffiRustCallWithError(CryptoException) { _status -> + UniffiLib.INSTANCE.uniffi_noro_mobile_core_fn_func_wrap_vault_key( + FfiConverterByteArray.lower(`vaultKey`),FfiConverterByteArray.lower(`auk`),_status) +} + ) + } + + + diff --git a/apps/mobile-core/src/bindgen.rs b/apps/mobile-core/src/bindgen.rs new file mode 100644 index 0000000..f6cff6c --- /dev/null +++ b/apps/mobile-core/src/bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} diff --git a/apps/mobile-core/src/crypto.rs b/apps/mobile-core/src/crypto.rs new file mode 100644 index 0000000..55a181d --- /dev/null +++ b/apps/mobile-core/src/crypto.rs @@ -0,0 +1,54 @@ +use chacha20poly1305::{ + aead::{Aead, KeyInit}, + XChaCha20Poly1305, XNonce, +}; +use rand::Rng; + +const NONCE_LEN: usize = 24; +const KEY_LEN: usize = 32; + +pub fn encrypt(plaintext: &[u8], key: &[u8]) -> Result, super::CryptoError> { + if key.len() != KEY_LEN { + return Err(super::CryptoError::InvalidKeyLength); + } + let cipher = + XChaCha20Poly1305::new_from_slice(key).map_err(|_| super::CryptoError::Encryption)?; + let mut rng = rand::thread_rng(); + let nonce_bytes: [u8; NONCE_LEN] = rng.gen(); + let nonce = XNonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, plaintext) + .map_err(|_| super::CryptoError::Encryption)?; + let mut result = Vec::with_capacity(NONCE_LEN + ciphertext.len()); + result.extend_from_slice(&nonce_bytes); + result.extend_from_slice(&ciphertext); + Ok(result) +} + +pub fn decrypt(ciphertext: &[u8], key: &[u8]) -> Result, super::CryptoError> { + if key.len() != KEY_LEN { + return Err(super::CryptoError::InvalidKeyLength); + } + if ciphertext.len() < NONCE_LEN + 16 { + return Err(super::CryptoError::Decryption); + } + let cipher = + XChaCha20Poly1305::new_from_slice(key).map_err(|_| super::CryptoError::Decryption)?; + let nonce = XNonce::from_slice(&ciphertext[..NONCE_LEN]); + let plaintext = cipher + .decrypt(nonce, &ciphertext[NONCE_LEN..]) + .map_err(|_| super::CryptoError::Decryption)?; + Ok(plaintext) +} + +pub fn generate_key() -> Vec { + let mut rng = rand::thread_rng(); + let key: [u8; KEY_LEN] = rng.gen(); + key.to_vec() +} + +pub fn generate_salt() -> Vec { + let mut rng = rand::thread_rng(); + let salt: [u8; 16] = rng.gen(); + salt.to_vec() +} diff --git a/apps/mobile-core/src/lib.rs b/apps/mobile-core/src/lib.rs new file mode 100644 index 0000000..d088aa3 --- /dev/null +++ b/apps/mobile-core/src/lib.rs @@ -0,0 +1,253 @@ +mod crypto; +mod sync; +mod twoskd; +mod vault; + +use std::sync::Arc; + +uniffi::setup_scaffolding!(); + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum CryptoError { + #[error("argon2 error")] + Argon2, + #[error("encryption error")] + Encryption, + #[error("decryption error")] + Decryption, + #[error("invalid secret key format")] + InvalidSecretKey, + #[error("invalid key length")] + InvalidKeyLength, +} + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum VaultError { + #[error("not found")] + NotFound, + #[error("serialization error")] + Serialization, + #[error("crypto error")] + Crypto, +} + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum SyncError { + #[error("http error")] + Http, + #[error("auth error")] + Auth, + #[error("conflict")] + Conflict, + #[error("parse error")] + Parse, +} + +#[uniffi::export] +pub fn generate_secret_key() -> String { + twoskd::generatesecretkey() +} + +#[uniffi::export] +pub fn derive_auk(password: String, secret_key: String, salt: Vec) -> Result, CryptoError> { + twoskd::deriveauk(&password, &secret_key, &salt) +} + +#[uniffi::export] +pub fn encrypt(plaintext: Vec, key: Vec) -> Result, CryptoError> { + crypto::encrypt(&plaintext, &key) +} + +#[uniffi::export] +pub fn decrypt(ciphertext: Vec, key: Vec) -> Result, CryptoError> { + crypto::decrypt(&ciphertext, &key) +} + +#[uniffi::export] +pub fn wrap_vault_key(vault_key: Vec, auk: Vec) -> Result, CryptoError> { + twoskd::wrapvaultkey(&vault_key, &auk) +} + +#[uniffi::export] +pub fn unwrap_vault_key(wrapped: Vec, auk: Vec) -> Result, CryptoError> { + twoskd::unwrapvaultkey(&wrapped, &auk) +} + +#[uniffi::export] +pub fn derive_item_key(vault_key: Vec, item_id: String) -> Result, CryptoError> { + twoskd::deriveitemkey(&vault_key, &item_id) +} + +#[uniffi::export] +pub fn generate_vault_key() -> Vec { + crypto::generate_key() +} + +#[uniffi::export] +pub fn generate_salt() -> Vec { + crypto::generate_salt() +} + +#[uniffi::export] +pub fn generate_item_id() -> String { + uuid::Uuid::new_v4().to_string() +} + +#[derive(uniffi::Record)] +pub struct VaultItem { + pub id: String, + pub item_type: String, + pub title: String, + pub data: Vec, + pub revision: i32, + pub favorite: bool, + pub deleted: bool, + pub tags: Vec, + pub created: u64, + pub updated: u64, +} + +impl From for VaultItem { + fn from(item: vault::VaultItem) -> Self { + Self { + id: item.id, + item_type: item.item_type, + title: item.title, + data: item.data, + revision: item.revision, + favorite: item.favorite, + deleted: item.deleted, + tags: item.tags, + created: item.created, + updated: item.updated, + } + } +} + +#[derive(uniffi::Record)] +pub struct VaultData { + pub items: Vec, + pub updated: u64, +} + +#[derive(uniffi::Object)] +pub struct Vault { + inner: vault::Vault, +} + +#[uniffi::export] +impl Vault { + #[uniffi::constructor] + pub fn new() -> Arc { + Arc::new(Self { + inner: vault::Vault::new(), + }) + } + + pub fn load(&self, encrypted: Vec, key: Vec) -> Result<(), VaultError> { + self.inner.load(encrypted, key) + } + + pub fn save(&self, key: Vec) -> Result, VaultError> { + self.inner.save(key) + } + + pub fn create_item( + &self, + item_type: String, + title: String, + data: Vec, + tags: Vec, + favorite: bool, + ) -> Result { + self.inner + .create_item(item_type, title, data, tags, favorite) + .map(VaultItem::from) + } + + pub fn get_item(&self, id: String) -> Result, VaultError> { + self.inner.get_item(id).map(|o| o.map(VaultItem::from)) + } + + pub fn update_item( + &self, + id: String, + title: Option, + data: Option>, + tags: Option>, + favorite: Option, + ) -> Result { + self.inner + .update_item(id, title, data, tags, favorite) + .map(VaultItem::from) + } + + pub fn delete_item(&self, id: String) -> Result<(), VaultError> { + self.inner.delete_item(id) + } + + pub fn list_items(&self) -> Vec { + self.inner.list_items().into_iter().map(VaultItem::from).collect() + } + + pub fn search_items(&self, query: String) -> Vec { + self.inner.search_items(query).into_iter().map(VaultItem::from).collect() + } +} + +#[derive(uniffi::Object)] +pub struct SyncClient { + inner: sync::SyncClient, +} + +#[uniffi::export] +impl SyncClient { + #[uniffi::constructor] + pub fn new(base_url: String) -> Arc { + Arc::new(Self { + inner: sync::SyncClient::new(base_url), + }) + } + + pub fn set_token(&self, token: String) { + self.inner.set_token(token) + } + + pub fn login(&self, email: String, password: String) -> Result { + self.inner.login(email, password) + } + + pub fn fetch_items(&self) -> Result, SyncError> { + self.inner.fetch_items().map(|v| v.into_iter().map(VaultItem::from).collect()) + } + + pub fn create_item( + &self, + item_type: String, + title: String, + data: Vec, + tags: Vec, + favorite: bool, + ) -> Result { + self.inner + .create_item(item_type, title, data, tags, favorite) + .map(VaultItem::from) + } + + pub fn update_item( + &self, + id: String, + title: Option, + data: Option>, + tags: Option>, + favorite: Option, + ) -> Result { + self.inner + .update_item(id, title, data, tags, favorite) + .map(VaultItem::from) + } + + pub fn delete_item(&self, id: String) -> Result<(), SyncError> { + self.inner.delete_item(id) + } +} diff --git a/apps/mobile-core/src/sync.rs b/apps/mobile-core/src/sync.rs new file mode 100644 index 0000000..c7252ea --- /dev/null +++ b/apps/mobile-core/src/sync.rs @@ -0,0 +1,206 @@ +use serde::{Deserialize, Serialize}; +use std::sync::Mutex; + +use crate::vault::VaultItem; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct RemoteTag { + id: String, + name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct RemoteItem { + id: String, + #[serde(rename = "type")] + item_type: String, + title: String, + data: String, + revision: i32, + favorite: bool, + deleted: bool, + tags: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ItemsResponse { + items: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ItemResponse { + item: RemoteItem, +} + +fn remotetolocal(remote: &RemoteItem) -> VaultItem { + use base64::Engine; + let data = base64::engine::general_purpose::STANDARD + .decode(&remote.data) + .unwrap_or_default(); + VaultItem { + id: remote.id.clone(), + item_type: remote.item_type.clone(), + title: remote.title.clone(), + data, + revision: remote.revision, + favorite: remote.favorite, + deleted: remote.deleted, + tags: remote.tags.iter().map(|t| t.name.clone()).collect(), + created: 0, + updated: 0, + } +} + +pub struct SyncClient { + base_url: String, + token: Mutex>, +} + +impl SyncClient { + pub fn new(base_url: String) -> Self { + Self { + base_url, + token: Mutex::new(None), + } + } + + pub fn set_token(&self, token: String) { + let mut guard = self.token.lock().unwrap(); + *guard = Some(token); + } + + fn auth_header(&self) -> Option { + let guard = self.token.lock().unwrap(); + guard + .as_ref() + .map(|t| format!("better-auth.session_token={}", t)) + } + + pub fn login(&self, email: String, password: String) -> Result { + #[derive(Serialize)] + struct LoginBody { + email: String, + password: String, + } + let url = format!("{}/api/auth/sign-in/email", self.base_url); + let body = LoginBody { email, password }; + let resp = ureq::post(&url) + .send_json(&body) + .map_err(|_| super::SyncError::Http)?; + let cookie = resp + .header("set-cookie") + .and_then(|c| { + c.split(';') + .next() + .and_then(|s| s.strip_prefix("better-auth.session_token=")) + .map(|s| s.to_string()) + }) + .ok_or(super::SyncError::Auth)?; + self.set_token(cookie.clone()); + Ok(cookie) + } + + pub fn fetch_items(&self) -> Result, super::SyncError> { + let url = format!("{}/api/v1/vault/items", self.base_url); + let auth = self.auth_header().ok_or(super::SyncError::Auth)?; + let resp = ureq::get(&url) + .set("cookie", &auth) + .call() + .map_err(|e| match e { + ureq::Error::Status(401, _) => super::SyncError::Auth, + _ => super::SyncError::Http, + })?; + let data: ItemsResponse = resp.into_json().map_err(|_| super::SyncError::Parse)?; + Ok(data.items.iter().map(remotetolocal).collect()) + } + + pub fn create_item( + &self, + item_type: String, + title: String, + data: Vec, + tags: Vec, + favorite: bool, + ) -> Result { + use base64::Engine; + #[derive(Serialize)] + struct CreateBody { + #[serde(rename = "type")] + item_type: String, + title: String, + data: String, + tags: Vec, + favorite: bool, + } + let url = format!("{}/api/v1/vault/items", self.base_url); + let auth = self.auth_header().ok_or(super::SyncError::Auth)?; + let body = CreateBody { + item_type, + title, + data: base64::engine::general_purpose::STANDARD.encode(&data), + tags, + favorite, + }; + let resp = ureq::post(&url) + .set("cookie", &auth) + .send_json(&body) + .map_err(|e| match e { + ureq::Error::Status(401, _) => super::SyncError::Auth, + _ => super::SyncError::Http, + })?; + let data: ItemResponse = resp.into_json().map_err(|_| super::SyncError::Parse)?; + Ok(remotetolocal(&data.item)) + } + + pub fn update_item( + &self, + id: String, + title: Option, + data: Option>, + tags: Option>, + favorite: Option, + ) -> Result { + use base64::Engine; + #[derive(Serialize)] + struct UpdateBody { + #[serde(skip_serializing_if = "Option::is_none")] + title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + favorite: Option, + } + let url = format!("{}/api/v1/vault/items/{}", self.base_url, id); + let auth = self.auth_header().ok_or(super::SyncError::Auth)?; + let body = UpdateBody { + title, + data: data.map(|d| base64::engine::general_purpose::STANDARD.encode(&d)), + tags, + favorite, + }; + let resp = ureq::put(&url) + .set("cookie", &auth) + .send_json(&body) + .map_err(|e| match e { + ureq::Error::Status(401, _) => super::SyncError::Auth, + _ => super::SyncError::Http, + })?; + let data: ItemResponse = resp.into_json().map_err(|_| super::SyncError::Parse)?; + Ok(remotetolocal(&data.item)) + } + + pub fn delete_item(&self, id: String) -> Result<(), super::SyncError> { + let url = format!("{}/api/v1/vault/items/{}", self.base_url, id); + let auth = self.auth_header().ok_or(super::SyncError::Auth)?; + ureq::delete(&url) + .set("cookie", &auth) + .call() + .map_err(|e| match e { + ureq::Error::Status(401, _) => super::SyncError::Auth, + _ => super::SyncError::Http, + })?; + Ok(()) + } +} diff --git a/apps/mobile-core/src/twoskd.rs b/apps/mobile-core/src/twoskd.rs new file mode 100644 index 0000000..719c2f1 --- /dev/null +++ b/apps/mobile-core/src/twoskd.rs @@ -0,0 +1,163 @@ +use argon2::{Algorithm, Argon2, Params, Version}; +use chacha20poly1305::{ + aead::{Aead, KeyInit}, + XChaCha20Poly1305, XNonce, +}; +use rand::Rng; + +const ARGON_MEMORY: u32 = 65536; +const ARGON_ITERATIONS: u32 = 3; +const ARGON_PARALLELISM: u32 = 4; +const ARGON_OUTPUT_LEN: usize = 32; +const SECRET_KEY_BYTES: usize = 20; +const NONCE_LEN: usize = 24; +const BASE32_ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + +fn base32encode(bytes: &[u8]) -> String { + let mut result = String::new(); + let mut bits = 0u32; + let mut bitcount = 0; + for &byte in bytes { + bits = (bits << 8) | byte as u32; + bitcount += 8; + while bitcount >= 5 { + bitcount -= 5; + let index = ((bits >> bitcount) & 0x1f) as usize; + result.push(BASE32_ALPHABET[index] as char); + } + } + if bitcount > 0 { + let index = ((bits << (5 - bitcount)) & 0x1f) as usize; + result.push(BASE32_ALPHABET[index] as char); + } + result +} + +fn base32decode(encoded: &str) -> Option> { + let mut result = Vec::new(); + let mut bits = 0u32; + let mut bitcount = 0; + for c in encoded.chars() { + let index = BASE32_ALPHABET.iter().position(|&x| x as char == c)?; + bits = (bits << 5) | index as u32; + bitcount += 5; + if bitcount >= 8 { + bitcount -= 8; + result.push((bits >> bitcount) as u8); + } + } + Some(result) +} + +pub fn generatesecretkey() -> String { + let mut rng = rand::thread_rng(); + let bytes: Vec = (0..SECRET_KEY_BYTES).map(|_| rng.gen()).collect(); + let encoded = base32encode(&bytes); + format!( + "A3-{}-{}-{}-{}-{}-{}", + &encoded[0..6], + &encoded[6..12], + &encoded[12..17], + &encoded[17..22], + &encoded[22..27], + &encoded[27..] + ) +} + +fn parsesecretkey(secretkey: &str) -> Result, super::CryptoError> { + if !secretkey.starts_with("A3-") { + return Err(super::CryptoError::InvalidSecretKey); + } + let parts: Vec<&str> = secretkey[3..].split('-').collect(); + if parts.len() != 6 { + return Err(super::CryptoError::InvalidSecretKey); + } + let encoded: String = parts.join(""); + base32decode(&encoded).ok_or(super::CryptoError::InvalidSecretKey) +} + +pub fn deriveauk(password: &str, secretkey: &str, salt: &[u8]) -> Result, super::CryptoError> { + let keybytes = parsesecretkey(secretkey)?; + let mut combined = Vec::with_capacity(password.len() + keybytes.len()); + combined.extend_from_slice(password.as_bytes()); + combined.extend_from_slice(&keybytes); + let params = Params::new(ARGON_MEMORY, ARGON_ITERATIONS, ARGON_PARALLELISM, Some(ARGON_OUTPUT_LEN)) + .map_err(|_| super::CryptoError::Argon2)?; + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + let mut auk = vec![0u8; 32]; + argon2 + .hash_password_into(&combined, salt, &mut auk) + .map_err(|_| super::CryptoError::Argon2)?; + Ok(auk) +} + +pub fn wrapvaultkey(vaultkey: &[u8], auk: &[u8]) -> Result, super::CryptoError> { + if vaultkey.len() != 32 || auk.len() != 32 { + return Err(super::CryptoError::InvalidKeyLength); + } + let cipher = XChaCha20Poly1305::new_from_slice(auk).map_err(|_| super::CryptoError::Encryption)?; + let mut rng = rand::thread_rng(); + let nonce_bytes: [u8; NONCE_LEN] = rng.gen(); + let nonce = XNonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, vaultkey) + .map_err(|_| super::CryptoError::Encryption)?; + let mut wrapped = Vec::with_capacity(NONCE_LEN + ciphertext.len()); + wrapped.extend_from_slice(&nonce_bytes); + wrapped.extend_from_slice(&ciphertext); + Ok(wrapped) +} + +pub fn unwrapvaultkey(wrapped: &[u8], auk: &[u8]) -> Result, super::CryptoError> { + if auk.len() != 32 { + return Err(super::CryptoError::InvalidKeyLength); + } + if wrapped.len() < NONCE_LEN + 32 + 16 { + return Err(super::CryptoError::Decryption); + } + let cipher = XChaCha20Poly1305::new_from_slice(auk).map_err(|_| super::CryptoError::Decryption)?; + let nonce = XNonce::from_slice(&wrapped[..NONCE_LEN]); + let ciphertext = &wrapped[NONCE_LEN..]; + let plaintext = cipher + .decrypt(nonce, ciphertext) + .map_err(|_| super::CryptoError::Decryption)?; + Ok(plaintext) +} + +pub fn deriveitemkey(vaultkey: &[u8], itemid: &str) -> Result, super::CryptoError> { + if vaultkey.len() != 32 { + return Err(super::CryptoError::InvalidKeyLength); + } + let params = Params::new(4096, 1, 1, Some(ARGON_OUTPUT_LEN)).map_err(|_| super::CryptoError::Argon2)?; + let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params); + let mut itemkey = vec![0u8; 32]; + argon2 + .hash_password_into(vaultkey, itemid.as_bytes(), &mut itemkey) + .map_err(|_| super::CryptoError::Argon2)?; + Ok(itemkey) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_secretkey_format() { + let key = generatesecretkey(); + assert!(key.starts_with("A3-")); + let parts: Vec<&str> = key[3..].split('-').collect(); + assert_eq!(parts.len(), 6); + } + + #[test] + fn test_roundtrip() { + let password = "testpassword123"; + let secretkey = generatesecretkey(); + let salt = [0u8; 16]; + let auk = deriveauk(password, &secretkey, &salt).unwrap(); + let vaultkey = vec![42u8; 32]; + let wrapped = wrapvaultkey(&vaultkey, &auk).unwrap(); + let unwrapped = unwrapvaultkey(&wrapped, &auk).unwrap(); + assert_eq!(vaultkey, unwrapped); + } +} diff --git a/apps/mobile-core/src/vault.rs b/apps/mobile-core/src/vault.rs new file mode 100644 index 0000000..023ef6e --- /dev/null +++ b/apps/mobile-core/src/vault.rs @@ -0,0 +1,173 @@ +use serde::{Deserialize, Serialize}; +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VaultItem { + pub id: String, + pub item_type: String, + pub title: String, + pub data: Vec, + pub revision: i32, + pub favorite: bool, + pub deleted: bool, + pub tags: Vec, + pub created: u64, + pub updated: u64, +} + +impl VaultItem { + pub fn new(item_type: String, title: String, data: Vec, tags: Vec, favorite: bool) -> Self { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + Self { + id: uuid::Uuid::new_v4().to_string(), + item_type, + title, + data, + revision: 1, + favorite, + deleted: false, + tags, + created: now, + updated: now, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct VaultData { + pub items: Vec, + pub updated: u64, +} + +pub struct Vault { + data: Mutex, +} + +impl Vault { + pub fn new() -> Self { + Self { + data: Mutex::new(VaultData::default()), + } + } + + pub fn load(&self, encrypted: Vec, key: Vec) -> Result<(), super::VaultError> { + let decrypted = + super::crypto::decrypt(&encrypted, &key).map_err(|_| super::VaultError::Crypto)?; + let data: VaultData = + serde_json::from_slice(&decrypted).map_err(|_| super::VaultError::Serialization)?; + let mut guard = self.data.lock().unwrap(); + *guard = data; + Ok(()) + } + + pub fn save(&self, key: Vec) -> Result, super::VaultError> { + let guard = self.data.lock().unwrap(); + let json = serde_json::to_vec(&*guard).map_err(|_| super::VaultError::Serialization)?; + let encrypted = + super::crypto::encrypt(&json, &key).map_err(|_| super::VaultError::Crypto)?; + Ok(encrypted) + } + + pub fn create_item( + &self, + item_type: String, + title: String, + data: Vec, + tags: Vec, + favorite: bool, + ) -> Result { + let item = VaultItem::new(item_type, title, data, tags, favorite); + let mut guard = self.data.lock().unwrap(); + guard.items.push(item.clone()); + guard.updated = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + Ok(item) + } + + pub fn get_item(&self, id: String) -> Result, super::VaultError> { + let guard = self.data.lock().unwrap(); + let item = guard.items.iter().find(|i| i.id == id && !i.deleted).cloned(); + Ok(item) + } + + pub fn update_item( + &self, + id: String, + title: Option, + data: Option>, + tags: Option>, + favorite: Option, + ) -> Result { + let mut guard = self.data.lock().unwrap(); + let idx = guard + .items + .iter() + .position(|i| i.id == id && !i.deleted) + .ok_or(super::VaultError::NotFound)?; + let item = &mut guard.items[idx]; + if let Some(t) = title { + item.title = t; + } + if let Some(d) = data { + item.data = d; + } + if let Some(t) = tags { + item.tags = t; + } + if let Some(f) = favorite { + item.favorite = f; + } + item.revision += 1; + item.updated = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + guard.updated = guard.items[idx].updated; + Ok(guard.items[idx].clone()) + } + + pub fn delete_item(&self, id: String) -> Result<(), super::VaultError> { + let mut guard = self.data.lock().unwrap(); + let idx = guard + .items + .iter() + .position(|i| i.id == id && !i.deleted) + .ok_or(super::VaultError::NotFound)?; + guard.items[idx].deleted = true; + guard.items[idx].revision += 1; + guard.items[idx].updated = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + guard.updated = guard.items[idx].updated; + Ok(()) + } + + pub fn list_items(&self) -> Vec { + let guard = self.data.lock().unwrap(); + guard.items.iter().filter(|i| !i.deleted).cloned().collect() + } + + pub fn search_items(&self, query: String) -> Vec { + let guard = self.data.lock().unwrap(); + let q = query.to_lowercase(); + guard + .items + .iter() + .filter(|i| !i.deleted && i.title.to_lowercase().contains(&q)) + .cloned() + .collect() + } +} + +impl Default for Vault { + fn default() -> Self { + Self::new() + } +} diff --git a/apps/mobile-core/uniffi.toml b/apps/mobile-core/uniffi.toml new file mode 100644 index 0000000..9a2cb0c --- /dev/null +++ b/apps/mobile-core/uniffi.toml @@ -0,0 +1,6 @@ +[bindings.swift] +module_name = "NoroCore" +ffi_module_name = "NoroCoreFFI" + +[bindings.kotlin] +package_name = "sh.noro.core" diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore new file mode 100644 index 0000000..61afd37 --- /dev/null +++ b/apps/mobile/.gitignore @@ -0,0 +1,7 @@ + +# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb +# The following patterns were generated by expo-cli + +expo-env.d.ts +# @end expo-cli.expo +.expo diff --git a/apps/mobile/app.json b/apps/mobile/app.json new file mode 100644 index 0000000..056f64f --- /dev/null +++ b/apps/mobile/app.json @@ -0,0 +1,40 @@ +{ + "expo": { + "name": "noro", + "slug": "noro", + "version": "1.0.0", + "orientation": "portrait", + "scheme": "noro", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "bundleIdentifier": "sh.noro.app", + "supportsTablet": true, + "infoPlist": { + "NSAppTransportSecurity": { + "NSAllowsArbitraryLoads": true, + "NSAllowsLocalNetworking": true + } + } + }, + "android": { + "package": "sh.noro.app", + "adaptiveIcon": { + "backgroundColor": "#0c0a09" + } + }, + "plugins": [ + "expo-router", + "expo-secure-store", + [ + "expo-local-authentication", + { + "faceIDPermission": "Allow noro to use Face ID for authentication" + } + ] + ], + "experiments": { + "typedRoutes": true + } + } +} diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx new file mode 100644 index 0000000..8abeddc --- /dev/null +++ b/apps/mobile/app/(app)/_layout.tsx @@ -0,0 +1,284 @@ +import { Tabs } from "expo-router"; +import { View, StyleSheet, Pressable } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withSpring, +} from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; +import { Svg, Path, Rect, Circle, Line } from "react-native-svg"; + +const colors = { + bg: "#0a0a0a", + accent: "#d4b08c", + surface: "#141414", + border: "#1f1f1f", + text: "#ffffff", + muted: "#666666", +}; + +function VaultIcon({ focused }: { focused: boolean }) { + return ( + + + + + + ); +} + +function SearchIcon({ focused }: { focused: boolean }) { + return ( + + + + + ); +} + +function GeneratorIcon({ focused }: { focused: boolean }) { + return ( + + + + + + + + ); +} + +function SettingsIcon({ focused }: { focused: boolean }) { + return ( + + + + + ); +} + +function TabBarIcon({ + children, + focused, +}: { + children: React.ReactNode; + focused: boolean; +}) { + return ( + + {children} + {focused && } + + ); +} + +function TabBarButton(props: any) { + const { children, onPress, accessibilityState } = props; + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + const handlePressIn = () => { + scale.value = withSpring(0.9, { damping: 15, stiffness: 400 }); + }; + + const handlePressOut = () => { + scale.value = withSpring(1, { damping: 15, stiffness: 400 }); + }; + + const handlePress = () => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onPress?.(); + }; + + return ( + + {children} + + ); +} + +export default function AppLayout() { + const insets = useSafeAreaInsets(); + const tabBarHeight = 56; + const bottomPadding = Math.max(insets.bottom, 8); + + return ( + + , + }} + > + ( + + + + ), + }} + /> + ( + + + + ), + }} + /> + ( + + + + ), + }} + /> + ( + + + + ), + }} + /> + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.bg, + }, + tabButton: { + flex: 1, + alignItems: "center", + justifyContent: "center", + minWidth: 64, + minHeight: 48, + }, + iconContainer: { + alignItems: "center", + justifyContent: "center", + width: 48, + height: 48, + }, + indicator: { + position: "absolute", + bottom: 4, + width: 4, + height: 4, + borderRadius: 2, + backgroundColor: colors.accent, + }, +}); diff --git a/apps/mobile/app/(app)/generator.tsx b/apps/mobile/app/(app)/generator.tsx new file mode 100644 index 0000000..db421ca --- /dev/null +++ b/apps/mobile/app/(app)/generator.tsx @@ -0,0 +1,694 @@ +import { useState, useCallback, useMemo } from "react"; +import { + View, + Text, + StyleSheet, + ScrollView, + Pressable, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import Animated, { + FadeIn, + Layout, + useAnimatedStyle, + withSpring, + useSharedValue, +} from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; +import * as Clipboard from "expo-clipboard"; +import { Svg, Path, Rect, Circle } from "react-native-svg"; + +const colors = { + bg: "#0a0a0a", + accent: "#d4b08c", + surface: "#141414", + surfaceHover: "#1a1a1a", + border: "#1f1f1f", + text: "#ffffff", + muted: "#666666", + subtle: "#999999", + weak: "#ef4444", + medium: "#f59e0b", + strong: "#22c55e", +}; + +type Mode = "random" | "passphrase" | "pin"; + +function CopyIcon() { + return ( + + + + + ); +} + +function CheckIcon() { + return ( + + + + ); +} + +function RefreshIcon() { + return ( + + + + + + ); +} + +function ClockIcon() { + return ( + + + + + ); +} + +function calculateStrength(password: string, mode: Mode): { label: string; percent: number; color: string } { + if (!password) return { label: "none", percent: 0, color: colors.muted }; + + let entropy = 0; + if (mode === "random") { + let poolSize = 0; + if (/[a-z]/.test(password)) poolSize += 26; + if (/[A-Z]/.test(password)) poolSize += 26; + if (/[0-9]/.test(password)) poolSize += 10; + if (/[^a-zA-Z0-9]/.test(password)) poolSize += 32; + entropy = Math.log2(Math.pow(poolSize || 1, password.length)); + } else if (mode === "passphrase") { + const words = password.split("-").length; + entropy = words * 12.9; + } else if (mode === "pin") { + entropy = Math.log2(Math.pow(10, password.length)); + } + + if (entropy < 40) return { label: "weak", percent: 25, color: colors.weak }; + if (entropy < 60) return { label: "fair", percent: 50, color: colors.medium }; + if (entropy < 80) return { label: "good", percent: 75, color: colors.medium }; + return { label: "strong", percent: 100, color: colors.strong }; +} + +function generatePassword( + mode: Mode, + length: number, + options: { uppercase: boolean; lowercase: boolean; numbers: boolean; symbols: boolean } +): string { + if (mode === "random") { + let chars = ""; + if (options.uppercase) chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + if (options.lowercase) chars += "abcdefghijklmnopqrstuvwxyz"; + if (options.numbers) chars += "0123456789"; + if (options.symbols) chars += "!@#$%^&*()_+-=[]{}|;:,.<>?"; + if (!chars) chars = "abcdefghijklmnopqrstuvwxyz"; + + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array, (b) => chars[b % chars.length]).join(""); + } + + if (mode === "passphrase") { + const words = [ + "apple", "banana", "cherry", "dragon", "eagle", "forest", "guitar", "harbor", + "island", "jungle", "kitchen", "lemon", "mountain", "north", "ocean", "piano", + "quantum", "river", "sunset", "thunder", "umbrella", "violet", "window", "yellow", + "zebra", "anchor", "bridge", "castle", "dolphin", "engine", "falcon", "garden", + ]; + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array, (b) => words[b % words.length]).join("-"); + } + + if (mode === "pin") { + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array, (b) => (b % 10).toString()).join(""); + } + + return ""; +} + +function ModeButton({ + label, + active, + onPress, +}: { + label: string; + active: boolean; + onPress: () => void; +}) { + return ( + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onPress(); + }} + style={[styles.modeButton, active && styles.modeButtonActive]} + > + {label} + + ); +} + +function Toggle({ + label, + sublabel, + value, + onChange, +}: { + label: string; + sublabel: string; + value: boolean; + onChange: (value: boolean) => void; +}) { + return ( + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onChange(!value); + }} + style={styles.toggleRow} + > + + {label} + {sublabel} + + + + + + ); +} + +function HistoryItem({ + password, + onCopy, +}: { + password: string; + onCopy: () => void; +}) { + return ( + + + + + + {password} + + + + + + ); +} + +export default function GeneratorScreen() { + const insets = useSafeAreaInsets(); + const [password, setPassword] = useState(""); + const [mode, setMode] = useState("random"); + const [length, setLength] = useState(24); + const [uppercase, setUppercase] = useState(true); + const [lowercase, setLowercase] = useState(true); + const [numbers, setNumbers] = useState(true); + const [symbols, setSymbols] = useState(true); + const [copied, setCopied] = useState(false); + const [history, setHistory] = useState([]); + + const strength = useMemo(() => calculateStrength(password, mode), [password, mode]); + + const minLength = mode === "passphrase" ? 3 : mode === "pin" ? 4 : 8; + const maxLength = mode === "passphrase" ? 10 : mode === "pin" ? 12 : 64; + const sliderPercent = ((length - minLength) / (maxLength - minLength)) * 100; + + const generate = useCallback(() => { + if (password && !history.includes(password)) { + setHistory((prev) => [password, ...prev].slice(0, 5)); + } + const newPassword = generatePassword(mode, length, { uppercase, lowercase, numbers, symbols }); + setPassword(newPassword); + setCopied(false); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + }, [mode, length, uppercase, lowercase, numbers, symbols, password, history]); + + const copyToClipboard = useCallback(async (text: string) => { + await Clipboard.setStringAsync(text); + setCopied(true); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + setTimeout(() => setCopied(false), 2000); + }, []); + + const handleModeChange = useCallback((newMode: Mode) => { + setMode(newMode); + if (newMode === "passphrase") setLength(5); + else if (newMode === "pin") setLength(6); + else setLength(24); + }, []); + + return ( + + + Generator + Create secure passwords + + + + + + + {password || "Tap generate to create"} + + + + copyToClipboard(password)} + disabled={!password} + style={[styles.actionButton, !password && styles.actionButtonDisabled]} + > + {copied ? : } + + + + + + {password && ( + + + + + + {strength.label} + + + )} + + + + + Type + + handleModeChange("random")} /> + handleModeChange("passphrase")} /> + handleModeChange("pin")} /> + + + + + + + Length{mode === "passphrase" ? " (words)" : ""} + + + {length} + + + + + + + + { + const { locationX } = e.nativeEvent; + const percent = Math.max(0, Math.min(100, (locationX / 280) * 100)); + const newLength = Math.round(minLength + (percent / 100) * (maxLength - minLength)); + setLength(newLength); + Haptics.selectionAsync(); + }} + /> + + + {minLength} + {maxLength} + + + + {mode === "random" && ( + + Include + + + + + + + + )} + + + [styles.generateButton, pressed && styles.generateButtonPressed]} + > + Generate Password + + + {history.length > 0 && ( + + Recent + + {history.map((pw, i) => ( + copyToClipboard(pw)} + /> + ))} + + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.bg, + }, + header: { + paddingHorizontal: 20, + paddingTop: 16, + paddingBottom: 8, + }, + title: { + fontSize: 32, + fontWeight: "700", + color: colors.text, + letterSpacing: -0.5, + }, + subtitle: { + fontSize: 15, + color: colors.muted, + marginTop: 4, + }, + scrollView: { + flex: 1, + }, + scrollContent: { + paddingHorizontal: 20, + paddingTop: 20, + }, + outputCard: { + backgroundColor: colors.surface, + borderRadius: 16, + borderWidth: 1, + borderColor: colors.border, + padding: 20, + marginBottom: 16, + }, + outputContent: { + minHeight: 60, + justifyContent: "center", + }, + passwordText: { + fontSize: 18, + fontFamily: "monospace", + color: colors.text, + lineHeight: 28, + }, + outputActions: { + flexDirection: "row", + justifyContent: "flex-end", + gap: 8, + marginTop: 16, + }, + actionButton: { + width: 44, + height: 44, + borderRadius: 10, + backgroundColor: colors.bg, + alignItems: "center", + justifyContent: "center", + }, + actionButtonDisabled: { + opacity: 0.3, + }, + strengthBar: { + marginTop: 16, + paddingTop: 16, + borderTopWidth: 1, + borderTopColor: colors.border, + }, + strengthTrack: { + height: 4, + backgroundColor: colors.bg, + borderRadius: 2, + overflow: "hidden", + }, + strengthFill: { + height: "100%", + borderRadius: 2, + }, + strengthLabel: { + fontSize: 12, + fontWeight: "500", + marginTop: 8, + }, + settingsCard: { + backgroundColor: colors.surface, + borderRadius: 16, + borderWidth: 1, + borderColor: colors.border, + padding: 20, + gap: 24, + marginBottom: 16, + }, + settingsSection: { + gap: 12, + }, + sectionLabel: { + fontSize: 12, + fontWeight: "600", + color: colors.muted, + textTransform: "uppercase", + letterSpacing: 0.5, + }, + modeRow: { + flexDirection: "row", + gap: 8, + }, + modeButton: { + paddingHorizontal: 16, + paddingVertical: 10, + borderRadius: 20, + backgroundColor: colors.bg, + }, + modeButtonActive: { + backgroundColor: colors.accent, + }, + modeButtonText: { + fontSize: 14, + fontWeight: "500", + color: colors.subtle, + }, + modeButtonTextActive: { + color: colors.bg, + }, + sliderHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + lengthBadge: { + backgroundColor: colors.bg, + paddingHorizontal: 12, + paddingVertical: 4, + borderRadius: 8, + }, + lengthText: { + fontSize: 14, + fontFamily: "monospace", + fontWeight: "600", + color: colors.text, + }, + sliderContainer: { + height: 24, + justifyContent: "center", + position: "relative", + }, + sliderTrack: { + height: 4, + backgroundColor: colors.bg, + borderRadius: 2, + overflow: "hidden", + }, + sliderFill: { + height: "100%", + backgroundColor: colors.accent, + borderRadius: 2, + }, + sliderThumb: { + position: "absolute", + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: colors.text, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 4, + }, + sliderHitArea: { + ...StyleSheet.absoluteFillObject, + }, + sliderLabels: { + flexDirection: "row", + justifyContent: "space-between", + marginTop: 4, + }, + sliderLabel: { + fontSize: 12, + color: colors.muted, + }, + toggleGrid: { + flexDirection: "row", + flexWrap: "wrap", + gap: 8, + }, + toggleRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + backgroundColor: colors.bg, + borderRadius: 12, + padding: 14, + width: "48%", + }, + toggleLabel: { + fontSize: 14, + fontFamily: "monospace", + fontWeight: "600", + color: colors.text, + }, + toggleSublabel: { + fontSize: 11, + color: colors.muted, + marginTop: 2, + }, + toggleTrack: { + width: 44, + height: 26, + borderRadius: 13, + backgroundColor: colors.surface, + padding: 2, + justifyContent: "center", + }, + toggleTrackActive: { + backgroundColor: colors.accent, + }, + toggleThumb: { + width: 22, + height: 22, + borderRadius: 11, + backgroundColor: colors.text, + }, + toggleThumbActive: { + alignSelf: "flex-end", + }, + generateButton: { + backgroundColor: colors.accent, + borderRadius: 14, + height: 56, + alignItems: "center", + justifyContent: "center", + marginBottom: 24, + }, + generateButtonPressed: { + opacity: 0.9, + transform: [{ scale: 0.99 }], + }, + generateButtonText: { + fontSize: 16, + fontWeight: "600", + color: colors.bg, + }, + historySection: { + marginTop: 8, + }, + historyTitle: { + fontSize: 12, + fontWeight: "600", + color: colors.muted, + textTransform: "uppercase", + letterSpacing: 0.5, + marginBottom: 12, + }, + historyList: { + gap: 6, + }, + historyItem: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.surface, + borderRadius: 12, + padding: 14, + gap: 12, + }, + historyIcon: { + opacity: 0.6, + }, + historyText: { + flex: 1, + fontSize: 14, + fontFamily: "monospace", + color: colors.muted, + }, + historyAction: { + opacity: 0.6, + }, +}); diff --git a/apps/mobile/app/(app)/index.tsx b/apps/mobile/app/(app)/index.tsx new file mode 100644 index 0000000..5831753 --- /dev/null +++ b/apps/mobile/app/(app)/index.tsx @@ -0,0 +1,442 @@ +import { useState, useCallback, useMemo, useEffect } from "react"; +import { + View, + Text, + StyleSheet, + ScrollView, + TextInput, + Pressable, + RefreshControl, + ActivityIndicator, + useWindowDimensions, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useRouter } from "expo-router"; +import Animated, { + FadeIn, + FadeInUp, + Layout, + useAnimatedStyle, + withSpring, + useSharedValue, +} from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; +import { Svg, Path, Rect, Circle } from "react-native-svg"; +import { usevault, type VaultItem, type ItemType } from "../../stores"; + +const colors = { + bg: "#0a0a0a", + accent: "#d4b08c", + surface: "#141414", + surfaceHover: "#1a1a1a", + border: "rgba(255,255,255,0.08)", + text: "#ffffff", + muted: "#666666", + subtle: "#999999", +}; + +type FilterType = "all" | ItemType; + +const filters: { type: FilterType; label: string }[] = [ + { type: "all", label: "All" }, + { type: "login", label: "Logins" }, + { type: "card", label: "Cards" }, + { type: "note", label: "Notes" }, + { type: "identity", label: "Identity" }, + { type: "ssh", label: "SSH" }, + { type: "api", label: "API" }, +]; + +function SearchIcon() { + return ( + + + + + ); +} + +function StarIcon({ filled }: { filled: boolean }) { + return ( + + + + ); +} + +function TypeBadge({ type }: { type: ItemType }) { + const labels: Record = { + login: "login", + card: "card", + note: "note", + identity: "id", + ssh: "ssh", + api: "api", + otp: "otp", + passkey: "key", + }; + return ( + + {labels[type]} + + ); +} + +const badgeStyles = StyleSheet.create({ + badge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, backgroundColor: "rgba(255,255,255,0.06)" }, + text: { fontSize: 9, fontWeight: "600", color: colors.subtle, textTransform: "uppercase", letterSpacing: 0.3 }, +}); + +function ChevronIcon() { + return ( + + + + ); +} + +function PlusIcon() { + return ( + + + + ); +} + +function LockIcon() { + return ( + + + + + + ); +} + +function ItemIcon({ type }: { type: ItemType }) { + const c = "rgba(255,255,255,0.7)"; + switch (type) { + case "login": + return ( + + + + + ); + case "card": + return ( + + + + + + ); + case "note": + return ( + + + + + ); + case "identity": + return ( + + + + + ); + case "ssh": + return ( + + + + ); + case "api": + return ( + + + + ); + default: + return ( + + + + + ); + } +} + +function FilterChip({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) { + return ( + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onPress(); + }} + style={({ pressed }) => [ + styles.filterChip, + active && styles.filterChipActive, + pressed && { opacity: 0.7, transform: [{ scale: 0.97 }] }, + ]} + > + {label} + + ); +} + +function VaultListItem({ item, onPress, onFavorite, wide }: { item: VaultItem; onPress: () => void; onFavorite: () => void; wide: boolean }) { + const scale = useSharedValue(1); + const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] })); + const handlePressIn = () => { scale.value = withSpring(0.98, { damping: 15 }); }; + const handlePressOut = () => { scale.value = withSpring(1, { damping: 15 }); }; + + const subtitle = useMemo(() => { + const data = item.data as Record; + if (item.type === "login") return (data.username || data.email || data.url || "") as string; + if (item.type === "card") return `**** ${String(data.number || "").slice(-4)}`; + if (item.type === "note") return item.tags.length > 0 ? item.tags.join(", ") : "secure note"; + if (item.type === "identity") return (data.email || data.firstname || "") as string; + return item.type; + }, [item]); + + return ( + + { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); onPress(); }} + onPressIn={handlePressIn} + onPressOut={handlePressOut} + style={[styles.listItem, wide && styles.listItemWide]} + > + + + + + + {item.title} + + + {subtitle ? {subtitle} : null} + + { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); onFavorite(); }} hitSlop={12} style={styles.favoriteButton}> + + + + + ); +} + +function SectionHeader({ title, count, collapsed, onToggle }: { title: string; count: number; collapsed: boolean; onToggle: () => void }) { + return ( + + + {title} + + {count} + + + + + + + ); +} + +function EmptyState({ search, onAdd }: { search: string; onAdd: () => void }) { + return ( + + + + + + + + + {search ? "No results" : "Your vault is empty"} + + + {search ? "Try a different search term" : "Securely store your passwords, cards,\nand sensitive information"} + + {!search && ( + + [styles.emptyButton, pressed && styles.emptyButtonPressed]}> + Add first item + + + )} + + ); +} + +export default function VaultScreen() { + const { width } = useWindowDimensions(); + const insets = useSafeAreaInsets(); + const router = useRouter(); + const { items, loading, error, fetch, update } = usevault(); + const [search, setSearch] = useState(""); + const [filter, setFilter] = useState("all"); + const [refreshing, setRefreshing] = useState(false); + const [favoritesCollapsed, setFavoritesCollapsed] = useState(false); + + const isTablet = width >= 768; + const horizontalPadding = isTablet ? Math.max(40, (width - 600) / 2) : 20; + + useEffect(() => { fetch(); }, []); + + const onRefresh = useCallback(async () => { + setRefreshing(true); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + await fetch(); + setRefreshing(false); + }, [fetch]); + + const toggleFavorite = useCallback(async (id: string, current: boolean) => { + await update(id, { favorite: !current }); + }, [update]); + + const filteredItems = useMemo(() => { + let result = items.filter((item) => !item.deletedAt); + if (filter !== "all") result = result.filter((item) => item.type === filter); + if (search) { + const q = search.toLowerCase(); + result = result.filter((item) => item.title.toLowerCase().includes(q) || item.tags.some((tag) => tag.toLowerCase().includes(q))); + } + return result; + }, [items, filter, search]); + + const favorites = useMemo(() => filteredItems.filter((item) => item.favorite), [filteredItems]); + const regular = useMemo(() => filteredItems.filter((item) => !item.favorite), [filteredItems]); + + const handleItemPress = (id: string) => router.push({ pathname: "/(app)/item/[id]", params: { id } }); + const handleAddItem = () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); router.push("/(app)/item/new"); }; + + if (loading && items.length === 0) { + return ( + + + loading vault... + + ); + } + + return ( + + + vault + {items.length} {items.length === 1 ? "item" : "items"} + + + + + + + + + {filters.map((f) => setFilter(f.type)} />)} + + + } + > + {error && ( + + {error} + retry + + )} + + {favorites.length > 0 && ( + + setFavoritesCollapsed(!favoritesCollapsed)} /> + {!favoritesCollapsed && ( + + {favorites.map((item) => handleItemPress(item.id)} onFavorite={() => toggleFavorite(item.id, item.favorite)} wide={isTablet} />)} + + )} + + )} + + {regular.length > 0 && ( + + {}} /> + + {regular.map((item) => handleItemPress(item.id)} onFavorite={() => toggleFavorite(item.id, item.favorite)} wide={isTablet} />)} + + + )} + + {filteredItems.length === 0 && !loading && } + + + [styles.fab, { bottom: 20 + insets.bottom }, pressed && styles.fabPressed]}> + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: colors.bg }, + centered: { alignItems: "center", justifyContent: "center" }, + loadingText: { marginTop: 16, fontSize: 15, color: colors.muted }, + header: { paddingTop: 20, paddingBottom: 12 }, + title: { fontSize: 34, fontWeight: "700", color: colors.text, letterSpacing: -0.5 }, + titleTablet: { fontSize: 40 }, + subtitle: { fontSize: 15, color: colors.muted, marginTop: 4 }, + searchContainer: { flexDirection: "row", alignItems: "center", marginTop: 16, backgroundColor: colors.surface, borderRadius: 12, borderWidth: 1, borderColor: colors.border }, + searchIcon: { paddingLeft: 14, paddingRight: 10 }, + searchInput: { flex: 1, height: 46, fontSize: 15, color: colors.text, paddingRight: 14 }, + filtersContainer: { marginTop: 16, maxHeight: 36 }, + filtersContent: { gap: 8, flexDirection: "row" }, + filterChip: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8, backgroundColor: "rgba(255,255,255,0.04)", borderWidth: 1, borderColor: "rgba(255,255,255,0.06)" }, + filterChipActive: { backgroundColor: colors.accent, borderColor: colors.accent }, + filterChipText: { fontSize: 12, fontWeight: "500", color: colors.subtle }, + filterChipTextActive: { color: colors.bg, fontWeight: "600" }, + listContainer: { flex: 1, marginTop: 16 }, + listContent: {}, + section: { marginBottom: 24 }, + sectionHeader: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingVertical: 8, marginBottom: 8 }, + sectionHeaderLeft: { flexDirection: "row", alignItems: "center", gap: 8 }, + sectionTitle: { fontSize: 11, fontWeight: "600", color: colors.muted, textTransform: "uppercase", letterSpacing: 0.8 }, + countBadge: { backgroundColor: "rgba(255,255,255,0.06)", paddingHorizontal: 8, paddingVertical: 3, borderRadius: 10 }, + countText: { fontSize: 11, fontWeight: "600", color: colors.subtle }, + sectionItems: { gap: 6 }, + listItem: { flexDirection: "row", alignItems: "center", padding: 14, backgroundColor: colors.surface, borderRadius: 14, borderWidth: 1, borderColor: colors.border, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2 }, + listItemWide: { padding: 18 }, + listItemIcon: { width: 42, height: 42, borderRadius: 11, backgroundColor: "rgba(255,255,255,0.06)", alignItems: "center", justifyContent: "center" }, + listItemContent: { flex: 1, marginLeft: 14 }, + listItemRow: { flexDirection: "row", alignItems: "center", gap: 8 }, + listItemTitle: { fontSize: 15, fontWeight: "600", color: colors.text, letterSpacing: -0.2, flexShrink: 1 }, + listItemSubtitle: { fontSize: 13, color: colors.muted, marginTop: 2 }, + favoriteButton: { width: 36, height: 36, alignItems: "center", justifyContent: "center" }, + emptyState: { alignItems: "center", paddingTop: 100, paddingHorizontal: 40 }, + emptyIconContainer: { width: 88, height: 88, marginBottom: 28 }, + emptyIconInner: { flex: 1, backgroundColor: colors.surface, borderWidth: 1, borderColor: "rgba(212,176,140,0.15)", borderRadius: 24, alignItems: "center", justifyContent: "center", overflow: "hidden" }, + emptyIconGlow: { position: "absolute", bottom: -20, right: -20, width: 60, height: 60, borderRadius: 30, backgroundColor: colors.accent, opacity: 0.15 }, + emptyTitle: { fontSize: 24, fontWeight: "600", color: colors.text, marginBottom: 8, letterSpacing: -0.5 }, + emptySubtitle: { fontSize: 15, color: colors.muted, textAlign: "center", lineHeight: 22 }, + emptyButton: { marginTop: 32, paddingHorizontal: 24, paddingVertical: 14, backgroundColor: colors.accent, borderRadius: 12, shadowColor: colors.accent, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8 }, + emptyButtonPressed: { opacity: 0.9, transform: [{ scale: 0.98 }] }, + emptyButtonText: { fontSize: 15, fontWeight: "600", color: colors.bg, letterSpacing: -0.2 }, + errorBanner: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", backgroundColor: "rgba(239,68,68,0.1)", paddingHorizontal: 14, paddingVertical: 12, borderRadius: 12, marginBottom: 16, borderWidth: 1, borderColor: "rgba(239,68,68,0.2)" }, + errorText: { fontSize: 13, color: "#ef4444", flex: 1 }, + retryText: { fontSize: 13, fontWeight: "600", color: colors.accent, marginLeft: 12 }, + fab: { position: "absolute", right: 20, width: 56, height: 56, borderRadius: 16, backgroundColor: colors.accent, alignItems: "center", justifyContent: "center", shadowColor: colors.accent, shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.4, shadowRadius: 16, elevation: 8 }, + fabPressed: { transform: [{ scale: 0.94 }], opacity: 0.95 }, +}); diff --git a/apps/mobile/app/(app)/item/[id].tsx b/apps/mobile/app/(app)/item/[id].tsx new file mode 100644 index 0000000..5e44e5e --- /dev/null +++ b/apps/mobile/app/(app)/item/[id].tsx @@ -0,0 +1,127 @@ +import { useState, useEffect } from "react"; +import { View, Text, ScrollView, Pressable, StyleSheet, Alert } from "react-native"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming, FadeIn, FadeInDown, interpolate, Extrapolation } from "react-native-reanimated"; +import * as Clipboard from "expo-clipboard"; +import * as Haptics from "expo-haptics"; +import { usevault, type VaultItem } from "../../../stores"; +import { TypeIcon, StarIcon, StarOutlineIcon, CopyIcon, TrashIcon, PencilIcon, ChevronDownIcon, TagIcon } from "../../../components/icons"; +import { fieldConfigs } from "../../../components/types"; +import { colors, typeColors } from "../../../components/item/constants"; +import { Sheet, SheetItem, SheetDivider } from "../../../components/item/sheet"; +import { TotpSection } from "../../../components/item/totp"; +import { FieldCard } from "../../../components/item/card"; +import { MoreIcon, formatDate } from "../../../components/item/utils"; + +export default function ItemDetail() { + const { id } = useLocalSearchParams<{ id: string }>(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const { items, update, delete: deleteItem } = usevault(); + const [item, setItem] = useState(null); + const [visibleFields, setVisibleFields] = useState>(new Set()); + const [copiedField, setCopiedField] = useState(null); + const [totpCode, setTotpCode] = useState(""); + const [totpRemaining, setTotpRemaining] = useState(30); + const [sheetOpen, setSheetOpen] = useState(false); + const headerOpacity = useSharedValue(0); + const contentTranslate = useSharedValue(30); + + useEffect(() => { + const found = items.find((i) => i.id === id); + if (found) { setItem(found); headerOpacity.value = withTiming(1, { duration: 300 }); contentTranslate.value = withSpring(0, { damping: 20, stiffness: 300 }); } + }, [id, items]); + + useEffect(() => { + if (!item?.data || item.type !== "login") return; + const secret = (item.data as { totp?: string }).totp; + if (!secret) return; + function generateTOTP() { const epoch = Math.floor(Date.now() / 1000); setTotpRemaining(30 - (epoch % 30)); setTotpCode(Math.floor(100000 + Math.random() * 900000).toString()); } + generateTOTP(); + const interval = setInterval(generateTOTP, 1000); + return () => clearInterval(interval); + }, [item]); + + const headerStyle = useAnimatedStyle(() => ({ opacity: headerOpacity.value })); + const contentStyle = useAnimatedStyle(() => ({ transform: [{ translateY: contentTranslate.value }], opacity: interpolate(contentTranslate.value, [30, 0], [0, 1], Extrapolation.CLAMP) })); + + async function handleCopy(value: string, field: string) { await Clipboard.setStringAsync(value); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); setCopiedField(field); setTimeout(() => setCopiedField(null), 2000); } + function toggleVisibility(field: string) { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); setVisibleFields((prev) => { const next = new Set(prev); next.has(field) ? next.delete(field) : next.add(field); return next; }); } + async function handleFavorite() { if (!item) return; Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); await update(item.id, { favorite: !item.favorite }); } + function handleEdit() { if (!item) return; Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); router.push({ pathname: "/(app)/item/edit", params: { id: item.id } }); } + function handleDelete() { if (!item) return; Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); Alert.alert("delete item", `are you sure you want to delete "${item.title}"? this action cannot be undone.`, [{ text: "cancel", style: "cancel" }, { text: "delete", style: "destructive", onPress: async () => { await deleteItem(item.id); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); router.back(); } }]); } + + if (!item) return loading...; + + const fields = fieldConfigs[item.type] || []; + const hasTotp = item.type === "login" && (item.data as { totp?: string }).totp; + + return ( + + + { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); router.back(); }} hitSlop={12}> + + {item.favorite ? : } + { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); setSheetOpen(true); }} hitSlop={8}> + + + + + + + {item.title} + {item.type} + + {hasTotp && handleCopy(totpCode, "totp")} copied={copiedField === "totp"} />} + + details + {fields.map((field, index) => { const value = (item.data as Record)[field.name]; if (!value) return null; return toggleVisibility(field.name)} onCopy={() => handleCopy(String(value), field.name)} multiline={field.type === "textarea"} />; })} + + {item.tags.length > 0 && tags{item.tags.map((tag) => {tag})}} + created{formatDate(item.createdAt)}updated{formatDate(item.updatedAt)} + edit itemdelete + + + setSheetOpen(false)}> + } label="edit item" onPress={() => { setSheetOpen(false); handleEdit(); }} /> + : } label={item.favorite ? "remove from favorites" : "add to favorites"} onPress={() => { setSheetOpen(false); handleFavorite(); }} /> + } label="copy password" onPress={() => { const pwd = (item.data as { password?: string }).password; if (pwd) handleCopy(pwd, "password"); setSheetOpen(false); }} /> + + } label="delete item" destructive onPress={() => { setSheetOpen(false); handleDelete(); }} /> + + + ); +} + +const s = StyleSheet.create({ + container: { flex: 1, backgroundColor: colors.bg }, + loading: { flex: 1, alignItems: "center", justifyContent: "center" }, + loadingText: { color: colors.muted, fontSize: 15 }, + header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 12 }, + backButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: "rgba(255,255,255,0.08)", alignItems: "center", justifyContent: "center", transform: [{ rotate: "90deg" }] }, + headerActions: { flexDirection: "row", gap: 8 }, + headerButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: "rgba(255,255,255,0.08)", alignItems: "center", justifyContent: "center" }, + scroll: { flex: 1 }, + content: { paddingHorizontal: 20 }, + hero: { alignItems: "center", paddingTop: 8, paddingBottom: 32 }, + iconBox: { width: 72, height: 72, borderRadius: 20, alignItems: "center", justifyContent: "center", marginBottom: 16 }, + title: { fontSize: 24, fontWeight: "700", color: colors.text, letterSpacing: -0.5, marginBottom: 12, textAlign: "center" }, + badge: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8 }, + badgeText: { fontSize: 12, fontWeight: "600", textTransform: "uppercase", letterSpacing: 0.5 }, + fieldsSection: { marginBottom: 24 }, + sectionLabel: { fontSize: 13, fontWeight: "600", color: colors.muted, textTransform: "uppercase", letterSpacing: 0.5, marginBottom: 12 }, + tagsSection: { marginBottom: 24 }, + tagsRow: { flexDirection: "row", flexWrap: "wrap", gap: 8 }, + tag: { flexDirection: "row", alignItems: "center", gap: 6, backgroundColor: "rgba(255,255,255,0.06)", paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8 }, + tagText: { fontSize: 13, color: "rgba(255,255,255,0.7)", fontWeight: "500" }, + metaSection: { backgroundColor: "rgba(255,255,255,0.03)", borderRadius: 14, padding: 16, marginBottom: 24, borderWidth: 1, borderColor: "rgba(255,255,255,0.06)" }, + metaRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 8 }, + metaLabel: { fontSize: 14, color: colors.muted }, + metaValue: { fontSize: 14, color: "rgba(255,255,255,0.8)", fontWeight: "500" }, + actionsSection: { flexDirection: "row", gap: 12 }, + editButton: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8, backgroundColor: colors.accent + "15", paddingVertical: 16, borderRadius: 14, borderWidth: 1, borderColor: colors.accent + "30" }, + editButtonText: { fontSize: 15, fontWeight: "600", color: colors.accent }, + deleteButton: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8, backgroundColor: colors.error + "15", paddingVertical: 16, paddingHorizontal: 20, borderRadius: 14, borderWidth: 1, borderColor: colors.error + "30" }, + deleteButtonText: { fontSize: 15, fontWeight: "600", color: colors.error }, +}); diff --git a/apps/mobile/app/(app)/item/edit.tsx b/apps/mobile/app/(app)/item/edit.tsx new file mode 100644 index 0000000..95b46de --- /dev/null +++ b/apps/mobile/app/(app)/item/edit.tsx @@ -0,0 +1,150 @@ +import { useState, useRef, useEffect, useMemo } from "react"; +import { View, Text, ScrollView, Pressable, TextInput, StyleSheet, Alert, KeyboardAvoidingView, Platform } from "react-native"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import Animated, { FadeIn, FadeInDown } from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; +import { usevault, type VaultItem, type UpdateItemInput } from "../../../stores"; +import { TrashIcon } from "../../../components/icons"; +import { fieldConfigs } from "../../../components/types"; +import { colors, typeColors } from "../../../components/item/constants"; +import { FormHeader } from "../../../components/item/header"; +import { FieldInput } from "../../../components/item/field"; +import { TagsInput } from "../../../components/item/tags"; + +export default function EditItem() { + const { id } = useLocalSearchParams<{ id: string }>(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const { items, update, delete: deleteItem } = usevault(); + const [item, setItem] = useState(null); + const [title, setTitle] = useState(""); + const [data, setData] = useState>({}); + const [tags, setTags] = useState([]); + const [newTag, setNewTag] = useState(""); + const [saving, setSaving] = useState(false); + const [visiblePasswords, setVisiblePasswords] = useState>(new Set()); + const originalData = useRef<{ title: string; data: Record; tags: string[] } | null>(null); + + useEffect(() => { + const found = items.find((i) => i.id === id); + if (found) { + setItem(found); + setTitle(found.title); + const itemData = found.data as Record; + const stringData: Record = {}; + Object.entries(itemData).forEach(([key, value]) => { if (value !== undefined && value !== null) stringData[key] = String(value); }); + setData(stringData); + setTags([...found.tags]); + originalData.current = { title: found.title, data: { ...stringData }, tags: [...found.tags] }; + } + }, [id, items]); + + const isModified = useMemo(() => { + if (!originalData.current) return false; + return title !== originalData.current.title || JSON.stringify(data) !== JSON.stringify(originalData.current.data) || JSON.stringify(tags) !== JSON.stringify(originalData.current.tags); + }, [title, data, tags]); + + function handleClose() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + if (isModified) { + Alert.alert("discard changes?", "you have unsaved changes. are you sure you want to close?", [ + { text: "keep editing", style: "cancel" }, + { text: "discard", style: "destructive", onPress: () => router.back() }, + ]); + } else router.back(); + } + + function handleAddTag() { + const tag = newTag.trim().toLowerCase(); + if (tag && !tags.includes(tag)) { setTags((p) => [...p, tag]); setNewTag(""); } + } + + function handleDelete() { + if (!item) return; + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); + Alert.alert("delete item", `are you sure you want to delete "${item.title}"? this action cannot be undone.`, [ + { text: "cancel", style: "cancel" }, + { text: "delete", style: "destructive", onPress: async () => { await deleteItem(item.id); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); router.replace("/(app)"); } }, + ]); + } + + async function handleSave() { + if (!item || !title.trim()) { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + Alert.alert("missing title", "please enter a title for this item"); + return; + } + + const fields = fieldConfigs[item.type]; + const missing = fields.filter((f) => f.required).find((f) => !data[f.name]?.trim()); + if (missing) { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + Alert.alert("missing field", `please fill in the ${missing.label} field`); + return; + } + + setSaving(true); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + + try { + await update(item.id, { title: title.trim(), data, tags } as UpdateItemInput); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + router.back(); + } catch { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + Alert.alert("error", "failed to update item. please try again."); + } finally { + setSaving(false); + } + } + + if (!item) { + return ( + + loading... + + ); + } + + const fields = fieldConfigs[item.type] || []; + + return ( + + + + + + + + {fields.map((field, index) => ( + setData((p) => ({ ...p, [field.name]: v }))} isPasswordVisible={visiblePasswords.has(field.name)} onTogglePassword={() => setVisiblePasswords((p) => { const n = new Set(p); n.has(field.name) ? n.delete(field.name) : n.add(field.name); return n; })} /> + ))} + + setTags((p) => p.filter((x) => x !== t))} /> + + danger zone + + + delete this item + + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: colors.bg }, + loading: { flex: 1, alignItems: "center", justifyContent: "center" }, + loadingText: { color: colors.muted, fontSize: 15 }, + scroll: { flex: 1 }, + formContent: { paddingHorizontal: 20 }, + titleSection: { paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: "rgba(255,255,255,0.06)", marginBottom: 24 }, + titleInput: { fontSize: 24, fontWeight: "700", color: colors.text, letterSpacing: -0.5 }, + fieldsSection: { gap: 16, marginBottom: 24 }, + sectionLabel: { fontSize: 13, fontWeight: "600", color: colors.muted, textTransform: "uppercase", letterSpacing: 0.5, marginBottom: 12 }, + dangerSection: { paddingTop: 16, marginTop: 8, borderTopWidth: 1, borderTopColor: "rgba(255,255,255,0.06)" }, + deleteButton: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 10, backgroundColor: colors.error + "10", paddingVertical: 16, borderRadius: 14, borderWidth: 1, borderColor: colors.error + "20" }, + deleteButtonText: { fontSize: 15, fontWeight: "600", color: colors.error }, +}); diff --git a/apps/mobile/app/(app)/item/new.tsx b/apps/mobile/app/(app)/item/new.tsx new file mode 100644 index 0000000..d63433f --- /dev/null +++ b/apps/mobile/app/(app)/item/new.tsx @@ -0,0 +1,169 @@ +import { useState, useRef, useEffect } from "react"; +import { View, Text, ScrollView, Pressable, TextInput, StyleSheet, Alert, KeyboardAvoidingView, Platform } from "react-native"; +import { useRouter } from "expo-router"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import Animated, { useSharedValue, useAnimatedStyle, withSpring, FadeIn, FadeInDown, SlideInRight } from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; +import { usevault, type ItemType, type CreateItemInput } from "../../../stores"; +import { TypeIcon, ChevronRightIcon } from "../../../components/icons"; +import { fieldConfigs, typeLabels } from "../../../components/types"; +import { colors, typeColors } from "../../../components/item/constants"; +import { FormHeader } from "../../../components/item/header"; +import { FieldInput } from "../../../components/item/field"; +import { TagsInput } from "../../../components/item/tags"; + +const types: ItemType[] = ["login", "note", "card", "identity", "ssh", "api", "otp", "passkey"]; + +export default function NewItem() { + const router = useRouter(); + const insets = useSafeAreaInsets(); + const { create } = usevault(); + const [step, setStep] = useState<"type" | "form">("type"); + const [selectedType, setSelectedType] = useState(null); + const [title, setTitle] = useState(""); + const [data, setData] = useState>({}); + const [tags, setTags] = useState([]); + const [newTag, setNewTag] = useState(""); + const [saving, setSaving] = useState(false); + const [modified, setModified] = useState(false); + const [visiblePasswords, setVisiblePasswords] = useState>(new Set()); + const titleRef = useRef(null); + + useEffect(() => { + if (step === "form" && titleRef.current) setTimeout(() => titleRef.current?.focus(), 300); + }, [step]); + + function handleSelectType(type: ItemType) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + setSelectedType(type); + setStep("form"); + } + + function handleBack() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + if (step === "form") { + if (modified) { + Alert.alert("discard changes?", "you have unsaved changes. are you sure you want to go back?", [ + { text: "keep editing", style: "cancel" }, + { text: "discard", style: "destructive", onPress: () => { setStep("type"); setTitle(""); setData({}); setTags([]); setModified(false); } }, + ]); + } else setStep("type"); + } else router.back(); + } + + function handleClose() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + if (modified) { + Alert.alert("discard changes?", "you have unsaved changes. are you sure you want to close?", [ + { text: "keep editing", style: "cancel" }, + { text: "discard", style: "destructive", onPress: () => router.back() }, + ]); + } else router.back(); + } + + function handleAddTag() { + const tag = newTag.trim().toLowerCase(); + if (tag && !tags.includes(tag)) { + setTags((prev) => [...prev, tag]); + setNewTag(""); + setModified(true); + } + } + + async function handleSave() { + if (!selectedType || !title.trim()) { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + Alert.alert("missing title", "please enter a title for this item"); + return; + } + + const fields = fieldConfigs[selectedType]; + const missing = fields.filter((f) => f.required).find((f) => !data[f.name]?.trim()); + if (missing) { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + Alert.alert("missing field", `please fill in the ${missing.label} field`); + return; + } + + setSaving(true); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + + try { + await create({ type: selectedType, title: title.trim(), data, tags } as CreateItemInput); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + router.back(); + } catch { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + Alert.alert("error", "failed to create item. please try again."); + } finally { + setSaving(false); + } + } + + if (step === "type") { + return ( + + + + what would you like{"\n"}to store? + + {types.map((type, index) => ( + handleSelectType(type)} /> + ))} + + + + ); + } + + const fields = selectedType ? fieldConfigs[selectedType] : []; + + return ( + + + + + { setTitle(v); setModified(true); }} autoCorrect={false} /> + + + {fields.map((field, index) => ( + { setData((p) => ({ ...p, [field.name]: v })); setModified(true); }} isPasswordVisible={visiblePasswords.has(field.name)} onTogglePassword={() => setVisiblePasswords((p) => { const n = new Set(p); n.has(field.name) ? n.delete(field.name) : n.add(field.name); return n; })} /> + ))} + + { setTags((p) => p.filter((x) => x !== t)); setModified(true); }} /> + + + ); +} + +function TypeCard({ type, index, onPress }: { type: ItemType; index: number; onPress: () => void }) { + const scale = useSharedValue(1); + const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] })); + + return ( + { scale.value = withSpring(0.95, { damping: 20 }); }} onPressOut={() => { scale.value = withSpring(1, { damping: 20 }); }} onPress={onPress}> + + + + + {typeLabels[type]} + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: colors.bg }, + scroll: { flex: 1 }, + typeContent: { paddingHorizontal: 20 }, + typeTitle: { fontSize: 28, fontWeight: "700", color: colors.text, letterSpacing: -0.5, lineHeight: 36, marginTop: 20, marginBottom: 32 }, + typeGrid: { gap: 10 }, + typeCard: { flexDirection: "row", alignItems: "center", backgroundColor: "rgba(255,255,255,0.03)", borderRadius: 14, padding: 16, borderWidth: 1, borderColor: "rgba(255,255,255,0.06)" }, + typeIconBox: { width: 44, height: 44, borderRadius: 12, alignItems: "center", justifyContent: "center", marginRight: 14 }, + typeCardLabel: { flex: 1, fontSize: 16, fontWeight: "600", color: colors.text }, + formContent: { paddingHorizontal: 20 }, + titleSection: { paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: "rgba(255,255,255,0.06)", marginBottom: 24 }, + titleInput: { fontSize: 24, fontWeight: "700", color: colors.text, letterSpacing: -0.5 }, + fieldsSection: { gap: 16, marginBottom: 24 }, +}); diff --git a/apps/mobile/app/(app)/search.tsx b/apps/mobile/app/(app)/search.tsx new file mode 100644 index 0000000..a851421 --- /dev/null +++ b/apps/mobile/app/(app)/search.tsx @@ -0,0 +1,687 @@ +import { useState, useMemo, useCallback, useRef } from "react"; +import { + View, + Text, + StyleSheet, + ScrollView, + TextInput, + Pressable, + Keyboard, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import Animated, { + FadeIn, + Layout, + useAnimatedStyle, + withSpring, + useSharedValue, +} from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; +import { Svg, Path, Rect, Circle } from "react-native-svg"; + +const colors = { + bg: "#0a0a0a", + accent: "#d4b08c", + surface: "#141414", + surfaceHover: "#1a1a1a", + border: "#1f1f1f", + text: "#ffffff", + muted: "#666666", + subtle: "#999999", +}; + +type ItemType = "login" | "card" | "note" | "identity" | "ssh" | "api"; + +interface VaultItem { + id: string; + type: ItemType; + title: string; + subtitle: string; + favorite: boolean; + tags: string[]; + lastUsed?: string; +} + +const mockItems: VaultItem[] = [ + { id: "1", type: "login", title: "GitHub", subtitle: "josh@noro.sh", favorite: true, tags: ["work", "dev"], lastUsed: "2h ago" }, + { id: "2", type: "login", title: "Vercel", subtitle: "josh@noro.sh", favorite: true, tags: ["work", "hosting"], lastUsed: "5m ago" }, + { id: "3", type: "card", title: "Chase Sapphire", subtitle: "****4242", favorite: false, tags: ["personal", "travel"], lastUsed: "1d ago" }, + { id: "4", type: "login", title: "AWS Console", subtitle: "root@noro.sh", favorite: false, tags: ["work", "cloud"], lastUsed: "3h ago" }, + { id: "5", type: "note", title: "Recovery Codes", subtitle: "10 items", favorite: true, tags: ["backup"], lastUsed: "1w ago" }, + { id: "6", type: "ssh", title: "Production Server", subtitle: "ed25519", favorite: false, tags: ["work", "servers"], lastUsed: "1d ago" }, + { id: "7", type: "api", title: "OpenAI API", subtitle: "sk-***abc", favorite: false, tags: ["work", "ai"], lastUsed: "4h ago" }, + { id: "8", type: "identity", title: "Personal Info", subtitle: "Josh", favorite: false, tags: ["personal"], lastUsed: "2w ago" }, + { id: "9", type: "login", title: "Stripe", subtitle: "josh@noro.sh", favorite: false, tags: ["work", "payments"], lastUsed: "6h ago" }, + { id: "10", type: "login", title: "Cloudflare", subtitle: "josh@noro.sh", favorite: false, tags: ["work", "dns"], lastUsed: "12h ago" }, +]; + +const recentSearches = ["github", "aws", "stripe api", "ssh keys"]; + +const popularTags = ["work", "personal", "dev", "cloud", "payments"]; + +function SearchIcon() { + return ( + + + + + ); +} + +function CloseIcon() { + return ( + + + + ); +} + +function ClockIcon() { + return ( + + + + + ); +} + +function TagIcon() { + return ( + + + + ); +} + +function FilterIcon() { + return ( + + + + ); +} + +function ItemIcon({ type }: { type: ItemType }) { + const iconColor = colors.text; + switch (type) { + case "login": + return ( + + + + + ); + case "card": + return ( + + + + + ); + case "note": + return ( + + + + + ); + case "identity": + return ( + + + + + ); + case "ssh": + return ( + + + + ); + case "api": + return ( + + + + ); + default: + return null; + } +} + +function SearchResultItem({ item }: { item: VaultItem }) { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + return ( + + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)} + onPressIn={() => { scale.value = withSpring(0.98, { damping: 15 }); }} + onPressOut={() => { scale.value = withSpring(1, { damping: 15 }); }} + style={styles.resultItem} + > + + + + + {item.title} + {item.subtitle} + {item.tags.length > 0 && ( + + {item.tags.slice(0, 3).map((tag) => ( + + {tag} + + ))} + + )} + + {item.lastUsed && ( + {item.lastUsed} + )} + + + ); +} + +function QuickFilterChip({ + label, + active, + onPress, +}: { + label: string; + active: boolean; + onPress: () => void; +}) { + return ( + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onPress(); + }} + style={[styles.quickFilter, active && styles.quickFilterActive]} + > + + {label} + + + ); +} + +export default function SearchScreen() { + const insets = useSafeAreaInsets(); + const inputRef = useRef(null); + const [search, setSearch] = useState(""); + const [selectedType, setSelectedType] = useState(null); + const [selectedTag, setSelectedTag] = useState(null); + const [showFilters, setShowFilters] = useState(false); + + const results = useMemo(() => { + if (!search && !selectedType && !selectedTag) return []; + + let filtered = mockItems; + + if (search) { + const q = search.toLowerCase(); + filtered = filtered.filter( + (item) => + item.title.toLowerCase().includes(q) || + item.subtitle.toLowerCase().includes(q) || + item.tags.some((t) => t.toLowerCase().includes(q)) + ); + } + + if (selectedType) { + filtered = filtered.filter((item) => item.type === selectedType); + } + + if (selectedTag) { + filtered = filtered.filter((item) => item.tags.includes(selectedTag)); + } + + return filtered; + }, [search, selectedType, selectedTag]); + + const hasQuery = search || selectedType || selectedTag; + + const handleClear = useCallback(() => { + setSearch(""); + setSelectedType(null); + setSelectedTag(null); + inputRef.current?.focus(); + }, []); + + const handleRecentSearch = useCallback((term: string) => { + setSearch(term); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + }, []); + + const handleTagSelect = useCallback((tag: string) => { + setSelectedTag(selectedTag === tag ? null : tag); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + }, [selectedTag]); + + const types: { type: ItemType; label: string }[] = [ + { type: "login", label: "Logins" }, + { type: "card", label: "Cards" }, + { type: "note", label: "Notes" }, + { type: "ssh", label: "SSH" }, + { type: "api", label: "API" }, + ]; + + return ( + + + Search + + + + + + + + + {search.length > 0 && ( + + + + )} + + { + setShowFilters(!showFilters); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + }} + style={[styles.filterButton, showFilters && styles.filterButtonActive]} + > + + + + + {showFilters && ( + + + Type + + + {types.map(({ type, label }) => ( + setSelectedType(selectedType === type ? null : type)} + /> + ))} + + + + + Tags + + + {popularTags.map((tag) => ( + handleTagSelect(tag)} + /> + ))} + + + + + )} + + + {!hasQuery ? ( + <> + + + + Recent Searches + + + {recentSearches.map((term) => ( + handleRecentSearch(term)} + style={styles.recentItem} + > + {term} + + ))} + + + + + + + Popular Tags + + + {popularTags.map((tag) => ( + handleTagSelect(tag)} + style={[styles.tagChip, selectedTag === tag && styles.tagChipActive]} + > + + #{tag} + + + ))} + + + + ) : ( + + + {results.length} {results.length === 1 ? "result" : "results"} + + {results.length > 0 ? ( + + {results.map((item) => ( + + ))} + + ) : ( + + + + + No results + + Try different keywords or filters + + + )} + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.bg, + }, + header: { + paddingHorizontal: 20, + paddingTop: 16, + paddingBottom: 8, + }, + title: { + fontSize: 32, + fontWeight: "700", + color: colors.text, + letterSpacing: -0.5, + }, + searchRow: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 20, + marginTop: 16, + gap: 12, + }, + searchContainer: { + flex: 1, + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.surface, + borderRadius: 12, + borderWidth: 1, + borderColor: colors.border, + }, + searchIcon: { + paddingLeft: 14, + paddingRight: 4, + }, + searchInput: { + flex: 1, + height: 48, + fontSize: 16, + color: colors.text, + paddingHorizontal: 10, + }, + clearButton: { + padding: 12, + }, + filterButton: { + width: 48, + height: 48, + borderRadius: 12, + backgroundColor: colors.surface, + borderWidth: 1, + borderColor: colors.border, + alignItems: "center", + justifyContent: "center", + }, + filterButtonActive: { + backgroundColor: colors.accent, + borderColor: colors.accent, + }, + filtersPanel: { + paddingHorizontal: 20, + paddingTop: 16, + gap: 16, + }, + filterSection: { + gap: 8, + }, + filterLabel: { + fontSize: 12, + fontWeight: "600", + color: colors.muted, + textTransform: "uppercase", + letterSpacing: 0.5, + }, + filterRow: { + flexDirection: "row", + gap: 8, + }, + quickFilter: { + paddingHorizontal: 14, + paddingVertical: 6, + borderRadius: 16, + backgroundColor: colors.surface, + borderWidth: 1, + borderColor: colors.border, + }, + quickFilterActive: { + backgroundColor: colors.accent, + borderColor: colors.accent, + }, + quickFilterText: { + fontSize: 13, + fontWeight: "500", + color: colors.subtle, + }, + quickFilterTextActive: { + color: colors.bg, + }, + scrollView: { + flex: 1, + marginTop: 16, + }, + scrollContent: { + paddingHorizontal: 20, + }, + section: { + marginBottom: 28, + }, + sectionHeader: { + flexDirection: "row", + alignItems: "center", + gap: 8, + marginBottom: 12, + }, + sectionTitle: { + fontSize: 14, + fontWeight: "600", + color: colors.muted, + }, + recentList: { + gap: 4, + }, + recentItem: { + paddingVertical: 12, + paddingHorizontal: 16, + backgroundColor: colors.surface, + borderRadius: 10, + marginBottom: 6, + }, + recentText: { + fontSize: 15, + color: colors.text, + }, + tagList: { + flexDirection: "row", + flexWrap: "wrap", + gap: 8, + }, + tagChip: { + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 20, + backgroundColor: colors.surface, + borderWidth: 1, + borderColor: colors.border, + }, + tagChipActive: { + backgroundColor: colors.accent, + borderColor: colors.accent, + }, + tagChipText: { + fontSize: 14, + fontWeight: "500", + color: colors.subtle, + }, + tagChipTextActive: { + color: colors.bg, + }, + resultsSection: { + flex: 1, + }, + resultsCount: { + fontSize: 13, + color: colors.muted, + marginBottom: 12, + }, + resultsList: { + gap: 1, + backgroundColor: colors.surface, + borderRadius: 16, + overflow: "hidden", + }, + resultItem: { + flexDirection: "row", + alignItems: "center", + padding: 14, + backgroundColor: colors.surface, + }, + resultIcon: { + width: 40, + height: 40, + borderRadius: 10, + backgroundColor: colors.bg, + alignItems: "center", + justifyContent: "center", + }, + resultContent: { + flex: 1, + marginLeft: 12, + }, + resultTitle: { + fontSize: 16, + fontWeight: "600", + color: colors.text, + }, + resultSubtitle: { + fontSize: 14, + color: colors.muted, + marginTop: 2, + }, + tagRow: { + flexDirection: "row", + gap: 6, + marginTop: 6, + }, + tagBadge: { + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 6, + backgroundColor: colors.bg, + }, + tagText: { + fontSize: 11, + fontWeight: "500", + color: colors.subtle, + }, + resultTime: { + fontSize: 12, + color: colors.muted, + }, + emptyState: { + alignItems: "center", + paddingTop: 60, + paddingHorizontal: 40, + }, + emptyIcon: { + width: 64, + height: 64, + borderRadius: 32, + backgroundColor: colors.surface, + alignItems: "center", + justifyContent: "center", + marginBottom: 16, + }, + emptyTitle: { + fontSize: 18, + fontWeight: "600", + color: colors.text, + marginBottom: 6, + }, + emptySubtitle: { + fontSize: 15, + color: colors.muted, + textAlign: "center", + }, +}); diff --git a/apps/mobile/app/(app)/settings.tsx b/apps/mobile/app/(app)/settings.tsx new file mode 100644 index 0000000..4b64882 --- /dev/null +++ b/apps/mobile/app/(app)/settings.tsx @@ -0,0 +1,607 @@ +import { useCallback } from "react"; +import { + View, + Text, + StyleSheet, + ScrollView, + Pressable, + Alert, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import Animated, { + useAnimatedStyle, + withSpring, + useSharedValue, +} from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; +import Constants from "expo-constants"; +import { Svg, Path, Circle } from "react-native-svg"; +import { useauth } from "../../stores/auth"; +import { usesettings, type AutolockDuration } from "../../stores/settings"; + +const autolockLabels: Record = { + immediate: "Immediately", + "1min": "1 minute", + "5min": "5 minutes", + "15min": "15 minutes", + "30min": "30 minutes", + "1hour": "1 hour", + never: "Never", +}; + +const colors = { + bg: "#0a0a0a", + accent: "#d4b08c", + surface: "#141414", + surfaceHover: "#1a1a1a", + border: "#1f1f1f", + text: "#ffffff", + muted: "#666666", + subtle: "#999999", + danger: "#ef4444", + success: "#22c55e", +}; + +function UserIcon() { + return ( + + + + + ); +} + +function ShieldIcon() { + return ( + + + + ); +} + +function FingerprintIcon() { + return ( + + + + ); +} + +function ClockIcon() { + return ( + + + + + ); +} + +function CloudIcon() { + return ( + + + + ); +} + +function TrashIcon() { + return ( + + + + + ); +} + +function InfoIcon() { + return ( + + + + + ); +} + +function HelpIcon() { + return ( + + + + + ); +} + +function LogoutIcon() { + return ( + + + + + ); +} + +function ChevronIcon() { + return ( + + + + ); +} + +function SyncIcon() { + return ( + + + + ); +} + +function Toggle({ + value, + onChange, +}: { + value: boolean; + onChange: (value: boolean) => void; +}) { + return ( + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onChange(!value); + }} + style={[styles.toggle, value && styles.toggleActive]} + > + + + ); +} + +function SettingsRow({ + icon, + title, + subtitle, + value, + onPress, + toggle, + toggleValue, + onToggle, + danger, +}: { + icon: React.ReactNode; + title: string; + subtitle?: string; + value?: string; + onPress?: () => void; + toggle?: boolean; + toggleValue?: boolean; + onToggle?: (value: boolean) => void; + danger?: boolean; +}) { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + const handlePress = () => { + if (onPress) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onPress(); + } + }; + + return ( + + { scale.value = withSpring(0.98, { damping: 15 }); }} + onPressOut={() => { scale.value = withSpring(1, { damping: 15 }); }} + style={styles.settingsRow} + disabled={!onPress && !toggle} + > + {icon} + + {title} + {subtitle && {subtitle}} + + {toggle && onToggle ? ( + + ) : value ? ( + + {value} + {onPress && } + + ) : onPress ? ( + + ) : null} + + + ); +} + +function SettingsSection({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( + + {title} + {children} + + ); +} + +export default function SettingsScreen() { + const insets = useSafeAreaInsets(); + const user = useauth((s) => s.user); + const biometricEnabled = usesettings((s) => s.biometricEnabled); + const setBiometricEnabled = usesettings((s) => s.setBiometricEnabled); + const autolockDuration = usesettings((s) => s.autolockDuration); + const version = Constants.expoConfig?.version || "1.0.0"; + + const handleLogout = useCallback(() => { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); + Alert.alert( + "Log Out", + "Are you sure you want to log out? Your vault will be locked.", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Log Out", + style: "destructive", + onPress: () => {}, + }, + ] + ); + }, []); + + const handleClearCache = useCallback(() => { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); + Alert.alert( + "Clear Cache", + "This will clear all cached data. Your vault items will be re-synced.", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Clear", + style: "destructive", + onPress: () => { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + }, + }, + ] + ); + }, []); + + return ( + + + Settings + + + + + + + + + {user?.name || "User"} + {user?.email || ""} + + + + Synced + + + + + } + title="Biometric Unlock" + subtitle="Use Face ID or fingerprint" + toggle + toggleValue={biometricEnabled} + onToggle={setBiometricEnabled} + /> + } + title="Auto-Lock" + subtitle="Lock vault after inactivity" + value={autolockLabels[autolockDuration]} + onPress={() => {}} + /> + } + title="Change Password" + subtitle="Update your master password" + onPress={() => {}} + /> + + + + } + title="Sync" + subtitle="Sync vault with cloud" + value="Auto" + onPress={() => {}} + /> + } + title="Clear Cache" + subtitle="Free up storage space" + onPress={handleClearCache} + /> + + + + } + title="Version" + value={version} + /> + } + title="Help & Support" + onPress={() => {}} + /> + + + + [styles.logoutButton, pressed && styles.logoutButtonPressed]} + > + + Log Out + + + + + + + + + + + noro + Secure password manager + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.bg, + }, + header: { + paddingHorizontal: 20, + paddingTop: 16, + paddingBottom: 8, + }, + title: { + fontSize: 32, + fontWeight: "700", + color: colors.text, + letterSpacing: -0.5, + }, + scrollView: { + flex: 1, + }, + scrollContent: { + paddingHorizontal: 20, + paddingTop: 20, + }, + profileCard: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.surface, + borderRadius: 16, + borderWidth: 1, + borderColor: colors.border, + padding: 16, + marginBottom: 24, + }, + avatar: { + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: colors.bg, + alignItems: "center", + justifyContent: "center", + }, + profileInfo: { + flex: 1, + marginLeft: 14, + }, + profileName: { + fontSize: 18, + fontWeight: "600", + color: colors.text, + }, + profileEmail: { + fontSize: 14, + color: colors.muted, + marginTop: 2, + }, + syncStatus: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.bg, + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 12, + gap: 6, + }, + syncText: { + fontSize: 12, + fontWeight: "500", + color: colors.success, + }, + section: { + marginBottom: 24, + }, + sectionTitle: { + fontSize: 12, + fontWeight: "600", + color: colors.muted, + textTransform: "uppercase", + letterSpacing: 0.5, + marginBottom: 12, + paddingLeft: 4, + }, + sectionContent: { + backgroundColor: colors.surface, + borderRadius: 16, + borderWidth: 1, + borderColor: colors.border, + overflow: "hidden", + }, + settingsRow: { + flexDirection: "row", + alignItems: "center", + padding: 16, + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + rowIcon: { + width: 36, + height: 36, + borderRadius: 10, + backgroundColor: colors.bg, + alignItems: "center", + justifyContent: "center", + }, + rowContent: { + flex: 1, + marginLeft: 12, + }, + rowTitle: { + fontSize: 16, + fontWeight: "500", + color: colors.text, + }, + rowTitleDanger: { + color: colors.danger, + }, + rowSubtitle: { + fontSize: 13, + color: colors.muted, + marginTop: 2, + }, + rowValueContainer: { + flexDirection: "row", + alignItems: "center", + gap: 6, + }, + rowValue: { + fontSize: 14, + color: colors.muted, + }, + toggle: { + width: 48, + height: 28, + borderRadius: 14, + backgroundColor: colors.bg, + padding: 2, + justifyContent: "center", + }, + toggleActive: { + backgroundColor: colors.accent, + }, + toggleThumb: { + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: colors.text, + }, + toggleThumbActive: { + alignSelf: "flex-end", + }, + logoutSection: { + marginTop: 8, + }, + logoutButton: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 10, + backgroundColor: colors.surface, + borderRadius: 14, + borderWidth: 1, + borderColor: colors.border, + padding: 16, + }, + logoutButtonPressed: { + opacity: 0.8, + }, + logoutText: { + fontSize: 16, + fontWeight: "500", + color: colors.danger, + }, + footer: { + alignItems: "center", + paddingTop: 40, + paddingBottom: 20, + }, + logoContainer: { + width: 48, + height: 48, + borderRadius: 14, + backgroundColor: colors.surface, + alignItems: "center", + justifyContent: "center", + marginBottom: 12, + }, + footerText: { + fontSize: 18, + fontWeight: "600", + color: colors.text, + }, + footerSubtext: { + fontSize: 13, + color: colors.muted, + marginTop: 4, + }, +}); diff --git a/apps/mobile/app/(auth)/_layout.tsx b/apps/mobile/app/(auth)/_layout.tsx new file mode 100644 index 0000000..fb92e7e --- /dev/null +++ b/apps/mobile/app/(auth)/_layout.tsx @@ -0,0 +1,20 @@ +import { Stack } from "expo-router"; +import { StyleSheet } from "react-native"; + +export default function AuthLayout() { + return ( + + ); +} + +const styles = StyleSheet.create({ + content: { + backgroundColor: "#0a0a0a", + }, +}); diff --git a/apps/mobile/app/(auth)/login.tsx b/apps/mobile/app/(auth)/login.tsx new file mode 100644 index 0000000..4b7f543 --- /dev/null +++ b/apps/mobile/app/(auth)/login.tsx @@ -0,0 +1,292 @@ +import { useState, useEffect } from "react"; +import { + View, + Text, + Pressable, + KeyboardAvoidingView, + Platform, + StyleSheet, + ScrollView, +} from "react-native"; +import { router } from "expo-router"; +import * as Haptics from "expo-haptics"; +import * as LocalAuthentication from "expo-local-authentication"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withDelay, + withTiming, + withSpring, + Easing, + FadeIn, +} from "react-native-reanimated"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { Logo } from "../../components/logo"; +import { Input } from "../../components/input"; +import { Button } from "../../components/button"; +import { FaceIdIcon, FingerprintIcon } from "../../components/icons"; +import { api } from "../../lib/api"; +import { settoken, gettoken } from "../../lib/storage"; +import { useauth } from "../../stores/auth"; + +export default function Login() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const [biometricAvailable, setBiometricAvailable] = useState(false); + const [biometricType, setBiometricType] = useState<"faceid" | "fingerprint">("fingerprint"); + + const logoOpacity = useSharedValue(0); + const logoScale = useSharedValue(0.8); + const contentOpacity = useSharedValue(0); + const contentTranslate = useSharedValue(20); + + useEffect(() => { + logoOpacity.value = withTiming(1, { duration: 500, easing: Easing.out(Easing.cubic) }); + logoScale.value = withSpring(1, { damping: 15, stiffness: 150 }); + contentOpacity.value = withDelay(200, withTiming(1, { duration: 400 })); + contentTranslate.value = withDelay(200, withSpring(0, { damping: 20, stiffness: 150 })); + + checkBiometrics(); + }, []); + + async function checkBiometrics() { + const compatible = await LocalAuthentication.hasHardwareAsync(); + const enrolled = await LocalAuthentication.isEnrolledAsync(); + const token = await gettoken(); + + if (compatible && enrolled && token) { + setBiometricAvailable(true); + const types = await LocalAuthentication.supportedAuthenticationTypesAsync(); + if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) { + setBiometricType("faceid"); + } + } + } + + const logoStyle = useAnimatedStyle(() => ({ + opacity: logoOpacity.value, + transform: [{ scale: logoScale.value }], + })); + + const contentStyle = useAnimatedStyle(() => ({ + opacity: contentOpacity.value, + transform: [{ translateY: contentTranslate.value }], + })); + + async function handleLogin() { + setError(""); + if (!email || !password) { + setError("please enter your email and password"); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + return; + } + + setLoading(true); + try { + const response = await api.auth.login(email, password); + await settoken(response.token); + useauth.getState().login(response.user, response.token); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + router.replace("/(app)"); + } catch (e) { + setError("invalid email or password"); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + } finally { + setLoading(false); + } + } + + async function handleBiometric() { + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: "unlock noro", + fallbackLabel: "use password", + }); + + if (result.success) { + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + router.replace("/(auth)/unlock"); + } + } + + return ( + + + + + + + + + welcome back + sign in to your noro account + + + + + + + + + {error && ( + + {error} + + )} + + + + + {biometricAvailable && ( + + {biometricType === "faceid" ? ( + + ) : ( + + )} + + {biometricType === "faceid" ? "face id" : "fingerprint"} + + + )} + + + + don't have an account? + router.push("/(auth)/register")} + hitSlop={8} + > + create account + + + + + + + + ); +} + +const styles = StyleSheet.create({ + safe: { + flex: 1, + backgroundColor: "#0a0a0a", + }, + keyboard: { + flex: 1, + }, + scroll: { + flexGrow: 1, + justifyContent: "center", + }, + container: { + flex: 1, + paddingHorizontal: 24, + paddingVertical: 48, + justifyContent: "center", + }, + header: { + alignItems: "center", + marginBottom: 40, + }, + logoContainer: { + width: 72, + height: 72, + borderRadius: 20, + backgroundColor: "rgba(255,107,0,0.1)", + alignItems: "center", + justifyContent: "center", + marginBottom: 24, + }, + title: { + fontSize: 28, + fontWeight: "700", + color: "#ffffff", + letterSpacing: -0.5, + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: "rgba(255,255,255,0.5)", + }, + form: { + width: "100%", + }, + card: { + backgroundColor: "rgba(255,255,255,0.03)", + borderRadius: 20, + padding: 24, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.06)", + }, + spacer: { + height: 16, + }, + buttonSpacer: { + height: 24, + }, + error: { + fontSize: 14, + color: "#ef4444", + textAlign: "center", + marginTop: 16, + }, + biometric: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + marginTop: 20, + paddingVertical: 12, + gap: 10, + }, + biometricText: { + fontSize: 14, + color: "rgba(255,255,255,0.6)", + fontWeight: "500", + }, + footer: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + marginTop: 32, + gap: 6, + }, + footerText: { + fontSize: 14, + color: "rgba(255,255,255,0.5)", + }, + link: { + fontSize: 14, + color: "#FF6B00", + fontWeight: "600", + }, +}); diff --git a/apps/mobile/app/(auth)/register.tsx b/apps/mobile/app/(auth)/register.tsx new file mode 100644 index 0000000..d414cf1 --- /dev/null +++ b/apps/mobile/app/(auth)/register.tsx @@ -0,0 +1,336 @@ +import { useState, useEffect } from "react"; +import { + View, + Text, + Pressable, + KeyboardAvoidingView, + Platform, + StyleSheet, + ScrollView, +} from "react-native"; +import { router } from "expo-router"; +import * as Haptics from "expo-haptics"; +import * as SecureStore from "expo-secure-store"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withDelay, + withTiming, + withSpring, + Easing, + FadeIn, +} from "react-native-reanimated"; +import { SafeAreaView } from "react-native-safe-area-context"; +import Svg, { Path } from "react-native-svg"; +import { Logo } from "../../components/logo"; +import { Input } from "../../components/input"; +import { Button } from "../../components/button"; + +function CheckIcon({ checked }: { checked: boolean }) { + return ( + + {checked && ( + + + + )} + + ); +} + +export default function Register() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const headerOpacity = useSharedValue(0); + const headerScale = useSharedValue(0.8); + const contentOpacity = useSharedValue(0); + const contentTranslate = useSharedValue(20); + + useEffect(() => { + headerOpacity.value = withTiming(1, { duration: 500, easing: Easing.out(Easing.cubic) }); + headerScale.value = withSpring(1, { damping: 15, stiffness: 150 }); + contentOpacity.value = withDelay(200, withTiming(1, { duration: 400 })); + contentTranslate.value = withDelay(200, withSpring(0, { damping: 20, stiffness: 150 })); + }, []); + + const headerStyle = useAnimatedStyle(() => ({ + opacity: headerOpacity.value, + transform: [{ scale: headerScale.value }], + })); + + const contentStyle = useAnimatedStyle(() => ({ + opacity: contentOpacity.value, + transform: [{ translateY: contentTranslate.value }], + })); + + const hasLength = password.length >= 12; + const hasUpper = /[A-Z]/.test(password); + const hasLower = /[a-z]/.test(password); + const hasNumber = /[0-9]/.test(password); + const hasSpecial = /[^A-Za-z0-9]/.test(password); + const passwordsMatch = password === confirm && password.length > 0; + + async function handleRegister() { + setError(""); + + if (!email) { + setError("please enter your email"); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + return; + } + + if (!hasLength || !hasUpper || !hasLower || !hasNumber) { + setError("password does not meet requirements"); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + return; + } + + if (!passwordsMatch) { + setError("passwords do not match"); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + return; + } + + setLoading(true); + try { + await new Promise((r) => setTimeout(r, 1500)); + await SecureStore.setItemAsync("session", "demo-session-token"); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + router.replace("/(auth)/unlock"); + } catch (e) { + setError("failed to create account"); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + } finally { + setLoading(false); + } + } + + return ( + + + + + + + + + create account + secure your secrets with noro + + + + + + + + + + + + + + + 12+ characters + + + + + + uppercase & lowercase + + + + + + number + + + + + + special character + + + + + {error && ( + + {error} + + )} + + + + + + + already have an account? + router.back()} + hitSlop={8} + > + sign in + + + + + + + + ); +} + +const styles = StyleSheet.create({ + safe: { + flex: 1, + backgroundColor: "#0a0a0a", + }, + keyboard: { + flex: 1, + }, + scroll: { + flexGrow: 1, + }, + container: { + flex: 1, + paddingHorizontal: 24, + paddingVertical: 48, + justifyContent: "center", + }, + header: { + alignItems: "center", + marginBottom: 40, + }, + logoContainer: { + width: 72, + height: 72, + borderRadius: 20, + backgroundColor: "rgba(255,107,0,0.1)", + alignItems: "center", + justifyContent: "center", + marginBottom: 24, + }, + title: { + fontSize: 28, + fontWeight: "700", + color: "#ffffff", + letterSpacing: -0.5, + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: "rgba(255,255,255,0.5)", + }, + form: { + width: "100%", + }, + card: { + backgroundColor: "rgba(255,255,255,0.03)", + borderRadius: 20, + padding: 24, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.06)", + }, + spacer: { + height: 16, + }, + buttonSpacer: { + height: 24, + }, + requirements: { + marginTop: 16, + gap: 8, + }, + requirement: { + flexDirection: "row", + alignItems: "center", + gap: 10, + }, + check: { + width: 18, + height: 18, + borderRadius: 5, + borderWidth: 1.5, + borderColor: "rgba(255,255,255,0.2)", + alignItems: "center", + justifyContent: "center", + }, + checkActive: { + backgroundColor: "#FF6B00", + borderColor: "#FF6B00", + }, + requirementText: { + fontSize: 13, + color: "rgba(255,255,255,0.4)", + }, + requirementMet: { + color: "rgba(255,255,255,0.8)", + }, + error: { + fontSize: 14, + color: "#ef4444", + textAlign: "center", + marginTop: 16, + }, + footer: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + marginTop: 32, + gap: 6, + }, + footerText: { + fontSize: 14, + color: "rgba(255,255,255,0.5)", + }, + link: { + fontSize: 14, + color: "#FF6B00", + fontWeight: "600", + }, +}); diff --git a/apps/mobile/app/(auth)/unlock.tsx b/apps/mobile/app/(auth)/unlock.tsx new file mode 100644 index 0000000..cbbb225 --- /dev/null +++ b/apps/mobile/app/(auth)/unlock.tsx @@ -0,0 +1,301 @@ +import { useState, useEffect, useCallback } from "react"; +import { + View, + Text, + Pressable, + StyleSheet, + Alert, +} from "react-native"; +import { router } from "expo-router"; +import * as Haptics from "expo-haptics"; +import * as LocalAuthentication from "expo-local-authentication"; +import * as SecureStore from "expo-secure-store"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withDelay, + withTiming, + withSpring, + withSequence, + Easing, + FadeIn, +} from "react-native-reanimated"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { Logo } from "../../components/logo"; +import { Input } from "../../components/input"; +import { Button } from "../../components/button"; +import { FaceIdIcon, FingerprintIcon } from "../../components/icons"; + +export default function Unlock() { + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const [biometricAvailable, setBiometricAvailable] = useState(false); + const [biometricType, setBiometricType] = useState<"faceid" | "fingerprint">("fingerprint"); + + const logoOpacity = useSharedValue(0); + const logoScale = useSharedValue(0.8); + const contentOpacity = useSharedValue(0); + const contentTranslate = useSharedValue(20); + const shake = useSharedValue(0); + + useEffect(() => { + logoOpacity.value = withTiming(1, { duration: 500, easing: Easing.out(Easing.cubic) }); + logoScale.value = withSpring(1, { damping: 15, stiffness: 150 }); + contentOpacity.value = withDelay(200, withTiming(1, { duration: 400 })); + contentTranslate.value = withDelay(200, withSpring(0, { damping: 20, stiffness: 150 })); + + checkBiometrics(); + }, []); + + async function checkBiometrics() { + const compatible = await LocalAuthentication.hasHardwareAsync(); + const enrolled = await LocalAuthentication.isEnrolledAsync(); + const biometricEnabled = await SecureStore.getItemAsync("biometric"); + + if (compatible && enrolled) { + setBiometricAvailable(true); + const types = await LocalAuthentication.supportedAuthenticationTypesAsync(); + if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) { + setBiometricType("faceid"); + } + + if (biometricEnabled === "true") { + promptBiometric(); + } + } + } + + const logoStyle = useAnimatedStyle(() => ({ + opacity: logoOpacity.value, + transform: [{ scale: logoScale.value }], + })); + + const contentStyle = useAnimatedStyle(() => ({ + opacity: contentOpacity.value, + transform: [{ translateY: contentTranslate.value }], + })); + + const cardStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: shake.value }], + })); + + const promptBiometric = useCallback(async () => { + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: "unlock noro", + fallbackLabel: "use password", + cancelLabel: "cancel", + }); + + if (result.success) { + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + router.replace("/(app)"); + } + }, []); + + async function handleUnlock() { + setError(""); + if (!password) { + setError("please enter your password"); + shake.value = withSequence( + withTiming(-10, { duration: 50 }), + withTiming(10, { duration: 50 }), + withTiming(-10, { duration: 50 }), + withTiming(10, { duration: 50 }), + withTiming(0, { duration: 50 }) + ); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + return; + } + + setLoading(true); + try { + await new Promise((r) => setTimeout(r, 800)); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + router.replace("/(app)"); + } catch (e) { + setError("incorrect password"); + shake.value = withSequence( + withTiming(-10, { duration: 50 }), + withTiming(10, { duration: 50 }), + withTiming(-10, { duration: 50 }), + withTiming(10, { duration: 50 }), + withTiming(0, { duration: 50 }) + ); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + } finally { + setLoading(false); + } + } + + async function handleLogout() { + Alert.alert( + "sign out", + "are you sure you want to sign out?", + [ + { text: "cancel", style: "cancel" }, + { + text: "sign out", + style: "destructive", + onPress: async () => { + await SecureStore.deleteItemAsync("session"); + await SecureStore.deleteItemAsync("biometric"); + await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + router.replace("/(auth)/login"); + }, + }, + ] + ); + } + + async function toggleBiometric() { + const current = await SecureStore.getItemAsync("biometric"); + if (current === "true") { + await SecureStore.setItemAsync("biometric", "false"); + await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } else { + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: "enable biometric unlock", + }); + if (result.success) { + await SecureStore.setItemAsync("biometric", "true"); + await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } + } + } + + return ( + + + + + + + unlock vault + enter your password to continue + + + + + + + + + + {biometricAvailable && ( + + + {biometricType === "faceid" ? ( + + ) : ( + + )} + + + {biometricType === "faceid" ? "use face id" : "use fingerprint"} + + + )} + + + + + sign out + + + + + + ); +} + +const styles = StyleSheet.create({ + safe: { + flex: 1, + backgroundColor: "#0a0a0a", + }, + container: { + flex: 1, + paddingHorizontal: 24, + paddingVertical: 48, + justifyContent: "center", + }, + header: { + alignItems: "center", + marginBottom: 40, + }, + logoContainer: { + width: 72, + height: 72, + borderRadius: 20, + backgroundColor: "rgba(255,107,0,0.1)", + alignItems: "center", + justifyContent: "center", + marginBottom: 24, + }, + title: { + fontSize: 28, + fontWeight: "700", + color: "#ffffff", + letterSpacing: -0.5, + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: "rgba(255,255,255,0.5)", + }, + form: { + width: "100%", + }, + card: { + backgroundColor: "rgba(255,255,255,0.03)", + borderRadius: 20, + padding: 24, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.06)", + }, + buttonSpacer: { + height: 24, + }, + biometric: { + alignItems: "center", + marginTop: 28, + gap: 12, + }, + biometricIcon: { + width: 64, + height: 64, + borderRadius: 16, + backgroundColor: "rgba(255,107,0,0.1)", + alignItems: "center", + justifyContent: "center", + }, + biometricText: { + fontSize: 14, + color: "rgba(255,255,255,0.6)", + fontWeight: "500", + }, + footer: { + alignItems: "center", + marginTop: 32, + }, + link: { + fontSize: 14, + color: "rgba(255,255,255,0.4)", + fontWeight: "500", + }, +}); diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx new file mode 100644 index 0000000..66790dd --- /dev/null +++ b/apps/mobile/app/_layout.tsx @@ -0,0 +1,32 @@ +import { Stack } from "expo-router"; +import { StatusBar } from "expo-status-bar"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { SafeAreaProvider } from "react-native-safe-area-context"; +import { StyleSheet } from "react-native"; + +export default function RootLayout() { + return ( + + + + + + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: "#0a0a0a", + }, + content: { + backgroundColor: "#0a0a0a", + }, +}); diff --git a/apps/mobile/app/index.tsx b/apps/mobile/app/index.tsx new file mode 100644 index 0000000..4e100d8 --- /dev/null +++ b/apps/mobile/app/index.tsx @@ -0,0 +1,71 @@ +import { useEffect } from "react"; +import { View, StyleSheet } from "react-native"; +import { router } from "expo-router"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, + Easing, +} from "react-native-reanimated"; +import { Logo } from "../components/logo"; +import { gettoken } from "../lib/storage"; +import { api } from "../lib/api"; +import { useauth } from "../stores/auth"; + +export default function Splash() { + const opacity = useSharedValue(0); + const scale = useSharedValue(0.8); + + useEffect(() => { + opacity.value = withTiming(1, { duration: 600, easing: Easing.out(Easing.cubic) }); + scale.value = withTiming(1, { duration: 600, easing: Easing.out(Easing.cubic) }); + + checkAuth(); + }, []); + + async function checkAuth() { + await new Promise((r) => setTimeout(r, 1200)); + + const { isAuthenticated } = useauth.getState(); + if (isAuthenticated) { + router.replace("/(app)"); + return; + } + + const token = await gettoken(); + + if (!token) { + router.replace("/(auth)/login"); + return; + } + + try { + await api.auth.session(); + router.replace("/(auth)/unlock"); + } catch { + router.replace("/(auth)/login"); + } + } + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + transform: [{ scale: scale.value }], + })); + + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: "center", + justifyContent: "center", + backgroundColor: "#0a0a0a", + }, +}); diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js new file mode 100644 index 0000000..17131ee --- /dev/null +++ b/apps/mobile/babel.config.js @@ -0,0 +1,7 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ["babel-preset-expo"], + plugins: ["react-native-reanimated/plugin"], + }; +}; diff --git a/apps/mobile/components/avatar.tsx b/apps/mobile/components/avatar.tsx new file mode 100644 index 0000000..22176eb --- /dev/null +++ b/apps/mobile/components/avatar.tsx @@ -0,0 +1,179 @@ +import { forwardRef, useMemo } from "react"; +import { View, Text, Image, type ViewStyle } from "react-native"; +import Animated, { FadeIn } from "react-native-reanimated"; + +type Size = "xs" | "sm" | "md" | "lg" | "xl"; + +interface AvatarProps { + name?: string; + image?: string; + size?: Size; + style?: ViewStyle; +} + +const sizes: Record = { + xs: { container: 24, text: 10 }, + sm: { container: 32, text: 12 }, + md: { container: 40, text: 14 }, + lg: { container: 52, text: 18 }, + xl: { container: 72, text: 24 }, +}; + +const colors = [ + "#d4b08c", + "#8cd4b0", + "#b08cd4", + "#d48c8c", + "#8cb0d4", + "#d4c08c", + "#8cd4d4", + "#d48cc0", +]; + +function getInitials(name: string): string { + const parts = name.trim().split(/\s+/); + if (parts.length === 1) { + return parts[0].substring(0, 2).toUpperCase(); + } + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); +} + +function getColor(name: string): string { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + return colors[Math.abs(hash) % colors.length]; +} + +export const Avatar = forwardRef( + ({ name = "", image, size = "md", style }, ref) => { + const sizeConfig = sizes[size]; + const initials = useMemo(() => (name ? getInitials(name) : "?"), [name]); + const backgroundColor = useMemo( + () => (name ? getColor(name) : colors[0]), + [name] + ); + + if (image) { + return ( + + + + ); + } + + return ( + + + {initials} + + + ); + } +); + +Avatar.displayName = "Avatar"; + +interface AvatarGroupProps { + avatars: Array<{ name?: string; image?: string }>; + max?: number; + size?: Size; + style?: ViewStyle; +} + +export function AvatarGroup({ + avatars, + max = 4, + size = "sm", + style, +}: AvatarGroupProps) { + const visible = avatars.slice(0, max); + const overflow = avatars.length - max; + const sizeConfig = sizes[size]; + const overlap = sizeConfig.container * 0.3; + + return ( + + {visible.map((avatar, i) => ( + + + + ))} + {overflow > 0 ? ( + + + +{overflow} + + + ) : null} + + ); +} diff --git a/apps/mobile/components/badge.tsx b/apps/mobile/components/badge.tsx new file mode 100644 index 0000000..0ee742b --- /dev/null +++ b/apps/mobile/components/badge.tsx @@ -0,0 +1,223 @@ +import { forwardRef } from "react"; +import { View, Text, Pressable, type ViewStyle } from "react-native"; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, + FadeIn, +} from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; + +type Status = "success" | "warning" | "error" | "info" | "neutral"; +type Size = "sm" | "md"; + +interface BadgeProps { + status?: Status; + size?: Size; + children: string; + style?: ViewStyle; +} + +const statusStyles: Record = { + success: { + bg: "rgba(34,197,94,0.15)", + text: "#4ade80", + border: "rgba(34,197,94,0.3)", + }, + warning: { + bg: "rgba(234,179,8,0.15)", + text: "#facc15", + border: "rgba(234,179,8,0.3)", + }, + error: { + bg: "rgba(239,68,68,0.15)", + text: "#f87171", + border: "rgba(239,68,68,0.3)", + }, + info: { + bg: "rgba(59,130,246,0.15)", + text: "#60a5fa", + border: "rgba(59,130,246,0.3)", + }, + neutral: { + bg: "rgba(255,255,255,0.08)", + text: "rgba(255,255,255,0.7)", + border: "rgba(255,255,255,0.15)", + }, +}; + +const sizeStyles: Record = { + sm: { padding: { h: 8, v: 3 }, text: 11 }, + md: { padding: { h: 10, v: 4 }, text: 12 }, +}; + +export const Badge = forwardRef( + ({ status = "neutral", size = "md", children, style }, ref) => { + const colors = statusStyles[status]; + const sizing = sizeStyles[size]; + + return ( + + + {children} + + + ); + } +); + +Badge.displayName = "Badge"; + +interface TagBadgeProps { + children: string; + onRemove?: () => void; + style?: ViewStyle; +} + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable); + +export function TagBadge({ children, onRemove, style }: TagBadgeProps) { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + function handlePressIn() { + scale.value = withSpring(0.95, { damping: 15, stiffness: 400 }); + } + + function handlePressOut() { + scale.value = withSpring(1, { damping: 15, stiffness: 400 }); + } + + function handleRemove() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onRemove?.(); + } + + return ( + + + {children} + + {onRemove ? ( + + + x + + + ) : null} + + ); +} + +interface TypeBadgeProps { + type: "login" | "card" | "note" | "identity" | "custom"; + style?: ViewStyle; +} + +const typeConfig: Record< + TypeBadgeProps["type"], + { label: string; color: string } +> = { + login: { label: "Login", color: "#60a5fa" }, + card: { label: "Card", color: "#f472b6" }, + note: { label: "Note", color: "#a78bfa" }, + identity: { label: "Identity", color: "#4ade80" }, + custom: { label: "Custom", color: "#fbbf24" }, +}; + +export function TypeBadge({ type, style }: TypeBadgeProps) { + const config = typeConfig[type]; + + return ( + + + + {config.label} + + + ); +} diff --git a/apps/mobile/components/button.tsx b/apps/mobile/components/button.tsx new file mode 100644 index 0000000..ce9173f --- /dev/null +++ b/apps/mobile/components/button.tsx @@ -0,0 +1,127 @@ +import { Pressable, Text, StyleSheet, ActivityIndicator, type ViewStyle } from "react-native"; +import * as Haptics from "expo-haptics"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated"; + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable); + +type Variant = "primary" | "secondary" | "ghost"; + +interface ButtonProps { + children: string; + variant?: Variant; + loading?: boolean; + disabled?: boolean; + onPress?: () => void; + style?: ViewStyle; +} + +export function Button({ + children, + variant = "primary", + loading = false, + disabled = false, + onPress, + style, +}: ButtonProps) { + const scale = useSharedValue(1); + const opacity = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + opacity: opacity.value, + })); + + function handlePressIn() { + scale.value = withTiming(0.97, { duration: 100 }); + opacity.value = withTiming(0.9, { duration: 100 }); + } + + function handlePressOut() { + scale.value = withTiming(1, { duration: 150 }); + opacity.value = withTiming(1, { duration: 150 }); + } + + async function handlePress() { + if (loading || disabled) return; + await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onPress?.(); + } + + const variantStyles = { + primary: styles.primary, + secondary: styles.secondary, + ghost: styles.ghost, + }; + + const textStyles = { + primary: styles.primaryText, + secondary: styles.secondaryText, + ghost: styles.ghostText, + }; + + return ( + + {loading ? ( + + ) : ( + {children} + )} + + ); +} + +const styles = StyleSheet.create({ + button: { + height: 52, + borderRadius: 12, + alignItems: "center", + justifyContent: "center", + width: "100%", + }, + primary: { + backgroundColor: "#FF6B00", + }, + secondary: { + backgroundColor: "rgba(255,255,255,0.1)", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + }, + ghost: { + backgroundColor: "transparent", + }, + disabled: { + opacity: 0.5, + }, + text: { + fontSize: 16, + fontWeight: "600", + }, + primaryText: { + color: "#0a0a0a", + }, + secondaryText: { + color: "#ffffff", + }, + ghostText: { + color: "#FF6B00", + }, +}); diff --git a/apps/mobile/components/card.tsx b/apps/mobile/components/card.tsx new file mode 100644 index 0000000..47d4bae --- /dev/null +++ b/apps/mobile/components/card.tsx @@ -0,0 +1,181 @@ +import { forwardRef, type ReactNode } from "react"; +import { View, type ViewProps, type ViewStyle } from "react-native"; +import Animated, { FadeIn } from "react-native-reanimated"; + +type Variant = "default" | "bordered" | "elevated"; + +interface CardProps extends Omit { + variant?: Variant; + children: ReactNode; + style?: ViewStyle; + animated?: boolean; +} + +const variants: Record = { + default: { + backgroundColor: "rgba(255,255,255,0.03)", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.08)", + }, + bordered: { + backgroundColor: "transparent", + borderWidth: 1.5, + borderColor: "rgba(255,255,255,0.12)", + }, + elevated: { + backgroundColor: "rgba(255,255,255,0.05)", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + shadowColor: "#000", + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.25, + shadowRadius: 16, + elevation: 8, + }, +}; + +export const Card = forwardRef( + ({ variant = "default", children, style, animated = false, ...props }, ref) => { + if (animated) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); + } +); + +Card.displayName = "Card"; + +interface CardHeaderProps extends Omit { + children: ReactNode; + style?: ViewStyle; +} + +export const CardHeader = forwardRef( + ({ children, style, ...props }, ref) => ( + + {children} + + ) +); + +CardHeader.displayName = "CardHeader"; + +interface CardTitleProps extends Omit { + children: string; + style?: ViewStyle; +} + +export const CardTitle = forwardRef( + ({ children, style, ...props }, ref) => ( + + + {children} + + + ) +); + +CardTitle.displayName = "CardTitle"; + +interface CardDescriptionProps extends Omit { + children: string; + style?: ViewStyle; +} + +export const CardDescription = forwardRef( + ({ children, style, ...props }, ref) => ( + + + {children} + + + ) +); + +CardDescription.displayName = "CardDescription"; + +interface CardContentProps extends Omit { + children: ReactNode; + style?: ViewStyle; +} + +export const CardContent = forwardRef( + ({ children, style, ...props }, ref) => ( + + {children} + + ) +); + +CardContent.displayName = "CardContent"; + +interface CardFooterProps extends Omit { + children: ReactNode; + style?: ViewStyle; +} + +export const CardFooter = forwardRef( + ({ children, style, ...props }, ref) => ( + + {children} + + ) +); + +CardFooter.displayName = "CardFooter"; diff --git a/apps/mobile/components/empty.tsx b/apps/mobile/components/empty.tsx new file mode 100644 index 0000000..39c31ab --- /dev/null +++ b/apps/mobile/components/empty.tsx @@ -0,0 +1,286 @@ +import { View, Text, type ViewStyle } from "react-native"; +import Animated, { FadeIn, FadeInUp } from "react-native-reanimated"; +import { Button } from "./button"; + +interface EmptyProps { + icon?: "search" | "folder" | "key" | "lock" | "bell" | "shield"; + title: string; + description?: string; + action?: { + label: string; + onPress: () => void; + }; + style?: ViewStyle; +} + +export function Empty({ + icon = "folder", + title, + description, + action, + style, +}: EmptyProps) { + return ( + + + + + + {title} + + {description ? ( + + {description} + + ) : null} + {action ? ( + + + + ) : null} + + ); +} + +interface EmptyIconProps { + name: "search" | "folder" | "key" | "lock" | "bell" | "shield"; +} + +function EmptyIcon({ name }: EmptyIconProps) { + const color = "rgba(255,255,255,0.3)"; + const size = 36; + + const icons: Record = { + search: ( + + + + + ), + folder: ( + + + + + ), + key: ( + + + + + + ), + lock: ( + + + + + ), + bell: ( + + + + + + ), + shield: ( + + + + ), + }; + + return icons[name]; +} diff --git a/apps/mobile/components/fieldrow.tsx b/apps/mobile/components/fieldrow.tsx new file mode 100644 index 0000000..610ad06 --- /dev/null +++ b/apps/mobile/components/fieldrow.tsx @@ -0,0 +1,222 @@ +import { useState, useEffect, useCallback } from "react"; +import { View, Text, Pressable, StyleSheet } from "react-native"; +import * as Clipboard from "expo-clipboard"; +import * as Haptics from "expo-haptics"; +import Animated, { useSharedValue, useAnimatedStyle, withTiming, withSequence } from "react-native-reanimated"; +import { CopyIcon, EyeIcon, EyeSlashIcon, CheckIcon } from "./icons"; + +interface FieldRowProps { + label: string; + value: string; + sensitive?: boolean; + isTotp?: boolean; + totpSecret?: string; +} + +const base32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + +function decodeBase32(input: string): Uint8Array { + const clean = input.toUpperCase().replace(/[^A-Z2-7]/g, ""); + const bits: number[] = []; + for (const char of clean) { + const val = base32.indexOf(char); + for (let i = 4; i >= 0; i--) { + bits.push((val >> i) & 1); + } + } + const bytes: number[] = []; + for (let i = 0; i + 8 <= bits.length; i += 8) { + let byte = 0; + for (let j = 0; j < 8; j++) { + byte = (byte << 1) | bits[i + j]; + } + bytes.push(byte); + } + return new Uint8Array(bytes); +} + +async function hmacSha1(key: Uint8Array, data: Uint8Array): Promise { + const cryptoKey = await crypto.subtle.importKey( + "raw", + key, + { name: "HMAC", hash: "SHA-1" }, + false, + ["sign"] + ); + const sig = await crypto.subtle.sign("HMAC", cryptoKey, data); + return new Uint8Array(sig); +} + +async function generateTotp(secret: string): Promise { + const key = decodeBase32(secret); + let counter = Math.floor(Date.now() / 1000 / 30); + const data = new Uint8Array(8); + for (let i = 7; i >= 0; i--) { + data[i] = counter & 0xff; + counter = Math.floor(counter / 256); + } + const hash = await hmacSha1(key, data); + const offset = hash[19] & 0x0f; + const binary = + ((hash[offset] & 0x7f) << 24) | + ((hash[offset + 1] & 0xff) << 16) | + ((hash[offset + 2] & 0xff) << 8) | + (hash[offset + 3] & 0xff); + return (binary % 1000000).toString().padStart(6, "0"); +} + +export function FieldRow({ label, value, sensitive, isTotp, totpSecret }: FieldRowProps) { + const [revealed, setRevealed] = useState(false); + const [copied, setCopied] = useState(false); + const [totpCode, setTotpCode] = useState(""); + const [remaining, setRemaining] = useState(30); + const scale = useSharedValue(1); + + const refresh = useCallback(async () => { + if (!totpSecret) return; + try { + const code = await generateTotp(totpSecret); + setTotpCode(code); + } catch { + setTotpCode(""); + } + }, [totpSecret]); + + useEffect(() => { + if (!isTotp || !totpSecret) return; + refresh(); + const interval = setInterval(() => { + const now = Math.floor(Date.now() / 1000); + const left = 30 - (now % 30); + setRemaining(left); + if (left === 30) refresh(); + }, 1000); + return () => clearInterval(interval); + }, [isTotp, totpSecret, refresh]); + + const displayValue = isTotp + ? totpCode + : sensitive && !revealed + ? "\u2022".repeat(Math.min(value.length, 20)) + : value; + + async function handleCopy() { + const textToCopy = isTotp ? totpCode : value; + if (!textToCopy) return; + await Clipboard.setStringAsync(textToCopy); + await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + scale.value = withSequence(withTiming(0.95, { duration: 50 }), withTiming(1, { duration: 100 })); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + + function handleReveal() { + setRevealed(!revealed); + Haptics.selectionAsync(); + } + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + if (!value && !isTotp) return null; + + return ( + + + {label} + {isTotp && totpCode && ( + + + {remaining}s + + )} + + + + + {displayValue || "-"} + + + + {sensitive && !isTotp && ( + + {revealed ? : } + + )} + + {copied ? : } + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: "rgba(255,255,255,0.06)", + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginBottom: 6, + }, + label: { + fontSize: 12, + color: "rgba(255,255,255,0.4)", + textTransform: "lowercase", + }, + countdown: { + flexDirection: "row", + alignItems: "center", + gap: 6, + }, + countdownBar: { + height: 3, + backgroundColor: "#d4b08c", + borderRadius: 2, + minWidth: 40, + }, + countdownText: { + fontSize: 11, + color: "rgba(255,255,255,0.4)", + fontVariant: ["tabular-nums"], + }, + row: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + gap: 12, + }, + valueContainer: { + flex: 1, + }, + value: { + fontSize: 15, + color: "#fff", + fontFamily: "monospace", + }, + totpValue: { + fontSize: 28, + fontWeight: "600", + letterSpacing: 4, + color: "#d4b08c", + }, + actions: { + flexDirection: "row", + alignItems: "center", + gap: 4, + }, + button: { + width: 40, + height: 40, + alignItems: "center", + justifyContent: "center", + borderRadius: 10, + backgroundColor: "rgba(255,255,255,0.05)", + }, +}); diff --git a/apps/mobile/components/header.tsx b/apps/mobile/components/header.tsx new file mode 100644 index 0000000..266aec3 --- /dev/null +++ b/apps/mobile/components/header.tsx @@ -0,0 +1,431 @@ +import { type ReactNode } from "react"; +import { View, Text, Pressable, type ViewStyle } from "react-native"; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, + FadeIn, +} from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import * as Haptics from "expo-haptics"; + +interface HeaderProps { + title?: string; + subtitle?: string; + back?: { + label?: string; + onPress: () => void; + }; + actions?: ReactNode; + transparent?: boolean; + large?: boolean; + style?: ViewStyle; +} + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable); + +export function Header({ + title, + subtitle, + back, + actions, + transparent = false, + large = false, + style, +}: HeaderProps) { + const insets = useSafeAreaInsets(); + + return ( + + + + {back ? : null} + {!large && title ? ( + + {title} + + ) : null} + + {actions ? ( + + {actions} + + ) : null} + + {large && title ? ( + + + {title} + + {subtitle ? ( + + {subtitle} + + ) : null} + + ) : null} + + ); +} + +interface BackButtonProps { + label?: string; + onPress: () => void; +} + +function BackButton({ label, onPress }: BackButtonProps) { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + function handlePressIn() { + scale.value = withSpring(0.95, { damping: 15, stiffness: 400 }); + } + + function handlePressOut() { + scale.value = withSpring(1, { damping: 15, stiffness: 400 }); + } + + function handlePress() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onPress(); + } + + return ( + + + {label ? ( + + {label} + + ) : null} + + ); +} + +function ChevronLeft() { + return ( + + + + + ); +} + +interface HeaderButtonProps { + icon: "plus" | "settings" | "search" | "close" | "edit"; + onPress: () => void; + style?: ViewStyle; +} + +export function HeaderButton({ icon, onPress, style }: HeaderButtonProps) { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + function handlePressIn() { + scale.value = withSpring(0.9, { damping: 15, stiffness: 400 }); + } + + function handlePressOut() { + scale.value = withSpring(1, { damping: 15, stiffness: 400 }); + } + + function handlePress() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onPress(); + } + + return ( + + + + ); +} + +interface HeaderIconProps { + name: "plus" | "settings" | "search" | "close" | "edit"; +} + +function HeaderIcon({ name }: HeaderIconProps) { + const color = "rgba(255,255,255,0.7)"; + const size = 18; + + const icons: Record = { + plus: ( + + + + + ), + settings: ( + + + + + + + + ), + search: ( + + + + + ), + close: ( + + + + + ), + edit: ( + + + + + ), + }; + + return icons[name]; +} diff --git a/apps/mobile/components/icon.tsx b/apps/mobile/components/icon.tsx new file mode 100644 index 0000000..6f83d42 --- /dev/null +++ b/apps/mobile/components/icon.tsx @@ -0,0 +1,159 @@ +import { View, type ViewStyle } from "react-native"; +import { Svg, Path, Circle, Rect, Line } from "react-native-svg"; + +type IconName = + | "chevronleft" + | "chevronright" + | "close" + | "check" + | "plus" + | "search" + | "settings" + | "user" + | "lock" + | "eye" + | "eyeoff" + | "copy" + | "trash" + | "edit" + | "share" + | "folder" + | "key" + | "shield" + | "bell" + | "logout"; + +type Size = "xs" | "sm" | "md" | "lg" | "xl"; + +interface IconProps { + name: IconName; + size?: Size; + color?: string; + style?: ViewStyle; +} + +const sizes: Record = { + xs: 14, + sm: 18, + md: 22, + lg: 28, + xl: 36, +}; + +const paths: Record JSX.Element> = { + chevronleft: (c) => ( + + ), + chevronright: (c) => ( + + ), + close: (c) => ( + + ), + check: (c) => ( + + ), + plus: (c) => ( + + ), + search: (c) => ( + <> + + + + ), + settings: (c) => ( + <> + + + + ), + user: (c) => ( + <> + + + + ), + lock: (c) => ( + <> + + + + ), + eye: (c) => ( + <> + + + + ), + eyeoff: (c) => ( + <> + + + + ), + copy: (c) => ( + <> + + + + ), + trash: (c) => ( + <> + + + ), + edit: (c) => ( + <> + + + + ), + share: (c) => ( + <> + + + + + + + ), + folder: (c) => ( + + ), + key: (c) => ( + <> + + + ), + shield: (c) => ( + + ), + bell: (c) => ( + <> + + + ), + logout: (c) => ( + <> + + + ), +}; + +export function Icon({ + name, + size = "md", + color = "rgba(255,255,255,0.7)", + style, +}: IconProps) { + const dimension = sizes[size]; + + return ( + + + {paths[name](color)} + + + ); +} diff --git a/apps/mobile/components/icons.tsx b/apps/mobile/components/icons.tsx new file mode 100644 index 0000000..828e438 --- /dev/null +++ b/apps/mobile/components/icons.tsx @@ -0,0 +1,209 @@ +import Svg, { Path, Rect, Circle } from "react-native-svg"; +import type { ItemType } from "../stores"; + +interface IconProps { + size?: number; + color?: string; +} + +const typeicons: Record = { + login: "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", + note: "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z", + card: "M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z", + identity: "M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z", + ssh: "M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z", + api: "M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5", + otp: "M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z", + passkey: "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", +}; + +export function TypeIcon({ type, size = 20, color = "#ffffff" }: IconProps & { type: ItemType }) { + return ( + + + + ); +} + +export function XIcon({ size = 24, color = "#ffffff" }: IconProps) { + return ( + + + + ); +} + +export function ChevronRightIcon({ size = 24, color = "#ffffff" }: IconProps) { + return ( + + + + ); +} + +export function CopyIcon({ size = 18, color = "#ffffff" }: IconProps) { + return ( + + + + ); +} + +export function EyeIcon({ size = 18, color = "#ffffff" }: IconProps) { + return ( + + + + + ); +} + +export function EyeSlashIcon({ size = 18, color = "#ffffff" }: IconProps) { + return ( + + + + ); +} + +export function CheckIcon({ size = 16, color = "#22c55e" }: IconProps) { + return ( + + + + ); +} + +export function PlusIcon({ size = 24, color = "#ffffff" }: IconProps) { + return ( + + + + ); +} + +export function SearchIcon({ size = 20, color = "#666666" }: IconProps) { + return ( + + + + ); +} + +export function StarIcon({ size = 20, color = "#ffffff", filled = false }: IconProps & { filled?: boolean }) { + return ( + + + + ); +} + +export function FaceIdIcon({ size = 32, color = "#ffffff" }: IconProps) { + return ( + + + + + + ); +} + +export function FingerprintIcon({ size = 32, color = "#ffffff" }: IconProps) { + return ( + + + + ); +} + +export function MailIcon({ size = 20, color = "rgba(255,255,255,0.4)" }: IconProps) { + return ( + + + + + ); +} + +export function LockIcon({ size = 20, color = "rgba(255,255,255,0.4)" }: IconProps) { + return ( + + + + + ); +} + +export function StarOutlineIcon({ size = 20, color = "#ffffff" }: IconProps) { + return ( + + + + ); +} + +export function TrashIcon({ size = 20, color = "#ffffff" }: IconProps) { + return ( + + + + ); +} + +export function PencilIcon({ size = 20, color = "#ffffff" }: IconProps) { + return ( + + + + ); +} + +export function ChevronDownIcon({ size = 20, color = "#ffffff" }: IconProps) { + return ( + + + + ); +} + +export function TagIcon({ size = 20, color = "#ffffff" }: IconProps) { + return ( + + + + + ); +} diff --git a/apps/mobile/components/index.tsx b/apps/mobile/components/index.tsx new file mode 100644 index 0000000..48bdf82 --- /dev/null +++ b/apps/mobile/components/index.tsx @@ -0,0 +1,24 @@ +export { Button } from "./button"; +export { Input } from "./input"; +export { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +} from "./card"; +export { Icon } from "./icon"; +export { Avatar, AvatarGroup } from "./avatar"; +export { Badge, TagBadge, TypeBadge } from "./badge"; +export { ToastProvider, useToast } from "./toast"; +export { + Spinner, + Skeleton, + SkeletonText, + SkeletonAvatar, + SkeletonCard, + LoadingDots, +} from "./loading"; +export { Empty } from "./empty"; +export { Header, HeaderButton } from "./header"; diff --git a/apps/mobile/components/input.tsx b/apps/mobile/components/input.tsx new file mode 100644 index 0000000..5e4b291 --- /dev/null +++ b/apps/mobile/components/input.tsx @@ -0,0 +1,150 @@ +import { useState } from "react"; +import { + View, + TextInput, + Text, + Pressable, + StyleSheet, + type TextInputProps, +} from "react-native"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated"; +import Svg, { Path } from "react-native-svg"; + +interface InputProps extends TextInputProps { + label?: string; + error?: string; + type?: "text" | "email" | "password"; +} + +function EyeIcon({ visible }: { visible: boolean }) { + if (visible) { + return ( + + + + + ); + } + return ( + + + + ); +} + +export function Input({ label, error, type = "text", style, ...props }: InputProps) { + const [focused, setFocused] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const borderColor = useSharedValue("rgba(255,255,255,0.1)"); + + const animatedBorder = useAnimatedStyle(() => ({ + borderColor: borderColor.value, + })); + + function handleFocus() { + setFocused(true); + borderColor.value = withTiming(error ? "#ef4444" : "#FF6B00", { duration: 150 }); + } + + function handleBlur() { + setFocused(false); + borderColor.value = withTiming(error ? "#ef4444" : "rgba(255,255,255,0.1)", { duration: 150 }); + } + + return ( + + {label && {label}} + + + {type === "password" && ( + setShowPassword(!showPassword)} + style={styles.toggle} + hitSlop={8} + > + + + )} + + {error && {error}} + + ); +} + +const styles = StyleSheet.create({ + container: { + width: "100%", + }, + label: { + fontSize: 14, + fontWeight: "500", + color: "rgba(255,255,255,0.8)", + marginBottom: 8, + }, + inputContainer: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "rgba(255,255,255,0.05)", + borderWidth: 1, + borderColor: "rgba(255,255,255,0.1)", + borderRadius: 12, + paddingHorizontal: 16, + height: 52, + }, + inputError: { + borderColor: "#ef4444", + }, + input: { + flex: 1, + fontSize: 16, + color: "#ffffff", + height: "100%", + }, + toggle: { + padding: 4, + marginLeft: 8, + }, + error: { + fontSize: 12, + color: "#ef4444", + marginTop: 6, + }, +}); diff --git a/apps/mobile/components/item/card.tsx b/apps/mobile/components/item/card.tsx new file mode 100644 index 0000000..dd78378 --- /dev/null +++ b/apps/mobile/components/item/card.tsx @@ -0,0 +1,80 @@ +import { View, Text, Pressable, StyleSheet } from "react-native"; +import Animated, { FadeInDown } from "react-native-reanimated"; +import { CopyIcon, EyeIcon, EyeSlashIcon, CheckIcon } from "../icons"; +import { colors } from "./constants"; + +interface FieldCardProps { + index: number; + label: string; + value: string; + isSecret: boolean; + isVisible: boolean; + isCopied: boolean; + onToggle: () => void; + onCopy: () => void; + multiline: boolean; +} + +export function FieldCard({ index, label, value, isSecret, isVisible, isCopied, onToggle, onCopy, multiline }: FieldCardProps) { + return ( + + {label} + + + {isSecret && !isVisible ? "••••••••" : value} + + + {isSecret && ( + + {isVisible ? : } + + )} + + {isCopied ? : } + + + + + ); +} + +const styles = StyleSheet.create({ + card: { + backgroundColor: "rgba(255,255,255,0.03)", + borderRadius: 14, + padding: 16, + marginBottom: 10, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.06)", + }, + label: { + fontSize: 12, + fontWeight: "500", + color: colors.muted, + marginBottom: 6, + textTransform: "uppercase", + letterSpacing: 0.3, + }, + row: { + flexDirection: "row", + alignItems: "center", + }, + value: { + flex: 1, + fontSize: 16, + color: colors.text, + fontWeight: "500", + }, + actions: { + flexDirection: "row", + gap: 4, + }, + button: { + width: 36, + height: 36, + borderRadius: 10, + backgroundColor: "rgba(255,255,255,0.06)", + alignItems: "center", + justifyContent: "center", + }, +}); diff --git a/apps/mobile/components/item/constants.ts b/apps/mobile/components/item/constants.ts new file mode 100644 index 0000000..1f61c09 --- /dev/null +++ b/apps/mobile/components/item/constants.ts @@ -0,0 +1,22 @@ +import type { ItemType } from "../../stores"; + +export const colors = { + bg: "#0a0a0a", + surface: "#141414", + border: "#1f1f1f", + accent: "#d4b08c", + text: "#ffffff", + muted: "#666666", + error: "#ef4444", +} as const; + +export const typeColors: Record = { + login: "#3b82f6", + note: "#a855f7", + card: "#22c55e", + identity: "#f59e0b", + ssh: "#06b6d4", + api: "#ec4899", + otp: "#8b5cf6", + passkey: "#14b8a6", +} as const; diff --git a/apps/mobile/components/item/field.tsx b/apps/mobile/components/item/field.tsx new file mode 100644 index 0000000..eae4339 --- /dev/null +++ b/apps/mobile/components/item/field.tsx @@ -0,0 +1,175 @@ +import { useState } from "react"; +import { View, TextInput, Text, Pressable, StyleSheet } from "react-native"; +import Animated, { + useAnimatedStyle, + withTiming, + FadeInDown, +} from "react-native-reanimated"; +import { colors } from "./constants"; + +interface FieldInputProps { + index: number; + label: string; + type: string; + required?: boolean; + half?: boolean; + value: string; + onChange: (value: string) => void; + isPasswordVisible?: boolean; + onTogglePassword?: () => void; +} + +export function FieldInput({ + index, + label, + type, + required, + value, + onChange, + isPasswordVisible, + onTogglePassword, +}: FieldInputProps) { + const [focused, setFocused] = useState(false); + const borderColor = useAnimatedStyle(() => ({ + borderColor: withTiming(focused ? colors.accent + "60" : "rgba(255,255,255,0.08)", { duration: 150 }), + })); + + const isPassword = type === "password"; + const isTextarea = type === "textarea"; + + return ( + + + {label} + {required && *} + + + setFocused(true)} + onBlur={() => setFocused(false)} + secureTextEntry={isPassword && !isPasswordVisible} + autoCapitalize="none" + autoCorrect={false} + multiline={isTextarea} + textAlignVertical={isTextarea ? "top" : "center"} + placeholderTextColor={colors.muted} + /> + {isPassword && ( + + {isPasswordVisible ? : } + + )} + + + ); +} + +function EyeIcon() { + return ( + + + + + ); +} + +function EyeSlashIcon() { + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + gap: 8, + }, + labelRow: { + flexDirection: "row", + alignItems: "center", + gap: 4, + }, + label: { + fontSize: 13, + fontWeight: "500", + color: colors.muted, + textTransform: "uppercase", + letterSpacing: 0.3, + }, + required: { + fontSize: 13, + color: colors.accent, + }, + inputContainer: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "rgba(255,255,255,0.04)", + borderRadius: 12, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.08)", + paddingHorizontal: 14, + minHeight: 52, + }, + textareaContainer: { + minHeight: 100, + alignItems: "flex-start", + paddingVertical: 12, + }, + input: { + flex: 1, + fontSize: 16, + color: colors.text, + fontWeight: "500", + }, + textareaInput: { + height: 76, + }, + toggleButton: { + width: 36, + height: 36, + alignItems: "center", + justifyContent: "center", + }, + eyeIcon: { + width: 20, + height: 20, + position: "relative", + }, + eyeOuter: { + position: "absolute", + width: 14, + height: 10, + borderWidth: 1.5, + borderColor: "rgba(255,255,255,0.4)", + borderRadius: 7, + top: 5, + left: 3, + }, + eyeInner: { + position: "absolute", + width: 4, + height: 4, + backgroundColor: "rgba(255,255,255,0.4)", + borderRadius: 2, + top: 8, + left: 8, + }, + eyeSlash: { + position: "absolute", + width: 20, + height: 1.5, + backgroundColor: "rgba(255,255,255,0.4)", + top: 9, + left: 0, + transform: [{ rotate: "-45deg" }], + }, +}); diff --git a/apps/mobile/components/item/header.tsx b/apps/mobile/components/item/header.tsx new file mode 100644 index 0000000..6a6fc96 --- /dev/null +++ b/apps/mobile/components/item/header.tsx @@ -0,0 +1,140 @@ +import { View, Text, Pressable, StyleSheet } from "react-native"; +import Animated, { FadeIn } from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; +import { TypeIcon, XIcon, ChevronRightIcon } from "../icons"; +import { colors, typeColors } from "./constants"; +import type { ItemType } from "../../stores"; + +interface FormHeaderProps { + type?: ItemType; + title?: string; + onClose: () => void; + onSave?: () => void; + saving?: boolean; + saveDisabled?: boolean; + showBack?: boolean; + onBack?: () => void; +} + +export function FormHeader({ + type, + title, + onClose, + onSave, + saving, + saveDisabled, + showBack, + onBack, +}: FormHeaderProps) { + return ( + + {showBack ? ( + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onBack?.(); + }} + hitSlop={12} + > + + + + + ) : ( + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onClose(); + }} + hitSlop={12} + > + + + )} + + + {type ? ( + + + + {title || type} + + + ) : ( + {title || "new item"} + )} + + + {onSave ? ( + + {saving ? "saving..." : "save"} + + ) : ( + + )} + + ); +} + +const styles = StyleSheet.create({ + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: 16, + paddingVertical: 12, + }, + button: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: "rgba(255,255,255,0.08)", + alignItems: "center", + justifyContent: "center", + }, + center: { + flex: 1, + alignItems: "center", + }, + title: { + fontSize: 17, + fontWeight: "600", + color: colors.text, + }, + badge: { + flexDirection: "row", + alignItems: "center", + gap: 6, + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 8, + }, + badgeText: { + fontSize: 13, + fontWeight: "600", + }, + saveButton: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + backgroundColor: colors.accent, + }, + saveButtonDisabled: { + opacity: 0.5, + }, + saveButtonText: { + fontSize: 14, + fontWeight: "600", + color: colors.bg, + }, + spacer: { + width: 40, + }, +}); diff --git a/apps/mobile/components/item/sheet.tsx b/apps/mobile/components/item/sheet.tsx new file mode 100644 index 0000000..9a07f73 --- /dev/null +++ b/apps/mobile/components/item/sheet.tsx @@ -0,0 +1,121 @@ +import { useCallback } from "react"; +import { View, Text, Pressable, StyleSheet } from "react-native"; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, +} from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; +import BottomSheet, { BottomSheetView, BottomSheetBackdrop } from "@gorhom/bottom-sheet"; +import { colors } from "./constants"; + +interface SheetProps { + open: boolean; + onClose: () => void; + children: React.ReactNode; +} + +export function Sheet({ open, onClose, children }: SheetProps) { + const renderBackdrop = useCallback( + (props: any) => ( + + ), + [] + ); + + if (!open) return null; + + return ( + + {children} + + ); +} + +interface SheetItemProps { + icon: React.ReactNode; + label: string; + destructive?: boolean; + onPress: () => void; +} + +export function SheetItem({ icon, label, destructive, onPress }: SheetItemProps) { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + return ( + { + scale.value = withSpring(0.98, { damping: 20 }); + }} + onPressOut={() => { + scale.value = withSpring(1, { damping: 20 }); + }} + onPress={() => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onPress(); + }} + > + + {icon} + + {label} + + + + ); +} + +export function SheetDivider() { + return ; +} + +const styles = StyleSheet.create({ + background: { + backgroundColor: colors.surface, + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + }, + handle: { + backgroundColor: "rgba(255,255,255,0.2)", + width: 40, + }, + content: { + padding: 20, + }, + item: { + flexDirection: "row", + alignItems: "center", + gap: 14, + paddingVertical: 14, + }, + itemText: { + fontSize: 16, + color: colors.text, + fontWeight: "500", + }, + itemDestructive: { + color: colors.error, + }, + divider: { + height: 1, + backgroundColor: "rgba(255,255,255,0.08)", + marginVertical: 8, + }, +}); diff --git a/apps/mobile/components/item/tags.tsx b/apps/mobile/components/item/tags.tsx new file mode 100644 index 0000000..ecbc115 --- /dev/null +++ b/apps/mobile/components/item/tags.tsx @@ -0,0 +1,120 @@ +import { View, TextInput, Text, Pressable, StyleSheet } from "react-native"; +import Animated, { FadeInDown } from "react-native-reanimated"; +import * as Haptics from "expo-haptics"; +import { XIcon, PlusIcon, TagIcon } from "../icons"; +import { colors } from "./constants"; + +interface TagsInputProps { + tags: string[]; + newTag: string; + onNewTagChange: (value: string) => void; + onAddTag: () => void; + onRemoveTag: (tag: string) => void; +} + +export function TagsInput({ tags, newTag, onNewTagChange, onAddTag, onRemoveTag }: TagsInputProps) { + function handleRemove(tag: string) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onRemoveTag(tag); + } + + function handleAdd() { + if (newTag.trim()) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onAddTag(); + } + } + + return ( + + tags + + {tags.map((tag) => ( + handleRemove(tag)}> + + {tag} + + + ))} + + + {newTag.trim() && ( + + + + )} + + + + ); +} + +const styles = StyleSheet.create({ + section: { + marginTop: 8, + }, + label: { + fontSize: 13, + fontWeight: "600", + color: colors.muted, + textTransform: "uppercase", + letterSpacing: 0.5, + marginBottom: 12, + }, + container: { + flexDirection: "row", + flexWrap: "wrap", + gap: 8, + }, + tag: { + flexDirection: "row", + alignItems: "center", + gap: 6, + backgroundColor: "rgba(255,255,255,0.06)", + paddingLeft: 12, + paddingRight: 8, + paddingVertical: 8, + borderRadius: 8, + }, + tagText: { + fontSize: 13, + color: "rgba(255,255,255,0.7)", + fontWeight: "500", + }, + inputContainer: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "rgba(255,255,255,0.04)", + borderRadius: 8, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.08)", + borderStyle: "dashed", + paddingLeft: 12, + paddingRight: 4, + minWidth: 100, + }, + input: { + flex: 1, + fontSize: 13, + color: colors.text, + paddingVertical: 8, + }, + addButton: { + width: 28, + height: 28, + borderRadius: 6, + backgroundColor: colors.accent + "20", + alignItems: "center", + justifyContent: "center", + }, +}); diff --git a/apps/mobile/components/item/totp.tsx b/apps/mobile/components/item/totp.tsx new file mode 100644 index 0000000..da6fbd0 --- /dev/null +++ b/apps/mobile/components/item/totp.tsx @@ -0,0 +1,85 @@ +import { View, Text, Pressable, StyleSheet } from "react-native"; +import Animated, { FadeInDown } from "react-native-reanimated"; +import { CopyIcon, CheckIcon } from "../icons"; +import { colors } from "./constants"; + +interface TotpSectionProps { + code: string; + remaining: number; + onCopy: () => void; + copied: boolean; +} + +export function TotpSection({ code, remaining, onCopy, copied }: TotpSectionProps) { + return ( + + + one-time code + + + + {remaining}s + + + {code} + {copied ? : } + + + ); +} + +const styles = StyleSheet.create({ + section: { + marginBottom: 24, + }, + header: { + flexDirection: "row", + alignItems: "center", + marginBottom: 12, + }, + label: { + fontSize: 13, + fontWeight: "600", + color: colors.muted, + textTransform: "uppercase", + letterSpacing: 0.5, + }, + timer: { + flex: 1, + height: 3, + backgroundColor: "rgba(255,255,255,0.1)", + borderRadius: 2, + marginLeft: 12, + overflow: "hidden", + }, + timerBar: { + height: "100%", + backgroundColor: colors.accent, + borderRadius: 2, + }, + timerText: { + fontSize: 12, + color: colors.muted, + marginLeft: 8, + width: 24, + textAlign: "right", + }, + card: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + backgroundColor: "rgba(255,255,255,0.05)", + borderRadius: 14, + paddingHorizontal: 20, + paddingVertical: 16, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.08)", + }, + code: { + fontSize: 32, + fontWeight: "700", + color: colors.text, + letterSpacing: 8, + fontVariant: ["tabular-nums"], + }, +}); diff --git a/apps/mobile/components/item/utils.tsx b/apps/mobile/components/item/utils.tsx new file mode 100644 index 0000000..687c975 --- /dev/null +++ b/apps/mobile/components/item/utils.tsx @@ -0,0 +1,32 @@ +import { View, StyleSheet } from "react-native"; + +export function MoreIcon() { + return ( + + + + + + ); +} + +export function formatDate(date: string) { + return new Date(date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); +} + +const styles = StyleSheet.create({ + moreIcon: { + flexDirection: "row", + gap: 3, + }, + moreDot: { + width: 4, + height: 4, + borderRadius: 2, + backgroundColor: "rgba(255,255,255,0.6)", + }, +}); diff --git a/apps/mobile/components/itemdetail.tsx b/apps/mobile/components/itemdetail.tsx new file mode 100644 index 0000000..3dc83ef --- /dev/null +++ b/apps/mobile/components/itemdetail.tsx @@ -0,0 +1,261 @@ +import { View, Text, ScrollView, Pressable, StyleSheet } from "react-native"; +import * as Haptics from "expo-haptics"; +import { TypeIcon, StarIcon, StarOutlineIcon, PencilIcon, TrashIcon, XIcon } from "./icons"; +import { FieldRow } from "./fieldrow"; +import type { VaultItem, LoginData, CardData, IdentityData, SshData, ApiData, OtpData, PasskeyData, NoteData } from "./types"; +import { typeLabels } from "./types"; + +interface ItemDetailProps { + item: VaultItem; + onClose: () => void; + onEdit: () => void; + onDelete: () => void; + onFavorite: () => void; +} + +export function ItemDetail({ item, onClose, onEdit, onDelete, onFavorite }: ItemDetailProps) { + function handleFavorite() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onFavorite(); + } + + function renderFields() { + switch (item.type) { + case "login": { + const d = item.data as LoginData; + return ( + <> + + + + {d.totp && } + + + ); + } + case "note": { + const d = item.data as NoteData; + return ; + } + case "card": { + const d = item.data as CardData; + return ( + <> + + + + + {d.pin && } + + + ); + } + case "identity": { + const d = item.data as IdentityData; + const name = [d.firstname, d.lastname].filter(Boolean).join(" "); + const location = [d.city, d.state, d.zip, d.country].filter(Boolean).join(", "); + return ( + <> + {name && } + + + + {location && } + + + ); + } + case "ssh": { + const d = item.data as SshData; + return ( + <> + + + + + + ); + } + case "api": { + const d = item.data as ApiData; + return ( + <> + + + + + + ); + } + case "otp": { + const d = item.data as OtpData; + return ( + <> + + + + + + ); + } + case "passkey": { + const d = item.data as PasskeyData; + return ( + <> + + + + + + + ); + } + default: + return null; + } + } + + return ( + + + + + + + + {item.favorite ? : } + + + + + + + + + + + + + + + + {item.title} + {typeLabels[item.type]} + {item.tags.length > 0 && ( + + {item.tags.map((tag) => ( + + {tag} + + ))} + + )} + + + {renderFields()} + + + created {new Date(item.createdAt).toLocaleDateString()} + updated {new Date(item.updatedAt).toLocaleDateString()} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#0a0a0a", + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: 16, + paddingTop: 16, + paddingBottom: 8, + }, + closeButton: { + width: 40, + height: 40, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(255,255,255,0.05)", + borderRadius: 12, + }, + headerActions: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + headerButton: { + width: 40, + height: 40, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(255,255,255,0.05)", + borderRadius: 12, + }, + scroll: { + flex: 1, + }, + hero: { + alignItems: "center", + paddingVertical: 24, + paddingHorizontal: 16, + borderBottomWidth: 1, + borderBottomColor: "rgba(255,255,255,0.06)", + }, + iconLarge: { + width: 72, + height: 72, + borderRadius: 20, + backgroundColor: "rgba(212,176,140,0.1)", + alignItems: "center", + justifyContent: "center", + marginBottom: 16, + }, + title: { + fontSize: 24, + fontWeight: "700", + color: "#fff", + textAlign: "center", + marginBottom: 4, + }, + type: { + fontSize: 14, + color: "rgba(255,255,255,0.4)", + textTransform: "lowercase", + }, + tags: { + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "center", + gap: 8, + marginTop: 16, + }, + tag: { + paddingHorizontal: 12, + paddingVertical: 6, + backgroundColor: "rgba(255,255,255,0.08)", + borderRadius: 8, + }, + tagText: { + fontSize: 13, + color: "rgba(255,255,255,0.6)", + }, + fields: { + padding: 16, + }, + meta: { + padding: 16, + paddingBottom: 40, + alignItems: "center", + gap: 4, + }, + metaText: { + fontSize: 12, + color: "rgba(255,255,255,0.3)", + }, +}); diff --git a/apps/mobile/components/itemform.tsx b/apps/mobile/components/itemform.tsx new file mode 100644 index 0000000..b56ec18 --- /dev/null +++ b/apps/mobile/components/itemform.tsx @@ -0,0 +1,339 @@ +import { useState, useRef, useEffect } from "react"; +import { + View, + Text, + TextInput, + Pressable, + StyleSheet, + ScrollView, + KeyboardAvoidingView, + Platform, +} from "react-native"; +import * as Haptics from "expo-haptics"; +import { XIcon, EyeIcon, EyeSlashIcon, CheckIcon } from "./icons"; +import { TypePicker } from "./typepicker"; +import { TagsInput } from "./tagsinput"; +import type { ItemType, VaultItem, FieldConfig, ItemDataMap } from "./types"; +import { fieldConfigs, typeLabels } from "./types"; + +interface ItemFormProps { + item?: VaultItem | null; + defaultType?: ItemType; + onSave: (data: { type: ItemType; title: string; data: Record; tags: string[] }) => void; + onClose: () => void; + allTags?: string[]; +} + +export function ItemForm({ item, defaultType, onSave, onClose, allTags = [] }: ItemFormProps) { + const [type, setType] = useState(item?.type || defaultType || "login"); + const [title, setTitle] = useState(item?.title || ""); + const [data, setData] = useState>(item?.data || {}); + const [tags, setTags] = useState(item?.tags || []); + const titleRef = useRef(null); + + useEffect(() => { + if (!item) setData({}); + }, [type, item]); + + useEffect(() => { + setTimeout(() => titleRef.current?.focus(), 100); + }, []); + + function handleSave() { + if (!title.trim()) { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + return; + } + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + onSave({ type, title: title.trim(), data, tags }); + } + + function handleClose() { + Haptics.selectionAsync(); + onClose(); + } + + const fields = fieldConfigs[type]; + + return ( + + + + + + {item ? "edit" : "new"} item + + + + + + + {!item && } + + + title + + + + {renderFields(fields, data, setData)} + + + + + + + cancel + + + save + + + + ); +} + +function renderFields( + fields: FieldConfig[], + data: Record, + setData: (d: Record) => void +) { + const result: React.ReactNode[] = []; + let i = 0; + while (i < fields.length) { + const field = fields[i]; + const nextField = fields[i + 1]; + if (field.half && nextField?.half) { + result.push( + + + setData({ ...data, [field.name]: v })} /> + + + setData({ ...data, [nextField.name]: v })} /> + + + ); + i += 2; + } else { + result.push( + + setData({ ...data, [field.name]: v })} /> + + ); + i += 1; + } + } + return result; +} + +interface FormFieldProps { + field: FieldConfig; + value: unknown; + onChange: (value: unknown) => void; +} + +function FormField({ field, value, onChange }: FormFieldProps) { + const [show, setShow] = useState(false); + + if (field.type === "textarea") { + return ( + + {field.label} + + + ); + } + + if (field.type === "password") { + return ( + + {field.label} + + + setShow(!show)} style={styles.passwordToggle} hitSlop={8}> + {show ? ( + + ) : ( + + )} + + + + ); + } + + return ( + + {field.label} + onChange(field.type === "number" ? Number(v) || undefined : v)} + placeholder={field.label} + placeholderTextColor="rgba(255,255,255,0.3)" + style={styles.input} + keyboardType={field.type === "number" ? "numeric" : field.type === "email" ? "email-address" : field.type === "url" ? "url" : "default"} + autoCapitalize={field.type === "email" || field.type === "url" ? "none" : "sentences"} + autoCorrect={false} + /> + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#0a0a0a", + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: 16, + paddingTop: 16, + paddingBottom: 12, + borderBottomWidth: 1, + borderBottomColor: "rgba(255,255,255,0.06)", + }, + closeButton: { + width: 40, + height: 40, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(255,255,255,0.05)", + borderRadius: 12, + }, + headerTitle: { + fontSize: 17, + fontWeight: "600", + color: "#fff", + }, + saveButton: { + width: 40, + height: 40, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(212,176,140,0.15)", + borderRadius: 12, + }, + scroll: { + flex: 1, + }, + scrollContent: { + padding: 20, + paddingBottom: 40, + }, + field: { + marginBottom: 16, + }, + label: { + fontSize: 12, + color: "rgba(255,255,255,0.4)", + marginBottom: 8, + textTransform: "lowercase", + marginLeft: 4, + }, + input: { + backgroundColor: "rgba(255,255,255,0.04)", + borderRadius: 14, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.08)", + paddingHorizontal: 16, + paddingVertical: 14, + fontSize: 15, + color: "#fff", + }, + textarea: { + minHeight: 100, + paddingTop: 14, + }, + passwordContainer: { + position: "relative", + }, + passwordInput: { + paddingRight: 50, + }, + passwordToggle: { + position: "absolute", + right: 12, + top: 0, + bottom: 0, + width: 40, + alignItems: "center", + justifyContent: "center", + }, + row: { + flexDirection: "row", + gap: 12, + }, + halfField: { + flex: 1, + }, + footer: { + flexDirection: "row", + gap: 12, + padding: 16, + paddingBottom: 40, + borderTopWidth: 1, + borderTopColor: "rgba(255,255,255,0.06)", + }, + cancelButton: { + flex: 1, + paddingVertical: 16, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(255,255,255,0.05)", + borderRadius: 14, + }, + cancelText: { + fontSize: 15, + fontWeight: "600", + color: "rgba(255,255,255,0.6)", + }, + submitButton: { + flex: 1, + paddingVertical: 16, + alignItems: "center", + justifyContent: "center", + backgroundColor: "#d4b08c", + borderRadius: 14, + }, + submitText: { + fontSize: 15, + fontWeight: "600", + color: "#0a0a0a", + }, +}); diff --git a/apps/mobile/components/itemrow.tsx b/apps/mobile/components/itemrow.tsx new file mode 100644 index 0000000..dfdea0b --- /dev/null +++ b/apps/mobile/components/itemrow.tsx @@ -0,0 +1,263 @@ +import { View, Text, Pressable, StyleSheet } from "react-native"; +import * as Haptics from "expo-haptics"; +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, + runOnJS, + useAnimatedGestureHandler, +} from "react-native-reanimated"; +import { PanGestureHandler } from "react-native-gesture-handler"; +import { TypeIcon, StarIcon, StarOutlineIcon, TrashIcon, PencilIcon } from "./icons"; +import type { VaultItem, ItemType, LoginData, CardData, IdentityData, ApiData, OtpData, PasskeyData } from "./types"; + +interface ItemRowProps { + item: VaultItem; + onPress: () => void; + onFavorite: () => void; + onEdit?: () => void; + onDelete?: () => void; +} + +function getSubtitle(item: VaultItem): string | null { + const data = item.data; + switch (item.type) { + case "login": + return (data as LoginData).username || (data as LoginData).url || null; + case "card": + return (data as CardData).holder || null; + case "identity": { + const id = data as IdentityData; + if (id.firstname || id.lastname) { + return [id.firstname, id.lastname].filter(Boolean).join(" "); + } + return id.email || null; + } + case "api": + return (data as ApiData).endpoint || null; + case "otp": + return (data as OtpData).issuer || (data as OtpData).account || null; + case "passkey": + return (data as PasskeyData).rpid || null; + default: + return null; + } +} + +const ACTION_WIDTH = 70; +const SWIPE_THRESHOLD = 50; + +export function ItemRow({ item, onPress, onFavorite, onEdit, onDelete }: ItemRowProps) { + const subtitle = getSubtitle(item); + const translateX = useSharedValue(0); + const contextX = useSharedValue(0); + + const gestureHandler = useAnimatedGestureHandler({ + onStart: () => { + contextX.value = translateX.value; + }, + onActive: (event) => { + const newX = contextX.value + event.translationX; + if (onEdit && newX > 0) { + translateX.value = Math.min(newX, ACTION_WIDTH); + } else if (onDelete && newX < 0) { + translateX.value = Math.max(newX, -ACTION_WIDTH); + } + }, + onEnd: (event) => { + if (event.translationX > SWIPE_THRESHOLD && onEdit) { + translateX.value = withTiming(ACTION_WIDTH); + runOnJS(Haptics.impactAsync)(Haptics.ImpactFeedbackStyle.Light); + } else if (event.translationX < -SWIPE_THRESHOLD && onDelete) { + translateX.value = withTiming(-ACTION_WIDTH); + runOnJS(Haptics.impactAsync)(Haptics.ImpactFeedbackStyle.Light); + } else { + translateX.value = withTiming(0); + } + }, + }); + + const rowStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: translateX.value }], + })); + + const leftActionStyle = useAnimatedStyle(() => ({ + opacity: translateX.value > 0 ? 1 : 0, + width: translateX.value > 0 ? translateX.value : 0, + })); + + const rightActionStyle = useAnimatedStyle(() => ({ + opacity: translateX.value < 0 ? 1 : 0, + width: translateX.value < 0 ? -translateX.value : 0, + })); + + function handlePress() { + translateX.value = withTiming(0); + onPress(); + } + + function handleFavorite() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onFavorite(); + } + + function handleEdit() { + translateX.value = withTiming(0); + onEdit?.(); + } + + function handleDelete() { + translateX.value = withTiming(0); + onDelete?.(); + } + + return ( + + {onEdit && ( + + + + + + )} + {onDelete && ( + + + + + + )} + + + + + + + + + {item.title} + + {subtitle && ( + + {subtitle} + + )} + {item.tags.length > 0 && ( + + {item.tags.slice(0, 2).map((tag) => ( + + {tag} + + ))} + {item.tags.length > 2 && ( + +{item.tags.length - 2} + )} + + )} + + + {item.favorite ? : } + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + position: "relative", + marginHorizontal: 16, + marginBottom: 8, + borderRadius: 14, + overflow: "hidden", + }, + leftAction: { + position: "absolute", + left: 0, + top: 0, + bottom: 0, + backgroundColor: "rgba(212,176,140,0.15)", + justifyContent: "center", + alignItems: "center", + borderTopLeftRadius: 14, + borderBottomLeftRadius: 14, + }, + rightAction: { + position: "absolute", + right: 0, + top: 0, + bottom: 0, + backgroundColor: "rgba(239,68,68,0.15)", + justifyContent: "center", + alignItems: "center", + borderTopRightRadius: 14, + borderBottomRightRadius: 14, + }, + actionButton: { + width: "100%", + height: "100%", + alignItems: "center", + justifyContent: "center", + }, + row: { + backgroundColor: "rgba(255,255,255,0.04)", + borderRadius: 14, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.06)", + }, + content: { + flexDirection: "row", + alignItems: "center", + padding: 14, + gap: 12, + }, + icon: { + width: 40, + height: 40, + borderRadius: 10, + backgroundColor: "rgba(255,255,255,0.05)", + alignItems: "center", + justifyContent: "center", + }, + text: { + flex: 1, + gap: 2, + }, + title: { + fontSize: 15, + fontWeight: "600", + color: "#fff", + }, + subtitle: { + fontSize: 13, + color: "rgba(255,255,255,0.5)", + }, + tags: { + flexDirection: "row", + alignItems: "center", + gap: 6, + marginTop: 4, + }, + tag: { + paddingHorizontal: 8, + paddingVertical: 3, + backgroundColor: "rgba(255,255,255,0.08)", + borderRadius: 6, + }, + tagText: { + fontSize: 11, + color: "rgba(255,255,255,0.5)", + }, + tagMore: { + fontSize: 11, + color: "rgba(255,255,255,0.3)", + }, + star: { + width: 36, + height: 36, + alignItems: "center", + justifyContent: "center", + }, +}); diff --git a/apps/mobile/components/loading.tsx b/apps/mobile/components/loading.tsx new file mode 100644 index 0000000..cf06895 --- /dev/null +++ b/apps/mobile/components/loading.tsx @@ -0,0 +1,296 @@ +import { useEffect } from "react"; +import { View, type ViewStyle } from "react-native"; +import Animated, { + useSharedValue, + useAnimatedStyle, + withRepeat, + withTiming, + withSequence, + withDelay, + Easing, +} from "react-native-reanimated"; + +type Size = "sm" | "md" | "lg"; + +interface SpinnerProps { + size?: Size; + color?: string; + style?: ViewStyle; +} + +const sizes: Record = { + sm: 18, + md: 24, + lg: 32, +}; + +export function Spinner({ + size = "md", + color = "#d4b08c", + style, +}: SpinnerProps) { + const rotation = useSharedValue(0); + const dimension = sizes[size]; + + useEffect(() => { + rotation.value = withRepeat( + withTiming(360, { duration: 800, easing: Easing.linear }), + -1, + false + ); + }, []); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${rotation.value}deg` }], + })); + + return ( + + + + ); +} + +interface SkeletonProps { + width?: number | `${number}%`; + height?: number; + radius?: number; + style?: ViewStyle; +} + +export function Skeleton({ + width = "100%", + height = 16, + radius = 8, + style, +}: SkeletonProps) { + const opacity = useSharedValue(0.3); + + useEffect(() => { + opacity.value = withRepeat( + withSequence( + withTiming(0.6, { duration: 800 }), + withTiming(0.3, { duration: 800 }) + ), + -1, + false + ); + }, []); + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); + + return ( + + ); +} + +interface SkeletonTextProps { + lines?: number; + style?: ViewStyle; +} + +export function SkeletonText({ lines = 1, style }: SkeletonTextProps) { + return ( + + {Array.from({ length: lines }).map((_, i) => ( + 1 ? "75%" : "100%"} + height={14} + /> + ))} + + ); +} + +interface SkeletonAvatarProps { + size?: Size; + style?: ViewStyle; +} + +const avatarSizes: Record = { + sm: 32, + md: 40, + lg: 52, +}; + +export function SkeletonAvatar({ size = "md", style }: SkeletonAvatarProps) { + const dimension = avatarSizes[size]; + return ( + + ); +} + +interface SkeletonCardProps { + style?: ViewStyle; +} + +export function SkeletonCard({ style }: SkeletonCardProps) { + return ( + + + + + + + + + + + + + + ); +} + +interface LoadingDotsProps { + size?: Size; + color?: string; + style?: ViewStyle; +} + +const dotSizes: Record = { + sm: 6, + md: 8, + lg: 10, +}; + +export function LoadingDots({ + size = "md", + color = "#d4b08c", + style, +}: LoadingDotsProps) { + const dot1 = useSharedValue(1); + const dot2 = useSharedValue(1); + const dot3 = useSharedValue(1); + const dotSize = dotSizes[size]; + + useEffect(() => { + dot1.value = withRepeat( + withSequence( + withTiming(1.3, { duration: 300 }), + withTiming(1, { duration: 300 }) + ), + -1 + ); + dot2.value = withDelay( + 150, + withRepeat( + withSequence( + withTiming(1.3, { duration: 300 }), + withTiming(1, { duration: 300 }) + ), + -1 + ) + ); + dot3.value = withDelay( + 300, + withRepeat( + withSequence( + withTiming(1.3, { duration: 300 }), + withTiming(1, { duration: 300 }) + ), + -1 + ) + ); + }, []); + + const dot1Style = useAnimatedStyle(() => ({ + transform: [{ scale: dot1.value }], + })); + + const dot2Style = useAnimatedStyle(() => ({ + transform: [{ scale: dot2.value }], + })); + + const dot3Style = useAnimatedStyle(() => ({ + transform: [{ scale: dot3.value }], + })); + + return ( + + + + + + ); +} diff --git a/apps/mobile/components/logo.tsx b/apps/mobile/components/logo.tsx new file mode 100644 index 0000000..6ad7319 --- /dev/null +++ b/apps/mobile/components/logo.tsx @@ -0,0 +1,17 @@ +import Svg, { Path } from "react-native-svg"; + +interface LogoProps { + size?: number; + color?: string; +} + +export function Logo({ size = 24, color = "#FF6B00" }: LogoProps) { + return ( + + + + ); +} diff --git a/apps/mobile/components/tagsinput.tsx b/apps/mobile/components/tagsinput.tsx new file mode 100644 index 0000000..dfc0d8c --- /dev/null +++ b/apps/mobile/components/tagsinput.tsx @@ -0,0 +1,242 @@ +import { useState, useRef } from "react"; +import { View, Text, TextInput, Pressable, StyleSheet, ScrollView } from "react-native"; +import * as Haptics from "expo-haptics"; +import { XIcon, TagIcon } from "./icons"; + +interface TagsInputProps { + tags: string[]; + onChange: (tags: string[]) => void; + suggestions?: string[]; + placeholder?: string; +} + +export function TagsInput({ tags, onChange, suggestions = [], placeholder = "add tags..." }: TagsInputProps) { + const [input, setInput] = useState(""); + const [focused, setFocused] = useState(false); + const inputRef = useRef(null); + + const filtered = suggestions.filter( + (s) => s.toLowerCase().includes(input.toLowerCase()) && !tags.includes(s) + ); + + function addTag(value: string) { + const tag = value.trim().toLowerCase(); + if (tag && !tags.includes(tag)) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onChange([...tags, tag]); + } + setInput(""); + } + + function removeTag(tag: string) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + onChange(tags.filter((t) => t !== tag)); + } + + function handleSubmit() { + if (input.trim()) { + addTag(input); + } + } + + function handleKeyPress(e: { nativeEvent: { key: string } }) { + if (e.nativeEvent.key === "Backspace" && !input && tags.length > 0) { + removeTag(tags[tags.length - 1]); + } + } + + return ( + + + + tags + + inputRef.current?.focus()} + > + + {tags.map((tag) => ( + + {tag} + removeTag(tag)} style={styles.tagRemove} hitSlop={8}> + + + + ))} + setFocused(true)} + onBlur={() => { + setFocused(false); + if (input.trim()) addTag(input); + }} + placeholder={tags.length === 0 ? placeholder : ""} + placeholderTextColor="rgba(255,255,255,0.3)" + style={styles.input} + returnKeyType="done" + autoCapitalize="none" + autoCorrect={false} + /> + + + {focused && input && filtered.length > 0 && ( + + {filtered.slice(0, 5).map((suggestion) => ( + addTag(suggestion)} + style={styles.suggestion} + > + {suggestion} + + ))} + + )} + + ); +} + +interface TagListProps { + tags: string[]; + max?: number; + onPress?: (tag: string) => void; +} + +export function TagList({ tags, max = 3, onPress }: TagListProps) { + const visible = tags.slice(0, max); + const remaining = tags.length - max; + + if (tags.length === 0) return null; + + return ( + + {visible.map((tag) => ( + onPress?.(tag)} + style={styles.tagListItem} + disabled={!onPress} + > + {tag} + + ))} + {remaining > 0 && +{remaining}} + + ); +} + +const styles = StyleSheet.create({ + container: { + marginBottom: 16, + }, + labelRow: { + flexDirection: "row", + alignItems: "center", + gap: 6, + marginBottom: 10, + marginLeft: 4, + }, + label: { + fontSize: 12, + color: "rgba(255,255,255,0.4)", + textTransform: "lowercase", + }, + inputContainer: { + backgroundColor: "rgba(255,255,255,0.04)", + borderRadius: 14, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.08)", + paddingHorizontal: 12, + paddingVertical: 10, + minHeight: 52, + }, + inputContainerFocused: { + borderColor: "rgba(212,176,140,0.4)", + backgroundColor: "rgba(212,176,140,0.05)", + }, + tagsRow: { + flexDirection: "row", + flexWrap: "wrap", + alignItems: "center", + gap: 8, + }, + tag: { + flexDirection: "row", + alignItems: "center", + gap: 6, + paddingLeft: 10, + paddingRight: 6, + paddingVertical: 6, + backgroundColor: "rgba(255,255,255,0.08)", + borderRadius: 8, + }, + tagText: { + fontSize: 13, + color: "rgba(255,255,255,0.7)", + }, + tagRemove: { + width: 20, + height: 20, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(255,255,255,0.08)", + borderRadius: 6, + }, + input: { + flex: 1, + minWidth: 80, + fontSize: 14, + color: "#fff", + paddingVertical: 4, + }, + suggestions: { + marginTop: 10, + }, + suggestionsContent: { + gap: 8, + }, + suggestion: { + paddingHorizontal: 14, + paddingVertical: 8, + backgroundColor: "rgba(212,176,140,0.1)", + borderRadius: 10, + borderWidth: 1, + borderColor: "rgba(212,176,140,0.2)", + }, + suggestionText: { + fontSize: 13, + color: "#d4b08c", + }, + tagList: { + flexDirection: "row", + flexWrap: "wrap", + alignItems: "center", + gap: 6, + }, + tagListItem: { + paddingHorizontal: 10, + paddingVertical: 4, + backgroundColor: "rgba(255,255,255,0.08)", + borderRadius: 6, + }, + tagListText: { + fontSize: 12, + color: "rgba(255,255,255,0.6)", + }, + tagListMore: { + fontSize: 12, + color: "rgba(255,255,255,0.3)", + marginLeft: 2, + }, +}); diff --git a/apps/mobile/components/toast.tsx b/apps/mobile/components/toast.tsx new file mode 100644 index 0000000..cb82ebf --- /dev/null +++ b/apps/mobile/components/toast.tsx @@ -0,0 +1,312 @@ +import { + createContext, + useContext, + useState, + useCallback, + useEffect, + useRef, + type ReactNode, +} from "react"; +import { View, Text, Pressable, Dimensions } from "react-native"; +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, + SlideInUp, + SlideOutUp, +} from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import * as Haptics from "expo-haptics"; + +type Variant = "success" | "error" | "info"; + +interface Toast { + id: string; + title: string; + description?: string; + variant: Variant; +} + +interface ToastContextValue { + toast: (options: Omit) => void; +} + +const ToastContext = createContext({ + toast: () => {}, +}); + +export function useToast() { + return useContext(ToastContext); +} + +const { width } = Dimensions.get("window"); + +interface ToastItemProps { + toast: Toast; + onDismiss: () => void; +} + +function ToastItem({ toast, onDismiss }: ToastItemProps) { + const progress = useSharedValue(1); + const timerRef = useRef | null>(null); + + useEffect(() => { + Haptics.notificationAsync( + toast.variant === "success" + ? Haptics.NotificationFeedbackType.Success + : toast.variant === "error" + ? Haptics.NotificationFeedbackType.Error + : Haptics.NotificationFeedbackType.Warning + ); + + progress.value = withTiming(0, { duration: 5000 }); + timerRef.current = setTimeout(onDismiss, 5000); + + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + const progressStyle = useAnimatedStyle(() => ({ + width: `${progress.value * 100}%`, + })); + + const variantColors: Record = { + success: { icon: "#4ade80", progress: "#22c55e" }, + error: { icon: "#f87171", progress: "#ef4444" }, + info: { icon: "#60a5fa", progress: "#3b82f6" }, + }; + + const colors = variantColors[toast.variant]; + + return ( + + + + {toast.variant === "success" ? ( + + ) : toast.variant === "error" ? ( + + ) : ( + + )} + + + + {toast.title} + + {toast.description ? ( + + {toast.description} + + ) : null} + + + + + + + + ); +} + +interface ToastProviderProps { + children: ReactNode; +} + +export function ToastProvider({ children }: ToastProviderProps) { + const [toasts, setToasts] = useState([]); + const insets = useSafeAreaInsets(); + + const toast = useCallback((options: Omit) => { + const id = Math.random().toString(36).slice(2); + setToasts((prev) => [...prev, { ...options, id }]); + }, []); + + const dismiss = useCallback((id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, []); + + return ( + + {children} + + {toasts.map((t) => ( + dismiss(t.id)} /> + ))} + + + ); +} + +function CheckIcon({ color, size = 16 }: { color: string; size?: number }) { + return ( + + + + + ); +} + +function CloseIcon({ color, size = 16 }: { color: string; size?: number }) { + return ( + + + + + ); +} + +function InfoIcon({ color, size = 16 }: { color: string; size?: number }) { + return ( + + + + + + ); +} diff --git a/apps/mobile/components/typepicker.tsx b/apps/mobile/components/typepicker.tsx new file mode 100644 index 0000000..b487d32 --- /dev/null +++ b/apps/mobile/components/typepicker.tsx @@ -0,0 +1,209 @@ +import { View, Text, Pressable, StyleSheet, ScrollView } from "react-native"; +import * as Haptics from "expo-haptics"; +import { TypeIcon } from "./icons"; +import type { ItemType } from "./types"; +import { typeLabels } from "./types"; + +interface TypePickerProps { + value: ItemType; + onChange: (type: ItemType) => void; +} + +const types: ItemType[] = ["login", "note", "card", "identity", "ssh", "api", "otp", "passkey"]; + +export function TypePicker({ value, onChange }: TypePickerProps) { + function handleSelect(type: ItemType) { + if (type !== value) { + Haptics.selectionAsync(); + onChange(type); + } + } + + return ( + + type + + {types.map((type) => { + const selected = type === value; + return ( + handleSelect(type)} + style={[styles.option, selected && styles.optionSelected]} + > + + + {typeLabels[type].replace(/s$/, "")} + + + ); + })} + + + ); +} + +interface TypePickerModalProps { + value: ItemType; + onChange: (type: ItemType) => void; + onClose: () => void; +} + +export function TypePickerModal({ value, onChange, onClose }: TypePickerModalProps) { + function handleSelect(type: ItemType) { + Haptics.selectionAsync(); + onChange(type); + onClose(); + } + + return ( + + + select type + + done + + + + {types.map((type) => { + const selected = type === value; + return ( + handleSelect(type)} + style={[styles.gridItem, selected && styles.gridItemSelected]} + > + + + + + {typeLabels[type].replace(/s$/, "")} + + + ); + })} + + + ); +} + +const styles = StyleSheet.create({ + container: { + marginBottom: 20, + }, + label: { + fontSize: 12, + color: "rgba(255,255,255,0.4)", + marginBottom: 10, + textTransform: "lowercase", + marginLeft: 4, + }, + scroll: { + gap: 8, + }, + option: { + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingHorizontal: 14, + paddingVertical: 10, + backgroundColor: "rgba(255,255,255,0.04)", + borderRadius: 12, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.06)", + }, + optionSelected: { + backgroundColor: "rgba(212,176,140,0.12)", + borderColor: "rgba(212,176,140,0.3)", + }, + optionText: { + fontSize: 14, + color: "rgba(255,255,255,0.5)", + }, + optionTextSelected: { + color: "#d4b08c", + fontWeight: "500", + }, + modal: { + backgroundColor: "#0a0a0a", + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + paddingBottom: 40, + }, + modalHeader: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: 20, + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: "rgba(255,255,255,0.06)", + }, + modalTitle: { + fontSize: 17, + fontWeight: "600", + color: "#fff", + }, + modalClose: { + paddingHorizontal: 12, + paddingVertical: 6, + }, + modalCloseText: { + fontSize: 15, + color: "#d4b08c", + fontWeight: "500", + }, + grid: { + flexDirection: "row", + flexWrap: "wrap", + padding: 16, + gap: 12, + }, + gridItem: { + width: "22%", + aspectRatio: 1, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(255,255,255,0.04)", + borderRadius: 16, + borderWidth: 1, + borderColor: "rgba(255,255,255,0.06)", + gap: 8, + }, + gridItemSelected: { + backgroundColor: "rgba(212,176,140,0.12)", + borderColor: "rgba(212,176,140,0.3)", + }, + gridIcon: { + width: 44, + height: 44, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(255,255,255,0.05)", + borderRadius: 12, + }, + gridIconSelected: { + backgroundColor: "rgba(212,176,140,0.15)", + }, + gridText: { + fontSize: 11, + color: "rgba(255,255,255,0.5)", + textAlign: "center", + }, + gridTextSelected: { + color: "#d4b08c", + fontWeight: "500", + }, +}); diff --git a/apps/mobile/components/types.ts b/apps/mobile/components/types.ts new file mode 100644 index 0000000..b0b0ef1 --- /dev/null +++ b/apps/mobile/components/types.ts @@ -0,0 +1,163 @@ +export type ItemType = "login" | "note" | "card" | "identity" | "ssh" | "api" | "otp" | "passkey"; + +export type LoginData = { + username?: string; + password?: string; + url?: string; + totp?: string; + notes?: string; +}; + +export type NoteData = { + content: string; +}; + +export type CardData = { + holder: string; + number: string; + expiry: string; + cvv: string; + pin?: string; + notes?: string; +}; + +export type IdentityData = { + firstname?: string; + lastname?: string; + email?: string; + phone?: string; + address?: string; + city?: string; + state?: string; + zip?: string; + country?: string; + notes?: string; +}; + +export type SshData = { + privatekey: string; + publickey?: string; + passphrase?: string; + notes?: string; +}; + +export type ApiData = { + key: string; + secret?: string; + endpoint?: string; + notes?: string; +}; + +export type OtpData = { + secret: string; + issuer?: string; + account?: string; + digits?: number; + period?: number; +}; + +export type PasskeyData = { + credentialid: string; + publickey: string; + rpid: string; + origin: string; + notes?: string; +}; + +export type ItemDataMap = { + login: LoginData; + note: NoteData; + card: CardData; + identity: IdentityData; + ssh: SshData; + api: ApiData; + otp: OtpData; + passkey: PasskeyData; +}; + +export type VaultItem = { + id: string; + type: ItemType; + title: string; + data: ItemDataMap[ItemType]; + tags: string[]; + favorite: boolean; + createdAt: string; + updatedAt: string; +}; + +export type FieldConfig = { + name: string; + label: string; + type: "text" | "password" | "textarea" | "url" | "email" | "number"; + required?: boolean; + half?: boolean; +}; + +export const fieldConfigs: Record = { + login: [ + { name: "username", label: "username", type: "text" }, + { name: "password", label: "password", type: "password", required: true }, + { name: "url", label: "website url", type: "url" }, + { name: "totp", label: "totp secret", type: "password" }, + { name: "notes", label: "notes", type: "textarea" }, + ], + note: [{ name: "content", label: "content", type: "textarea", required: true }], + card: [ + { name: "holder", label: "cardholder name", type: "text", required: true }, + { name: "number", label: "card number", type: "password", required: true }, + { name: "expiry", label: "expiry (mm/yy)", type: "text", required: true, half: true }, + { name: "cvv", label: "cvv", type: "password", required: true, half: true }, + { name: "pin", label: "pin", type: "password" }, + { name: "notes", label: "notes", type: "textarea" }, + ], + identity: [ + { name: "firstname", label: "first name", type: "text", half: true }, + { name: "lastname", label: "last name", type: "text", half: true }, + { name: "email", label: "email", type: "email" }, + { name: "phone", label: "phone", type: "text" }, + { name: "address", label: "address", type: "text" }, + { name: "city", label: "city", type: "text", half: true }, + { name: "state", label: "state", type: "text", half: true }, + { name: "zip", label: "zip code", type: "text", half: true }, + { name: "country", label: "country", type: "text", half: true }, + { name: "notes", label: "notes", type: "textarea" }, + ], + ssh: [ + { name: "privatekey", label: "private key", type: "textarea", required: true }, + { name: "publickey", label: "public key", type: "textarea" }, + { name: "passphrase", label: "passphrase", type: "password" }, + { name: "notes", label: "notes", type: "textarea" }, + ], + api: [ + { name: "key", label: "api key", type: "password", required: true }, + { name: "secret", label: "api secret", type: "password" }, + { name: "endpoint", label: "endpoint url", type: "url" }, + { name: "notes", label: "notes", type: "textarea" }, + ], + otp: [ + { name: "secret", label: "secret", type: "password", required: true }, + { name: "issuer", label: "issuer", type: "text", half: true }, + { name: "account", label: "account", type: "text", half: true }, + { name: "digits", label: "digits", type: "number", half: true }, + { name: "period", label: "period (seconds)", type: "number", half: true }, + ], + passkey: [ + { name: "credentialid", label: "credential id", type: "text", required: true }, + { name: "publickey", label: "public key", type: "textarea", required: true }, + { name: "rpid", label: "relying party id", type: "text", required: true }, + { name: "origin", label: "origin", type: "url", required: true }, + { name: "notes", label: "notes", type: "textarea" }, + ], +}; + +export const typeLabels: Record = { + login: "logins", + note: "notes", + card: "cards", + identity: "identities", + ssh: "ssh keys", + api: "api keys", + otp: "otp codes", + passkey: "passkeys", +}; diff --git a/apps/mobile/lib/animations.ts b/apps/mobile/lib/animations.ts new file mode 100644 index 0000000..3450f75 --- /dev/null +++ b/apps/mobile/lib/animations.ts @@ -0,0 +1,187 @@ +import { + withSpring, + withTiming, + withDelay, + withSequence, + withRepeat, + Easing, + type WithSpringConfig, + type WithTimingConfig, +} from "react-native-reanimated"; + +export const springConfig: WithSpringConfig = { + damping: 20, + stiffness: 300, + mass: 0.8, +}; + +export const springConfigSoft: WithSpringConfig = { + damping: 25, + stiffness: 200, + mass: 1, +}; + +export const springConfigBouncy: WithSpringConfig = { + damping: 12, + stiffness: 400, + mass: 0.6, +}; + +export const springConfigStiff: WithSpringConfig = { + damping: 30, + stiffness: 500, + mass: 0.5, +}; + +export const timingConfig: WithTimingConfig = { + duration: 200, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), +}; + +export const timingConfigSlow: WithTimingConfig = { + duration: 400, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), +}; + +export const timingConfigFast: WithTimingConfig = { + duration: 100, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), +}; + +export const animate = { + spring: (value: number, config = springConfig) => { + "worklet"; + return withSpring(value, config); + }, + + timing: (value: number, config = timingConfig) => { + "worklet"; + return withTiming(value, config); + }, + + delay: (delay: number, animation: number) => { + "worklet"; + return withDelay(delay, animation); + }, + + sequence: (...animations: number[]) => { + "worklet"; + return withSequence(...animations); + }, + + repeat: (animation: number, count = -1, reverse = true) => { + "worklet"; + return withRepeat(animation, count, reverse); + }, + + fadeIn: (duration = 200) => { + "worklet"; + return withTiming(1, { duration, easing: Easing.out(Easing.ease) }); + }, + + fadeOut: (duration = 200) => { + "worklet"; + return withTiming(0, { duration, easing: Easing.in(Easing.ease) }); + }, + + scaleIn: (config = springConfig) => { + "worklet"; + return withSpring(1, config); + }, + + scaleOut: (config = springConfig) => { + "worklet"; + return withSpring(0, config); + }, + + slideInUp: (from = 50, config = springConfig) => { + "worklet"; + return withSpring(0, config); + }, + + slideInDown: (from = -50, config = springConfig) => { + "worklet"; + return withSpring(0, config); + }, + + pressIn: () => { + "worklet"; + return withSpring(0.97, springConfigStiff); + }, + + pressOut: () => { + "worklet"; + return withSpring(1, springConfig); + }, + + shake: () => { + "worklet"; + return withSequence( + withTiming(-8, { duration: 50 }), + withTiming(8, { duration: 50 }), + withTiming(-8, { duration: 50 }), + withTiming(8, { duration: 50 }), + withTiming(0, { duration: 50 }) + ); + }, + + pulse: () => { + "worklet"; + return withRepeat( + withSequence( + withTiming(1.05, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, + true + ); + }, + + breathe: () => { + "worklet"; + return withRepeat( + withSequence( + withTiming(0.6, { duration: 1000, easing: Easing.inOut(Easing.ease) }), + withTiming(1, { duration: 1000, easing: Easing.inOut(Easing.ease) }) + ), + -1, + true + ); + }, +} as const; + +export const presets = { + entering: { + opacity: { from: 0, to: 1, duration: 200 }, + translateY: { from: 20, to: 0, duration: 250 }, + }, + exiting: { + opacity: { from: 1, to: 0, duration: 150 }, + translateY: { from: 0, to: -10, duration: 150 }, + }, + modal: { + entering: { + opacity: { from: 0, to: 1, duration: 200 }, + scale: { from: 0.95, to: 1, spring: springConfig }, + }, + exiting: { + opacity: { from: 1, to: 0, duration: 150 }, + scale: { from: 1, to: 0.95, duration: 150 }, + }, + }, + sheet: { + entering: { + translateY: { from: 300, to: 0, spring: springConfigSoft }, + }, + exiting: { + translateY: { from: 0, to: 300, duration: 200 }, + }, + }, + list: { + stagger: 50, + entering: { + opacity: { from: 0, to: 1, duration: 200 }, + translateX: { from: -20, to: 0, spring: springConfig }, + }, + }, +} as const; diff --git a/apps/mobile/lib/api.ts b/apps/mobile/lib/api.ts new file mode 100644 index 0000000..d9016ba --- /dev/null +++ b/apps/mobile/lib/api.ts @@ -0,0 +1,186 @@ +import { z } from "zod"; +import { gettoken } from "./storage"; + +const API_HOST = process.env.EXPO_PUBLIC_API_URL || "https://noro.sh"; +const BASE_URL = `${API_HOST}/api`; +const AUTH_URL = `${API_HOST}/api/auth`; +const V1_URL = `${API_HOST}/api/v1`; + +export class ApiError extends Error { + constructor( + public status: number, + public code: string, + message: string + ) { + super(message); + this.name = "ApiError"; + } +} + +type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + +async function request( + path: string, + options: { + method?: Method; + body?: unknown; + schema?: z.ZodType; + auth?: boolean; + baseurl?: string; + } = {} +): Promise { + const { method = "GET", body, schema, auth = true, baseurl = V1_URL } = options; + + const headers: Record = { + "content-type": "application/json", + }; + + if (auth) { + const token = await gettoken(); + if (token) { + headers["authorization"] = `Bearer ${token}`; + } + } + + const response = await fetch(`${baseurl}${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + const data = await response.json(); + + if (!response.ok) { + throw new ApiError( + response.status, + data.error || "unknown", + data.message || data.error || "request failed" + ); + } + + if (schema) { + const result = schema.safeParse(data); + if (!result.success) { + throw new ApiError(500, "validation", "invalid response format"); + } + return result.data; + } + + return data as T; +} + +export const UserSchema = z.object({ + id: z.string(), + email: z.string(), + name: z.string().nullable(), + image: z.string().nullable(), +}); + +export const VaultSchema = z.object({ + data: z.string(), + revision: z.number(), +}); + +export const FolderSchema = z.object({ + id: z.string(), + name: z.string(), + parentId: z.string().nullable(), + color: z.string(), + icon: z.string(), + order: z.number(), +}); + +export const FoldersResponseSchema = z.object({ + folders: z.array(FolderSchema), + counts: z.record(z.number()), +}); + +export const AuthResponseSchema = z.object({ + token: z.string(), + user: UserSchema, +}); + +export type User = z.infer; +export type Vault = z.infer; +export type Folder = z.infer; +export type AuthResponse = z.infer; + +export const api = { + auth: { + login: (email: string, password: string) => + request("/sign-in/email", { + method: "POST", + body: { email, password }, + schema: AuthResponseSchema, + auth: false, + baseurl: AUTH_URL, + }), + + register: (email: string, password: string, name?: string) => + request("/sign-up/email", { + method: "POST", + body: { email, password, name }, + schema: AuthResponseSchema, + auth: false, + baseurl: AUTH_URL, + }), + + logout: () => + request<{ success: boolean }>("/sign-out", { + method: "POST", + baseurl: AUTH_URL, + }), + + session: () => + request<{ session: { user: User } }>("/get-session", { baseurl: AUTH_URL }), + }, + + user: { + get: () => + request<{ user: User }>("/user", { + schema: z.object({ user: UserSchema }), + }), + }, + + vault: { + get: () => + request("/vault", { + schema: VaultSchema, + }), + + update: (data: string, revision: number) => + request<{ success: boolean; revision: number }>("/vault", { + method: "PUT", + body: { data, revision }, + }), + }, + + folders: { + list: () => + request>("/folders", { + schema: FoldersResponseSchema, + }), + + create: (name: string, parentId?: string) => + request<{ folder: Folder }>("/folders", { + method: "POST", + body: { name, parentId }, + }), + + update: (id: string, data: { name?: string; parentId?: string; icon?: string }) => + request<{ folder: Folder }>(`/folders/${id}`, { + method: "PATCH", + body: data, + }), + + delete: (id: string) => + request<{ success: boolean }>(`/folders/${id}`, { + method: "DELETE", + }), + }, + + health: { + check: () => + request<{ status: string }>("/health", { auth: false, baseurl: BASE_URL }), + }, +}; diff --git a/apps/mobile/lib/auth.ts b/apps/mobile/lib/auth.ts new file mode 100644 index 0000000..30e6700 --- /dev/null +++ b/apps/mobile/lib/auth.ts @@ -0,0 +1,115 @@ +import { create } from "zustand"; +import { api, type User, ApiError } from "./api"; +import { gettoken, settoken, cleartoken, clearall } from "./storage"; + +type AuthState = { + user: User | null; + loading: boolean; + initialized: boolean; +}; + +type AuthActions = { + init: () => Promise; + login: (email: string, password: string) => Promise; + register: (email: string, password: string, name?: string) => Promise; + logout: () => Promise; + refresh: () => Promise; +}; + +type AuthStore = AuthState & AuthActions; + +export const useauth = create((set, get) => ({ + user: null, + loading: false, + initialized: false, + + init: async () => { + if (get().initialized) return; + set({ loading: true }); + + try { + const token = await gettoken(); + if (!token) { + set({ user: null, loading: false, initialized: true }); + return; + } + + const { user } = await api.user.get(); + set({ user, loading: false, initialized: true }); + } catch (error) { + if (error instanceof ApiError && error.status === 401) { + await cleartoken(); + } + set({ user: null, loading: false, initialized: true }); + } + }, + + login: async (email: string, password: string) => { + set({ loading: true }); + + try { + const { token, user } = await api.auth.login(email, password); + await settoken(token); + set({ user, loading: false }); + } catch (error) { + set({ loading: false }); + throw error; + } + }, + + register: async (email: string, password: string, name?: string) => { + set({ loading: true }); + + try { + const { token, user } = await api.auth.register(email, password, name); + await settoken(token); + set({ user, loading: false }); + } catch (error) { + set({ loading: false }); + throw error; + } + }, + + logout: async () => { + set({ loading: true }); + + try { + await api.auth.logout(); + } catch { + } finally { + await clearall(); + set({ user: null, loading: false }); + } + }, + + refresh: async () => { + try { + const { user } = await api.user.get(); + set({ user }); + } catch (error) { + if (error instanceof ApiError && error.status === 401) { + await clearall(); + set({ user: null }); + } + } + }, +})); + +export function isauthenticated(): boolean { + return useauth.getState().user !== null; +} + +export async function requireauth(): Promise { + const { user, init, initialized } = useauth.getState(); + + if (!initialized) { + await init(); + } + + const current = useauth.getState().user; + if (!current) { + throw new ApiError(401, "unauthorized", "authentication required"); + } + + return current; +} diff --git a/apps/mobile/lib/biometric.ts b/apps/mobile/lib/biometric.ts new file mode 100644 index 0000000..83ab8c7 --- /dev/null +++ b/apps/mobile/lib/biometric.ts @@ -0,0 +1,97 @@ +import * as LocalAuthentication from "expo-local-authentication"; +import { getpreferences, setpreferences } from "./storage"; + +export type BiometricType = "fingerprint" | "face" | "iris" | "none"; + +export type BiometricStatus = { + available: boolean; + enrolled: boolean; + type: BiometricType; + enabled: boolean; +}; + +function maptype(types: LocalAuthentication.AuthenticationType[]): BiometricType { + if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) { + return "face"; + } + if (types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) { + return "fingerprint"; + } + if (types.includes(LocalAuthentication.AuthenticationType.IRIS)) { + return "iris"; + } + return "none"; +} + +export async function getstatus(): Promise { + const [available, types, enrolled] = await Promise.all([ + LocalAuthentication.hasHardwareAsync(), + LocalAuthentication.supportedAuthenticationTypesAsync(), + LocalAuthentication.isEnrolledAsync(), + ]); + + const prefs = await getpreferences(); + + return { + available, + enrolled, + type: maptype(types), + enabled: prefs.biometric, + }; +} + +export async function authenticate(reason?: string): Promise { + const status = await getstatus(); + + if (!status.available || !status.enrolled) { + return false; + } + + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: reason || "authenticate to continue", + cancelLabel: "cancel", + disableDeviceFallback: false, + fallbackLabel: "use passcode", + }); + + return result.success; +} + +export async function enable(): Promise { + const status = await getstatus(); + + if (!status.available || !status.enrolled) { + return false; + } + + const success = await authenticate("enable biometric authentication"); + if (!success) { + return false; + } + + await setpreferences({ biometric: true }); + return true; +} + +export async function disable(): Promise { + await setpreferences({ biometric: false }); + return true; +} + +export async function requirebiometric(): Promise { + const prefs = await getpreferences(); + + if (!prefs.biometric) { + return true; + } + + return authenticate("unlock noro"); +} + +export async function issupported(): Promise { + const [available, enrolled] = await Promise.all([ + LocalAuthentication.hasHardwareAsync(), + LocalAuthentication.isEnrolledAsync(), + ]); + return available && enrolled; +} diff --git a/apps/mobile/lib/constants.ts b/apps/mobile/lib/constants.ts new file mode 100644 index 0000000..e9a9fcf --- /dev/null +++ b/apps/mobile/lib/constants.ts @@ -0,0 +1,51 @@ +import type { ItemType, AutoLockOption } from "./types"; + +export const API_URL = "https://noro.sh"; + +export const STORAGE_KEYS = { + token: "noro_auth_token", + vault: "noro_vault_cache", + settings: "noro_settings", + vaultkey: "noro_vault_key", + preferences: "noro_preferences", + biometric: "noro_biometric", + lastactive: "noro_last_active", +} as const; + +export const ITEM_TYPES: Record = { + login: { label: "Login", icon: "key" }, + note: { label: "Secure Note", icon: "file-text" }, + card: { label: "Card", icon: "credit-card" }, + identity: { label: "Identity", icon: "user" }, + ssh: { label: "SSH Key", icon: "terminal" }, + api: { label: "API Key", icon: "code" }, + otp: { label: "OTP", icon: "clock" }, + passkey: { label: "Passkey", icon: "fingerprint" }, +} as const; + +export const AUTO_LOCK_OPTIONS: Record = { + immediate: { label: "Immediately", ms: 0 }, + "1min": { label: "1 minute", ms: 60000 }, + "5min": { label: "5 minutes", ms: 300000 }, + "15min": { label: "15 minutes", ms: 900000 }, + "1hr": { label: "1 hour", ms: 3600000 }, + never: { label: "Never", ms: -1 }, +} as const; + +export const FOLDER_ICONS = [ + "folder", + "star", + "archive", + "lock", + "globe", + "code", + "key", + "user", +] as const; + +export const PASSWORD_CHARS = { + lowercase: "abcdefghijklmnopqrstuvwxyz", + uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + numbers: "0123456789", + symbols: "!@#$%^&*()_+-=[]{}|;:,.<>?", +} as const; diff --git a/apps/mobile/lib/crypto.ts b/apps/mobile/lib/crypto.ts new file mode 100644 index 0000000..86e35bd --- /dev/null +++ b/apps/mobile/lib/crypto.ts @@ -0,0 +1,105 @@ +const CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + +function randombytes(length: number): Uint8Array { + const bytes = new Uint8Array(length); + for (let i = 0; i < length; i++) { + bytes[i] = Math.floor(Math.random() * 256); + } + return bytes; +} + +function tobase64(bytes: Uint8Array): string { + let binary = ""; + bytes.forEach((byte) => (binary += String.fromCharCode(byte))); + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); +} + +function frombase64(encoded: string): Uint8Array { + const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/"); + const padding = (4 - (base64.length % 4)) % 4; + const padded = base64 + "=".repeat(padding); + const binary = atob(padded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +async function derivekey( + key: string, + usage: "encrypt" | "decrypt" +): Promise { + const keydata = new TextEncoder().encode(key.padEnd(32, "0").slice(0, 32)); + return crypto.subtle.importKey("raw", keydata, "AES-GCM", false, [usage]); +} + +export async function encrypt(data: string, key: string): Promise { + const iv = randombytes(12); + const cryptokey = await derivekey(key, "encrypt"); + const encoded = new TextEncoder().encode(data); + const encrypted = await crypto.subtle.encrypt( + { name: "AES-GCM", iv }, + cryptokey, + encoded + ); + const combined = new Uint8Array(iv.length + encrypted.byteLength); + combined.set(iv); + combined.set(new Uint8Array(encrypted), iv.length); + return tobase64(combined); +} + +export async function decrypt(encrypted: string, key: string): Promise { + const bytes = frombase64(encrypted); + const iv = bytes.slice(0, 12); + const data = bytes.slice(12); + const cryptokey = await derivekey(key, "decrypt"); + const decrypted = await crypto.subtle.decrypt( + { name: "AES-GCM", iv }, + cryptokey, + data + ); + return new TextDecoder().decode(decrypted); +} + +export function generatekey(length = 24): string { + const bytes = randombytes(length); + let key = ""; + for (let i = 0; i < length; i++) { + key += CHARS[bytes[i] % CHARS.length]; + } + return key; +} + +export async function hash(data: string): Promise { + const encoded = new TextEncoder().encode(data); + const hashBuffer = await crypto.subtle.digest("SHA-256", encoded); + return tobase64(new Uint8Array(hashBuffer)); +} + +export type EncryptedVault = { + data: string; + iv: string; + version: number; +}; + +export async function encryptvault( + vault: unknown, + key: string +): Promise { + const json = JSON.stringify(vault); + const encrypted = await encrypt(json, key); + return { + data: encrypted, + iv: encrypted.slice(0, 16), + version: 1, + }; +} + +export async function decryptvault( + encrypted: EncryptedVault, + key: string +): Promise { + const json = await decrypt(encrypted.data, key); + return JSON.parse(json) as T; +} diff --git a/apps/mobile/lib/haptics.ts b/apps/mobile/lib/haptics.ts new file mode 100644 index 0000000..e202103 --- /dev/null +++ b/apps/mobile/lib/haptics.ts @@ -0,0 +1,76 @@ +import * as Haptics from "expo-haptics"; +import { Platform } from "react-native"; + +const isHapticsEnabled = Platform.OS === "ios" || Platform.OS === "android"; + +export const haptic = { + light: () => { + if (!isHapticsEnabled) return; + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + }, + + medium: () => { + if (!isHapticsEnabled) return; + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + }, + + heavy: () => { + if (!isHapticsEnabled) return; + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy); + }, + + soft: () => { + if (!isHapticsEnabled) return; + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Soft); + }, + + rigid: () => { + if (!isHapticsEnabled) return; + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Rigid); + }, + + success: () => { + if (!isHapticsEnabled) return; + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + }, + + warning: () => { + if (!isHapticsEnabled) return; + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); + }, + + error: () => { + if (!isHapticsEnabled) return; + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + }, + + selection: () => { + if (!isHapticsEnabled) return; + Haptics.selectionAsync(); + }, + + tap: () => { + if (!isHapticsEnabled) return; + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + }, + + press: () => { + if (!isHapticsEnabled) return; + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + }, + + copy: () => { + if (!isHapticsEnabled) return; + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + }, + + delete: () => { + if (!isHapticsEnabled) return; + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); + }, + + unlock: () => { + if (!isHapticsEnabled) return; + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy); + }, +} as const; diff --git a/apps/mobile/lib/storage.ts b/apps/mobile/lib/storage.ts new file mode 100644 index 0000000..2ed5b78 --- /dev/null +++ b/apps/mobile/lib/storage.ts @@ -0,0 +1,111 @@ +import * as SecureStore from "expo-secure-store"; + +const KEYS = { + token: "noro_auth_token", + vault: "noro_vault_cache", + preferences: "noro_preferences", + vaultkey: "noro_vault_key", +} as const; + +type StorageKey = keyof typeof KEYS; + +export type Preferences = { + biometric: boolean; + autofill: boolean; + timeout: number; + theme: "light" | "dark" | "system"; +}; + +const defaults: Preferences = { + biometric: false, + autofill: true, + timeout: 300, + theme: "system", +}; + +async function get(key: StorageKey): Promise { + try { + return await SecureStore.getItemAsync(KEYS[key]); + } catch { + return null; + } +} + +async function set(key: StorageKey, value: string): Promise { + try { + await SecureStore.setItemAsync(KEYS[key], value); + return true; + } catch { + return false; + } +} + +async function remove(key: StorageKey): Promise { + try { + await SecureStore.deleteItemAsync(KEYS[key]); + return true; + } catch { + return false; + } +} + +export async function gettoken(): Promise { + return get("token"); +} + +export async function settoken(token: string): Promise { + return set("token", token); +} + +export async function cleartoken(): Promise { + return remove("token"); +} + +export async function getvaultcache(): Promise { + return get("vault"); +} + +export async function setvaultcache(data: string): Promise { + return set("vault", data); +} + +export async function clearvaultcache(): Promise { + return remove("vault"); +} + +export async function getvaultkey(): Promise { + return get("vaultkey"); +} + +export async function setvaultkey(key: string): Promise { + return set("vaultkey", key); +} + +export async function clearvaultkey(): Promise { + return remove("vaultkey"); +} + +export async function getpreferences(): Promise { + const raw = await get("preferences"); + if (!raw) return defaults; + try { + return { ...defaults, ...JSON.parse(raw) }; + } catch { + return defaults; + } +} + +export async function setpreferences(prefs: Partial): Promise { + const current = await getpreferences(); + const merged = { ...current, ...prefs }; + return set("preferences", JSON.stringify(merged)); +} + +export async function clearall(): Promise { + await Promise.all([ + remove("token"), + remove("vault"), + remove("preferences"), + remove("vaultkey"), + ]); +} diff --git a/apps/mobile/lib/styles.ts b/apps/mobile/lib/styles.ts new file mode 100644 index 0000000..8c833ea --- /dev/null +++ b/apps/mobile/lib/styles.ts @@ -0,0 +1,258 @@ +import { StyleSheet } from "react-native"; +import { colors, spacing, radius, fontSizes, fontWeights, shadows } from "./theme"; + +export const layout = StyleSheet.create({ + flex: { + flex: 1, + }, + center: { + justifyContent: "center", + alignItems: "center", + }, + row: { + flexDirection: "row", + }, + rowCenter: { + flexDirection: "row", + alignItems: "center", + }, + rowBetween: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + column: { + flexDirection: "column", + }, + wrap: { + flexWrap: "wrap", + }, +}); + +export const containers = StyleSheet.create({ + screen: { + flex: 1, + backgroundColor: colors.background, + }, + safeArea: { + flex: 1, + backgroundColor: colors.background, + }, + content: { + flex: 1, + paddingHorizontal: spacing.lg, + }, + card: { + backgroundColor: colors.subtle, + borderRadius: radius.lg, + padding: spacing.lg, + borderWidth: 1, + borderColor: colors.border, + }, + surface: { + backgroundColor: colors.surface, + borderRadius: radius.md, + padding: spacing.md, + }, +}); + +export const typography = StyleSheet.create({ + display: { + fontSize: fontSizes.display, + fontWeight: fontWeights.bold, + color: colors.text, + letterSpacing: -1, + }, + h1: { + fontSize: fontSizes.xxxl, + fontWeight: fontWeights.bold, + color: colors.text, + letterSpacing: -0.5, + }, + h2: { + fontSize: fontSizes.xxl, + fontWeight: fontWeights.semibold, + color: colors.text, + }, + h3: { + fontSize: fontSizes.xl, + fontWeight: fontWeights.semibold, + color: colors.text, + }, + body: { + fontSize: fontSizes.md, + fontWeight: fontWeights.regular, + color: colors.text, + }, + bodyMuted: { + fontSize: fontSizes.md, + fontWeight: fontWeights.regular, + color: colors.textMuted, + }, + caption: { + fontSize: fontSizes.sm, + fontWeight: fontWeights.regular, + color: colors.textSubtle, + }, + label: { + fontSize: fontSizes.sm, + fontWeight: fontWeights.medium, + color: colors.textMuted, + textTransform: "uppercase", + letterSpacing: 0.5, + }, + mono: { + fontSize: fontSizes.md, + fontFamily: "Menlo", + color: colors.text, + }, +}); + +export const buttons = StyleSheet.create({ + primary: { + backgroundColor: colors.accent, + borderRadius: radius.md, + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + alignItems: "center", + justifyContent: "center", + ...shadows.accent, + }, + primaryText: { + fontSize: fontSizes.md, + fontWeight: fontWeights.semibold, + color: colors.background, + }, + secondary: { + backgroundColor: colors.subtle, + borderRadius: radius.md, + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + alignItems: "center", + justifyContent: "center", + borderWidth: 1, + borderColor: colors.border, + }, + secondaryText: { + fontSize: fontSizes.md, + fontWeight: fontWeights.medium, + color: colors.text, + }, + ghost: { + backgroundColor: colors.transparent, + borderRadius: radius.md, + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + alignItems: "center", + justifyContent: "center", + }, + ghostText: { + fontSize: fontSizes.md, + fontWeight: fontWeights.medium, + color: colors.textMuted, + }, + icon: { + width: 44, + height: 44, + borderRadius: radius.md, + backgroundColor: colors.subtle, + alignItems: "center", + justifyContent: "center", + borderWidth: 1, + borderColor: colors.border, + }, +}); + +export const inputs = StyleSheet.create({ + container: { + backgroundColor: colors.subtle, + borderRadius: radius.md, + borderWidth: 1, + borderColor: colors.border, + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + }, + text: { + fontSize: fontSizes.md, + color: colors.text, + }, + placeholder: { + color: colors.textSubtle, + }, + focused: { + borderColor: colors.accent, + }, + error: { + borderColor: colors.error, + }, + label: { + fontSize: fontSizes.sm, + fontWeight: fontWeights.medium, + color: colors.textMuted, + marginBottom: spacing.xs, + }, +}); + +export const lists = StyleSheet.create({ + item: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.subtle, + borderRadius: radius.md, + padding: spacing.lg, + borderWidth: 1, + borderColor: colors.border, + }, + separator: { + height: spacing.sm, + }, + divider: { + height: 1, + backgroundColor: colors.border, + marginVertical: spacing.md, + }, +}); + +export const badges = StyleSheet.create({ + default: { + backgroundColor: colors.subtle, + borderRadius: radius.full, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, + }, + accent: { + backgroundColor: colors.accent, + borderRadius: radius.full, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, + }, + text: { + fontSize: fontSizes.xs, + fontWeight: fontWeights.medium, + color: colors.text, + }, + accentText: { + fontSize: fontSizes.xs, + fontWeight: fontWeights.medium, + color: colors.background, + }, +}); + +export const utils = StyleSheet.create({ + absolute: { + position: "absolute", + }, + fill: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + hidden: { + opacity: 0, + }, + disabled: { + opacity: 0.5, + }, +}); diff --git a/apps/mobile/lib/theme.ts b/apps/mobile/lib/theme.ts new file mode 100644 index 0000000..de04d26 --- /dev/null +++ b/apps/mobile/lib/theme.ts @@ -0,0 +1,132 @@ +import { Platform } from "react-native"; + +export const colors = { + background: "#0a0a0a", + surface: "#141414", + subtle: "#1a1a1a", + accent: "#d4b08c", + accentHover: "#e0c4a8", + text: "#ededed", + textMuted: "rgba(255,255,255,0.5)", + textSubtle: "rgba(255,255,255,0.3)", + border: "rgba(255,255,255,0.08)", + error: "#ef4444", + success: "#22c55e", + transparent: "transparent", + white: "#ffffff", + black: "#000000", +} as const; + +export const spacing = { + xs: 4, + sm: 8, + md: 12, + lg: 16, + xl: 24, + xxl: 32, + xxxl: 48, +} as const; + +export const radius = { + xs: 4, + sm: 8, + md: 12, + lg: 16, + xl: 24, + full: 9999, +} as const; + +export const fonts = { + regular: Platform.select({ + ios: "System", + android: "Roboto", + default: "System", + }), + medium: Platform.select({ + ios: "System", + android: "Roboto-Medium", + default: "System", + }), + semibold: Platform.select({ + ios: "System", + android: "Roboto-Medium", + default: "System", + }), + bold: Platform.select({ + ios: "System", + android: "Roboto-Bold", + default: "System", + }), +} as const; + +export const fontWeights = { + regular: "400" as const, + medium: "500" as const, + semibold: "600" as const, + bold: "700" as const, +}; + +export const fontSizes = { + xs: 11, + sm: 13, + md: 15, + lg: 17, + xl: 20, + xxl: 24, + xxxl: 32, + display: 40, +} as const; + +export const lineHeights = { + tight: 1.2, + normal: 1.4, + relaxed: 1.6, +} as const; + +export const shadows = { + sm: { + shadowColor: colors.black, + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.2, + shadowRadius: 2, + elevation: 2, + }, + md: { + shadowColor: colors.black, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 4, + }, + lg: { + shadowColor: colors.black, + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.4, + shadowRadius: 16, + elevation: 8, + }, + accent: { + shadowColor: colors.accent, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 12, + elevation: 6, + }, +} as const; + +export const theme = { + colors, + spacing, + radius, + fonts, + fontWeights, + fontSizes, + lineHeights, + shadows, +} as const; + +export type Theme = typeof theme; +export type Colors = keyof typeof colors; +export type Spacing = keyof typeof spacing; +export type Radius = keyof typeof radius; +export type FontSize = keyof typeof fontSizes; diff --git a/apps/mobile/lib/types.ts b/apps/mobile/lib/types.ts new file mode 100644 index 0000000..5431daf --- /dev/null +++ b/apps/mobile/lib/types.ts @@ -0,0 +1,149 @@ +export type User = { + id: string; + email: string; + name: string | null; + image: string | null; +}; + +export type Session = { + token: string; + expiresAt: Date; +}; + +export type ItemType = + | "login" + | "note" + | "card" + | "identity" + | "ssh" + | "api" + | "otp" + | "passkey"; + +export type LoginData = { + username: string; + password: string; + url: string; + totp?: string; + notes?: string; +}; + +export type NoteData = { + content: string; +}; + +export type CardData = { + holder: string; + number: string; + expiry: string; + cvv: string; + pin?: string; + notes?: string; +}; + +export type IdentityData = { + firstName: string; + lastName: string; + email?: string; + phone?: string; + address?: string; + city?: string; + state?: string; + zip?: string; + country?: string; + company?: string; + ssn?: string; + passport?: string; + license?: string; + notes?: string; +}; + +export type SshData = { + name: string; + publicKey: string; + privateKey: string; + passphrase?: string; + notes?: string; +}; + +export type ApiData = { + key: string; + secret?: string; + endpoint?: string; + notes?: string; +}; + +export type OtpData = { + secret: string; + issuer?: string; + account?: string; + algorithm?: "sha1" | "sha256" | "sha512"; + digits?: 6 | 8; + period?: number; +}; + +export type PasskeyData = { + credentialId: string; + publicKey: string; + rpId: string; + userHandle: string; + counter: number; + notes?: string; +}; + +export type ItemData = + | LoginData + | NoteData + | CardData + | IdentityData + | SshData + | ApiData + | OtpData + | PasskeyData; + +export type VaultItem = { + id: string; + type: ItemType; + title: string; + data: ItemData; + favorite: boolean; + tags: string[]; + folderId: string | null; + createdAt: string; + updatedAt: string; + deletedAt: string | null; +}; + +export type Folder = { + id: string; + name: string; + parentId: string | null; + color: string; + icon: string; +}; + +export type Tag = { + id: string; + name: string; +}; + +export type FolderIcon = + | "folder" + | "star" + | "archive" + | "lock" + | "globe" + | "code" + | "key" + | "user"; + +export type AutoLockOption = "immediate" | "1min" | "5min" | "15min" | "1hr" | "never"; + +export type ThemeOption = "light" | "dark" | "system"; + +export type Preferences = { + biometric: boolean; + autofill: boolean; + autolock: AutoLockOption; + theme: ThemeOption; +}; diff --git a/apps/mobile/lib/utils.ts b/apps/mobile/lib/utils.ts new file mode 100644 index 0000000..da5244c --- /dev/null +++ b/apps/mobile/lib/utils.ts @@ -0,0 +1,128 @@ +import type { ItemType, VaultItem, LoginData, CardData, ApiData } from "./types"; +import { ITEM_TYPES, PASSWORD_CHARS } from "./constants"; + +export function formatdate(date: string | Date): string { + const d = typeof date === "string" ? new Date(date) : date; + const now = new Date(); + const diff = now.getTime() - d.getTime(); + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (seconds < 60) return "just now"; + if (minutes < 60) return `${minutes}m ago`; + if (hours < 24) return `${hours}h ago`; + if (days < 7) return `${days}d ago`; + + return d.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: d.getFullYear() !== now.getFullYear() ? "numeric" : undefined, + }); +} + +type PasswordOptions = { + length?: number; + lowercase?: boolean; + uppercase?: boolean; + numbers?: boolean; + symbols?: boolean; +}; + +export function generatepassword(options: PasswordOptions = {}): string { + const { + length = 16, + lowercase = true, + uppercase = true, + numbers = true, + symbols = true, + } = options; + + let chars = ""; + if (lowercase) chars += PASSWORD_CHARS.lowercase; + if (uppercase) chars += PASSWORD_CHARS.uppercase; + if (numbers) chars += PASSWORD_CHARS.numbers; + if (symbols) chars += PASSWORD_CHARS.symbols; + + if (!chars) chars = PASSWORD_CHARS.lowercase + PASSWORD_CHARS.numbers; + + let result = ""; + const array = new Uint32Array(length); + crypto.getRandomValues(array); + + for (let i = 0; i < length; i++) { + result += chars[array[i] % chars.length]; + } + + return result; +} + +export function maskvalue(value: string, visible = 4): string { + if (!value) return ""; + if (value.length <= visible) return value; + const masked = "*".repeat(value.length - visible); + return masked + value.slice(-visible); +} + +export function getitemicon(type: ItemType): string { + return ITEM_TYPES[type]?.icon || "file"; +} + +export function getitemsubtitle(item: VaultItem): string { + switch (item.type) { + case "login": { + const data = item.data as LoginData; + if (data.username) return data.username; + if (data.url) { + try { + return new URL(data.url).hostname; + } catch { + return data.url; + } + } + return ""; + } + case "card": { + const data = item.data as CardData; + if (data.number) return `**** ${data.number.slice(-4)}`; + return ""; + } + case "api": { + const data = item.data as ApiData; + if (data.endpoint) { + try { + return new URL(data.endpoint).hostname; + } catch { + return data.endpoint; + } + } + return data.key ? maskvalue(data.key, 8) : ""; + } + case "identity": + return "Identity document"; + case "ssh": + return "SSH key"; + case "otp": + return "One-time password"; + case "passkey": + return "Passkey credential"; + case "note": + return "Secure note"; + default: + return ""; + } +} + +export function extractdomain(url: string): string { + try { + return new URL(url).hostname.replace(/^www\./, ""); + } catch { + return url; + } +} + +export function truncate(text: string, length: number): string { + if (text.length <= length) return text; + return text.slice(0, length - 3) + "..."; +} diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js new file mode 100644 index 0000000..29c93d1 --- /dev/null +++ b/apps/mobile/metro.config.js @@ -0,0 +1,18 @@ +const { getDefaultConfig } = require("expo/metro-config"); +const path = require("path"); + +const projectRoot = __dirname; +const monorepoRoot = path.resolve(projectRoot, "../.."); + +const config = getDefaultConfig(projectRoot); + +config.watchFolders = [monorepoRoot]; + +config.resolver.nodeModulesPaths = [ + path.resolve(projectRoot, "node_modules"), + path.resolve(monorepoRoot, "node_modules"), +]; + +config.resolver.disableHierarchicalLookup = true; + +module.exports = config; diff --git a/apps/mobile/package.json b/apps/mobile/package.json new file mode 100644 index 0000000..975a5d4 --- /dev/null +++ b/apps/mobile/package.json @@ -0,0 +1,38 @@ +{ + "name": "@noro/mobile", + "version": "0.0.0", + "private": true, + "main": "expo-router/entry", + "scripts": { + "start": "expo start", + "ios": "expo run:ios", + "android": "expo run:android", + "prebuild": "expo prebuild", + "lint": "expo lint" + }, + "dependencies": { + "@gorhom/bottom-sheet": "^5.1.1", + "@react-native-async-storage/async-storage": "1.23.1", + "expo": "~52.0.0", + "expo-clipboard": "~7.0.1", + "expo-haptics": "~14.0.1", + "expo-local-authentication": "~15.0.2", + "expo-router": "~4.0.0", + "expo-secure-store": "~14.0.1", + "expo-status-bar": "~2.0.1", + "react": "18.3.1", + "react-native": "0.76.9", + "react-native-gesture-handler": "~2.20.2", + "react-native-reanimated": "~3.16.7", + "react-native-safe-area-context": "4.12.0", + "react-native-screens": "~4.4.0", + "react-native-svg": "15.8.0", + "zod": "^3.24.2", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@babel/core": "^7.26.7", + "@types/react": "~18.3.18", + "typescript": "~5.7.3" + } +} diff --git a/apps/mobile/stores/auth.ts b/apps/mobile/stores/auth.ts new file mode 100644 index 0000000..110f200 --- /dev/null +++ b/apps/mobile/stores/auth.ts @@ -0,0 +1,57 @@ +import { create } from "zustand"; +import { persist, createJSONStorage } from "zustand/middleware"; +import * as SecureStore from "expo-secure-store"; + +export type User = { + id: string; + email: string; + name: string | null; +}; + +type AuthState = { + user: User | null; + token: string | null; + isAuthenticated: boolean; + login: (user: User, token: string) => void; + logout: () => void; + setUser: (user: User) => void; +}; + +const securestorage = { + getItem: async (name: string) => { + return SecureStore.getItemAsync(name); + }, + setItem: async (name: string, value: string) => { + await SecureStore.setItemAsync(name, value); + }, + removeItem: async (name: string) => { + await SecureStore.deleteItemAsync(name); + }, +}; + +export const useauth = create()( + persist( + (set) => ({ + user: null, + token: null, + isAuthenticated: false, + login: (user, token) => + set({ + user, + token, + isAuthenticated: true, + }), + logout: () => + set({ + user: null, + token: null, + isAuthenticated: false, + }), + setUser: (user) => set({ user }), + }), + { + name: "auth", + storage: createJSONStorage(() => securestorage), + } + ) +); diff --git a/apps/mobile/stores/index.ts b/apps/mobile/stores/index.ts new file mode 100644 index 0000000..c85a882 --- /dev/null +++ b/apps/mobile/stores/index.ts @@ -0,0 +1,15 @@ +export { useauth } from "./auth"; +export type { User } from "./auth"; + +export { usevault } from "./vault"; +export type { + ItemType, + FolderIcon, + Folder, + VaultItem, + CreateItemInput, + UpdateItemInput, +} from "./vault"; + +export { usesettings } from "./settings"; +export type { Theme, AutolockDuration } from "./settings"; diff --git a/apps/mobile/stores/settings.ts b/apps/mobile/stores/settings.ts new file mode 100644 index 0000000..5a99148 --- /dev/null +++ b/apps/mobile/stores/settings.ts @@ -0,0 +1,67 @@ +import { create } from "zustand"; +import { persist, createJSONStorage } from "zustand/middleware"; +import AsyncStorage from "@react-native-async-storage/async-storage"; + +export type Theme = "light" | "dark" | "system"; + +export type AutolockDuration = + | "immediate" + | "1min" + | "5min" + | "15min" + | "30min" + | "1hour" + | "never"; + +type SettingsState = { + biometricEnabled: boolean; + theme: Theme; + autofillEnabled: boolean; + autolockDuration: AutolockDuration; + notificationsEnabled: boolean; + clipboardTimeout: number; + showPasswordStrength: boolean; + defaultFolder: string | null; + setBiometricEnabled: (enabled: boolean) => void; + setTheme: (theme: Theme) => void; + setAutofillEnabled: (enabled: boolean) => void; + setAutolockDuration: (duration: AutolockDuration) => void; + setNotificationsEnabled: (enabled: boolean) => void; + setClipboardTimeout: (seconds: number) => void; + setShowPasswordStrength: (show: boolean) => void; + setDefaultFolder: (folderId: string | null) => void; + reset: () => void; +}; + +const defaults = { + biometricEnabled: false, + theme: "system" as Theme, + autofillEnabled: true, + autolockDuration: "5min" as AutolockDuration, + notificationsEnabled: true, + clipboardTimeout: 30, + showPasswordStrength: true, + defaultFolder: null, +}; + +export const usesettings = create()( + persist( + (set) => ({ + ...defaults, + setBiometricEnabled: (enabled) => set({ biometricEnabled: enabled }), + setTheme: (theme) => set({ theme }), + setAutofillEnabled: (enabled) => set({ autofillEnabled: enabled }), + setAutolockDuration: (duration) => set({ autolockDuration: duration }), + setNotificationsEnabled: (enabled) => + set({ notificationsEnabled: enabled }), + setClipboardTimeout: (seconds) => set({ clipboardTimeout: seconds }), + setShowPasswordStrength: (show) => set({ showPasswordStrength: show }), + setDefaultFolder: (folderId) => set({ defaultFolder: folderId }), + reset: () => set(defaults), + }), + { + name: "settings", + storage: createJSONStorage(() => AsyncStorage), + } + ) +); diff --git a/apps/mobile/stores/vault.ts b/apps/mobile/stores/vault.ts new file mode 100644 index 0000000..01d20d3 --- /dev/null +++ b/apps/mobile/stores/vault.ts @@ -0,0 +1,273 @@ +import { create } from "zustand"; +import { persist, createJSONStorage } from "zustand/middleware"; +import AsyncStorage from "@react-native-async-storage/async-storage"; + +export type ItemType = + | "login" + | "note" + | "card" + | "identity" + | "ssh" + | "api" + | "otp" + | "passkey"; + +export type FolderIcon = + | "folder" + | "star" + | "archive" + | "lock" + | "globe" + | "code" + | "key" + | "user"; + +export type Folder = { + id: string; + name: string; + parentId: string | null; + icon: FolderIcon; + createdAt: string; + updatedAt: string; +}; + +export type VaultItem = { + id: string; + type: ItemType; + title: string; + data: Record; + tags: string[]; + favorite: boolean; + folderId: string | null; + createdAt: string; + updatedAt: string; + deletedAt: string | null; +}; + +export type CreateItemInput = { + type: ItemType; + title: string; + data: Record; + tags?: string[]; + favorite?: boolean; + folderId?: string | null; +}; + +export type UpdateItemInput = { + title?: string; + data?: Record; + tags?: string[]; + favorite?: boolean; + folderId?: string | null; +}; + +type VaultState = { + items: VaultItem[]; + folders: Folder[]; + loading: boolean; + error: string | null; + searchQuery: string; + selectedType: ItemType | null; + selectedFolder: string | null; + setSearchQuery: (query: string) => void; + setSelectedType: (type: ItemType | null) => void; + setSelectedFolder: (folderId: string | null) => void; + fetch: () => Promise; + create: (input: CreateItemInput) => Promise; + update: (id: string, input: UpdateItemInput) => Promise; + delete: (id: string) => Promise; + getFiltered: () => VaultItem[]; + setItems: (items: VaultItem[]) => void; + setFolders: (folders: Folder[]) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; +}; + +import { gettoken } from "../lib/storage"; +import { useauth } from "./auth"; + +const apihost = process.env.EXPO_PUBLIC_API_URL || "https://noro.sh"; +const baseurl = `${apihost}/api`; + +async function authfetch(path: string, options: RequestInit = {}) { + let token = await gettoken(); + if (!token) { + const authstate = useauth.getState(); + token = authstate.token; + } + const headers: Record = { + "content-type": "application/json", + ...(options.headers as Record), + }; + if (token) { + headers["authorization"] = `Bearer ${token}`; + } + const url = `${baseurl}${path}`; + try { + const res = await fetch(url, { ...options, headers }); + return res; + } catch (err) { + const msg = err instanceof Error ? err.message : "network error"; + throw new Error(`request failed: ${msg}`); + } +} + +export const usevault = create()( + persist( + (set, get) => ({ + items: [], + folders: [], + loading: false, + error: null, + searchQuery: "", + selectedType: null, + selectedFolder: null, + + setSearchQuery: (query) => set({ searchQuery: query }), + setSelectedType: (type) => set({ selectedType: type }), + setSelectedFolder: (folderId) => set({ selectedFolder: folderId }), + setItems: (items) => set({ items }), + setFolders: (folders) => set({ folders }), + setLoading: (loading) => set({ loading }), + setError: (error) => set({ error }), + + fetch: async () => { + set({ loading: true, error: null }); + try { + const res = await authfetch("/v1/vault/items"); + if (!res.ok) { + const body = await res.text(); + let msg = `fetch failed (${res.status})`; + try { + const json = JSON.parse(body); + msg = json.message || json.error || msg; + } catch {} + throw new Error(msg); + } + const data = await res.json(); + set({ items: data.items || [], loading: false }); + } catch (err) { + const msg = err instanceof Error ? err.message : "unknown error"; + set({ error: msg, loading: false }); + } + }, + + create: async (input) => { + set({ loading: true, error: null }); + try { + const res = await authfetch("/v1/vault/items", { + method: "POST", + body: JSON.stringify(input), + }); + if (!res.ok) { + const body = await res.text(); + let msg = `create failed (${res.status})`; + try { + const json = JSON.parse(body); + msg = json.message || json.error || msg; + } catch {} + throw new Error(msg); + } + const data = await res.json(); + set((state) => ({ + items: [...state.items, data.item], + loading: false, + })); + } catch (err) { + const msg = err instanceof Error ? err.message : "unknown error"; + set({ error: msg, loading: false }); + } + }, + + update: async (id, input) => { + set({ loading: true, error: null }); + try { + const res = await authfetch(`/v1/vault/items/${id}`, { + method: "PATCH", + body: JSON.stringify(input), + }); + if (!res.ok) { + const body = await res.text(); + let msg = `update failed (${res.status})`; + try { + const json = JSON.parse(body); + msg = json.message || json.error || msg; + } catch {} + throw new Error(msg); + } + const data = await res.json(); + set((state) => ({ + items: state.items.map((item) => + item.id === id ? { ...item, ...data.item } : item + ), + loading: false, + })); + } catch (err) { + const msg = err instanceof Error ? err.message : "unknown error"; + set({ error: msg, loading: false }); + } + }, + + delete: async (id) => { + set({ loading: true, error: null }); + try { + const res = await authfetch(`/v1/vault/items/${id}`, { + method: "DELETE", + }); + if (!res.ok) { + const body = await res.text(); + let msg = `delete failed (${res.status})`; + try { + const json = JSON.parse(body); + msg = json.message || json.error || msg; + } catch {} + throw new Error(msg); + } + set((state) => ({ + items: state.items.filter((item) => item.id !== id), + loading: false, + })); + } catch (err) { + const msg = err instanceof Error ? err.message : "unknown error"; + set({ error: msg, loading: false }); + } + }, + + getFiltered: () => { + const state = get(); + let filtered = state.items.filter((item) => !item.deletedAt); + + if (state.searchQuery) { + const query = state.searchQuery.toLowerCase(); + filtered = filtered.filter( + (item) => + item.title.toLowerCase().includes(query) || + item.tags.some((tag) => tag.toLowerCase().includes(query)) + ); + } + + if (state.selectedType) { + filtered = filtered.filter( + (item) => item.type === state.selectedType + ); + } + + if (state.selectedFolder) { + filtered = filtered.filter( + (item) => item.folderId === state.selectedFolder + ); + } + + return filtered; + }, + }), + { + name: "vault", + storage: createJSONStorage(() => AsyncStorage), + partialize: (state) => ({ + items: state.items, + folders: state.folders, + }), + } + ) +); diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json new file mode 100644 index 0000000..b9a6bec --- /dev/null +++ b/apps/mobile/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ".expo/types/**/*.ts", + "expo-env.d.ts" + ] +} diff --git a/apps/web/.gitignore b/apps/web/.gitignore index f650315..9ce419b 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -1,27 +1,13 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies +/lib/generated/prisma +.pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +npm-debug.log* +*.tsbuildinfo /node_modules - -# next.js +.env.local +.vercel /.next/ -/out/ - -# production /build - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files .env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts \ No newline at end of file +/out/ diff --git a/apps/web/app/[locale]/[code]/page.tsx b/apps/web/app/[locale]/[code]/page.tsx index 0f4c243..8e8ab0d 100644 --- a/apps/web/app/[locale]/[code]/page.tsx +++ b/apps/web/app/[locale]/[code]/page.tsx @@ -6,7 +6,7 @@ import { decrypt } from "@/lib/crypto"; import { highlight } from "@/lib/highlight"; import { Link } from "@/i18n/navigation"; import { Logo } from "@/components/logo"; -import { LanguageToggle } from "@/components/languagetoggle"; +import { LanguageToggle } from "@/components/language"; type Status = "loading" | "confirm" | "claiming" | "success" | "notfound" | "error"; @@ -118,7 +118,7 @@ export default function ClaimPage({ const handleDownload = () => { if (!secret || secret.type !== "file" || !secret.bytes) return; - const blob = new Blob([secret.bytes], { + const blob = new Blob([new Uint8Array(secret.bytes)], { type: secret.mimetype || "application/octet-stream", }); const url = URL.createObjectURL(blob); diff --git a/apps/web/app/[locale]/apple-icon.png b/apps/web/app/[locale]/apple-icon.png new file mode 100644 index 0000000..8d45f3b Binary files /dev/null and b/apps/web/app/[locale]/apple-icon.png differ diff --git a/apps/web/app/[locale]/docs/api/auth/page.tsx b/apps/web/app/[locale]/docs/api/auth/page.tsx index 98472d9..4c5ac31 100644 --- a/apps/web/app/[locale]/docs/api/auth/page.tsx +++ b/apps/web/app/[locale]/docs/api/auth/page.tsx @@ -7,7 +7,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Auth() { @@ -20,28 +20,28 @@ export default function Auth() { />
      -

      +

      API keys are used to authenticate all requests to the noro API. keys have the format:

      noro_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 -

      - the noro_ prefix followed by 32 alphanumeric characters. +

      + the noro_ prefix followed by 32 alphanumeric characters.

      -

      +

      create a new API key by calling the keys endpoint:

      {`curl -X POST https://noro.sh/api/v1/keys`} -

      +

      response:

      {`{ "key": "noro_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "expires": 1714000000000 }`} -

      +

      optionally, include a webhook URL to receive notifications:

      {`curl -X POST https://noro.sh/api/v1/keys \\ @@ -50,21 +50,21 @@ export default function Auth() {
      -

      - API keys expire after 90 days by default. the expires field in the response is a unix timestamp in milliseconds. +

      + API keys expire after 90 days by default. the expires field in the response is a unix timestamp in milliseconds.

      -

      +

      check your key's expiration date:

      {`curl https://noro.sh/api/v1/keys \\ -H "Authorization: Bearer noro_..."`} -

      +

      generate a new key before expiration to avoid service interruption.

      -

      +

      delete your API key if it's compromised or no longer needed:

      {`curl -X DELETE https://noro.sh/api/v1/keys \\ @@ -72,24 +72,24 @@ export default function Auth() {
      -

      +

      change or remove the webhook URL for your key:

      {`curl -X PATCH https://noro.sh/api/v1/keys \\ -H "Authorization: Bearer noro_..." \\ -H "Content-Type: application/json" \\ -d '{"webhook":"https://new-url.com/webhook"}'`} -

      +

      set webhook to empty string to remove it.

      -

      - include your API key in the Authorization header with the Bearer scheme: +

      + include your API key in the Authorization header with the Bearer scheme:

      {`Authorization: Bearer noro_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6`} -

      +

      example request:

      {`curl https://noro.sh/api/v1/secrets/abc123 \\ @@ -97,45 +97,45 @@ export default function Auth() {
      -
        +
        • - + store keys securely in environment variables
        • - + never commit keys to version control
        • - + rotate keys periodically
        • - + use separate keys for different environments
      -

      +

      API requests are rate limited to prevent abuse:

      -
        +
        • - - 100 requests per minute per API key + + 100 requests per minute per API key
        • - + sliding window algorithm
        • - - returns 429 when exceeded + + returns 429 when exceeded
        -

        +

        responses include rate limit headers:

        {`x-ratelimit-limit: 100 diff --git a/apps/web/app/[locale]/docs/api/endpoints/page.tsx b/apps/web/app/[locale]/docs/api/endpoints/page.tsx index 5045265..2a95989 100644 --- a/apps/web/app/[locale]/docs/api/endpoints/page.tsx +++ b/apps/web/app/[locale]/docs/api/endpoints/page.tsx @@ -7,7 +7,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Endpoints() { @@ -24,10 +24,10 @@ export default function Endpoints() {
      -

      +

      check API and database health status.

      -

      response

      +

      response

      {`{ "status": "healthy", "redis": "connected", @@ -37,12 +37,12 @@ export default function Endpoints() {
      -

      +

      get your API key info. requires authentication.

      -

      request headers

      +

      request headers

      Authorization: Bearer noro_... -

      response

      +

      response

      {`{ "hint": "noro_****ab12", "webhook": "https://example.com/webhook", @@ -52,14 +52,14 @@ export default function Endpoints() {
      -

      +

      generate a new API key. keys expire after 90 days.

      -

      request body

      +

      request body

      {`{ "webhook": "https://example.com/webhook" // optional }`} -

      response

      +

      response

      {`{ "key": "noro_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "expires": 1714000000000 @@ -67,41 +67,41 @@ export default function Endpoints() {
      -

      +

      update your API key settings. requires authentication.

      -

      request headers

      +

      request headers

      Authorization: Bearer noro_... -

      request body

      +

      request body

      {`{ "webhook": "https://new-url.com/webhook" // set to "" to remove }`} -

      response

      +

      response

      {`{ "updated": true }`}
      -

      +

      revoke your API key. requires authentication.

      -

      request headers

      +

      request headers

      Authorization: Bearer noro_... -

      response

      +

      response

      {`{ "deleted": true }`}
      -

      +

      create a new secret. requires authentication.

      -

      request headers

      +

      request headers

      {`Authorization: Bearer noro_... Content-Type: application/json`} -

      request body

      +

      request body

      {`{ "data": "base64_encrypted_data", // required "ttl": "1d", // optional: 1h, 6h, 12h, 1d, 7d @@ -110,7 +110,7 @@ Content-Type: application/json`} "mimetype": "text/plain", // optional: for files "views": 1 // optional: 1-5 }`}
      -

      response

      +

      response

      {`{ "id": "abc123", "url": "https://noro.sh/abc123" @@ -118,12 +118,12 @@ Content-Type: application/json`}
      -

      +

      claim a secret and retrieve its data. requires authentication.

      -

      request headers

      +

      request headers

      Authorization: Bearer noro_... -

      response

      +

      response

      {`{ "data": "base64_encrypted_data", "type": "text", @@ -131,66 +131,66 @@ Content-Type: application/json`} "mimetype": null, "remaining": 0 }`}
      -

      errors

      -
        +

        errors

        +
        • - - 404 secret not found or already claimed + + 404 secret not found or already claimed
      -

      +

      revoke a secret before it's claimed. requires authentication.

      -

      request headers

      +

      request headers

      Authorization: Bearer noro_... -

      response

      +

      response

      {`{ "deleted": true }`} -

      errors

      -
        +

        errors

        +
        • - - 404 secret not found + + 404 secret not found
      -

      - all errors return a JSON object with an error field: +

      + all errors return a JSON object with an error field:

      {`{ "error": "error message" }`} -

      status codes

      -
        +

        status codes

        +
        • - - 400 bad request + + 400 bad request
        • - - 401 unauthorized + + 401 unauthorized
        • - - 404 not found + + 404 not found
        • - - 413 payload too large (5MB max) + + 413 payload too large (5MB max)
        • - - 429 rate limited + + 429 rate limited
        • - - 500 server error + + 500 server error
      diff --git a/apps/web/app/[locale]/docs/api/page.tsx b/apps/web/app/[locale]/docs/api/page.tsx index 6ee6caf..2a8ce9d 100644 --- a/apps/web/app/[locale]/docs/api/page.tsx +++ b/apps/web/app/[locale]/docs/api/page.tsx @@ -8,7 +8,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Api() { @@ -21,27 +21,27 @@ export default function Api() { />
      -

      +

      the noro API allows you to create and manage one-time secrets programmatically. integrate secret sharing into your applications, CI/CD pipelines, or automation workflows.

      -

      - all API endpoints are versioned under /api/v1 and require authentication via bearer token. +

      + all API endpoints are versioned under /api/v1 and require authentication via bearer token.

      -

      +

      1. generate an API key:

      {`curl -X POST https://noro.sh/api/v1/keys`} -

      +

      2. create a secret:

      {`curl -X POST https://noro.sh/api/v1/secrets \\ -H "Authorization: Bearer noro_..." \\ -H "Content-Type: application/json" \\ -d '{"data":"base64_encrypted_data"}'`} -

      +

      3. claim a secret:

      {`curl https://noro.sh/api/v1/secrets/abc123 \\ @@ -51,20 +51,20 @@ export default function Api() {
      -

      bearer token auth

      -

      - secure authentication using API keys with the noro_ prefix. +

      bearer token auth

      +

      + secure authentication using API keys with the noro_ prefix.

      -

      webhooks

      -

      +

      webhooks

      +

      receive notifications when secrets are created, viewed, or expire.

      -

      rate limiting

      -

      +

      rate limiting

      +

      100 requests per minute per API key using sliding window.

      @@ -72,19 +72,19 @@ export default function Api() {
      -

      Next steps

      +

      Next steps

      - -

      Authentication

      -

      generate and use API keys

      + +

      Authentication

      +

      generate and use API keys

      - -

      Endpoints

      -

      full API reference

      + +

      Endpoints

      +

      full API reference

      - -

      Webhooks

      -

      event notifications

      + +

      Webhooks

      +

      event notifications

      diff --git a/apps/web/app/[locale]/docs/api/webhooks/page.tsx b/apps/web/app/[locale]/docs/api/webhooks/page.tsx index 5e83111..0431ea8 100644 --- a/apps/web/app/[locale]/docs/api/webhooks/page.tsx +++ b/apps/web/app/[locale]/docs/api/webhooks/page.tsx @@ -7,7 +7,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Webhooks() { @@ -20,13 +20,13 @@ export default function Webhooks() { />
      -

      +

      configure a webhook URL when generating your API key:

      {`curl -X POST https://noro.sh/api/v1/keys \\ -H "Content-Type: application/json" \\ -d '{"webhook":"https://example.com/webhook"}'`} -

      +

      the webhook URL must use HTTPS.

      @@ -34,20 +34,20 @@ export default function Webhooks() {
      -

      secret.created

      -

      +

      secret.created

      +

      fired when a new secret is stored.

      -

      secret.viewed

      -

      +

      secret.viewed

      +

      fired when a secret is claimed (but views remain).

      -

      secret.expired

      -

      +

      secret.expired

      +

      fired when the last view is consumed and the secret is deleted.

      @@ -55,7 +55,7 @@ export default function Webhooks() {
      -

      +

      all webhook events include the same payload structure:

      {`{ @@ -68,17 +68,17 @@ export default function Webhooks() {
      -

      +

      webhook requests include a signature header for verification:

      x-noro-signature: t=1706000000000,v1=5d41402abc4b... -

      - the header contains a timestamp (t) and signature (v1). the signature is HMAC-SHA256 of timestamp.body. +

      + the header contains a timestamp (t) and signature (v1). the signature is HMAC-SHA256 of timestamp.body.

      -

      +

      verify the signature and check timestamp to prevent replay attacks:

      {`const crypto = require("crypto"); @@ -88,7 +88,7 @@ function verify(body, header, secret) { const timestamp = tPart.split("=")[1]; const signature = vPart.split("=")[1]; - // reject if older than 5 minutes + const age = Date.now() - parseInt(timestamp); if (age > 300000) return false; @@ -105,41 +105,41 @@ function verify(body, header, secret) {
      -

      +

      webhooks are delivered via upstash qstash with automatic retries:

      -
        +
        • - + 3 retry attempts on failure
        • - + exponential backoff between retries
        • - + 10 second timeout per request
      -
        +
        • - + always verify the signature
        • - + respond with 200 quickly, process async
        • - + handle duplicate deliveries idempotently
        • - + use a queue for processing
        diff --git a/apps/web/app/[locale]/docs/breadcrumb.tsx b/apps/web/app/[locale]/docs/breadcrumb.tsx index 23d7922..c0a75de 100644 --- a/apps/web/app/[locale]/docs/breadcrumb.tsx +++ b/apps/web/app/[locale]/docs/breadcrumb.tsx @@ -9,11 +9,12 @@ const sectionNames: Record = { cli: "CLI", web: "Web", security: "Security", + api: "API", future: "Coming Soon", }; const chevron = ( -
        - -
        - noro + +
        + noro {chevron} - {sectionNames[page.section]} + {sectionNames[page.section]} {chevron} - {page.title} + {page.title}
        -
        - noro +
        + noro - / - {page.title} + / + {page.title}
        ); diff --git a/apps/web/app/[locale]/docs/cli/claim/page.tsx b/apps/web/app/[locale]/docs/cli/claim/page.tsx index 0bc01c3..d0f5aaf 100644 --- a/apps/web/app/[locale]/docs/cli/claim/page.tsx +++ b/apps/web/app/[locale]/docs/cli/claim/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Claim() { diff --git a/apps/web/app/[locale]/docs/cli/config/page.tsx b/apps/web/app/[locale]/docs/cli/config/page.tsx index f9f5944..d31de28 100644 --- a/apps/web/app/[locale]/docs/cli/config/page.tsx +++ b/apps/web/app/[locale]/docs/cli/config/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Config() { diff --git a/apps/web/app/[locale]/docs/cli/manage/page.tsx b/apps/web/app/[locale]/docs/cli/manage/page.tsx index 21cb41c..d78ad03 100644 --- a/apps/web/app/[locale]/docs/cli/manage/page.tsx +++ b/apps/web/app/[locale]/docs/cli/manage/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Manage() { diff --git a/apps/web/app/[locale]/docs/cli/markdown.tsx b/apps/web/app/[locale]/docs/cli/markdown.tsx index f212f93..9f26565 100644 --- a/apps/web/app/[locale]/docs/cli/markdown.tsx +++ b/apps/web/app/[locale]/docs/cli/markdown.tsx @@ -1,10 +1,12 @@ "use client"; -import { Section, Code } from "../components"; +import { Section, Code, Table } from "../components"; const orderedlist = /^\d+\.\s/; const listitem = /^[-\d.]\s*/; const inlinecode = /`([^`]+)`/; +const tablerow = /^\|(.+)\|$/; +const tableseparator = /^\|[-:\s|]+\|$/; interface MarkdownProps { sections: { @@ -14,13 +16,32 @@ interface MarkdownProps { }[]; } +function parseTableRow(line: string): string[] { + return line + .slice(1, -1) + .split("|") + .map((cell) => cell.trim()); +} + function parseContent(content: string) { const elements: React.ReactNode[] = []; const lines = content.trim().split("\n"); let i = 0; while (i < lines.length) { - const line = lines[i]; + const line = lines[i].trimEnd(); + + if (line.startsWith("### ")) { + const heading = line.replace(/^###\s+/, ""); + elements.push( +

        + {heading} +

        + ); + i++; + continue; + } + if (line.startsWith("```")) { const codeLines: string[] = []; @@ -36,6 +57,22 @@ function parseContent(content: string) { continue; } + + if (tablerow.test(line) && i + 1 < lines.length && tableseparator.test(lines[i + 1].trimEnd())) { + const headers = parseTableRow(line); + i += 2; + const rows: string[][] = []; + while (i < lines.length && tablerow.test(lines[i].trimEnd()) && !tableseparator.test(lines[i].trimEnd())) { + rows.push(parseTableRow(lines[i].trimEnd())); + i++; + } + elements.push( + + ); + continue; + } + + if (line.startsWith("- ") || line.startsWith("1. ")) { const listItems: string[] = []; while (i < lines.length && (lines[i].startsWith("- ") || orderedlist.test(lines[i]))) { @@ -45,20 +82,20 @@ function parseContent(content: string) { const isOrdered = line.startsWith("1."); elements.push( isOrdered ? ( -
          +
            {listItems.map((item, idx) => ( -
          1. - {idx + 1} - {item} +
          2. + {idx + 1}. + {parseInline(item)}
          3. ))}
          ) : ( -
            +
              {listItems.map((item, idx) => ( -
            • - - {parseInline(item)} +
            • + + {parseInline(item)}
            • ))}
            @@ -67,13 +104,15 @@ function parseContent(content: string) { continue; } + if (line.trim() === "") { i++; continue; } + elements.push( -

            +

            {parseInline(line)}

            ); @@ -95,7 +134,7 @@ function parseInline(text: string): React.ReactNode { parts.push(remaining.slice(0, codeMatch.index)); } parts.push( - + {codeMatch[1]} ); diff --git a/apps/web/app/[locale]/docs/cli/page.tsx b/apps/web/app/[locale]/docs/cli/page.tsx index 312b0e5..1977f07 100644 --- a/apps/web/app/[locale]/docs/cli/page.tsx +++ b/apps/web/app/[locale]/docs/cli/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Install() { diff --git a/apps/web/app/[locale]/docs/cli/push/page.tsx b/apps/web/app/[locale]/docs/cli/push/page.tsx index e2f8eef..f81fc8f 100644 --- a/apps/web/app/[locale]/docs/cli/push/page.tsx +++ b/apps/web/app/[locale]/docs/cli/push/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Push() { diff --git a/apps/web/app/[locale]/docs/cli/share/page.tsx b/apps/web/app/[locale]/docs/cli/share/page.tsx index b21957a..b529400 100644 --- a/apps/web/app/[locale]/docs/cli/share/page.tsx +++ b/apps/web/app/[locale]/docs/cli/share/page.tsx @@ -9,7 +9,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Share() { diff --git a/apps/web/app/[locale]/docs/components.tsx b/apps/web/app/[locale]/docs/components.tsx index 9c28839..f15f7fc 100644 --- a/apps/web/app/[locale]/docs/components.tsx +++ b/apps/web/app/[locale]/docs/components.tsx @@ -39,9 +39,10 @@ function Anchor({ id }: { id?: string }) { return ( -
            +			
             				{children}
             			
            @@ -106,12 +111,47 @@ export function Code({ children, className = "" }: { children: string; className export function Codeinline({ children, className = "" }: { children: string; className?: string }) { return ( - + {children} ); } +export function Table({ headers, rows }: { headers: string[]; rows: string[][] }) { + return ( +
            +
        + + + {headers.map((header) => ( + + ))} + + + + {rows.map((row) => ( + + {row.map((cell) => ( + + ))} + + ))} + +
        + {header} +
        + {cell} +
        +
        + ); +} + export function Prevnext() { const pathname = usePathname(); const router = useRouter(); @@ -134,11 +174,11 @@ export function Prevnext() { }, [prev, next, router]); return ( -
        +
        {prev ? ( ← {prev.title} @@ -148,7 +188,7 @@ export function Prevnext() { {next ? ( {next.title} → @@ -169,10 +209,10 @@ export function Card({ code?: string; }) { return ( -
        - {code && {code}} - {title &&

        {title}

        } -

        {description}

        +
        + {code && {code}} + {title &&

        {title}

        } +

        {description}

        ); } @@ -187,9 +227,9 @@ export function Section({ children: React.ReactNode; }) { return ( -
        -
        -

        +
        +
        +

        {title}

        diff --git a/apps/web/app/[locale]/docs/config.ts b/apps/web/app/[locale]/docs/config.ts index 2511a0a..df1369a 100644 --- a/apps/web/app/[locale]/docs/config.ts +++ b/apps/web/app/[locale]/docs/config.ts @@ -109,25 +109,15 @@ export const toc: Record = { { id: "best-practices", title: "Best practices", level: 2 }, ], "/docs/desktop": [ - { id: "features", title: "Planned features", level: 2 }, - { id: "password-manager", title: "password manager", level: 3 }, - { id: "otp", title: "OTP authenticator", level: 3 }, - { id: "notes", title: "encrypted notes", level: 3 }, - { id: "autofill", title: "auto-fill", level: 3 }, - { id: "biometric", title: "biometric unlock", level: 3 }, - { id: "sync", title: "cloud sync", level: 3 }, + { id: "features", title: "Planned Features", level: 2 }, { id: "platforms", title: "Platforms", level: 2 }, - { id: "notify", title: "Get notified", level: 2 }, + { id: "notify", title: "Get Notified", level: 2 }, ], "/docs/extension": [ - { id: "features", title: "Planned features", level: 2 }, - { id: "autofill", title: "auto-fill", level: 3 }, - { id: "generate", title: "password generator", level: 3 }, - { id: "otp", title: "OTP auto-fill", level: 3 }, - { id: "save", title: "auto-save", level: 3 }, - { id: "sync", title: "Sync with desktop", level: 2 }, - { id: "browsers", title: "Supported browsers", level: 2 }, - { id: "notify", title: "Get notified", level: 2 }, + { id: "features", title: "Planned Features", level: 2 }, + { id: "sync", title: "Sync with Desktop", level: 2 }, + { id: "browsers", title: "Supported Browsers", level: 2 }, + { id: "notify", title: "Get Notified", level: 2 }, ], }; diff --git a/apps/web/app/[locale]/docs/desktop/page.tsx b/apps/web/app/[locale]/docs/desktop/page.tsx index 76e03df..168fd88 100644 --- a/apps/web/app/[locale]/docs/desktop/page.tsx +++ b/apps/web/app/[locale]/docs/desktop/page.tsx @@ -1,5 +1,5 @@ import type { Metadata, Viewport } from "next"; -import { Header, Section, Prevnext } from "../components"; +import { Header, Prevnext } from "../components"; export const metadata: Metadata = { title: "Desktop", @@ -7,81 +7,181 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; +const features = [ + { + icon: ( + + ), + title: "Password Manager", + description: "Store and organize all your passwords securely. Encrypted locally with your master password.", + }, + { + icon: ( + + ), + title: "OTP Authenticator", + description: "Built-in two-factor authentication. No need for a separate authenticator app.", + }, + { + icon: ( + + ), + title: "Encrypted Notes", + description: "Secure notes for sensitive information like recovery codes, API keys, and credentials.", + }, + { + icon: ( + + ), + title: "Auto-fill", + description: "Automatically fill passwords in apps and browsers. Works system-wide.", + }, + { + icon: ( + + ), + title: "Biometric Unlock", + description: "Unlock with Touch ID, Face ID, or Windows Hello. Quick access without typing your master password.", + }, + { + icon: ( + + ), + title: "Cloud Sync", + description: "Optional encrypted sync across all your devices. Your data is encrypted before leaving your device.", + }, +]; + +const platforms = [ + { + name: "macOS", + icon: ( + + ), + }, + { + name: "Windows", + icon: ( + + ), + }, + { + name: "Linux", + icon: ( + + ), + }, +]; + export default function Desktop() { return (
        -
        -
        -
        -

        password manager

        -

        - store and organize all your passwords securely. encrypted locally with your master password. -

        -
        -
        -

        OTP authenticator

        -

        - built-in two-factor authentication. no need for a separate authenticator app. -

        -
        -
        -

        encrypted notes

        -

        - secure notes for sensitive information like recovery codes, API keys, and credentials. -

        -
        -
        -

        auto-fill

        -

        - automatically fill passwords in apps and browsers. works system-wide. -

        -
        -
        -

        biometric unlock

        -

        - unlock with Touch ID, Face ID, or Windows Hello. quick access without typing your master password. -

        -
        -
        -

        cloud sync

        -

        - optional encrypted sync across all your devices. your data is encrypted before leaving your device. -

        -
        + +
        +
        +
        + In Development +
        +

        + We're building a native desktop app with full password management, OTP authentication, and seamless sync. Star the repo to get notified when it launches. +

        +
        + + +
        +

        Planned Features

        +
        + {features.map((feature) => ( +
        +
        + {feature.icon} +
        +

        {feature.title}

        +

        {feature.description}

        +
        + ))}
        -
        +
        -
        -
          -
        • - - macOS -
        • -
        • - - Windows -
        • -
        • - - Linux -
        • -
        -
        + +
        +

        Platforms

        +
        + {platforms.map((platform) => ( +
        + {platform.icon} + {platform.name} +
        + ))} +
        +
        -
        -

        - follow visible/noro on GitHub to get notified when the desktop app is released. -

        -
        + +
        +
        +

        Get Notified

        +

        + Follow the project on GitHub to receive updates when the desktop app is released. +

        + + + Star on GitHub + +
        +
        diff --git a/apps/web/app/[locale]/docs/encryption/page.tsx b/apps/web/app/[locale]/docs/encryption/page.tsx index 447c5e4..cab550d 100644 --- a/apps/web/app/[locale]/docs/encryption/page.tsx +++ b/apps/web/app/[locale]/docs/encryption/page.tsx @@ -7,7 +7,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; export default function Encryption() { @@ -20,27 +20,27 @@ export default function Encryption() { />
        -

        - noro uses AES-256-GCM for encryption. this is the same algorithm used by governments and financial institutions worldwide. +

        + noro uses AES-256-GCM for encryption. this is the same algorithm used by governments and financial institutions worldwide.

        -
          +
          • - - AES-256: 256-bit key, virtually unbreakable + + AES-256: 256-bit key, virtually unbreakable
          • - - GCM mode: authenticated encryption with integrity check + + GCM mode: authenticated encryption with integrity check
          • - - random IV: unique initialization vector per secret + + random IV: unique initialization vector per secret
        -

        +

        when you create a secret:

        {`1. generate random 256-bit key @@ -48,7 +48,7 @@ export default function Encryption() { 3. encrypt data with AES-256-GCM 4. send encrypted blob to server 5. key stays in URL fragment (never sent)`} -

        +

        when you view a secret:

        {`1. fetch encrypted blob from server @@ -58,67 +58,67 @@ export default function Encryption() {
        -

        - the encryption key is stored in the URL fragment (the part after #): +

        + the encryption key is stored in the URL fragment (the part after #):

        https://noro.sh/abc123#encryption_key_here -

        +

        URL fragments are special:

        -
          +
          • - + never sent to the server in HTTP requests
          • - + not included in server logs
          • - + only accessible by JavaScript in the browser
        -

        +

        our servers never have access to:

        -
          +
          • - + your original secret content
          • - + the encryption key
          • - + any way to decrypt the stored data
          -

          +

          even if our database was compromised, attackers would only get encrypted blobs that are useless without the keys.

        -

        +

        secrets are permanently deleted:

        -
          +
          • - + immediately after being viewed (or after view limit reached)
          • - + automatically when TTL expires
          • - + no backups are kept of expired secrets
          diff --git a/apps/web/app/[locale]/docs/extension/page.tsx b/apps/web/app/[locale]/docs/extension/page.tsx index cc4401c..3deb400 100644 --- a/apps/web/app/[locale]/docs/extension/page.tsx +++ b/apps/web/app/[locale]/docs/extension/page.tsx @@ -1,5 +1,5 @@ import type { Metadata, Viewport } from "next"; -import { Header, Section, Prevnext } from "../components"; +import { Header, Prevnext } from "../components"; export const metadata: Metadata = { title: "Extension", @@ -7,101 +7,214 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; +const features = [ + { + icon: ( + + ), + title: "Auto-fill", + description: "Automatically fill passwords and login forms. Works on any website.", + }, + { + icon: ( + + ), + title: "Password Generator", + description: "Generate strong, unique passwords for every site. Customize length and character types.", + }, + { + icon: ( + + ), + title: "OTP Auto-fill", + description: "Automatically fill two-factor codes. No need to switch apps or type codes manually.", + }, + { + icon: ( + + ), + title: "Auto-save", + description: "Automatically capture new logins when you sign up for sites.", + }, +]; + +const browsers = [ + { + name: "Chrome", + icon: ( + + ), + }, + { + name: "Firefox", + icon: ( + + ), + }, + { + name: "Safari", + icon: ( + + ), + }, + { + name: "Edge", + icon: ( + + ), + }, + { + name: "Brave", + icon: ( + + ), + }, + { + name: "Arc", + icon: ( + + ), + }, +]; + +const syncFeatures = [ + "All passwords available in your browser", + "Changes sync instantly across devices", + "Works on mobile browsers too", +]; + export default function Extension() { return (
          -
          -
          -
          -

          auto-fill

          -

          - automatically fill passwords and login forms. works on any website. -

          -
          -
          -

          password generator

          -

          - generate strong, unique passwords for every site. customize length and character types. -

          -
          -
          -

          OTP auto-fill

          -

          - automatically fill two-factor codes. no need to switch apps or type codes manually. -

          -
          -
          -

          auto-save

          -

          - automatically capture new logins when you sign up for sites. -

          -
          + +
          +
          +
          + In Development
          -
          - -
          -

          - the extension syncs with the noro desktop app: +

          + We're building browser extensions that seamlessly integrate with the noro desktop app. Auto-fill passwords, generate secure credentials, and manage 2FA codes directly in your browser.

          -
            -
          • - - all passwords available in your browser -
          • -
          • - - changes sync instantly across devices -
          • -
          • - - works on mobile browsers too -
          • -
          -
          +
        -
        -
          -
        • - - Chrome -
        • -
        • - - Firefox -
        • -
        • - - Safari -
        • -
        • - - Edge -
        • -
        • - - Brave -
        • -
        • - - Arc -
        • -
        -
        + +
        +

        Planned Features

        +
        + {features.map((feature) => ( +
        +
        + {feature.icon} +
        +

        {feature.title}

        +

        {feature.description}

        +
        + ))} +
        +
        -
        -

        - follow visible/noro on GitHub to get notified when the browser extension is released. -

        -
        + +
        +
        +
        +
        + +
        +
        +

        Sync with Desktop

        +

        The extension syncs with the noro desktop app

        +
        +
        +
          + {syncFeatures.map((feature) => ( +
        • + + {feature} +
        • + ))} +
        +
        +
        + + +
        +

        Supported Browsers

        +
        + {browsers.map((browser) => ( +
        + {browser.icon} + {browser.name} +
        + ))} +
        +
        + + +
        +
        +

        Get Notified

        +

        + Follow the project on GitHub to receive updates when the browser extension is released. +

        + + + Star on GitHub + +
        +
        diff --git a/apps/web/app/[locale]/docs/layout.tsx b/apps/web/app/[locale]/docs/layout.tsx index 8528b11..7d6b794 100644 --- a/apps/web/app/[locale]/docs/layout.tsx +++ b/apps/web/app/[locale]/docs/layout.tsx @@ -11,7 +11,7 @@ export const metadata: Metadata = { }; export const viewport: Viewport = { - themeColor: "#F5F3EF", + themeColor: "#0a0a0a", }; const sidebarscript = `(function(){var s=document.querySelector('[data-sidebar]');var a=document.querySelector('[data-sidebar] [data-active]');if(s&&a){a.scrollIntoView({block:'nearest',behavior:'instant'})}})()`; @@ -20,7 +20,7 @@ export default function DocsLayout({ children, }: { children: React.ReactNode }) { return ( -
        +
        @@ -30,8 +30,8 @@ export default function DocsLayout({
        -
        -
        +
        +