From 1a7d34936c93c7786d2d1b9af1fcade518d28960 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Tue, 30 Jun 2026 09:24:44 +0200 Subject: [PATCH] feat(truapi-platform): add host capability traits New crate defining the host syscall traits (storage, navigation, consent, permissions, ...) that host runtimes implement. Types are re-exported from truapi::versioned/v01 rather than redefined. --- Cargo.lock | 301 +++++++++++ rust/crates/truapi-platform/Cargo.toml | 17 + rust/crates/truapi-platform/README.md | 35 ++ rust/crates/truapi-platform/src/lib.rs | 522 ++++++++++++++++++++ rust/crates/truapi-platform/tests/bounds.rs | 100 ++++ 5 files changed, 975 insertions(+) create mode 100644 rust/crates/truapi-platform/Cargo.toml create mode 100644 rust/crates/truapi-platform/README.md create mode 100644 rust/crates/truapi-platform/src/lib.rs create mode 100644 rust/crates/truapi-platform/tests/bounds.rs diff --git a/Cargo.lock b/Cargo.lock index 515ec10f..7a2c5a54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -190,12 +201,32 @@ dependencies = [ "unicode-xid", ] +[[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 = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[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" @@ -308,6 +339,109 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "impl-trait-for-tuples" version = "0.2.3" @@ -365,6 +499,12 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "memchr" version = "2.8.0" @@ -405,12 +545,27 @@ dependencies = [ "syn", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -514,6 +669,18 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -531,12 +698,33 @@ dependencies = [ "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 = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -600,6 +788,18 @@ dependencies = [ "syn", ] +[[package]] +name = "truapi-platform" +version = "0.1.0" +dependencies = [ + "async-trait", + "derive_more", + "futures", + "parity-scale-codec", + "truapi", + "url", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -618,6 +818,24 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[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" @@ -648,6 +866,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + [[package]] name = "wyz" version = "0.5.1" @@ -657,6 +881,83 @@ dependencies = [ "tap", ] +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/rust/crates/truapi-platform/Cargo.toml b/rust/crates/truapi-platform/Cargo.toml new file mode 100644 index 00000000..dea02704 --- /dev/null +++ b/rust/crates/truapi-platform/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "truapi-platform" +version = "0.1.0" +edition.workspace = true +description = "Platform capability traits for TrUAPI host implementations" +license = "MIT" + +[dependencies] +truapi = { path = "../truapi" } +async-trait = "0.1" +derive_more = { version = "2", features = ["display"] } +futures = "0.3" +parity-scale-codec = { version = "3", features = ["derive"] } +url = "2" + +[lints.rust] +unsafe_code = "forbid" diff --git a/rust/crates/truapi-platform/README.md b/rust/crates/truapi-platform/README.md new file mode 100644 index 00000000..3605d440 --- /dev/null +++ b/rust/crates/truapi-platform/README.md @@ -0,0 +1,35 @@ +# truapi-platform + +Platform capability traits for TrUAPI host implementations. + +Each host (web/WASM, desktop, iOS/UniFFI, Android/UniFFI) implements these +traits to provide the native capabilities the shared Rust runtime cannot reach +directly. The dispatcher in `truapi-server` calls this surface while the Rust +runtime owns product account management, SSO signing, statement-store protocol +flows, permission state, and auth state transitions. + +## Type Imports + +Host-facing wire types are imported from `truapi::latest` by this crate and are +exposed through the trait signatures below. + +## Traits + +- `ProductStorage`: product-scoped key-value storage. +- `CoreStorage`: typed core-owned storage slots such as auth session, pairing + identity, and permission authorization state. +- `CoreAdmin`: host UI controls for logout, pairing cancellation, session-store + refresh, and permission administration. +- `Navigation`: open URLs in the system browser. +- `Notifications`: deliver and cancel push notifications. +- `Permissions`: prompt for device and remote authorizations. +- `Features`: report host feature support. +- `ChainProvider` / `JsonRpcConnection`: open JSON-RPC connections to chains. +- `AuthPresenter`: render core-owned auth state transitions. +- `UserConfirmation`: confirm signing, transaction, resource, alias, and + preimage actions before the core asks the paired wallet. +- `ThemeHost`: stream the host theme into the runtime. +- `PreimageHost`: submit and look up preimages through the host-selected backend. + +`Platform` is a blanket-implemented supertrait that combines the capability +traits above. diff --git a/rust/crates/truapi-platform/src/lib.rs b/rust/crates/truapi-platform/src/lib.rs new file mode 100644 index 00000000..566c076f --- /dev/null +++ b/rust/crates/truapi-platform/src/lib.rs @@ -0,0 +1,522 @@ +//! Capability traits a TrUAPI host must implement. +//! +//! Each trait covers a single OS-primitive surface the Rust core cannot reach +//! from its own process (key-value persistence, URL launching, push +//! notifications, permission UI, chain RPC, host-selected preimage backends). +//! Account management, signing, and statement-store protocol flows live in the +//! Rust core itself and are not part of this trait set. +//! +//! Async capability traits use `async_trait` so the combined [`Platform`] +//! surface can be used as a trait object by the runtime. + +use futures::stream::BoxStream; +use parity_scale_codec::{Decode, Encode}; + +pub use async_trait::async_trait; + +use truapi::latest::{ + GenericError, HostDevicePermissionRequest, HostDevicePermissionResponse, + HostFeatureSupportedRequest, HostFeatureSupportedResponse, HostLocalStorageReadError, + HostNavigateToError, HostPushNotificationRequest, HostPushNotificationResponse, + HostRequestResourceAllocationRequest, HostSignPayloadRequest, + HostSignPayloadWithLegacyAccountRequest, HostSignRawRequest, + HostSignRawWithLegacyAccountRequest, LegacyAccountTxPayload, NotificationId, + PreimageSubmitError, ProductAccountTxPayload, RemotePermissionRequest, + RemotePermissionResponse, ThemeVariant, +}; +use url::Url; + +/// Static runtime configuration supplied by the embedding host before the +/// core handles product-scoped calls. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeConfig { + /// Canonical product identifier used for account derivation. + pub product_id: String, + /// Host metadata shown by the wallet during SSO pairing. + pub host_info: HostInfo, + /// Platform metadata shown by the wallet during SSO pairing. + pub platform_info: PlatformInfo, + /// People-chain genesis hash used for statement-store SSO. + pub people_chain_genesis_hash: [u8; 32], + /// Deeplink URI scheme used in pairing QR payloads, without `://`. + pub pairing_deeplink_scheme: String, +} + +/// Host metadata shown by the wallet during SSO pairing. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HostInfo { + /// Host name shown by the wallet during SSO pairing. + pub name: String, + /// Optional host icon URL/CID shown by the wallet during SSO pairing. + pub icon: Option, + /// Optional host version shown by the wallet during SSO pairing. + pub version: Option, +} + +/// Platform metadata shown by the wallet during SSO pairing. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct PlatformInfo { + /// Optional platform/browser name shown by the wallet during SSO pairing. + pub kind: Option, + /// Optional platform/browser version shown by the wallet during SSO pairing. + pub version: Option, +} + +impl RuntimeConfig { + /// Build a runtime config, validating fields whose representation cannot + /// be made invalid by Rust types alone. + pub fn new( + product_id: String, + host_info: HostInfo, + platform_info: PlatformInfo, + people_chain_genesis_hash: [u8; 32], + pairing_deeplink_scheme: String, + ) -> Result { + let config = Self { + product_id, + host_info, + platform_info, + people_chain_genesis_hash, + pairing_deeplink_scheme, + }; + config.validate()?; + Ok(config) + } + + fn validate(&self) -> Result<(), RuntimeConfigValidationError> { + require_non_empty("product_id", &self.product_id)?; + require_non_empty("host_info.name", &self.host_info.name)?; + require_non_empty("pairing_deeplink_scheme", &self.pairing_deeplink_scheme)?; + if self.pairing_deeplink_scheme.contains("://") { + return Err(RuntimeConfigValidationError::InvalidDeeplinkScheme { + scheme: self.pairing_deeplink_scheme.clone(), + }); + } + if let Some(icon) = &self.host_info.icon { + let parsed = + Url::parse(icon).map_err(|err| RuntimeConfigValidationError::InvalidHostIcon { + reason: err.to_string(), + })?; + if parsed.scheme() != "https" { + return Err(RuntimeConfigValidationError::InsecureHostIcon { + scheme: parsed.scheme().to_string(), + }); + } + } + Ok(()) + } +} + +fn require_non_empty(field: &'static str, value: &str) -> Result<(), RuntimeConfigValidationError> { + if value.trim().is_empty() { + return Err(RuntimeConfigValidationError::EmptyField { field }); + } + Ok(()) +} + +/// Runtime config validation error. +#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] +pub enum RuntimeConfigValidationError { + /// Required string field was empty or whitespace-only. + #[display("{field} must not be empty")] + EmptyField { + /// Field name. + field: &'static str, + }, + /// Host icon URL could not be parsed as an absolute URL. + #[display("host_info.icon must be an absolute HTTPS URL: {reason}")] + InvalidHostIcon { + /// Parse failure reason. + reason: String, + }, + /// Host icon URL used a non-HTTPS scheme. + #[display("host_info.icon must use https scheme, got {scheme:?}")] + InsecureHostIcon { + /// Actual URL scheme. + scheme: String, + }, + /// Pairing deeplink scheme included a URL separator. + #[display("pairing_deeplink_scheme must not include ://, got {scheme:?}")] + InvalidDeeplinkScheme { + /// Actual deeplink scheme value. + scheme: String, + }, +} + +impl std::error::Error for RuntimeConfigValidationError {} + +/// Product-scoped key-value storage. The platform namespaces keys so different +/// products cannot read each other's data. +#[async_trait] +pub trait ProductStorage: Send + Sync { + /// Read a value by key. + async fn read(&self, key: String) -> Result>, HostLocalStorageReadError>; + + /// Write a value to a key. + async fn write(&self, key: String, value: Vec) -> Result<(), HostLocalStorageReadError>; + + /// Clear a value at a key. + async fn clear(&self, key: String) -> Result<(), HostLocalStorageReadError>; +} + +/// Open URLs in the system browser. Input is already trimmed, categorized, +/// and (where needed) normalized by the core; the host implementation only +/// needs to hand the URL to the OS URL handler. +#[async_trait] +pub trait Navigation: Send + Sync { + /// Open the given URL in the system browser. + async fn navigate_to(&self, url: String) -> Result<(), HostNavigateToError>; +} + +/// Deliver push notifications. +#[async_trait] +pub trait Notifications: Send + Sync { + /// Schedule or immediately display the given notification and return the + /// host-assigned id. + async fn push_notification( + &self, + notification: HostPushNotificationRequest, + ) -> Result; + + /// Cancel a notification by id. Idempotent: cancelling an already-fired or + /// unknown id still returns `Ok(())`. + async fn cancel_notification(&self, id: NotificationId) -> Result<(), GenericError> { + let _ = id; + Ok(()) + } +} + +/// Permission prompts. v0.1 keeps device permissions (camera, mic, NFC, ...) +/// separate from remote permissions (domain access, chain submit, ...), so the +/// platform surface mirrors that split. +#[async_trait] +pub trait Permissions: Send + Sync { + /// Prompt the user for a device-level permission. + async fn device_permission( + &self, + request: HostDevicePermissionRequest, + ) -> Result; + + /// Prompt the user for a remote (product-scoped) permission bundle. + async fn remote_permission( + &self, + request: RemotePermissionRequest, + ) -> Result; +} + +/// Permission request whose authorization status can be inspected or updated +/// by host administration UI. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum PermissionAuthorizationRequest { + /// Device-level permission such as camera, microphone, or location. + Device(HostDevicePermissionRequest), + /// Remote/product-scoped permission such as chain submit or HTTP access. + Remote(RemotePermissionRequest), +} + +/// Authorization status for a permission request. +/// +/// `NotDetermined` means the core has no persisted answer and will prompt the +/// host the next time the product requests this permission. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] +pub enum PermissionAuthorizationStatus { + /// No persisted authorization exists. + NotDetermined, + /// Access is denied. + Denied, + /// Access is authorized. + Authorized, +} + +/// Core-owned administration API exposed to host UI. +/// +/// Hosts call this surface to drive global runtime actions or inspect/update +/// core-owned state without going through a product-scoped TrUAPI request. +#[async_trait] +pub trait CoreAdmin: Send + Sync { + /// Best-effort logout/disconnect. Clears the active session and emits the + /// resulting auth state transition. + async fn disconnect_session(&self) -> Result<(), GenericError>; + + /// Cancel any in-flight pairing request. + fn cancel_pairing(&self); + + /// Notify the core that the host-global auth session slot may have + /// changed. The core re-reads storage and emits any resulting auth state. + fn notify_session_store_changed(&self); + + /// Read a stored permission authorization status without prompting. + async fn get_permission_authorization_status( + &self, + request: PermissionAuthorizationRequest, + ) -> Result; + + /// Read stored permission authorization statuses without prompting. + /// + /// Results are returned in the same order as `requests`. + async fn get_permission_authorization_statuses( + &self, + requests: Vec, + ) -> Result, GenericError>; + + /// Update a stored permission authorization status. `NotDetermined` clears + /// the stored value so the next product request prompts again. + async fn set_permission_authorization_status( + &self, + request: PermissionAuthorizationRequest, + status: PermissionAuthorizationStatus, + ) -> Result<(), GenericError>; +} + +/// Feature-support probing. The host answers whether it can service a given +/// capability (currently scoped to per-chain support). +#[async_trait] +pub trait Features: Send + Sync { + /// Report whether the requested feature is supported. + async fn feature_supported( + &self, + request: HostFeatureSupportedRequest, + ) -> Result; +} + +/// JSON-RPC provider factory for chain access. +/// +/// The platform provides a way to get a JSON-RPC connection for a given chain. +/// The server runtime manages the chainHead v1 state machine on top of this. +#[async_trait] +pub trait ChainProvider: Send + Sync { + /// Open a JSON-RPC connection for the chain identified by `genesis_hash`. + /// Drop the returned connection to disconnect. + async fn connect( + &self, + genesis_hash: Vec, + ) -> Result, GenericError>; +} + +/// A live JSON-RPC connection to a chain. +pub trait JsonRpcConnection: Send + Sync { + /// Send a JSON-RPC request string. + fn send(&self, request: String); + + /// Stream of JSON-RPC response strings. + fn responses(&self) -> BoxStream<'static, String>; + + /// Close the connection lease. + /// + /// Hosts may keep a shared underlying transport alive, but this handle + /// must stop receiving responses and release any per-caller resources. + fn close(&self); +} + +/// Core-owned host-private storage slots. Products never address these slots; +/// the host chooses the backing store for each slot. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum CoreStorageKey { + /// Opaque SSO/auth session blob. + AuthSession, + /// Pairing device identity used during SSO flows. + PairingDeviceIdentity, + /// Persisted authorization for one product-scoped permission request. + PermissionAuthorization { + /// Product whose permission decision is being stored. + product_id: String, + /// Permission request whose authorization is being stored. + request: PermissionAuthorizationRequest, + }, +} + +/// Host-private persistence for core-owned state. +#[async_trait] +pub trait CoreStorage: Send + Sync { + /// Read a core-owned value by typed slot. + async fn read_core_storage(&self, key: CoreStorageKey) + -> Result>, GenericError>; + + /// Write a core-owned value by typed slot. + async fn write_core_storage( + &self, + key: CoreStorageKey, + value: Vec, + ) -> Result<(), GenericError>; + + /// Clear a core-owned value by typed slot. + async fn clear_core_storage(&self, key: CoreStorageKey) -> Result<(), GenericError>; +} + +/// Decoded session fields a host shell needs to render account UI without +/// parsing the opaque session blob the core persists through [`CoreStorage`]. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SessionUiInfo { + /// 32-byte sr25519 root public key of the active session. + pub public_key: [u8; 32], + /// Wallet identity account id used for People-chain username lookup. + pub identity_account_id: Option<[u8; 32]>, + /// Short username from the People-chain identity record. + pub lite_username: Option, + /// Fully qualified username from the People-chain identity record. + pub full_username: Option, +} + +/// Auth/session lifecycle state the core projects for host UI. The core owns +/// every transition and emits states in order; hosts render the current state +/// and never derive auth UI from any other signal. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub enum AuthState { + /// No active session and no login in progress. + #[default] + Disconnected, + /// A login is in progress: present the pairing deeplink/QR. Leave this + /// state only on a subsequent emission (connected, failed, or + /// disconnected after cancellation). + Pairing { + /// Wallet pairing deeplink to render as a QR code or open directly. + deeplink: String, + }, + /// A session is active. + Connected(SessionUiInfo), + /// The last login attempt failed; show the reason and offer a retry. + LoginFailed { + /// Human-readable failure reason. + reason: String, + }, +} + +/// Host auth UI driven by core-owned [`AuthState`] transitions. +pub trait AuthPresenter: Send + Sync { + /// Observe an auth state change. Emitted only when the state actually + /// changes, in transition order. Default is a no-op for hosts that + /// render no auth UI. + fn auth_state_changed(&self, state: AuthState) { + let _ = state; + } +} + +/// Review shown before a sign-payload request is sent to the paired wallet. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum SignPayloadReview { + /// Product-account signing request. + Product(HostSignPayloadRequest), + /// Legacy-account signing request. + LegacyAccount(HostSignPayloadWithLegacyAccountRequest), +} + +/// Review shown before a sign-raw request is sent to the paired wallet. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum SignRawReview { + /// Product-account raw signing request. + Product(HostSignRawRequest), + /// Legacy-account raw signing request. + LegacyAccount(HostSignRawWithLegacyAccountRequest), +} + +/// Review shown before a transaction-creation request is sent to the paired wallet. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum CreateTransactionReview { + /// Product-account transaction request. + Product(ProductAccountTxPayload), + /// Legacy-account transaction request. + LegacyAccount(LegacyAccountTxPayload), +} + +/// Review shown before a product asks to alias another product account. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct AccountAliasReview { + /// Product currently handling the request. + pub requesting_product_id: String, + /// Product whose account is being requested. + pub target_product_id: String, +} + +/// Review shown before a preimage is submitted. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub struct PreimageSubmitReview { + /// Size of the preimage in bytes. + pub size: u64, +} + +/// Review shown before a user-confirmed core action continues. +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +pub enum UserConfirmationReview { + /// Sign a SCALE payload with a product or legacy account. + SignPayload(SignPayloadReview), + /// Sign raw bytes with a product or legacy account. + SignRaw(SignRawReview), + /// Create a transaction with a product or legacy account. + CreateTransaction(CreateTransactionReview), + /// Allow a product to request another product account alias. + AccountAlias(AccountAliasReview), + /// Allocate resources for the requesting product. + ResourceAllocation(HostRequestResourceAllocationRequest), + /// Submit a preimage to the host-selected backend. + PreimageSubmit(PreimageSubmitReview), +} + +/// Local user confirmation UI for session-channel operations. +#[async_trait] +pub trait UserConfirmation: Send + Sync { + /// Confirm a reviewed action before the core asks the SSO peer. + async fn confirm_user_action( + &self, + review: UserConfirmationReview, + ) -> Result { + let _ = review; + Ok(false) + } +} + +/// Host theme source. +pub trait ThemeHost: Send + Sync { + /// Emits current theme immediately, then future changes. + fn subscribe_theme(&self) -> BoxStream<'static, Result>; +} + +/// Host preimage backend. The core owns wire mapping and subscription +/// lifecycle; the host owns the selected backend. +#[async_trait] +pub trait PreimageHost: Send + Sync { + /// Submit the preimage and return its key. + async fn submit_preimage(&self, value: Vec) -> Result, PreimageSubmitError> { + let _ = value; + Err(PreimageSubmitError::Unknown { + reason: "submitPreimage callback not provided by host".to_string(), + }) + } + + /// Emits current value/miss immediately, then future updates. + fn lookup_preimage( + &self, + key: Vec, + ) -> BoxStream<'static, Result>, GenericError>>; +} + +/// Combined platform interface. A host must provide all capability traits. +pub trait Platform: + Navigation + + Notifications + + Permissions + + Features + + ProductStorage + + CoreStorage + + ChainProvider + + AuthPresenter + + UserConfirmation + + ThemeHost + + PreimageHost +{ +} + +impl Platform for T where + T: Navigation + + Notifications + + Permissions + + Features + + ProductStorage + + CoreStorage + + ChainProvider + + AuthPresenter + + UserConfirmation + + ThemeHost + + PreimageHost +{ +} diff --git a/rust/crates/truapi-platform/tests/bounds.rs b/rust/crates/truapi-platform/tests/bounds.rs new file mode 100644 index 00000000..5d8c8a43 --- /dev/null +++ b/rust/crates/truapi-platform/tests/bounds.rs @@ -0,0 +1,100 @@ +//! Compile-time check that the `Platform` super-trait composes its capability +//! traits with `Send + Sync + 'static` bounds and remains object-safe via +//! `async_trait`. + +use truapi_platform::{ + HostInfo, Platform, PlatformInfo, RuntimeConfig, RuntimeConfigValidationError, +}; + +fn _assert_platform_bounds() {} + +fn _assert_platform_object_safe(_: &(dyn Platform + 'static)) {} + +#[test] +fn runtime_config_validation_cases() { + struct TestCase { + name: &'static str, + product_id: &'static str, + host_name: &'static str, + host_icon: Option<&'static str>, + pairing_deeplink_scheme: &'static str, + expected: Result<(), RuntimeConfigValidationError>, + } + + let cases = vec![ + TestCase { + name: "accepts HTTPS host icon", + product_id: "dotli.dot", + host_name: "Polkadot Web", + host_icon: Some("https://dot.li/dotli.png"), + pairing_deeplink_scheme: "polkadotapp", + expected: Ok(()), + }, + TestCase { + name: "rejects empty product id", + product_id: "", + host_name: "Polkadot Web", + host_icon: Some("https://dot.li/dotli.png"), + pairing_deeplink_scheme: "polkadotapp", + expected: Err(RuntimeConfigValidationError::EmptyField { + field: "product_id", + }), + }, + TestCase { + name: "rejects empty host name", + product_id: "dotli.dot", + host_name: " ", + host_icon: Some("https://dot.li/dotli.png"), + pairing_deeplink_scheme: "polkadotapp", + expected: Err(RuntimeConfigValidationError::EmptyField { + field: "host_info.name", + }), + }, + TestCase { + name: "rejects relative host icon", + product_id: "dotli.dot", + host_name: "Polkadot Web", + host_icon: Some("/dotli.png"), + pairing_deeplink_scheme: "polkadotapp", + expected: Err(RuntimeConfigValidationError::InvalidHostIcon { + reason: "relative URL without a base".to_string(), + }), + }, + TestCase { + name: "rejects non-HTTPS host icon", + product_id: "dotli.dot", + host_name: "Polkadot Web", + host_icon: Some("http://localhost:3000/dotli.png"), + pairing_deeplink_scheme: "polkadotapp", + expected: Err(RuntimeConfigValidationError::InsecureHostIcon { + scheme: "http".to_string(), + }), + }, + TestCase { + name: "rejects malformed deeplink scheme", + product_id: "dotli.dot", + host_name: "Polkadot Web", + host_icon: Some("https://dot.li/dotli.png"), + pairing_deeplink_scheme: "polkadotapp://", + expected: Err(RuntimeConfigValidationError::InvalidDeeplinkScheme { + scheme: "polkadotapp://".to_string(), + }), + }, + ]; + + for case in cases { + let result = RuntimeConfig::new( + case.product_id.to_string(), + HostInfo { + name: case.host_name.to_string(), + icon: case.host_icon.map(str::to_string), + version: None, + }, + PlatformInfo::default(), + [0xa2; 32], + case.pairing_deeplink_scheme.to_string(), + ) + .map(|_| ()); + assert_eq!(result, case.expected, "{}", case.name); + } +}