From 11414e3bef5dec47ce9e4711a30b9c5d67602d3e Mon Sep 17 00:00:00 2001 From: Sahil Garg Date: Fri, 2 Oct 2026 16:43:19 +0530 Subject: [PATCH 1/6] feat(eid-wallet): load the apps ribbon from the marketplace's live platforms The apps ribbon on the main screen had five apps hard-coded. It now shows the platforms returned by the marketplace's GET /api/platforms, which lists what platforms have published through Awareness. - marketplaceApps.ts fetches the endpoint, keeps only entries with a name and an https link (logos must be https too), drops repeats, and caches the last good list so the ribbon fills at once and works offline. - The endpoint sends no CORS headers, so inside the app the request goes through the Tauri HTTP plugin. Its scope only allows the marketplace host. - Cards show placeholders while loading, an initial when a logo fails, and the published category (translated where the wallet has a label). - tauri-plugin-http is pinned to 2.7 on both sides so Tauri stays on 2.11. --- infrastructure/eid-wallet/package.json | 1 + .../eid-wallet/src-tauri/Cargo.lock | 495 +++++++++++++++++- .../eid-wallet/src-tauri/Cargo.toml | 1 + .../src-tauri/capabilities/desktop.json | 6 +- .../src-tauri/capabilities/mobile.json | 8 +- .../eid-wallet/src-tauri/src/lib.rs | 1 + .../src/lib/utils/marketplaceApps.spec.ts | 191 +++++++ .../src/lib/utils/marketplaceApps.ts | 148 ++++++ .../main/components/AppsMarketplace.svelte | 149 ++++-- pnpm-lock.yaml | 15 + 10 files changed, 953 insertions(+), 62 deletions(-) create mode 100644 infrastructure/eid-wallet/src/lib/utils/marketplaceApps.spec.ts create mode 100644 infrastructure/eid-wallet/src/lib/utils/marketplaceApps.ts diff --git a/infrastructure/eid-wallet/package.json b/infrastructure/eid-wallet/package.json index 0ab67d7a7..3eb1a69e7 100644 --- a/infrastructure/eid-wallet/package.json +++ b/infrastructure/eid-wallet/package.json @@ -38,6 +38,7 @@ "@tauri-apps/plugin-barcode-scanner": "^2.4.2", "@tauri-apps/plugin-biometric": "^2.3.2", "@tauri-apps/plugin-deep-link": "^2.4.5", + "@tauri-apps/plugin-http": "~2.7.0", "@tauri-apps/plugin-opener": "^2.5.2", "@tauri-apps/plugin-store": "^2.4.1", "axios": "^1.6.7", diff --git a/infrastructure/eid-wallet/src-tauri/Cargo.lock b/infrastructure/eid-wallet/src-tauri/Cargo.lock index 3d3de63dc..f8e1a8213 100644 --- a/infrastructure/eid-wallet/src-tauri/Cargo.lock +++ b/infrastructure/eid-wallet/src-tauri/Cargo.lock @@ -471,6 +471,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chacha20" version = "0.10.0" @@ -545,10 +551,39 @@ 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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +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" @@ -572,7 +607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -585,10 +620,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -738,6 +779,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + [[package]] name = "dbus" version = "0.9.11" @@ -880,6 +927,15 @@ dependencies = [ "const-random", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dom_query" version = "0.27.0" @@ -960,6 +1016,7 @@ dependencies = [ "tauri-plugin-biometric", "tauri-plugin-crypto-hw", "tauri-plugin-deep-link", + "tauri-plugin-http", "tauri-plugin-notifications", "tauri-plugin-opener", "tauri-plugin-process", @@ -988,6 +1045,20 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + [[package]] name = "endi" version = "1.1.1" @@ -1388,8 +1459,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1399,9 +1472,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1566,6 +1641,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +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" @@ -1688,6 +1782,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1699,6 +1794,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1717,9 +1828,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2119,6 +2232,12 @@ 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" @@ -2134,6 +2253,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + [[package]] name = "mac" version = "0.1.1" @@ -2257,6 +2382,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "ndk" version = "0.9.0" @@ -3024,6 +3155,22 @@ 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 = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -3042,6 +3189,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.5", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -3088,6 +3290,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.0" @@ -3119,6 +3331,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3137,6 +3359,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.0" @@ -3236,6 +3467,49 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[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", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -3270,6 +3544,20 @@ dependencies = [ "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 = "rust-ini" version = "0.21.3" @@ -3308,12 +3596,53 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -3522,6 +3851,18 @@ 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.17.0" @@ -3627,6 +3968,12 @@ 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" @@ -3876,6 +4223,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.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" @@ -3897,7 +4265,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ "bitflags 2.11.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dbus", @@ -3976,7 +4344,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4138,6 +4506,54 @@ dependencies = [ "windows-result 0.3.4", ] +[[package]] +name = "tauri-plugin-fs" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de22eef34fd78c0da050e748710edd50bf127e651d02ea1b2bfada1523cc5c51" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.0.6+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-http" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa402aca4e2ec043f4ced841b74f24b2b8c5bf8c16e2ae2fa494b651da765e07" +dependencies = [ + "bytes", + "cookie_store", + "data-url", + "http", + "regex", + "reqwest 0.12.28", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "tokio", + "url", + "urlpattern", +] + [[package]] name = "tauri-plugin-notifications" version = "0.4.3" @@ -4445,6 +4861,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "tokio" version = "1.50.0" @@ -4471,6 +4893,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4511,6 +4943,21 @@ dependencies = [ "winnow 0.7.15", ] +[[package]] +name = "toml" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + [[package]] name = "toml_datetime" version = "0.6.3" @@ -4775,6 +5222,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[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" @@ -5021,6 +5474,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.3" @@ -5077,6 +5540,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -5318,6 +5790,15 @@ 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" @@ -5787,6 +6268,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/infrastructure/eid-wallet/src-tauri/Cargo.toml b/infrastructure/eid-wallet/src-tauri/Cargo.toml index df50ff9ff..b53e15dcb 100644 --- a/infrastructure/eid-wallet/src-tauri/Cargo.toml +++ b/infrastructure/eid-wallet/src-tauri/Cargo.toml @@ -36,6 +36,7 @@ argon2 = { version = "0.5.3" } rand_core = { version = "0.6", features = ["std"] } thiserror = { version = "2.0.11" } tauri-plugin-process = "2" +tauri-plugin-http = "~2.7" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] tauri-plugin-barcode-scanner = "2" diff --git a/infrastructure/eid-wallet/src-tauri/capabilities/desktop.json b/infrastructure/eid-wallet/src-tauri/capabilities/desktop.json index d0147cab7..2e6c49c7b 100644 --- a/infrastructure/eid-wallet/src-tauri/capabilities/desktop.json +++ b/infrastructure/eid-wallet/src-tauri/capabilities/desktop.json @@ -12,7 +12,11 @@ "deep-link:default", "notifications:default", "process:default", - "opener:allow-default-urls" + "opener:allow-default-urls", + { + "identifier": "http:default", + "allow": [{ "url": "https://marketplace.w3ds.metastate.foundation/*" }] + } ], "platforms": [ "windows", diff --git a/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json b/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json index 871c2cede..f65ad407a 100644 --- a/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json +++ b/infrastructure/eid-wallet/src-tauri/capabilities/mobile.json @@ -16,10 +16,14 @@ "crypto-hw:default", "notifications:default", "process:default", - "opener:allow-default-urls" + "opener:allow-default-urls", + { + "identifier": "http:default", + "allow": [{ "url": "https://marketplace.w3ds.metastate.foundation/*" }] + } ], "platforms": [ "iOS", "android" ] -} \ No newline at end of file +} diff --git a/infrastructure/eid-wallet/src-tauri/src/lib.rs b/infrastructure/eid-wallet/src-tauri/src/lib.rs index f3c3dba3a..867e9302e 100644 --- a/infrastructure/eid-wallet/src-tauri/src/lib.rs +++ b/infrastructure/eid-wallet/src-tauri/src/lib.rs @@ -218,6 +218,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_http::init()) .plugin( tauri_plugin_store::Builder::new() .default_deserialize_fn(deserialize_with_recovery) diff --git a/infrastructure/eid-wallet/src/lib/utils/marketplaceApps.spec.ts b/infrastructure/eid-wallet/src/lib/utils/marketplaceApps.spec.ts new file mode 100644 index 000000000..61c436eb1 --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/utils/marketplaceApps.spec.ts @@ -0,0 +1,191 @@ +import { describe, expect, it, vi } from "vitest"; +import { + fetchRibbonApps, + readCachedRibbonApps, + toRibbonApps, + writeCachedRibbonApps, +} from "./marketplaceApps"; + +// Shape of a live `/api/platforms` entry. +const platform = (overrides: Record = {}) => ({ + id: "80hours", + name: "80hours Task Manager", + description: "Tasks", + category: "Productivity", + logoUrl: "https://80hours.postplatforms.com/logo.svg", + url: "https://80hours.postplatforms.com", + ename: "@29217af0", + ...overrides, +}); + +function memoryStorage() { + const data = new Map(); + return { + getItem: (k: string) => data.get(k) ?? null, + setItem: (k: string, v: string) => void data.set(k, v), + }; +} + +describe("toRibbonApps", () => { + it("maps a platform to a ribbon entry", () => { + expect(toRibbonApps({ platforms: [platform()], count: 1 })).toEqual([ + { + key: "@29217af0", + name: "80hours Task Manager", + category: "Productivity", + logo: "https://80hours.postplatforms.com/logo.svg", + url: "https://80hours.postplatforms.com/", + }, + ]); + }); + + it("keeps the endpoint's order", () => { + const apps = toRibbonApps({ + platforms: [ + platform({ ename: "@a", name: "A", url: "https://a.test" }), + platform({ ename: "@b", name: "B", url: "https://b.test" }), + ], + }); + expect(apps.map((a) => a.name)).toEqual(["A", "B"]); + }); + + it("skips platforms without an https link", () => { + const apps = toRibbonApps({ + platforms: [ + platform({ ename: "@1", url: "" }), + platform({ ename: "@2", url: "http://plain.test" }), + platform({ ename: "@3", url: "javascript:alert(1)" }), + platform({ ename: "@4", url: null }), + ], + }); + expect(apps).toEqual([]); + }); + + it("skips platforms without a name", () => { + expect(toRibbonApps({ platforms: [platform({ name: " " })] })).toEqual( + [], + ); + }); + + it("drops logos that are not https", () => { + const [app] = toRibbonApps({ + platforms: [platform({ logoUrl: "http://x.test/logo.png" })], + }); + expect(app.logo).toBeNull(); + const [noLogo] = toRibbonApps({ + platforms: [platform({ logoUrl: null })], + }); + expect(noLogo.logo).toBeNull(); + }); + + it("drops repeats by eName or by link", () => { + const apps = toRibbonApps({ + platforms: [ + platform(), + platform({ name: "Again" }), + platform({ ename: "@other", name: "Same link" }), + ], + }); + expect(apps).toHaveLength(1); + }); + + it("falls back to the link as key when there is no eName", () => { + const [app] = toRibbonApps({ platforms: [platform({ ename: "" })] }); + expect(app.key).toBe("https://80hours.postplatforms.com/"); + }); + + it("tolerates a missing category", () => { + const [app] = toRibbonApps({ + platforms: [platform({ category: undefined })], + }); + expect(app.category).toBe(""); + }); + + it("returns [] for unexpected payloads", () => { + expect(toRibbonApps(null)).toEqual([]); + expect(toRibbonApps({})).toEqual([]); + expect(toRibbonApps({ platforms: "nope" })).toEqual([]); + expect(toRibbonApps({ platforms: [null, 3, "x"] })).toEqual([]); + }); +}); + +describe("fetchRibbonApps", () => { + it("reads /api/platforms from the marketplace", async () => { + const fetchFn = vi.fn( + async () => + new Response(JSON.stringify({ platforms: [platform()] }), { + status: 200, + }), + ); + const apps = await fetchRibbonApps( + fetchFn as unknown as typeof fetch, + "https://market.test", + ); + expect(fetchFn).toHaveBeenCalledWith( + "https://market.test/api/platforms", + expect.objectContaining({ method: "GET" }), + ); + expect(apps).toHaveLength(1); + }); + + it("throws on an HTTP error", async () => { + const fetchFn = vi.fn(async () => new Response("", { status: 502 })); + await expect( + fetchRibbonApps(fetchFn as unknown as typeof fetch), + ).rejects.toThrow("502"); + }); + + it("propagates network errors", async () => { + const fetchFn = vi.fn(async () => { + throw new TypeError("offline"); + }); + await expect( + fetchRibbonApps(fetchFn as unknown as typeof fetch), + ).rejects.toThrow("offline"); + }); +}); + +describe("ribbon cache", () => { + it("round-trips the last list", () => { + const storage = memoryStorage(); + const apps = toRibbonApps({ + platforms: [ + platform(), + platform({ ename: "", url: "https://b.test" }), + ], + }); + writeCachedRibbonApps(apps, storage); + expect(readCachedRibbonApps(storage)).toEqual(apps); + }); + + it("returns null when empty or corrupt", () => { + const storage = memoryStorage(); + expect(readCachedRibbonApps(storage)).toBeNull(); + storage.setItem("marketplace-ribbon-apps:v1", "{not json"); + expect(readCachedRibbonApps(storage)).toBeNull(); + storage.setItem("marketplace-ribbon-apps:v1", "[]"); + expect(readCachedRibbonApps(storage)).toBeNull(); + }); + + it("re-validates cached entries", () => { + const storage = memoryStorage(); + storage.setItem( + "marketplace-ribbon-apps:v1", + JSON.stringify([{ name: "Evil", url: "javascript:alert(1)" }]), + ); + expect(readCachedRibbonApps(storage)).toBeNull(); + }); + + it("never throws when storage is unavailable", () => { + const broken = { + getItem: () => { + throw new Error("denied"); + }, + setItem: () => { + throw new Error("full"); + }, + }; + expect(readCachedRibbonApps(broken)).toBeNull(); + expect(() => writeCachedRibbonApps([], broken)).not.toThrow(); + }); +}); diff --git a/infrastructure/eid-wallet/src/lib/utils/marketplaceApps.ts b/infrastructure/eid-wallet/src/lib/utils/marketplaceApps.ts new file mode 100644 index 000000000..fa20d5206 --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/utils/marketplaceApps.ts @@ -0,0 +1,148 @@ +/** + * Apps for the main screen's ribbon, read live from the marketplace's + * `GET /api/platforms`. That endpoint lists the platforms published through + * Awareness (AaaS); the marketplace already drops drafts, archived and + * in-review entries. + * + * The endpoint sends no CORS headers, so inside the app the request goes + * through the Tauri HTTP plugin (native, not subject to CORS). The host is + * allow-listed in `src-tauri/capabilities/*.json`. + * + * Entries are published by the platforms themselves, so names are cleaned and + * only https links and logos are kept. + */ +import { cleanPlatformLogo, cleanPlatformName } from "./platformBranding"; + +export const MARKETPLACE_URL = "https://marketplace.w3ds.metastate.foundation"; + +const CACHE_KEY = "marketplace-ribbon-apps:v1"; +const REQUEST_TIMEOUT_MS = 10_000; + +export interface RibbonApp { + /** Stable key: the platform's eName, or its URL when it has none. */ + key: string; + name: string; + /** English category as published, e.g. "Productivity". May be empty. */ + category: string; + logo: string | null; + url: string; +} + +/** + * Turns a `/api/platforms` response into ribbon entries. Entries without a + * usable name or https link are skipped, as are repeats of the same app. + */ +export function toRibbonApps(payload: unknown): RibbonApp[] { + const items = (payload as { platforms?: unknown } | null)?.platforms; + if (!Array.isArray(items)) return []; + + const seen = new Set(); + const apps: RibbonApp[] = []; + for (const item of items) { + if (!item || typeof item !== "object") continue; + const { name, category, logoUrl, url, ename } = item as Record< + string, + unknown + >; + const cleanName = cleanPlatformName(asString(name)); + const link = cleanPlatformLogo(asString(url)); + if (!cleanName || !link) continue; + + const key = asString(ename)?.trim() || link; + if (seen.has(key) || seen.has(link)) continue; + seen.add(key); + seen.add(link); + + apps.push({ + key, + name: cleanName, + category: cleanPlatformName(asString(category)) ?? "", + logo: cleanPlatformLogo(asString(logoUrl)), + url: link, + }); + } + return apps; +} + +/** Fetches the live list. Throws on network or HTTP errors. */ +export async function fetchRibbonApps( + fetchFn?: typeof fetch, + baseUrl = MARKETPLACE_URL, +): Promise { + const doFetch = fetchFn ?? (await resolveFetch()); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + try { + const res = await doFetch(`${baseUrl}/api/platforms`, { + method: "GET", + headers: { Accept: "application/json" }, + signal: controller.signal, + }); + if (!res.ok) throw new Error(`Marketplace returned ${res.status}`); + return toRibbonApps(await res.json()); + } finally { + clearTimeout(timer); + } +} + +/** The last list that loaded successfully, or null. */ +export function readCachedRibbonApps( + storage: Pick | undefined = safeStorage(), +): RibbonApp[] | null { + try { + const raw = storage?.getItem(CACHE_KEY); + if (!raw) return null; + const apps = toRibbonApps({ platforms: JSON.parse(raw) }); + return apps.length ? apps : null; + } catch { + return null; + } +} + +export function writeCachedRibbonApps( + apps: RibbonApp[], + storage: Pick | undefined = safeStorage(), +): void { + try { + // Stored in the endpoint's own shape so reading goes through the + // same validation as a fresh response. + storage?.setItem( + CACHE_KEY, + JSON.stringify( + apps.map((a) => ({ + ename: a.key, + name: a.name, + category: a.category, + logoUrl: a.logo, + url: a.url, + })), + ), + ); + } catch { + // Storage full or unavailable: the next launch just refetches. + } +} + +// Inside the app, use the native client (no CORS). In a plain browser +// (vite dev), fall back to window.fetch, which the missing CORS headers block, +// so the ribbon shows the cached list or nothing there. +async function resolveFetch(): Promise { + const { isTauri } = await import("@tauri-apps/api/core"); + if (isTauri()) { + const { fetch: tauriFetch } = await import("@tauri-apps/plugin-http"); + return tauriFetch as typeof fetch; + } + return fetch; +} + +function safeStorage(): Storage | undefined { + try { + return globalThis.localStorage; + } catch { + return undefined; + } +} + +function asString(value: unknown): string | null { + return typeof value === "string" ? value : null; +} diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/AppsMarketplace.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/AppsMarketplace.svelte index f783be713..9507bfb6d 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/main/components/AppsMarketplace.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/AppsMarketplace.svelte @@ -1,55 +1,65 @@
@@ -72,30 +82,59 @@ const apps: IApp[] = [