diff --git a/CMakeLists.txt b/CMakeLists.txt index 3060d7e21..2f840cc29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -320,7 +320,10 @@ add_dependencies(CommonApiCommonLib_js ComponentParser) add_rs_component(services/system/Transact/plugin:TransactPlugin transact.wasm wasm32-wasi ) add_rs_component(services/system/Accounts/plugin:AccountsPlugin accounts.wasm wasm32-wasi ) add_rs_component(services/system/AuthSig/plugin:AuthPlugin auth_sig.wasm wasm32-wasi ) -add_rs_component(services/user/Invite/plugin:InvitesPlugin invite.wasm wasm32-wasi ${AuthPlugin_DEP}) +add_rs_component(services/system/AuthDelegate/plugin:AuthDelegatePlugin auth_delegate.wasm wasm32-wasi ) +add_rs_component(services/system/AuthAny/plugin:AuthAnyPlugin auth_any.wasm wasm32-wasi ) +add_rs_component(services/user/Invite/plugin:InvitesPlugin invite.wasm wasm32-wasi ) +add_rs_component(services/user/Invite/plugin_AuthInvite:AuthInvitePlugin auth_invite.wasm wasm32-wasi ) add_rs_component(services/user/Tokens/plugin:TokensPlugin tokens.wasm wasm32-wasi ) add_rs_component(services/user/Supervisor/plugin:TestsPlugin test.wasm wasm32-wasi ) add_rs_component(services/user/ClientData/plugin:ClientDataPlugin clientdata.wasm wasm32-wasi ) @@ -484,7 +487,11 @@ psibase_package( DESCRIPTION "Auth service that delegates authorization to another account" SERVICE auth-delegate WASM ${CMAKE_CURRENT_BINARY_DIR}/AuthDelegate.wasm + SERVER auth-delegate + DATA ${COMPONENT_BIN_DIR}/${AuthDelegatePlugin_OUTPUT_FILE} /plugin.wasm DEPENDS wasm + DEPENDS ${AuthDelegatePlugin_DEP} + PACKAGE_DEPENDS "HttpServer(^${PSIBASE_VERSION})" "Sites(^${PSIBASE_VERSION})" ) psibase_package( @@ -494,7 +501,10 @@ psibase_package( DESCRIPTION "Insecure auth service that allows any access" SERVICE auth-any WASM ${CMAKE_CURRENT_BINARY_DIR}/AuthAny.wasm + DATA ${COMPONENT_BIN_DIR}/${AuthAnyPlugin_OUTPUT_FILE} /plugin.wasm DEPENDS wasm + DEPENDS ${AuthAnyPlugin_DEP} + PACKAGE_DEPENDS "Sites(^${PSIBASE_VERSION})" ) psibase_package( @@ -645,8 +655,10 @@ psibase_package( DATA ${COMPONENT_BIN_DIR}/${InvitesPlugin_OUTPUT_FILE} /plugin.wasm SERVICE auth-invite WASM ${CMAKE_CURRENT_BINARY_DIR}/AuthInvite.wasm + DATA ${COMPONENT_BIN_DIR}/${AuthInvitePlugin_OUTPUT_FILE} /plugin.wasm DEPENDS wasm DEPENDS ${InvitesPlugin_DEP} + DEPENDS ${AuthInvitePlugin_DEP} ) psibase_package( diff --git a/dev/DemoApp1/plugin/Cargo.lock b/dev/DemoApp1/plugin/Cargo.lock index 0b062a10c..a74ba8160 100644 --- a/dev/DemoApp1/plugin/Cargo.lock +++ b/dev/DemoApp1/plugin/Cargo.lock @@ -704,7 +704,7 @@ dependencies = [ [[package]] name = "fracpack" -version = "0.12.0" +version = "0.13.0" dependencies = [ "custom_error", "indexmap", @@ -1114,9 +1114,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128" @@ -1461,7 +1461,7 @@ dependencies = [ [[package]] name = "psibase" -version = "0.12.0" +version = "0.13.0" dependencies = [ "anyhow", "async-graphql", @@ -1482,6 +1482,7 @@ dependencies = [ "indexmap", "indicatif", "jwt", + "lazy_static", "mime_guess", "pem", "percent-encoding", @@ -1514,7 +1515,7 @@ dependencies = [ [[package]] name = "psibase_macros" -version = "0.12.0" +version = "0.13.0" dependencies = [ "darling 0.14.4", "proc-macro-error", @@ -1526,7 +1527,7 @@ dependencies = [ [[package]] name = "psibase_names" -version = "0.12.0" +version = "0.13.0" dependencies = [ "seahash", ] diff --git a/dev/DemoApp1/plugin/src/lib.rs b/dev/DemoApp1/plugin/src/lib.rs index 2e15d5b12..7233cfdd9 100644 --- a/dev/DemoApp1/plugin/src/lib.rs +++ b/dev/DemoApp1/plugin/src/lib.rs @@ -1,10 +1,10 @@ #[allow(warnings)] mod bindings; -use bindings::host::common::types as CommonTypes; -use bindings::transact::plugin::intf as Transact; use bindings::exports::demoapp1::example::intf::Guest as Intf; +use bindings::host::common::types as CommonTypes; use bindings::invite; +use bindings::transact::plugin::intf as Transact; use bindings::Guest as MainInterface; use psibase::fracpack::Pack; @@ -20,7 +20,7 @@ impl MainInterface for Component { impl Intf for Component { fn helloworld2() -> Result { - Ok(invite::plugin::inviter::generate_invite("/subpath")?) + Ok(invite::plugin::inviter::generate_invite()?) } fn multiply(a: u32, b: u32) -> Result { diff --git a/dev/DemoApp1/service/Cargo.lock b/dev/DemoApp1/service/Cargo.lock index 68eb4806a..950f1f5e5 100644 --- a/dev/DemoApp1/service/Cargo.lock +++ b/dev/DemoApp1/service/Cargo.lock @@ -695,7 +695,7 @@ dependencies = [ [[package]] name = "fracpack" -version = "0.12.0" +version = "0.13.0" dependencies = [ "custom_error", "indexmap", @@ -1105,9 +1105,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128" @@ -1452,7 +1452,7 @@ dependencies = [ [[package]] name = "psibase" -version = "0.12.0" +version = "0.13.0" dependencies = [ "anyhow", "async-graphql", @@ -1473,6 +1473,7 @@ dependencies = [ "indexmap", "indicatif", "jwt", + "lazy_static", "mime_guess", "pem", "percent-encoding", @@ -1505,7 +1506,7 @@ dependencies = [ [[package]] name = "psibase_macros" -version = "0.12.0" +version = "0.13.0" dependencies = [ "darling 0.14.4", "proc-macro-error", @@ -1517,7 +1518,7 @@ dependencies = [ [[package]] name = "psibase_names" -version = "0.12.0" +version = "0.13.0" dependencies = [ "seahash", ] diff --git a/dev/demoapp1-build-and-deploy.sh b/dev/demoapp1-build-and-deploy.sh index 40cf5bd66..f522259c4 100755 --- a/dev/demoapp1-build-and-deploy.sh +++ b/dev/demoapp1-build-and-deploy.sh @@ -9,11 +9,11 @@ cd $root_dir # Account Creation echo "Creating demoapp1 account..." -psibase create -i $account_name +psibase -a dev create -i $account_name pushd ./service # Build service and deploy -cargo psibase deploy -p -i $account_name +cargo psibase deploy -a dev -p -i $account_name popd # Build plugin and copy to public dir @@ -26,6 +26,6 @@ cd ./ui rm -rf node_modules rm -rf dist yarn --mutex network && yarn build -psibase upload -r ./dist / -S $account_name +psibase -a dev upload -r ./dist / -S $account_name diff --git a/doc/psidk/src/specifications/blockchain/smart-authorization.md b/doc/psidk/src/specifications/blockchain/smart-authorization.md index 834e4e62b..f7393b674 100644 --- a/doc/psidk/src/specifications/blockchain/smart-authorization.md +++ b/doc/psidk/src/specifications/blockchain/smart-authorization.md @@ -1,28 +1,38 @@ # Smart authorization -Account authorization can be done in many different ways: simple signature verification, time locks, zero-knowledge password proofs, multi-user authentication, and more. As technology progresses, new cryptographic techniques are discovered to improve the capabilities and efficiency of proving technology. Rather than attempt to add and maintain native support for all major authorization techniques, psibase instead describes a mechanism by which accounts are given full programmatic control over account authorization. +For an account to call an action defined in a service requires that the posted payload satisfies the authorization of the account. + +Account authorization can use various techniques: simple signature verification, multi-signature verification, time locks, zero-knowledge proofs, and more. As technology progresses, new cryptographic techniques are discovered to improve the capabilities and efficiency of proving technology. Rather than attempt to add and maintain native support for all major authorization techniques, psibase instead describes a mechanism by which all accounts are given full programmatic control over how they should be authorized. ## Goals -* Enable accounts to have complete programmatic control over how their account can be authorized -* Allow developers to develop, deploy, and use custom cryptographic verification programs without requiring coordinated upgrades to the native node software +- Enable accounts to have complete programmatic control over how their account can be authorized +- Allow developers to develop, deploy, and use custom cryptographic verification programs without requiring coordinated upgrades to the native node software -## Design overview +## Design -### Definitions +### How it works -* **Claim** - Something that the submitter of a transaction claims to know (e.g. a public key) -* **Proof** - Conclusive evidence that the transaction submitter does actually know what she claims (e.g. a cryptographic signature) -* **Transaction** - A payload that specifies the intent of the sender to execute some code on the network -* **Auth service** - A service (configurable by each account) that specifies what claims are needed to authorize transactions from that account -* **Verify service** - A service that knows how to verify proof of a claim +#### Overview -### How it works +Any account may specify how it should be authorized. As the user interacts with psibase apps and publishes transactions to modify their state on the network, code is dynamically loaded on the client-side to ensure that the published transaction contains the necessary information to authorize the account. For example, if an account specifies that it uses a simple cryptographic signature for authorization, the psibase infrastructure on the client-side will automatically pull the corresponding signature-authorization plugin from the server and allow it to modify the transaction by adding signatures before it is published to the network. + +This is fully dynamic. Third-party developers can write their own authorization plugins, and accounts are free to use any authorization plugin. + +#### Definitions + +- **Claim** - Something that the submitter of a transaction claims to know (e.g. a public key) +- **Proof** - Conclusive evidence that the transaction submitter does actually know what she claims (e.g. a cryptographic signature) +- **Transaction** - A payload that specifies the intent of the sender to execute some code on the network +- **Auth service** - A service (configurable by each account) that specifies what claims are needed to authorize transactions from that account +- **Verify service** - A service that knows how to verify proof of a claim + +#### Detailed steps 1. Every psibase account must specify an auth service 2. The auth service must enforce that transactions submitted by an account make one or more specific claims 3. A transaction gets submitted with: the claim(s) required by the auth service, a corresponding proof(s), the name of a verify service(s) -4. The auth service will verify the existence of the transaction claims, and that the correct verify service was specified. +4. The auth service will verify the existence of the needed claims in the transaction and that the correct verify service was specified. 5. The claim(s) and proof(s) are sent to the associated verify service(s), which can authorize (or fail) the transaction. ## Architecture @@ -52,70 +62,66 @@ classDiagram } ``` -> Note: A `SignedTransaction` should be eventually renamed to something more generic, like `AuthedTransaction`, since "Signed" implies the specific claim/proof technique of using digital signatures. +> Note: A `SignedTransaction` should be renamed to something more generic, like `AuthedTransaction`, since "Signed" implies the specific claim/proof technique of using digital signatures. -The submitted `SignedTransaction` is sent to the AuthService defined by an account (e.g. [AuthSig](../../default-apps/auth-sig.md)) and the corresponding verification service. These services will fail the transaction if either the claims in the transaction are incorrect, or if the proof does not prove the claim. +The submitted `SignedTransaction` is sent to the auth service defined by an account (e.g. [AuthSig](../../default-apps/auth-sig.md)) and the corresponding verification service. These services will fail the transaction if either the claims in the transaction are incorrect, or if the proof does not prove the claim. -## Supervisor +## Client-side transaction construction -When building Psibase apps, the construction of each transaction submitted to the network is done automatically, including any claim and proof aggregation. The following sequence diagram shows how the supervisor aggregates claims and proofs. This entire interaction happens on the client-side. +When building psibase apps, the construction of each transaction submitted to the network is done automatically, including the aggregation of claims and generation of proofs. The following sequence diagram shows an example of a simple transaction being constructed. This entire interaction happens on the client-side. ```mermaid sequenceDiagram participant app -participant auth plugin +participant transact participant supervisor participant plugin +participant auth plugin -app->>supervisor: 1. action@plugin +app->>supervisor: call a plugin function activate supervisor -supervisor->>auth plugin: 2. GetClaim() -activate auth plugin -auth plugin-->>supervisor: -deactivate auth plugin -note over supervisor: Add to transaction - -supervisor->>plugin: 3. Call action +supervisor->>transact: start tx +activate transact +note right of transact: transaction {} +transact-->>supervisor: return +deactivate transact +supervisor->>plugin: function1() activate plugin -plugin->>supervisor: 4. AddClaim(claim) -activate supervisor -note over supervisor: Add to transaction -supervisor-->>plugin: -deactivate supervisor -plugin-->>supervisor: +plugin->>transact: add-action-to-transaction +activate transact +note right of transact: transaction { { action1 } } +transact-->>plugin: return +deactivate transact +plugin-->>supervisor: return deactivate plugin - -supervisor->>supervisor: 5. Generate `hashedTransaction`
from the transaction
and its claims - -supervisor->>auth plugin: 6. getProof(hashedTransaction, claim) +supervisor->>transact: finish tx +activate transact +note right of transact: Look up the user's auth service +transact->>auth plugin: get-claim activate auth plugin -auth plugin->>supervisor: +auth plugin-->>transact: return deactivate auth plugin -note over supervisor: Add to transaction - -loop For each added claim - supervisor->>plugin: 7. getProof(hashedTransaction, claim) - activate plugin - plugin->>supervisor: - deactivate plugin - note over supervisor: Add to transaction -end - -Note over supervisor: 8. Submit transaction +note right of transact: transaction { { action1, claim1 } } +transact->>auth plugin: get proof(transaction) +activate auth plugin +auth plugin-->>transact: return +deactivate auth plugin +note right of transact: transaction { { action1, claim1 }, proof1 } +transact-->>supervisor: return +deactivate transact +Note over supervisor: Submit transaction deactivate supervisor +supervisor-->>app: return ``` -The following is an explanation of each step in the diagram to aid understanding: +## Auth notifiers + +At any time, a plugin can notify the `transact` plugin (which is responsible for transaction construction) that it has one or more claims to be added to the transaction. If this is done, at the time the transaction is being constructed, the `transact` plugin will ask this notifier for claims and proofs in addition to the user's auth service plugin. + +## Transaction privacy -1. Alice calls an action on a plugin. -2. Supervisor gives the user's configured auth plugin the opportunity to add a claim to the transaction. -3. Supervisor calls the plugin action -4. If (and only if) a plugin calls a service action, then it is allowed to add a claim to the transaction. -5. Supervisor has accumulated all actions and claims for this transaction, so it calculates the hash of the transaction object which can be used for the generation of proofs (such as digital signatures). -6. The user's configured auth plugin is asked to generate a proof for the claim it added. -7. Each plugin that added a claim is asked for a proof of the claim. -8. Supervisor has collected all claims and proofs, therefore the final transaction object is packed and submitted to the network. +The only plugin that can see the full contents of a transaction is the user's auth service plugin. For every other plugin, it only knows what actions it was itself responsible for including in the transaction. This rule includes auth notifiers. While `transact` provides the user's auth service plugin with all actions in a transaction, it will only provide an auth-notifier with a list of actions that were added by the notifier plugin itself. ## Conclusion diff --git a/rust/Cargo.lock b/rust/Cargo.lock index dae3a6066..205080929 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1694,9 +1694,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128" diff --git a/rust/psibase/Cargo.toml b/rust/psibase/Cargo.toml index c4b8abcff..890aca3a8 100644 --- a/rust/psibase/Cargo.toml +++ b/rust/psibase/Cargo.toml @@ -41,7 +41,7 @@ serde_json = "1.0" serde-wasm-bindgen = "0.6.0" varisat = "0.2" wasm-bindgen = "0.2" -zip = { version = "0.6", default-features = false, features = ["deflate"]} +zip = { version = "0.6", default-features = false, features = ["deflate"] } sec1 = "0.7" spki = "0.7" der = { version = "0.7", features = ["alloc", "pem", "std"] } @@ -53,9 +53,16 @@ clap = {version = "4.5", features = ["derive", "env", "string"]} hmac = "0.12" indicatif = "0.17" jwt = "0.16" -reqwest = { version = "0.11", default-features = false, features = ["json","rustls-tls","gzip"] } +reqwest = { version = "0.11", default-features = false, features = [ + "json", + "rustls-tls", + "gzip", +] } rpassword = "7.2" -secp256k1 = { version="0.27", features = ["global-context", "bitcoin_hashes"] } +secp256k1 = { version = "0.27", features = [ + "global-context", + "bitcoin_hashes", +] } tempfile = "3" tokio = { version = "1", features = ["full"] } pkcs8 = { version = "0.10", features = ["pem"] } diff --git a/rust/psibase/src/boot.rs b/rust/psibase/src/boot.rs index 2eac2bd49..17d0f65d4 100644 --- a/rust/psibase/src/boot.rs +++ b/rust/psibase/src/boot.rs @@ -1,8 +1,8 @@ -use crate::services::{accounts, auth_delegate, producers, transact}; +use crate::services::{accounts, auth_delegate, auth_sig, producers, transact}; use crate::{ - method_raw, new_account_action, set_auth_service_action, validate_dependencies, AccountNumber, - Action, AnyPublicKey, Claim, ExactAccountNumber, GenesisActionData, MethodNumber, - PackagedService, Producer, SignedTransaction, Tapos, TimePointSec, Transaction, + method_raw, new_account_action, set_auth_service_action, set_key_action, validate_dependencies, + AccountNumber, Action, AnyPublicKey, Claim, ExactAccountNumber, GenesisActionData, + MethodNumber, PackagedService, Producer, SignedTransaction, Tapos, TimePointSec, Transaction, }; use fracpack::Pack; use serde_bytes::ByteBuf; @@ -101,16 +101,22 @@ pub fn get_initial_actions( s.postinstall(&mut actions)?; } - actions.push(set_producers_action( - initial_producer, - match initial_key { - Some(k) => to_claim(k), - None => Claim { - service: AccountNumber::new(0), - rawData: Default::default(), - }, - }, - )); + // Create producer account + actions.push(new_account_action(accounts::SERVICE, initial_producer)); + + let mut claim = Claim { + service: AccountNumber::new(0), + rawData: Default::default(), + }; + if let Some(key) = initial_key { + // Set transaction signing key for producer + actions.push(set_key_action(initial_producer, &key)); + actions.push(set_auth_service_action(initial_producer, auth_sig::SERVICE)); + claim = to_claim(&key); + } + + // Set the producers + actions.push(set_producers_action(initial_producer, claim)); actions.push(new_account_action(accounts::SERVICE, producers::ROOT)); actions.push( diff --git a/rust/psibase/src/services/auth_invite.rs b/rust/psibase/src/services/auth_invite.rs new file mode 100644 index 000000000..a53c60707 --- /dev/null +++ b/rust/psibase/src/services/auth_invite.rs @@ -0,0 +1,30 @@ +use crate::AccountNumber; +pub const VERIFY_SERVICE: AccountNumber = crate::services::verify_sig::SERVICE; + +#[crate::service(name = "auth-invite", dispatch = false, psibase_mod = "crate")] +#[allow(non_snake_case, unused_variables)] +mod service { + use crate::services::auth_sig::SubjectPublicKeyInfo; + use crate::{services::transact::ServiceMethod, AccountNumber, Action, Claim}; + + #[action] + fn checkAuthSys( + flags: u32, + requester: AccountNumber, + action: Action, + allowedActions: Vec, + claims: Vec, + ) { + unimplemented!() + } + + #[action] + fn canAuthUserSys(user: AccountNumber) { + unimplemented!() + } + + #[action] + fn requireAuth(pubkey: SubjectPublicKeyInfo) { + unimplemented!() + } +} diff --git a/rust/psibase/src/services/invite.rs b/rust/psibase/src/services/invite.rs index d3bf393ff..588a660a5 100644 --- a/rust/psibase/src/services/invite.rs +++ b/rust/psibase/src/services/invite.rs @@ -1,5 +1,5 @@ pub use crate::services::auth_sig::SubjectPublicKeyInfo; -use crate::AccountNumber; +use crate::{account, AccountNumber}; use async_graphql::{InputObject, SimpleObject}; use fracpack::{Pack, ToSchema, Unpack}; use serde::{Deserialize, Serialize}; @@ -44,6 +44,9 @@ pub struct NewAccountRecord { invitee: AccountNumber, } +use crate as psibase; +pub const PAYER_ACCOUNT: AccountNumber = account!("invited-sys"); + #[crate::service(name = "invite", dispatch = false, psibase_mod = "crate")] #[allow(non_snake_case, unused_variables)] mod service { diff --git a/rust/psibase/src/services/mod.rs b/rust/psibase/src/services/mod.rs index 79bd3820e..870d20e4f 100644 --- a/rust/psibase/src/services/mod.rs +++ b/rust/psibase/src/services/mod.rs @@ -2,6 +2,7 @@ pub mod accounts; pub mod auth_delegate; +pub mod auth_invite; pub mod auth_sig; pub mod chainmail; pub mod common_api; @@ -19,3 +20,4 @@ pub mod setcode; pub mod sites; pub mod tokens; pub mod transact; +pub mod verify_sig; diff --git a/rust/psibase/src/services/verify_sig.rs b/rust/psibase/src/services/verify_sig.rs new file mode 100644 index 000000000..50d6c6aab --- /dev/null +++ b/rust/psibase/src/services/verify_sig.rs @@ -0,0 +1,5 @@ +#[crate::service(name = "verify-sig", dispatch = false, psibase_mod = "crate")] +#[allow(non_snake_case, unused_variables)] +mod service { + const SERVICE_FLAGS: u64 = crate::CodeRow::IS_AUTH_SERVICE; +} diff --git a/rust/psibase_macros/src/lib.rs b/rust/psibase_macros/src/lib.rs index 85de39043..348fe3f70 100644 --- a/rust/psibase_macros/src/lib.rs +++ b/rust/psibase_macros/src/lib.rs @@ -262,7 +262,7 @@ pub fn to_schema(input: TokenStream) -> TokenStream { /// /// ```ignore /// #[psibase::service( -/// name = see_blow, // Account service is normally installed on +/// name = see_below, // Account service is normally installed on /// recursive = false, // Allow service to be recursively entered? /// constant = "SERVICE", // Name of generated constant /// actions = "Actions", // Name of generated struct diff --git a/rust/psibase_macros/src/service_macro.rs b/rust/psibase_macros/src/service_macro.rs index 3b2aa5a5e..97f181292 100644 --- a/rust/psibase_macros/src/service_macro.rs +++ b/rust/psibase_macros/src/service_macro.rs @@ -1232,6 +1232,10 @@ fn process_action_args( pub struct #fn_name { #struct_members } + + impl #fn_name { + pub const ACTION_NAME: &'static str = stringify!(#fn_name); + } }; if let Some(db) = event_db { diff --git a/services/system/Accounts/plugin/Cargo.toml b/services/system/Accounts/plugin/Cargo.toml index 06ea8d506..06fe463b6 100644 --- a/services/system/Accounts/plugin/Cargo.toml +++ b/services/system/Accounts/plugin/Cargo.toml @@ -29,6 +29,7 @@ world = "impl" [package.metadata.component.target.dependencies] "host:common" = { path = "../../../user/CommonApi/common/packages/wit/host-common.wit" } +"host:privileged" = { path = "../../../user/CommonApi/common/packages/wit/host-privileged.wit" } "transact:plugin" = { path = "../../Transact/plugin/wit/world.wit" } "clientdata:plugin" = { path = "../../../user/ClientData/plugin/wit/world.wit" } diff --git a/services/system/Accounts/plugin/smartAuth/README.md b/services/system/Accounts/plugin/smartAuth/README.md new file mode 100644 index 000000000..ce3d831ee --- /dev/null +++ b/services/system/Accounts/plugin/smartAuth/README.md @@ -0,0 +1,7 @@ +# Dynamic imports + +The `import.wit` interface is used to dynamically import the `smart-auth` interface from any of the compliant auth service plugins. + +The `export.wit` interface is exported by auth service plugins in order to be compliant and importable. + +This asymmetry arises because the interface exporter does not know/care that the importer must specify the specific auth plugin to which the host should dynamically link at runtime. But the importer needs an interface that allows it to specify the plugin dynamically. Therefore, the same interface cannot be used for both purposes. diff --git a/services/system/Accounts/plugin/smartAuth/export.wit b/services/system/Accounts/plugin/smartAuth/export.wit new file mode 100644 index 000000000..68fbf3387 --- /dev/null +++ b/services/system/Accounts/plugin/smartAuth/export.wit @@ -0,0 +1,52 @@ +package accounts:smart-auth; + +interface types { + /// A list of `claim` objects is added to every submitted transaction. + /// A claim is something the submitter of a transaction claims to know (e.g. a public key) + record claim { + /// The name of the service used to verify the claim is valid + verify-service: string, + /// The raw claim data + raw-data: list, + } + + /// The verify-service specified in the claim is the service that knows how to verify that + /// a proof actually proves the claim. + record proof { + signature: list, + } + + /// Action data from a transaction + record action { + /// The service that will execute this action + service: string, + /// The name of the action to execute + method: string, + /// The data passed to the action, packed in fracpack format + raw-data: list, + } +} + +/// Export this interface to implement an auth service plugin +/// This interface should only be exported, not imported. +interface smart-auth { + use host:common/types.{error}; + use types.{claim, proof, action}; + + /// Gets the claim(s) needed to satisfy the authorization of the sender of this transaction + /// + /// Parameters + /// * `account-name`: The name of the account whose authorization this transaction + /// claims to satisfy (The sender of the transaction). + /// * `actions`: The actions that the sender of this transaction is attempting to execute + get-claims: func(account-name: string, actions: list) -> result, error>; + + /// Provides proof(s) of the claim(s). + /// If there are multiple claims, the returned list of proofs must be in the same order as the + /// claims were specified. + /// + /// Parameters + /// * `account-name`: The name of the account whose claim is being proven + /// * `transaction-hash`: A hash of the current transaction. Often signed to generate a proof. + get-proofs: func(account-name: string, transaction-hash: list) -> result, error>; +} diff --git a/services/system/Accounts/plugin/smartAuth/import.wit b/services/system/Accounts/plugin/smartAuth/import.wit new file mode 100644 index 000000000..488f4b30f --- /dev/null +++ b/services/system/Accounts/plugin/smartAuth/import.wit @@ -0,0 +1,59 @@ +package accounts:smart-auth; + +interface types { + /// A list of `claim` objects is added to every submitted transaction. + /// A claim is something the submitter of a transaction claims to know (e.g. a public key) + record claim { + /// The name of the service used to verify the claim is valid + verify-service: string, + /// The raw claim data + raw-data: list, + } + + /// The verify-service specified in the claim is the service that knows how to verify that + /// a proof actually proves the claim. + record proof { + signature: list, + } + + /// Action data from a transaction + record action { + /// The service that will execute this action + service: string, + /// The name of the action to execute + method: string, + /// The data passed to the action, packed in fracpack format + raw-data: list, + } +} + +/// Import this interface to depend on an auth service. +/// This interface should only be imported, not exported. +interface smart-auth { + use host:common/types.{error, plugin-ref}; + use types.{claim, proof, action}; + + /// Gets the claim(s) needed to satisfy the authorization of the sender of this transaction + /// + /// Parameters + /// * `plugin`: The plugin that contains the auth service + /// * `account-name`: The name of the account whose authorization this transaction + /// claims to satisfy (The sender of the transaction). + /// * `actions`: The actions that the sender of this transaction is attempting to execute + get-claims: func(plugin: plugin-ref, account-name: string, actions: list) -> result, error>; + + /// Provides proof(s) of the claim(s). + /// If there are multiple claims, the returned list of proofs must be in the same order as the + /// claims were specified. + /// + /// Parameters + /// * `plugin`: The plugin that contains the auth service + /// * `account-name`: The name of the account whose claim is being proven + /// * `transaction-hash`: A hash of the current transaction. Often signed to generate a proof. + get-proofs: func(plugin: plugin-ref, account-name: string, transaction-hash: list) -> result, error>; +} + +world imports { + import host:common/types; + import smart-auth; +} diff --git a/services/system/Accounts/plugin/src/lib.rs b/services/system/Accounts/plugin/src/lib.rs index ad4c75827..c85a8c5e1 100644 --- a/services/system/Accounts/plugin/src/lib.rs +++ b/services/system/Accounts/plugin/src/lib.rs @@ -4,8 +4,8 @@ mod bindings; use base64::{engine::general_purpose::URL_SAFE, Engine}; use bindings::clientdata::plugin::keyvalue as Keyvalue; use bindings::exports::accounts::plugin::accounts::Guest as Accounts; -use bindings::exports::accounts::plugin::admin::Guest as Admin; use bindings::host::common::{client as Client, types as CommonTypes, server as Server}; +use bindings::host::privileged::intf as Privileged; use bindings::transact::plugin::intf as Transact; use bindings::accounts::plugin::types::{self as AccountTypes}; use psibase::fracpack::Pack; @@ -56,51 +56,66 @@ fn login_key(origin: String) -> String { return key_pre + "." + &encoded; } -fn from_supervisor() -> bool { - Client::get_sender_app() - .app - .map_or(false, |app| app == "supervisor") -} - -impl Admin for AccountsPlugin { - fn force_login(domain: String, user: String) { - assert!(from_supervisor(), "unauthorized"); - Keyvalue::set(&login_key(domain), &user.as_bytes()).expect("Failed to set logged-in user"); +impl Accounts for AccountsPlugin { + fn login() -> Result<(), CommonTypes::Error> { + println!("Login with popup window not yet supported."); + return Err(NotYetImplemented.err("login")); } - fn get_logged_in_user(caller_app: String, domain: String) -> Result, CommonTypes::Error> { - let sender = Client::get_sender_app().app; - assert!( - sender.is_some() && sender.as_ref().unwrap() == "supervisor", - "unauthorized" - ); - - // Todo: Allow other apps to ask for the logged in user by popping up an authorization window. - if caller_app != "transact" && caller_app != "supervisor" { - return Err(Unauthorized.err("Temporarily, only transact can ask for the logged-in user.")); - } + fn logout() -> Result<(), CommonTypes::Error> { + let sender = Client::get_sender_app(); + let top_level_domain = Privileged::get_active_app_domain(); - if let Some(user) = Keyvalue::get(&login_key(domain)) { - Ok(Some(String::from_utf8(user).unwrap())) + if sender.origin == top_level_domain || sender.app == Some("supervisor".to_string()) { + Keyvalue::delete(&login_key(top_level_domain)); } else { - Ok(None) + return Err(Unauthorized.err("logout can only be called by the top-level app domain")); } - } -} -impl Accounts for AccountsPlugin { - fn login() -> Result<(), CommonTypes::Error> { - println!("Login with popup window not yet supported."); - return Err(NotYetImplemented.err("login")); + Ok(()) } fn login_temp(user: String) -> Result<(), CommonTypes::Error> { let origin = Client::get_sender_app().origin; + let top_level_domain = Privileged::get_active_app_domain(); + + if origin != top_level_domain { + return Err(Unauthorized.err("login-temp can only be called by the top-level app domain")); + } + + Keyvalue::set(&login_key(top_level_domain), &user.as_bytes()).expect("Failed to set logged-in user"); - Client::login_temp(&origin, &user)?; Ok(()) } + fn is_logged_in() -> bool { + let active_domain = Privileged::get_active_app_domain(); + Keyvalue::get(&login_key(active_domain)).is_some() + } + + fn get_logged_in_user() -> Result, CommonTypes::Error> { + let sender =Client::get_sender_app(); + let active_domain = Privileged::get_active_app_domain(); + let sender_domain = sender.origin; + + if sender_domain != active_domain { + if let Some(sender_app) = sender.app + { + if sender_app != "supervisor" && sender_app != "transact" { + return Err(Unauthorized.err("Only callable by the top-level app domain")); + } + } else { + return Err(Unauthorized.err("Only callable by the top-level app domain")); + } + } + + if let Some(user) = Keyvalue::get(&login_key(active_domain)) { + Ok(Some(String::from_utf8(user).unwrap())) + } else { + Ok(None) + } + } + fn get_available_accounts() -> Result, CommonTypes::Error> { Ok(vec!["alice".to_string(), "bob".to_string()]) } diff --git a/services/system/Accounts/plugin/wit/impl.wit b/services/system/Accounts/plugin/wit/impl.wit index 2db04a204..f12cc4c0a 100644 --- a/services/system/Accounts/plugin/wit/impl.wit +++ b/services/system/Accounts/plugin/wit/impl.wit @@ -1,30 +1,10 @@ package accounts:plugin; -/// This interface is only accessible to the supervisor. -interface admin { - use host:common/types.{error}; - - /// Forces `user` to get logged into the app at domain `domain`. - force-login: func(domain: string, user: string); - - /// Logins on a per-app basis break the ability for plugins to ask who is currently - /// logged in. Therefore, they can only ask through the host, which is the entity - /// that reliably knows the domain of the top-level app into which the user is - /// interacting. - /// - /// Parameters: - /// * `caller-app` - The supervisor will always be the actual caller of the function, - /// but this parameter specifies the account of the app who originally - /// prompted the supervisor to ask. - /// * `domain` - The domain of the app whose logged-in user is being requested. - get-logged-in-user: func(caller-app: string, domain: string) -> result, error>; -} - world impl { include host:common/imports; + include host:privileged/imports; include clientdata:plugin/imports; include transact:plugin/imports; - export admin; export accounts; } diff --git a/services/system/Accounts/plugin/wit/world.wit b/services/system/Accounts/plugin/wit/world.wit index 4deb618a9..bd1d59e49 100644 --- a/services/system/Accounts/plugin/wit/world.wit +++ b/services/system/Accounts/plugin/wit/world.wit @@ -15,14 +15,30 @@ interface accounts { /// Creates a login popup allowing the user to log into an account. login: func() -> result<_, error>; + /// Logs out the currently logged-in user. + logout: func() -> result<_, error>; + /// Temporary function that will be deprecated once popup login windows are supported. /// This function will log in the specified user with zero error checking. login-temp: func(user: string) -> result<_, error>; + /// Returns true if there is a user logged in to the currently active top-level application. + /// Unlike get-logged-in-user, this function is callable by any plugin because it does not + /// leak any private information, and only notifies the caller whether there is a user logged in. + is-logged-in: func() -> bool; + + /// Gets the account name of the user logged into the currently active top-level application. + /// May also return None if no user is logged in. + /// + /// Note: This function is only callable by the top-level app, or its plugin, or other + /// privileged system plugins such as supervisor and transact. + get-logged-in-user: func() -> result, error>; + /// Retrieves a list of accounts to which the user can log in. get-available-accounts: func() -> result, error>; - /// Verifies an account name corresponds with an existing account + /// Gets the account details for the specified account name (if it exists) + /// Returns None if the account does not exist. get-account: func(name: string) -> result, error>; /// Adds a new auth service locally that can be used to authenticate diff --git a/services/system/AuthAny/plugin/.gitignore b/services/system/AuthAny/plugin/.gitignore new file mode 100644 index 000000000..eb5a316cb --- /dev/null +++ b/services/system/AuthAny/plugin/.gitignore @@ -0,0 +1 @@ +target diff --git a/services/system/AuthAny/plugin/Cargo.lock b/services/system/AuthAny/plugin/Cargo.lock new file mode 100644 index 000000000..4fea3ba5f --- /dev/null +++ b/services/system/AuthAny/plugin/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "auth-any" +version = "0.1.0" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "wit-bindgen-rt" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744845cde309b8fa32408d6fb67456449278c66ea4dcd96de29797b302721f02" +dependencies = [ + "bitflags", +] diff --git a/services/system/AuthAny/plugin/Cargo.toml b/services/system/AuthAny/plugin/Cargo.toml new file mode 100644 index 000000000..b90db40f2 --- /dev/null +++ b/services/system/AuthAny/plugin/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "auth-any" +version = "0.1.0" +edition = "2021" + +[dependencies] +wit-bindgen-rt = { version = "0.34.0", features = ["bitflags"] } + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "s" +debug = false +strip = true +lto = true + +[package.metadata.component] +package = "auth-any:plugin" + +[package.metadata.component.target] +world = "impl" + +[package.metadata.component.dependencies] + +[package.metadata.component.target.dependencies] +"host:common" = { path = "../../../user/CommonApi/common/packages/wit/host-common.wit" } +"accounts:smart-auth" = { path = "../../Accounts/plugin/smartAuth/export.wit" } diff --git a/services/system/AuthAny/plugin/src/lib.rs b/services/system/AuthAny/plugin/src/lib.rs new file mode 100644 index 000000000..c5bbcd93c --- /dev/null +++ b/services/system/AuthAny/plugin/src/lib.rs @@ -0,0 +1,20 @@ +#[allow(warnings)] +mod bindings; + +use bindings::accounts::smart_auth::types::{Action, Claim, Proof}; +use bindings::exports::accounts::smart_auth::smart_auth::Guest as SmartAuth; +use bindings::host::common::types as CommonTypes; + +struct AuthAny; + +impl SmartAuth for AuthAny { + fn get_claims(_: String, _: Vec) -> Result, CommonTypes::Error> { + Ok(vec![]) + } + + fn get_proofs(_: String, _: Vec) -> Result, CommonTypes::Error> { + Ok(vec![]) + } +} + +bindings::export!(AuthAny with_types_in bindings); diff --git a/services/system/AuthAny/plugin/wit/impl.wit b/services/system/AuthAny/plugin/wit/impl.wit new file mode 100644 index 000000000..8565e9026 --- /dev/null +++ b/services/system/AuthAny/plugin/wit/impl.wit @@ -0,0 +1,8 @@ +package auth-any:plugin; + +world impl { + include host:common/imports; + + import accounts:smart-auth/types; + export accounts:smart-auth/smart-auth; +} \ No newline at end of file diff --git a/services/system/AuthDelegate/plugin/.gitignore b/services/system/AuthDelegate/plugin/.gitignore new file mode 100644 index 000000000..1de565933 --- /dev/null +++ b/services/system/AuthDelegate/plugin/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/services/system/AuthDelegate/plugin/Cargo.lock b/services/system/AuthDelegate/plugin/Cargo.lock new file mode 100644 index 000000000..f36752f64 --- /dev/null +++ b/services/system/AuthDelegate/plugin/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "auth-delegate" +version = "0.12.0" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "wit-bindgen-rt" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744845cde309b8fa32408d6fb67456449278c66ea4dcd96de29797b302721f02" +dependencies = [ + "bitflags", +] diff --git a/services/system/AuthDelegate/plugin/Cargo.toml b/services/system/AuthDelegate/plugin/Cargo.toml new file mode 100644 index 000000000..24ca59e97 --- /dev/null +++ b/services/system/AuthDelegate/plugin/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "auth-delegate" +version = "0.12.0" +edition = "2021" + +[dependencies] +wit-bindgen-rt = { version = "0.34.0", features = ["bitflags"] } + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "s" +debug = false +strip = true +lto = true + +[package.metadata.component] +package = "auth-delegate:plugin" + +[package.metadata.component.target] +world = "impl" + +[package.metadata.component.dependencies] + +[package.metadata.component.target.dependencies] +"host:common" = { path = "../../../user/CommonApi/common/packages/wit/host-common.wit" } +"accounts:smart-auth" = { path = "../../Accounts/plugin/smartAuth/export.wit" } diff --git a/services/system/AuthDelegate/plugin/src/errors.rs b/services/system/AuthDelegate/plugin/src/errors.rs new file mode 100644 index 000000000..9aae6a775 --- /dev/null +++ b/services/system/AuthDelegate/plugin/src/errors.rs @@ -0,0 +1,25 @@ +use crate::bindings::host::common::types::{Error, PluginId}; + +#[derive(PartialEq, Eq, Hash)] +pub enum ErrorType { + NotYetImplemented, +} + +fn my_plugin_id() -> PluginId { + return PluginId { + service: "auth-delegate".to_string(), + plugin: "plugin".to_string(), + }; +} + +impl ErrorType { + pub fn err(self, msg: &str) -> Error { + match self { + ErrorType::NotYetImplemented => Error { + code: self as u32, + producer: my_plugin_id(), + message: format!("Not yet implemented: {}", msg), + }, + } + } +} diff --git a/services/system/AuthDelegate/plugin/src/lib.rs b/services/system/AuthDelegate/plugin/src/lib.rs new file mode 100644 index 000000000..e0a830571 --- /dev/null +++ b/services/system/AuthDelegate/plugin/src/lib.rs @@ -0,0 +1,31 @@ +#[allow(warnings)] +mod bindings; +mod errors; +use errors::ErrorType::*; + +// Other plugins +use bindings::accounts::smart_auth::types::{Action, Claim, Proof}; +use bindings::host::common::types as CommonTypes; + +// Exported interfaces +use bindings::exports::accounts::smart_auth::smart_auth::Guest as SmartAuth; + +struct AuthDelegate; + +impl SmartAuth for AuthDelegate { + fn get_claims( + _account_name: String, + _actions: Vec, + ) -> Result, CommonTypes::Error> { + Err(NotYetImplemented.err("get_claims")) + } + + fn get_proofs( + _account_name: String, + _transaction_hash: Vec, + ) -> Result, CommonTypes::Error> { + Err(NotYetImplemented.err("get_proofs")) + } +} + +bindings::export!(AuthDelegate with_types_in bindings); diff --git a/services/system/AuthDelegate/plugin/wit/impl.wit b/services/system/AuthDelegate/plugin/wit/impl.wit new file mode 100644 index 000000000..34718bafd --- /dev/null +++ b/services/system/AuthDelegate/plugin/wit/impl.wit @@ -0,0 +1,8 @@ +package auth-sig:plugin; + +world impl { + include host:common/imports; + + import accounts:smart-auth/types; + export accounts:smart-auth/smart-auth; +} diff --git a/services/system/AuthSig/plugin/Cargo.lock b/services/system/AuthSig/plugin/Cargo.lock index c7bb9c6ca..759a7fa7d 100644 --- a/services/system/AuthSig/plugin/Cargo.lock +++ b/services/system/AuthSig/plugin/Cargo.lock @@ -241,6 +241,8 @@ dependencies = [ "psibase", "rand_core", "seahash", + "serde", + "serde_json", "wit-bindgen-rt", ] @@ -1186,9 +1188,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128" diff --git a/services/system/AuthSig/plugin/Cargo.toml b/services/system/AuthSig/plugin/Cargo.toml index 86e2af293..3d99111aa 100644 --- a/services/system/AuthSig/plugin/Cargo.toml +++ b/services/system/AuthSig/plugin/Cargo.toml @@ -5,11 +5,13 @@ edition = "2021" [dependencies] wit-bindgen-rt = { version = "0.26.0", features = ["bitflags"] } -p256 = { version = "0.13", features = ["ecdsa", "pem"] } +p256 = { version = "0.13", features = ["ecdsa", "pem", "ecdsa-core"] } rand_core = { version = "0.6", features = ["getrandom"] } psibase = { path = "../../../../rust/psibase" } pem = "3" seahash = "4.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" [profile.release] codegen-units = 1 @@ -32,5 +34,6 @@ world = "impl" [package.metadata.component.target.dependencies] "host:common" = { path = "../../../user/CommonApi/common/packages/wit/host-common.wit" } "accounts:plugin" = { path = "../../Accounts/plugin/wit/world.wit" } +"accounts:smart-auth" = { path = "../../Accounts/plugin/smartAuth/export.wit" } "transact:plugin" = { path = "../../Transact/plugin/wit/world.wit" } "clientdata:plugin" = { path = "../../../user/ClientData/plugin/wit/world.wit" } diff --git a/services/system/AuthSig/plugin/src/db.rs b/services/system/AuthSig/plugin/src/db.rs new file mode 100644 index 000000000..9f9a5b390 --- /dev/null +++ b/services/system/AuthSig/plugin/src/db.rs @@ -0,0 +1,29 @@ +use crate::bindings::auth_sig::plugin::types::Pem; +use crate::bindings::clientdata::plugin::keyvalue as Keyvalue; +use crate::types::*; + +fn get_hash(key: &Pem) -> String { + let pem = pem::Pem::try_from_pem_str(&key).expect("Failed to hash key"); + let digest = seahash::hash(&pem.contents().to_vec()); + format!("{:x}", digest) +} + +pub struct ManagedKeys; + +impl ManagedKeys { + pub fn add(pubkey: &Pem, privkey: &[u8]) { + Keyvalue::set(&get_hash(pubkey), &privkey).expect("ManagedKeys::set: Failed to add key"); + } + + pub fn get(pubkey: &Pem) -> Vec { + Keyvalue::get(&get_hash(pubkey)).expect("ManagedKeys::get: Key not found") + } + + pub fn has(pubkey: &Pem) -> bool { + Keyvalue::get(&get_hash(pubkey)).is_some() + } + + pub fn _delete(pubkey: &Pem) { + Keyvalue::delete(&get_hash(pubkey)); + } +} diff --git a/services/system/AuthSig/plugin/src/errors.rs b/services/system/AuthSig/plugin/src/errors.rs index 0d90e52f6..2c4eb22d3 100644 --- a/services/system/AuthSig/plugin/src/errors.rs +++ b/services/system/AuthSig/plugin/src/errors.rs @@ -2,8 +2,10 @@ use crate::bindings::host::common::types::{Error, PluginId}; #[derive(PartialEq, Eq, Hash)] pub enum ErrorType { - NotYetImplemented, CryptoError, + Unauthorized, + JsonDecodeError, + KeyNotFound, } fn my_plugin_id() -> PluginId { @@ -16,15 +18,25 @@ fn my_plugin_id() -> PluginId { impl ErrorType { pub fn err(self, msg: &str) -> Error { match self { - ErrorType::NotYetImplemented => Error { + ErrorType::CryptoError => Error { code: self as u32, producer: my_plugin_id(), - message: format!("Not yet implemented: {}", msg), + message: format!("Crypto error: {}", msg), }, - ErrorType::CryptoError => Error { + ErrorType::Unauthorized => Error { code: self as u32, producer: my_plugin_id(), - message: format!("Crypto error: {}", msg), + message: format!("Unauthorized: {}", msg), + }, + ErrorType::JsonDecodeError => Error { + code: self as u32, + producer: my_plugin_id(), + message: format!("JSON decode error: {}", msg), + }, + ErrorType::KeyNotFound => Error { + code: self as u32, + producer: my_plugin_id(), + message: format!("Key not found: {}", msg), }, } } diff --git a/services/system/AuthSig/plugin/src/helpers.rs b/services/system/AuthSig/plugin/src/helpers.rs new file mode 100644 index 000000000..f77525d62 --- /dev/null +++ b/services/system/AuthSig/plugin/src/helpers.rs @@ -0,0 +1,22 @@ +use crate::bindings::auth_sig::plugin::types::Pem; +use crate::bindings::host::common::{client as Client, server as Server, types as CommonTypes}; +use crate::errors::ErrorType::*; +use crate::types::*; + +pub fn get_pubkey(account_name: &str) -> Result { + let user_key_json = Server::post_graphql_get_json(&format!( + "query {{ account(name: \"{}\") {{ pubkey }} }}", + account_name + ))?; + + let summary_val = serde_json::from_str::(&user_key_json) + .map_err(|e| JsonDecodeError.err(&e.to_string()))?; + + Ok(summary_val.data.account.pubkey) +} + +pub fn from_transact() -> bool { + Client::get_sender_app().app.map_or(false, |app| { + app == psibase::services::transact::SERVICE.to_string() + }) +} diff --git a/services/system/AuthSig/plugin/src/lib.rs b/services/system/AuthSig/plugin/src/lib.rs index fadf32abf..f20e52cf5 100644 --- a/services/system/AuthSig/plugin/src/lib.rs +++ b/services/system/AuthSig/plugin/src/lib.rs @@ -1,59 +1,74 @@ #[allow(warnings)] mod bindings; -use bindings::auth_sig::plugin::types::{Keypair, Pem}; -use bindings::clientdata::plugin::keyvalue as Keyvalue; -use bindings::exports::auth_sig::plugin::smart_auth; -use bindings::exports::auth_sig::plugin::{ - actions::Guest as Actions, keyvault::Guest as KeyVault, smart_auth::Guest as SmartAuth, -}; -use bindings::host::common::types as CommonTypes; -use bindings::transact::plugin::intf as Transact; - mod errors; use errors::ErrorType::*; +mod helpers; +use helpers::*; +mod db; +use db::*; +mod types; +use types::*; + +// Other plugins +use bindings::accounts::smart_auth::types::{Action, Claim, Proof}; +use bindings::auth_sig::plugin::types::{Keypair, Pem}; +use bindings::host::common::{client as Client, types as CommonTypes}; +use bindings::transact::plugin::intf as Transact; -use p256::ecdsa::{SigningKey, VerifyingKey}; -use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey, LineEnding}; -use rand_core::OsRng; +// Exported interfaces +use bindings::exports::accounts::smart_auth::smart_auth::Guest as SmartAuth; +use bindings::exports::auth_sig::plugin::{actions::Guest as Actions, keyvault::Guest as KeyVault}; -use psibase::fracpack::Pack; +// Services use psibase::services::auth_sig::action_structs as MyService; -use seahash; - -trait TryFromPemStr: Sized { - fn try_from_pem_str(p: &Pem) -> Result; -} - -impl TryFromPemStr for pem::Pem { - fn try_from_pem_str(key_string: &Pem) -> Result { - Ok(pem::parse(key_string.trim()).map_err(|e| CryptoError.err(&e.to_string()))?) - } -} - -fn get_hash(key: &Pem) -> Result { - let pem = pem::Pem::try_from_pem_str(&key)?; - let digest = seahash::hash(&pem.contents().to_vec()); - Ok(format!("{:x}", digest)) -} +// Third-party crates +use p256::ecdsa::{signature::hazmat::PrehashSigner, Signature, SigningKey, VerifyingKey}; +use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey, LineEnding}; +use psibase::fracpack::Pack; +use rand_core::OsRng; struct AuthSig; impl SmartAuth for AuthSig { - fn get_claim() -> Result { - Err(NotYetImplemented.err("get_claim")) + fn get_claims( + account_name: String, + _actions: Vec, + ) -> Result, CommonTypes::Error> { + if !from_transact() { + return Err(Unauthorized.err("get_claims")); + } + + let pubkey = get_pubkey(&account_name)?; + if !ManagedKeys::has(&pubkey) { + return Err(KeyNotFound.err("get_claims")); + } + + Ok(vec![Claim { + verify_service: psibase::services::verify_sig::SERVICE.to_string(), + raw_data: AuthSig::to_der(pubkey)?, + }]) } - fn get_proof(_message: Vec) -> Result { - Err(NotYetImplemented.err("get_proof")) + fn get_proofs( + account_name: String, + transaction_hash: Vec, + ) -> Result, CommonTypes::Error> { + if !from_transact() { + return Err(Unauthorized.err("get_proofs")); + } + + let pubkey = get_pubkey(&account_name)?; + let private_key = ManagedKeys::get(&pubkey); + let signature = AuthSig::sign(transaction_hash, private_key)?; + Ok(vec![Proof { signature }]) } } impl KeyVault for AuthSig { fn generate_keypair() -> Result { let keypair = AuthSig::generate_unmanaged_keypair()?; - let hash = get_hash(&keypair.public_key)?; - Keyvalue::set(&hash, &AuthSig::to_der(keypair.private_key)?)?; + ManagedKeys::add(&keypair.public_key, &AuthSig::to_der(keypair.private_key)?); Ok(keypair.public_key) } @@ -90,12 +105,30 @@ impl KeyVault for AuthSig { let pem = pem::Pem::try_from_pem_str(&key)?; Ok(pem.contents().to_vec()) } + + fn sign(hashed_message: Vec, private_key: Vec) -> Result, CommonTypes::Error> { + let signing_key = SigningKey::from_pkcs8_der(&private_key) + .map_err(|e| CryptoError.err(&e.to_string()))?; + let signature: Signature = signing_key + .sign_prehash(&hashed_message) + .map_err(|e| CryptoError.err(&e.to_string()))?; + Ok(signature.to_bytes().to_vec()) + } } impl Actions for AuthSig { fn set_key(public_key: Pem) -> Result<(), CommonTypes::Error> { + // TODO: check if sender authorizes caller app to set key + // Currently only an AuthSig app would be able to set a user's key + let auth = Client::get_sender_app().app.map_or(false, |app| { + app == psibase::services::auth_sig::SERVICE.to_string() + }); + if !auth { + return Err(Unauthorized.err("set_key")); + } + Transact::add_action_to_transaction( - "setKey", + MyService::setKey::ACTION_NAME, &MyService::setKey { key: AuthSig::to_der(public_key)?.into(), } diff --git a/services/system/AuthSig/plugin/src/types.rs b/services/system/AuthSig/plugin/src/types.rs new file mode 100644 index 000000000..8569dbd97 --- /dev/null +++ b/services/system/AuthSig/plugin/src/types.rs @@ -0,0 +1,29 @@ +use crate::bindings::auth_sig::plugin::types::Pem; +use crate::bindings::host::common::types as CommonTypes; +use crate::errors::ErrorType::*; +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct AuthRecord { + pub pubkey: Pem, +} + +#[derive(Deserialize, Debug)] +pub struct AccountDetails { + pub account: AuthRecord, +} + +#[derive(Deserialize, Debug)] +pub struct Response { + pub data: AccountDetails, +} + +pub trait TryFromPemStr: Sized { + fn try_from_pem_str(p: &Pem) -> Result; +} + +impl TryFromPemStr for pem::Pem { + fn try_from_pem_str(key_string: &Pem) -> Result { + Ok(pem::parse(key_string.trim()).map_err(|e| CryptoError.err(&e.to_string()))?) + } +} diff --git a/services/system/AuthSig/plugin/wit/impl.wit b/services/system/AuthSig/plugin/wit/impl.wit index 1045f433c..d8b9b74fb 100644 --- a/services/system/AuthSig/plugin/wit/impl.wit +++ b/services/system/AuthSig/plugin/wit/impl.wit @@ -6,7 +6,8 @@ world impl { include host:common/imports; include clientdata:plugin/imports; - export smart-auth; + import accounts:smart-auth/types; + export accounts:smart-auth/smart-auth; export keyvault; export actions; } diff --git a/services/system/AuthSig/plugin/wit/world.wit b/services/system/AuthSig/plugin/wit/world.wit index 9b10e1805..e3a9eae5f 100644 --- a/services/system/AuthSig/plugin/wit/world.wit +++ b/services/system/AuthSig/plugin/wit/world.wit @@ -3,20 +3,6 @@ package auth-sig:plugin; /// Common types used when dealing with auth service plugins interface types { - /// A list of `claim` objects is added to every submitted transaction. - /// A claim is something the submitter of a transaction claims to know (e.g. a public key) - record claim { - /// The name of the service used to verify the claim is valid - verify-service: string, - /// The raw claim data - raw-data: list, - } - - /// A list of `proof` is attached - record proof { - signature: list, - } - /// PEM type is just a string type pem = string; @@ -28,23 +14,6 @@ interface types { } } -/// Methods related to psibase smart authorization -/// Every auth service must export this interface -interface smart-auth { - use host:common/types.{error}; - use types.{claim, proof}; - - /// Gets the claim needed for this user to satisfy the authorization check - get-claim: func() -> result; - - /// Provides proof of the claim. - /// - /// Parameters - /// * `message`: A message that usually needs to be signed as - /// proof of the claim. - get-proof: func(message: list) -> result; -} - /// Methods related to key management interface keyvault { use host:common/types.{error}; @@ -64,6 +33,9 @@ interface keyvault { /// Returns the DER encoded key to-der: func(key: pem) -> result, error>; + + /// Signs a pre-hashed message with the specified DER-encoded private key + sign: func(hashed-message: list, private-key: list) -> result, error>; } interface actions { @@ -76,7 +48,6 @@ interface actions { } world imports { - import smart-auth; import keyvault; import actions; } \ No newline at end of file diff --git a/services/system/Transact/plugin/Cargo.lock b/services/system/Transact/plugin/Cargo.lock index 367264496..a89adcd79 100644 --- a/services/system/Transact/plugin/Cargo.lock +++ b/services/system/Transact/plugin/Cargo.lock @@ -2219,6 +2219,7 @@ dependencies = [ "regex", "serde", "serde_json", + "sha2", "wit-bindgen-rt", ] diff --git a/services/system/Transact/plugin/Cargo.toml b/services/system/Transact/plugin/Cargo.toml index 2065669f1..39d48b175 100644 --- a/services/system/Transact/plugin/Cargo.toml +++ b/services/system/Transact/plugin/Cargo.toml @@ -10,6 +10,7 @@ regex = "1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" chrono = "0.4.38" +sha2 = "0.10.6" [lib] crate-type = ["cdylib"] @@ -30,3 +31,8 @@ world = "impl" [package.metadata.component.target.dependencies] "host:common" = { path = "../../../user/CommonApi/common/packages/wit/host-common.wit" } "clientdata:plugin" = { path = "../../../user/ClientData/plugin/wit/world.wit" } +"accounts:plugin" = { path = "../../Accounts/plugin/wit/world.wit" } +"accounts:smart-auth" = { path = "../../Accounts/plugin/smartAuth/import.wit" } + +[package.metadata.component.bindings] +derives = ["psibase::Pack", "psibase::Unpack"] diff --git a/services/system/Transact/plugin/src/db.rs b/services/system/Transact/plugin/src/db.rs new file mode 100644 index 000000000..db0e2ba86 --- /dev/null +++ b/services/system/Transact/plugin/src/db.rs @@ -0,0 +1,62 @@ +use crate::bindings::accounts::smart_auth::smart_auth::Action as PartialAction; +use crate::bindings::clientdata::plugin::keyvalue as Keyvalue; +use psibase::fracpack::{Pack, Unpack}; + +pub struct AuthPlugins; +impl AuthPlugins { + const KEY_AUTH_PLUGINS: &'static str = "auth_plugins"; + pub fn push(auth_plugin: String) { + let mut auth_plugins = Self::get(); + auth_plugins.push(auth_plugin); + + Keyvalue::set(Self::KEY_AUTH_PLUGINS, &auth_plugins.packed()) + .expect("Failed to set auth plugins"); + } + + pub fn get() -> Vec { + Keyvalue::get(Self::KEY_AUTH_PLUGINS) + .map(|a| >::unpacked(&a).expect("Failed to unpack auth plugins")) + .unwrap_or(vec![]) + } + + pub fn has_plugins() -> bool { + Keyvalue::get(Self::KEY_AUTH_PLUGINS).is_some() + } + + pub fn clear() { + Keyvalue::delete(Self::KEY_AUTH_PLUGINS); + } +} + +pub struct CurrentActions; +impl CurrentActions { + const KEY_ACTIONS: &'static str = "actions"; + + pub fn push(service: String, method_name: String, packed_args: Vec) { + let mut actions = Keyvalue::get(Self::KEY_ACTIONS) + .map(|a| >::unpacked(&a).expect("Failed to unpack actions")) + .unwrap_or(vec![]); + + actions.push(PartialAction { + service, + method: method_name, + raw_data: packed_args, + }); + + Keyvalue::set(Self::KEY_ACTIONS, &actions.packed()).expect("Failed to set actions"); + } + + pub fn get() -> Vec { + Keyvalue::get(Self::KEY_ACTIONS) + .map(|a| >::unpacked(&a).expect("Failed to unpack actions")) + .unwrap_or(vec![]) + } + + pub fn has_actions() -> bool { + Keyvalue::get(Self::KEY_ACTIONS).is_some() + } + + pub fn clear() { + Keyvalue::delete(Self::KEY_ACTIONS); + } +} diff --git a/services/system/Transact/plugin/src/errors.rs b/services/system/Transact/plugin/src/errors.rs index 804c2f5d1..eaa2228ab 100644 --- a/services/system/Transact/plugin/src/errors.rs +++ b/services/system/Transact/plugin/src/errors.rs @@ -6,6 +6,7 @@ pub enum ErrorType { InvalidActionName, NotLoggedIn, TransactionError, + ClaimProofMismatch, } fn my_plugin_id() -> PluginId { @@ -38,6 +39,11 @@ impl ErrorType { producer: my_plugin_id(), message: format!("Transaction error: {}", msg), }, + ErrorType::ClaimProofMismatch => Error { + code: self as u32, + producer: my_plugin_id(), + message: format!("Number of proofs does not match number of claims: {}", msg), + }, } } } diff --git a/services/system/Transact/plugin/src/helpers.rs b/services/system/Transact/plugin/src/helpers.rs new file mode 100644 index 000000000..4e86fbf5d --- /dev/null +++ b/services/system/Transact/plugin/src/helpers.rs @@ -0,0 +1,173 @@ +use crate::bindings::accounts::plugin::accounts::{get_account, get_logged_in_user}; +use crate::bindings::accounts::smart_auth::smart_auth::{ + self as SmartAuth, Action as PartialAction, +}; +use crate::bindings::host::common as Host; +use crate::errors::ErrorType::*; +use crate::types::FromExpirationTime; +use crate::AuthPlugins; +use psibase::fracpack::Pack; +use psibase::{ + services, services::invite::action_structs as InviteService, AccountNumber, Hex, MethodNumber, + SignedTransaction, Tapos, Transaction, +}; + +use regex::Regex; +use sha2::{Digest, Sha256}; + +pub fn validate_action_name(action_name: &str) -> Result<(), Host::types::Error> { + let re = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap(); + if re.is_match(action_name) { + return Ok(()); + } + Err(InvalidActionName.err(action_name)) +} + +pub fn assert_from_supervisor() { + let sender_app = Host::client::get_sender_app() + .app + .expect("Sender app not set"); + assert!(sender_app == "supervisor", "Unauthorized"); +} + +fn inject_sender(actions: Vec, sender: AccountNumber) -> Vec { + actions + .into_iter() + .map(|a| psibase::Action { + sender, + service: AccountNumber::from(a.service.as_str()), + method: MethodNumber::from(a.method.as_str()), + rawData: Hex::from(a.raw_data), + }) + .collect() +} + +// The auth service can parameterize what claims to add to a transaction by both the sender +// and the actions in the transaction. +// The auth service is therefore the only service other than transact that gets to see +// in full what actions are being taken by a user. +fn get_claims( + sender: &AccountNumber, + actions: &[PartialAction], +) -> Result, Host::types::Error> { + let sender_str = sender.to_string(); + + // Get claims from their auth service + let auth_service_acc = get_account(&sender_str)?.unwrap().auth_service; + let plugin_ref = Host::types::PluginRef::new(&auth_service_acc); + let mut claims = SmartAuth::get_claims(plugin_ref, &sender_str, actions)?; + + // Also get claims from any auth notifiers + let auth_plugins = AuthPlugins::get(); + for plugin in auth_plugins { + if plugin == auth_service_acc { + continue; + } + + let plugin_ref = Host::types::PluginRef::new(&plugin); + + // An auth notifier should not be privy to the full action list, only its own actions + // (like every other plugin). + let action_subset: Vec = actions + .iter() + .filter(|&a| a.service == plugin) + .cloned() + .collect(); + + let mut c = SmartAuth::get_claims(plugin_ref, &sender_str, &action_subset)?; + claims.append(&mut c); + } + + Ok(claims) +} + +pub fn make_transaction( + actions: Vec, + sender: AccountNumber, + expiration_seconds: u64, +) -> Transaction { + let claims = get_claims(&sender, &actions).expect("Failed to retrieve claims from auth plugin"); + let claims: Vec = claims.into_iter().map(Into::into).collect(); + let actions = inject_sender(actions, sender); + + let t = Transaction { + tapos: Tapos::from_expiration_time(expiration_seconds), + actions, + claims, + }; + println!( + "Publishing transaction: \n{}", + serde_json::to_string_pretty(&t).unwrap() + ); + t +} + +pub fn get_tx_sender(actions: &[PartialAction]) -> Result { + let sender = get_logged_in_user()?; + if sender.is_none() { + if actions.len() == 1 { + let a = &actions[0]; + if AccountNumber::from(a.service.as_str()) == services::invite::SERVICE + && (a.method == InviteService::acceptCreate::ACTION_NAME + || a.method == InviteService::reject::ACTION_NAME) + { + return Ok(services::invite::PAYER_ACCOUNT); + } + } + } + + if sender.is_none() { + return Err(NotLoggedIn.err("get_tx_sender")); + } + + Ok(AccountNumber::from(sender.unwrap().as_str())) +} + +pub fn sha256(data: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(data); + hasher.finalize().into() +} + +pub fn get_proofs( + sender: &AccountNumber, + tx_hash: &[u8; 32], +) -> Result>>, Host::types::Error> { + let sender_str = sender.to_string(); + + // Get proofs from the sender's auth service + let auth_service_acc = get_account(&sender_str)?.unwrap().auth_service; + let plugin_ref = Host::types::PluginRef::new(&auth_service_acc); + let mut proofs = SmartAuth::get_proofs(plugin_ref, &sender_str, tx_hash)?; + + // Also get proofs from any auth notifiers + let auth_plugins = AuthPlugins::get(); + for plugin in auth_plugins { + if plugin == auth_service_acc { + continue; + } + + let plugin_ref = Host::types::PluginRef::new(&plugin); + let mut p = SmartAuth::get_proofs(plugin_ref, &sender_str, tx_hash)?; + proofs.append(&mut p); + } + AuthPlugins::clear(); + + Ok(proofs + .into_iter() + .map(|proof| Hex::from(proof.signature)) + .collect()) +} + +pub trait Publish { + fn publish(self) -> Result; +} + +impl Publish for SignedTransaction { + fn publish(self) -> Result { + Ok(Host::server::post(&Host::types::PostRequest { + endpoint: "/push_transaction".to_string(), + body: Host::types::BodyTypes::Bytes(self.packed()), + })?) + } +} diff --git a/services/system/Transact/plugin/src/lib.rs b/services/system/Transact/plugin/src/lib.rs index 352677cc2..3818f0de5 100644 --- a/services/system/Transact/plugin/src/lib.rs +++ b/services/system/Transact/plugin/src/lib.rs @@ -1,89 +1,54 @@ +#![allow(non_snake_case)] #[allow(warnings)] mod bindings; mod errors; - -use bindings::clientdata::plugin::keyvalue as Keyvalue; -use bindings::exports::transact::plugin::{admin::Guest as Admin, intf::Guest as Intf}; -use bindings::host::common as Host; -use bindings::host::common::types::{self as CommonTypes, BodyTypes::*, PostRequest}; use errors::ErrorType::*; -use psibase::fracpack::{Pack, Unpack}; -use psibase::{ - AccountNumber, Action, MethodNumber, Tapos, TimePointSec, Transaction, TransactionTrace, +mod helpers; +use helpers::*; +mod db; +mod types; +use db::*; + +// Other plugins +use bindings::host::common::{ + self as Host, + types::{self as CommonTypes}, }; -use psibase::{Hex, SignedTransaction}; -use regex::Regex; -use serde::Deserialize; -use serde_json; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -fn validate_action(action_name: &str) -> Result<(), CommonTypes::Error> { - let re = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap(); - if re.is_match(action_name) { - return Ok(()); - } - Err(InvalidActionName.err(action_name)) -} -#[allow(non_snake_case)] -#[derive(Deserialize)] -struct PartialTapos { - refBlockIndex: u8, - refBlockSuffix: u32, -} +// Exported interfaces/types +use bindings::exports::transact::plugin::{ + admin::Guest as Admin, auth_notifier::Guest as Auth, intf::Guest as Intf, +}; -#[derive(Pack)] -struct SignedTransactionAlt { - pub transaction: Vec, - pub proofs: Vec>, -} +// Third-party crates +use psibase::fracpack::Pack; +use psibase::{Hex, SignedTransaction, TransactionTrace}; +use serde_json::from_str; -fn assert_from_supervisor() { - let sender_app = Host::client::get_sender_app() - .app - .expect("Sender app not set"); - assert!(sender_app == "supervisor", "Unauthorized"); -} +struct TransactPlugin {} -fn ten_second_expiration() -> TimePointSec { - let expiration_time = SystemTime::now() + Duration::from_secs(10); - let expiration = expiration_time - .duration_since(UNIX_EPOCH) - .expect("Failed to get time") - .as_secs(); - assert!(expiration <= u32::MAX as u64, "expiration out of range"); - TimePointSec::from(expiration as u32) -} +impl Auth for TransactPlugin { + fn notify() { + let auth_plugin = Host::client::get_sender_app() + .app + .expect("Sender app not set"); -struct TransactPlugin; + AuthPlugins::push(auth_plugin); + } +} impl Intf for TransactPlugin { fn add_action_to_transaction( method_name: String, packed_args: Vec, ) -> Result<(), CommonTypes::Error> { - validate_action(&method_name)?; + validate_action_name(&method_name)?; let service = Host::client::get_sender_app() .app .ok_or_else(|| OnlyAvailableToPlugins.err("add_action_to_transaction"))?; - let sender = Host::client::get_logged_in_user()? - .ok_or_else(|| NotLoggedIn.err("add_action_to_transaction"))?; - - let new_action = Action { - sender: AccountNumber::from(sender.as_str()), - service: AccountNumber::from(service.as_str()), - method: MethodNumber::from(method_name.as_str()), - rawData: Hex::from(packed_args), - }; - - let mut actions = Keyvalue::get("actions") - .map(|a| >::unpacked(&a).expect("Failed to unpack Vec")) - .unwrap_or(vec![]); - actions.push(new_action); - - Keyvalue::set("actions", &actions.packed())?; + CurrentActions::push(service, method_name, packed_args); Ok(()) } @@ -92,62 +57,36 @@ impl Intf for TransactPlugin { impl Admin for TransactPlugin { fn start_tx() { assert_from_supervisor(); - let actions = Keyvalue::get("actions"); - if actions.is_some() { + if CurrentActions::has_actions() { println!("[Warning] Transaction list should already have been cleared."); - Keyvalue::delete("actions"); + CurrentActions::clear(); + } + if AuthPlugins::has_plugins() { + println!("[Warning] Auth plugins should already have been cleared."); + AuthPlugins::clear(); } } fn finish_tx() -> Result<(), CommonTypes::Error> { assert_from_supervisor(); - let actions = Keyvalue::get("actions") - .map(|a| >::unpacked(&a).expect("[finish_tx] Failed to unpack")) - .unwrap_or_default(); - + let actions = CurrentActions::get(); if actions.len() == 0 { return Ok(()); } + CurrentActions::clear(); - Keyvalue::delete("actions"); - - let tapos_str = - Host::server::get_json("/common/tapos/head").expect("[finish_tx] Failed to get TaPoS"); - let PartialTapos { - refBlockSuffix, - refBlockIndex, - } = serde_json::from_str::(&tapos_str) - .expect("[finish_tx] Failed to deserialize TaPoS"); - - let t = Transaction { - tapos: Tapos { - expiration: ten_second_expiration(), - refBlockSuffix, - flags: 0, - refBlockIndex, - }, - actions, - claims: vec![], - }; - - println!( - "Publishing transaction: \n{}", - serde_json::to_string_pretty(&t).expect("Can't format transaction as string") - ); - + let sender = get_tx_sender(&actions)?; + let tx = make_transaction(actions, sender, 10); let signed_tx = SignedTransaction { - transaction: Hex::from(t.packed()), - proofs: vec![], + transaction: Hex::from(tx.packed()), + proofs: get_proofs(&sender, &sha256(&tx.packed()))?, }; + if signed_tx.proofs.len() != tx.claims.len() { + return Err(ClaimProofMismatch.err("Number of proofs does not match number of claims")); + } - let response = Host::server::post(&PostRequest { - endpoint: "/push_transaction".to_string(), - body: Bytes(signed_tx.packed()), - })?; - - let trace = - serde_json::from_str::(&response).expect("Failed to get trace"); + let trace = from_str::(&signed_tx.publish()?).unwrap(); match trace.error { Some(err) => Err(TransactionError.err(&err)), diff --git a/services/system/Transact/plugin/src/types.rs b/services/system/Transact/plugin/src/types.rs new file mode 100644 index 000000000..58946d54a --- /dev/null +++ b/services/system/Transact/plugin/src/types.rs @@ -0,0 +1,49 @@ +use crate::bindings::accounts::smart_auth::smart_auth as SmartAuth; +use crate::bindings::host::common as Host; +use psibase::{AccountNumber, Hex, Tapos, TimePointSec}; +use serde::Deserialize; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +impl From for psibase::Claim { + fn from(claim: SmartAuth::Claim) -> Self { + psibase::Claim { + service: AccountNumber::from(claim.verify_service.as_str()), + rawData: Hex::from(claim.raw_data.clone()), + } + } +} + +#[allow(non_snake_case)] +#[derive(Deserialize)] +struct PartialTapos { + refBlockIndex: u8, + refBlockSuffix: u32, +} + +pub trait FromExpirationTime { + fn from_expiration_time(expiration_time: u64) -> Self; +} + +impl FromExpirationTime for Tapos { + fn from_expiration_time(seconds: u64) -> Self { + let expiration_time = SystemTime::now() + Duration::from_secs(seconds); + let expiration = expiration_time + .duration_since(UNIX_EPOCH) + .expect("Failed to get time") + .as_secs(); + assert!(expiration <= u32::MAX as u64, "expiration out of range"); + let expiration_timepoint = TimePointSec::from(expiration as u32); + + let tapos_str = + Host::server::get_json("/common/tapos/head").expect("[finish_tx] Failed to get TaPoS"); + let partial_tapos: PartialTapos = + serde_json::from_str(&tapos_str).expect("[finish_tx] Failed to deserialize TaPoS"); + + Tapos { + expiration: expiration_timepoint, + refBlockSuffix: partial_tapos.refBlockSuffix, + flags: 0, + refBlockIndex: partial_tapos.refBlockIndex, + } + } +} diff --git a/services/system/Transact/plugin/wit/impl.wit b/services/system/Transact/plugin/wit/impl.wit index 3eddd16f1..bfd3c903f 100644 --- a/services/system/Transact/plugin/wit/impl.wit +++ b/services/system/Transact/plugin/wit/impl.wit @@ -15,7 +15,10 @@ interface admin { world impl { include host:common/imports; include clientdata:plugin/imports; + include accounts:smart-auth/imports; + include accounts:plugin/imports; export admin; export intf; + export auth-notifier; } diff --git a/services/system/Transact/plugin/wit/world.wit b/services/system/Transact/plugin/wit/world.wit index 75249e406..b3ab27626 100644 --- a/services/system/Transact/plugin/wit/world.wit +++ b/services/system/Transact/plugin/wit/world.wit @@ -1,5 +1,15 @@ package transact:plugin; + +/// Interface used by auth notifiers. +/// Auth notifiers are plugins that add additional claims/proofs to transactions +/// without being the user's specified auth service plugin. +interface auth-notifier { + /// Notifies the transact plugin that the caller plugin has one or more claims/proofs to add to the + /// transaction. + notify: func(); +} + interface intf { use host:common/types.{error}; diff --git a/services/user/CommonApi/common/packages/common-lib/src/messaging/PluginId.ts b/services/user/CommonApi/common/packages/common-lib/src/messaging/PluginId.ts index 773859c35..0a9fbc50c 100644 --- a/services/user/CommonApi/common/packages/common-lib/src/messaging/PluginId.ts +++ b/services/user/CommonApi/common/packages/common-lib/src/messaging/PluginId.ts @@ -13,3 +13,7 @@ export function isEqual( ): boolean { return id1.service === id2.service && id1.plugin === id2.plugin; } + +export function pluginId(service: string, plugin: string): QualifiedPluginId { + return { service, plugin }; +} diff --git a/services/user/CommonApi/common/packages/common-lib/src/messaging/index.ts b/services/user/CommonApi/common/packages/common-lib/src/messaging/index.ts index 254e6caa0..a20a8309c 100644 --- a/services/user/CommonApi/common/packages/common-lib/src/messaging/index.ts +++ b/services/user/CommonApi/common/packages/common-lib/src/messaging/index.ts @@ -23,7 +23,12 @@ export { isPreLoadPluginsRequest, } from "./PreLoadPluginsRequest"; -export { type PluginId, type QualifiedPluginId, isEqual } from "./PluginId"; +export { + type PluginId, + type QualifiedPluginId, + isEqual, + pluginId, +} from "./PluginId"; export { isSupervisorInitialized, diff --git a/services/user/CommonApi/common/packages/component-parser/Cargo.lock b/services/user/CommonApi/common/packages/component-parser/Cargo.lock index cb465a399..17ac80b31 100644 --- a/services/user/CommonApi/common/packages/component-parser/Cargo.lock +++ b/services/user/CommonApi/common/packages/component-parser/Cargo.lock @@ -324,9 +324,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasm-compose" -version = "0.211.1" +version = "0.219.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d42de0e10832f85403b6f0dedd42ba31ad5c819b68194e811caa1af38037ee45" +checksum = "3255c03767633aaeab2232ef17d50074f928f4a518f6d356ccbf437622565b70" dependencies = [ "anyhow", "heck", @@ -345,9 +345,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.211.1" +version = "0.219.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e7d931a1120ef357f32b74547646b6fa68ea25e377772b72874b131a9ed70d4" +checksum = "29cbbd772edcb8e7d524a82ee8cef8dd046fc14033796a754c3ad246d019fa54" dependencies = [ "leb128", "wasmparser", @@ -355,9 +355,9 @@ dependencies = [ [[package]] name = "wasm-metadata" -version = "0.211.1" +version = "0.219.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f56bad1c68558c44e7f60be865117dc9d8c3a066bcf3b2232cb9a9858965fd5" +checksum = "2af5a8e37a5e996861e1813f8de30911c47609c9ff51a7284f7dbd754dc3a9f3" dependencies = [ "anyhow", "indexmap", @@ -371,9 +371,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.211.1" +version = "0.219.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3189cc8a91f547390e2f043ca3b3e3fe0892f7d581767fd4e4b7f3dc3fe8e561" +checksum = "5c771866898879073c53b565a6c7b49953795159836714ac56a5befb581227c5" dependencies = [ "ahash", "bitflags", @@ -385,9 +385,9 @@ dependencies = [ [[package]] name = "wast" -version = "211.0.1" +version = "219.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25506dd82d00da6b14a87436b3d52b1d264083fa79cdb72a0d1b04a8595ccaa" +checksum = "4f79a9d9df79986a68689a6b40bcc8d5d40d807487b235bebc2ac69a242b54a1" dependencies = [ "bumpalo", "leb128", @@ -398,27 +398,27 @@ dependencies = [ [[package]] name = "wat" -version = "1.211.1" +version = "1.219.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb716ca6c86eecac2d82541ffc39860118fc0af9309c4f2670637bea2e1bdd7d" +checksum = "8bc3cf014fb336883a411cd662f987abf6a1d2a27f2f0008616a0070bbf6bd0d" dependencies = [ "wast", ] [[package]] name = "wit-bindgen-rt" -version = "0.26.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c7526379ace8709ee9ab9f2bb50f112d95581063a59ef3097d9c10153886c9" +checksum = "744845cde309b8fa32408d6fb67456449278c66ea4dcd96de29797b302721f02" dependencies = [ "bitflags", ] [[package]] name = "wit-component" -version = "0.211.1" +version = "0.219.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "079a38b7d679867424bf2bcbdd553a2acf364525307e43dfb910fa4a2c6fd9f2" +checksum = "ad1673163c0cb14a6a19ddbf44dd4efe6f015ec1ebb8156710ac32501f19fba2" dependencies = [ "anyhow", "bitflags", @@ -435,9 +435,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.211.1" +version = "0.219.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cc90c50c7ec8a824b5d2cddddff13b2dc12b7a96bf8684d11474223c2ea22f" +checksum = "4a86f669283257e8e424b9a4fc3518e3ade0b95deb9fbc0f93a1876be3eda598" dependencies = [ "anyhow", "id-arena", diff --git a/services/user/CommonApi/common/packages/component-parser/Cargo.toml b/services/user/CommonApi/common/packages/component-parser/Cargo.toml index 897d4d6d5..9c759ca91 100644 --- a/services/user/CommonApi/common/packages/component-parser/Cargo.toml +++ b/services/user/CommonApi/common/packages/component-parser/Cargo.toml @@ -5,11 +5,11 @@ edition = "2021" [dependencies] anyhow = "1.0.79" -wasm-compose = "0.211.1" -wasmparser = "0.211.1" -wit-component = "0.211.1" -wit-bindgen-rt = { version = "0.26.0", features = ["bitflags"] } -wit-parser = "0.211.1" +wasm-compose = "0.219.1" +wasmparser = "0.219.1" +wit-component = "0.219.1" +wit-bindgen-rt = { version = "0.34.0", features = ["bitflags"] } +wit-parser = "0.219.1" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" indexmap = "2.2.5" diff --git a/services/user/CommonApi/common/packages/component-parser/src/lib.rs b/services/user/CommonApi/common/packages/component-parser/src/lib.rs index fc2a7473c..a971d3c77 100644 --- a/services/user/CommonApi/common/packages/component-parser/src/lib.rs +++ b/services/user/CommonApi/common/packages/component-parser/src/lib.rs @@ -1,12 +1,12 @@ #[allow(warnings)] mod bindings; -use bindings::psibase::component_parser::types::{ComponentExtraction, Functions, Intf}; +use bindings::psibase::component_parser::types::{ComponentExtraction, Function, Functions, Intf}; use bindings::Guest as Parser; use indexmap::IndexMap; use wasm_compose::graph::Component; use wit_component::{decode, WitPrinter}; -use wit_parser::{World, WorldItem, WorldKey}; +use wit_parser::{Type, TypeDefKind, TypeOwner, World, WorldItem, WorldKey}; pub struct ComponentParser; @@ -41,7 +41,7 @@ fn extract_wit(resolved_wit: &wit_parser::Resolve) -> Result { if i > 0 { wit.push_str("\n\n"); } - match printer.print(resolved_wit, &[id]) { + match printer.print(resolved_wit, id, &[]) { Ok(s) => wit.push_str(&s), Err(e) => { // If we can't print the document, just use the error text @@ -53,6 +53,68 @@ fn extract_wit(resolved_wit: &wit_parser::Resolve) -> Result { Ok(wit) } +// This imported function is dynamically linking to another plugin if: +// Its first parameter is a handle to an imported resource +// called "plugin-ref" that is owned by the "host:common" package +fn is_linking_dynamically(func: &wit_parser::Function, resolved_wit: &wit_parser::Resolve) -> bool { + if func.params.len() == 0 { + return false; + } + + let param_type = match func.params[0].1 { + Type::Id(id) => resolved_wit.types.get(id).expect("Get param type failed"), + _ => return false, + }; + + let handle = match param_type.kind { + TypeDefKind::Handle(handle) => handle, + _ => return false, + }; + + let guest_type = match handle { + wit_parser::Handle::Own(id) => resolved_wit.types.get(id).expect("Get guest type failed"), + _ => return false, + }; + + let guest_type_kind = match guest_type.kind { + TypeDefKind::Type(ty) => ty, + _ => return false, + }; + + let host_type = match guest_type_kind { + Type::Id(id) => resolved_wit.types.get(id).expect("Get host type failed"), + _ => return false, + }; + + let is_plugin_ref = + host_type.name.as_ref().is_some() && host_type.name.as_ref().unwrap() == "plugin-ref"; + let is_resource = host_type.kind == TypeDefKind::Resource; + + let owner_interface = match host_type.owner { + TypeOwner::Interface(id) => resolved_wit + .interfaces + .get(id) + .expect("Get host type owner interface failed"), + _ => return false, + }; + + if owner_interface.package.is_none() { + return false; + } + + let owner_package_id = owner_interface.package.unwrap(); + + let owner_package = resolved_wit + .packages + .get(owner_package_id) + .expect("Get host type owner package failed"); + + let is_owner_host = + owner_package.name.namespace == "host" && owner_package.name.name == "common"; + + is_plugin_ref && is_resource && is_owner_host +} + fn extract_functions(resolved_wit: &wit_parser::Resolve, get_world_item: F) -> Functions where F: Fn(&World) -> &IndexMap, @@ -73,19 +135,34 @@ where // (https://github.com/WebAssembly/component-model/pull/332) let intf = resolved_wit.interfaces.get(*id).unwrap(); let pkg = resolved_wit.packages.get(intf.package.unwrap()).unwrap(); + let intf_name = intf.name.as_ref().unwrap(); let mut new_intf = Intf { namespace: pkg.name.namespace.to_owned(), package: pkg.name.name.to_owned(), - name: intf.name.to_owned().unwrap(), + name: kebab_to_camel(&intf_name), funcs: vec![], }; - for (func_name, _) in &intf.functions { - new_intf.funcs.push(kebab_to_camel(&func_name.to_owned())); + for (func_name, func) in &intf.functions { + //if _func.kind == FunctionKind::Method || _func.kind == FunctionKind::Constructor { + // Then this is a method defined within a resource + // But we can also extract this information while generating js bindings + // from the function name + //} + + let dynamic_link = is_linking_dynamically(func, resolved_wit); + new_intf.funcs.push(Function { + name: kebab_to_camel(&func_name.to_owned()), + dynamic_link, + }); } funcs.interfaces.push(new_intf); } WorldItem::Function(func) => { - funcs.funcs.push(kebab_to_camel(&func.name.to_owned())); + let dynamic_link = is_linking_dynamically(func, resolved_wit); + funcs.funcs.push(Function { + name: kebab_to_camel(&func.name.to_owned()), + dynamic_link, + }); } WorldItem::Type(_) => {} }; diff --git a/services/user/CommonApi/common/packages/component-parser/wit/world.wit b/services/user/CommonApi/common/packages/component-parser/wit/world.wit index ab9ce9391..1ba35f0a7 100644 --- a/services/user/CommonApi/common/packages/component-parser/wit/world.wit +++ b/services/user/CommonApi/common/packages/component-parser/wit/world.wit @@ -2,18 +2,23 @@ package psibase:component-parser; interface types { + record function { + name: string, + dynamic-link: bool, + } + record intf { namespace: string, %package: string, name: string, - funcs: list + funcs: list } record functions { namespace: string, %package: string, interfaces: list, - funcs: list + funcs: list } record component-extraction { diff --git a/services/user/CommonApi/common/packages/wit/host-common.wit b/services/user/CommonApi/common/packages/wit/host-common.wit index a427cc3d4..19bb372a6 100644 --- a/services/user/CommonApi/common/packages/wit/host-common.wit +++ b/services/user/CommonApi/common/packages/wit/host-common.wit @@ -3,6 +3,11 @@ package host:common; /// Common types used by plugins interface types { + /// A reference to a plugin. Used for dynamically linking to a plugin. + resource plugin-ref { + constructor(name: string); + } + /// A standard error type plugins can use to propagate errors /// back up the callchain. /// @@ -84,19 +89,12 @@ interface server { interface client { use types.{origination-data, error}; - /// Temporarily facilitates login until we have popup windows to let the user decide - login-temp: func(app-origin: string, user: string) -> result<_, error>; - /// Gets details about the app responsible for the call get-sender-app: func() -> origination-data; /// Gets the account name of the user logged into the current top-level application get-logged-in-user: func() -> result, error>; - /// Returns `true` if any user is currently logged into the current top-level - /// application. Returns `false` otherwise. - is-logged-in: func() -> bool; - /// Gets the account name of the service from which the currently running /// plugin was fetched. my-service-account: func() -> string; diff --git a/services/user/CommonApi/common/packages/wit/host-privileged.wit b/services/user/CommonApi/common/packages/wit/host-privileged.wit new file mode 100644 index 000000000..d749a2358 --- /dev/null +++ b/services/user/CommonApi/common/packages/wit/host-privileged.wit @@ -0,0 +1,15 @@ +/// This is a special host interface with functionality +/// that is restricted to specific plugins. +package host:privileged; + +/// Host interface for privileged functionality +interface intf { + use host:common/types.{origination-data, error}; + + /// Get active app domain + get-active-app-domain: func() -> string; +} + +world imports { + import intf; +} diff --git a/services/user/Invite/plugin/Cargo.toml b/services/user/Invite/plugin/Cargo.toml index 80937c9a2..e5144462d 100644 --- a/services/user/Invite/plugin/Cargo.toml +++ b/services/user/Invite/plugin/Cargo.toml @@ -38,3 +38,4 @@ world = "impl" "auth-sig:plugin" = { path = "../../../system/AuthSig/plugin/wit/world.wit" } "accounts:plugin" = { path = "../../../system/Accounts/plugin/wit/world.wit" } "transact:plugin" = { path = "../../../system/Transact/plugin/wit/world.wit" } +"auth-invite:plugin" = { path = "../plugin_AuthInvite/wit/world.wit" } diff --git a/services/user/Invite/plugin/src/errors.rs b/services/user/Invite/plugin/src/errors.rs index e8d987bc3..67e0c4b7b 100644 --- a/services/user/Invite/plugin/src/errors.rs +++ b/services/user/Invite/plugin/src/errors.rs @@ -2,7 +2,6 @@ use crate::bindings::host::common::types::{Error, PluginId}; #[derive(PartialEq, Eq, Hash)] pub enum ErrorType { - NotYetImplemented, DecodeInviteError, QueryError, DatetimeError, @@ -21,11 +20,6 @@ fn my_plugin_id() -> PluginId { impl ErrorType { pub fn err(self, msg: &str) -> Error { match self { - ErrorType::NotYetImplemented => Error { - code: self as u32, - producer: my_plugin_id(), - message: format!("Not yet implemented: {}", msg), - }, ErrorType::DecodeInviteError => Error { code: self as u32, producer: my_plugin_id(), @@ -55,7 +49,7 @@ impl ErrorType { code: self as u32, producer: my_plugin_id(), message: format!("[{}] Error: Account already exists", msg), - } + }, } } } diff --git a/services/user/Invite/plugin/src/lib.rs b/services/user/Invite/plugin/src/lib.rs index c78a6d293..34750943e 100644 --- a/services/user/Invite/plugin/src/lib.rs +++ b/services/user/Invite/plugin/src/lib.rs @@ -1,23 +1,25 @@ #[allow(warnings)] mod bindings; -use base64::{engine::general_purpose::URL_SAFE, Engine}; +mod errors; +mod types; + use bindings::accounts::plugin as Accounts; +use bindings::auth_invite::plugin::intf as AuthInvite; use bindings::auth_sig::plugin::{keyvault, types::Pem}; use bindings::exports::invite; +use bindings::exports::invite::plugin::advanced::Guest as Advanced; +use bindings::exports::invite::plugin::advanced::InvKeys as InviteKeys; use bindings::host::common::{client as Client, server as Server, types as CommonTypes}; -use bindings::invite::plugin::types::{Invite, InviteId, InviteState, Url}; +use bindings::invite::plugin::types::{Invite, InviteState, InviteToken}; use bindings::transact::plugin::intf as Transact; use chrono::DateTime; +use errors::ErrorType::*; use fracpack::Pack; use invite::plugin::{invitee::Guest as Invitee, inviter::Guest as Inviter}; -use psibase::services::invite as InviteService; +use psibase::services::invite::{self as InviteService, action_structs::*}; +use types::*; use CommonTypes::OriginationData; -use serde::{Deserialize, Serialize}; - -mod errors; -use errors::ErrorType::*; - /* TODO: /// This doesn't need to be exposed, it can just be jammed into various plugin functions /// when the user is submitting another action anyways. @@ -26,79 +28,13 @@ use errors::ErrorType::*; void delExpired(uint32_t maxDeleted); */ -#[derive(Serialize, Deserialize)] -struct InviteParams { - app: String, - pk: String, - cb: String, -} - -#[derive(Deserialize)] -struct ResponseRoot { - data: T, -} -trait TryParseGqlResponse: Sized { - fn from_gql(s: String) -> Result; -} - -//#[qgl_query(name="getInvite")] // <-- Todo: Create this procedural macro ... -#[allow(non_snake_case)] -#[derive(Deserialize)] -struct InviteRecordSubset { - inviter: psibase::AccountNumber, - actor: psibase::AccountNumber, - expiry: u32, - state: u8, -} - -// ...Such that something like the below code is generated: -#[allow(non_snake_case)] -#[derive(Deserialize)] -struct GetInviteResponse { - getInvite: Option, -} -impl TryParseGqlResponse for InviteRecordSubset { - fn from_gql(response: String) -> Result { - let response_root: ResponseRoot = - serde_json::from_str(&response).map_err(|e| QueryError.err(&e.to_string()))?; - Ok(response_root.data.getInvite.ok_or_else(|| { - QueryError.err("Unable to extract InviteRecordSubset from query response") - })?) - } -} -// /> - -struct Component; - -impl From for InviteId { - fn from(params: InviteParams) -> Self { - let params_str = serde_json::to_string(¶ms).unwrap(); - URL_SAFE.encode(params_str) - } -} - -trait TryFromInviteId: Sized { - fn try_from_invite_id(id: InviteId) -> Result; -} +struct InvitePlugin; -impl TryFromInviteId for InviteParams { - fn try_from_invite_id(id: InviteId) -> Result { - let bytes = URL_SAFE - .decode(id.to_owned()) - .map_err(|_| DecodeInviteError.err("Error decoding base64"))?; - - let str = String::from_utf8(bytes) - .map_err(|_| DecodeInviteError.err("Error converting from UTF8"))?; - - let result: InviteParams = serde_json::from_str(&str) - .map_err(|_| DecodeInviteError.err("Error deserializing JSON string into object"))?; - - Ok(result) - } -} - -impl Invitee for Component { - fn accept_with_new_account(account: String, id: InviteId) -> Result<(), CommonTypes::Error> { +impl Invitee for InvitePlugin { + fn accept_with_new_account( + account: String, + token: InviteToken, + ) -> Result<(), CommonTypes::Error> { let accepted_by = psibase::AccountNumber::from_exact(&account).or_else(|_| { return Err(InvalidAccount.err(&account)); })?; @@ -107,12 +43,14 @@ impl Invitee for Component { return Err(AccountExists.err("accept_with_new_account")); } - let invite_params = InviteParams::try_from_invite_id(id)?; + AuthInvite::notify(&token)?; + + let invite_params = InviteParams::try_from_invite_id(&token)?; let invite_pubkey: Pem = keyvault::pub_from_priv(&invite_params.pk)?; Transact::add_action_to_transaction( - "acceptCreate", - &InviteService::action_structs::acceptCreate { + acceptCreate::ACTION_NAME, + &acceptCreate { inviteKey: keyvault::to_der(&invite_pubkey)?.into(), acceptedBy: accepted_by, newAccountKey: keyvault::to_der(&keyvault::generate_keypair()?)?.into(), @@ -123,12 +61,42 @@ impl Invitee for Component { Ok(()) } - fn reject(_id: InviteId) -> Result<(), CommonTypes::Error> { - Err(NotYetImplemented.err("reject")) + fn accept(token: InviteToken) -> Result<(), CommonTypes::Error> { + let invite_params = InviteParams::try_from_invite_id(&token)?; + let invite_pubkey: Pem = keyvault::pub_from_priv(&invite_params.pk)?; + + AuthInvite::notify(&token)?; + + Transact::add_action_to_transaction( + accept::ACTION_NAME, + &accept { + inviteKey: keyvault::to_der(&invite_pubkey)?.into(), + } + .packed(), + )?; + + Ok(()) + } + + fn reject(token: InviteToken) -> Result<(), CommonTypes::Error> { + let invite_params = InviteParams::try_from_invite_id(&token)?; + let invite_pubkey: Pem = keyvault::pub_from_priv(&invite_params.pk)?; + + AuthInvite::notify(&token)?; + + Transact::add_action_to_transaction( + reject::ACTION_NAME, + &reject { + inviteKey: keyvault::to_der(&invite_pubkey)?.into(), + } + .packed(), + )?; + + Ok(()) } - fn decode_invite(id: InviteId) -> Result { - let invite_params = InviteParams::try_from_invite_id(id)?; + fn decode_invite(token: InviteToken) -> Result { + let invite_params = InviteParams::try_from_invite_id(&token)?; let query = format!( r#"query {{ @@ -161,43 +129,39 @@ impl Invitee for Component { state: state, actor: invite.actor.to_string(), expiry, - callback: invite_params.cb, }) } } -impl Inviter for Component { - fn generate_invite(callback_subpath: String) -> Result { +impl Inviter for InvitePlugin { + fn generate_invite() -> Result { let keypair = keyvault::generate_unmanaged_keypair()?; Transact::add_action_to_transaction( - "createInvite", - &InviteService::action_structs::createInvite { + createInvite::ACTION_NAME, + &createInvite { inviteKey: keyvault::to_der(&keypair.public_key)?.into(), } .packed(), )?; - let link_root = format!("{}{}", Client::my_service_origin(), "/invited"); - let OriginationData { origin, app } = Client::get_sender_app(); - let cb = format!("{}{}", origin, callback_subpath); let params = InviteParams { app: app.unwrap_or(origin.clone()), pk: keypair.private_key, - cb, }; - let query_string = format!("id={}", InviteId::from(params)); - Ok(format!("{}?{}", link_root, query_string)) + Ok(InviteToken::from(params)) } - fn delete_invite(invite_public_key: Vec) -> Result<(), CommonTypes::Error> { + fn delete_invite(token: InviteToken) -> Result<(), CommonTypes::Error> { + let invite_keys = Self::deserialize(token)?; + Transact::add_action_to_transaction( "delInvite", &InviteService::action_structs::delInvite { - inviteKey: invite_public_key.into(), + inviteKey: invite_keys.pub_key.into(), } .packed(), )?; @@ -206,4 +170,16 @@ impl Inviter for Component { } } -bindings::export!(Component with_types_in bindings); +impl Advanced for InvitePlugin { + fn deserialize(token: InviteToken) -> Result { + let invite_params = InviteParams::try_from_invite_id(&token)?; + let invite_pubkey: Pem = keyvault::pub_from_priv(&invite_params.pk)?; + + Ok(InviteKeys { + pub_key: keyvault::to_der(&invite_pubkey)?, + priv_key: keyvault::to_der(&invite_params.pk)?, + }) + } +} + +bindings::export!(InvitePlugin with_types_in bindings); diff --git a/services/user/Invite/plugin/src/types.rs b/services/user/Invite/plugin/src/types.rs new file mode 100644 index 000000000..84bf96230 --- /dev/null +++ b/services/user/Invite/plugin/src/types.rs @@ -0,0 +1,74 @@ +use crate::bindings; +use crate::errors::ErrorType::*; + +use base64::{engine::general_purpose::URL_SAFE, Engine}; +use bindings::host::common::types as CommonTypes; +use bindings::invite::plugin::types::InviteToken; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct InviteParams { + pub app: String, + pub pk: String, +} + +#[derive(Deserialize)] +pub struct ResponseRoot { + pub data: T, +} + +pub trait TryParseGqlResponse: Sized { + fn from_gql(s: String) -> Result; +} + +#[allow(non_snake_case)] +#[derive(Deserialize)] +pub struct InviteRecordSubset { + pub inviter: psibase::AccountNumber, + pub actor: psibase::AccountNumber, + pub expiry: u32, + pub state: u8, +} + +#[allow(non_snake_case)] +#[derive(Deserialize)] +pub struct GetInviteResponse { + pub getInvite: Option, +} + +impl TryParseGqlResponse for InviteRecordSubset { + fn from_gql(response: String) -> Result { + let response_root: ResponseRoot = + serde_json::from_str(&response).map_err(|e| QueryError.err(&e.to_string()))?; + Ok(response_root.data.getInvite.ok_or_else(|| { + QueryError.err("Unable to extract InviteRecordSubset from query response") + })?) + } +} + +impl From for InviteToken { + fn from(params: InviteParams) -> Self { + let params_str = serde_json::to_string(¶ms).unwrap(); + URL_SAFE.encode(params_str) + } +} + +pub trait TryFromInviteToken: Sized { + fn try_from_invite_id(id: &str) -> Result; +} + +impl TryFromInviteToken for InviteParams { + fn try_from_invite_id(id: &str) -> Result { + let bytes = URL_SAFE + .decode(id) + .map_err(|_| DecodeInviteError.err("Error decoding base64"))?; + + let str = String::from_utf8(bytes) + .map_err(|_| DecodeInviteError.err("Error converting from UTF8"))?; + + let result: InviteParams = serde_json::from_str(&str) + .map_err(|_| DecodeInviteError.err("Error deserializing JSON string into object"))?; + + Ok(result) + } +} diff --git a/services/user/Invite/plugin/wit/impl.wit b/services/user/Invite/plugin/wit/impl.wit index 9e3060bc8..25bcca667 100644 --- a/services/user/Invite/plugin/wit/impl.wit +++ b/services/user/Invite/plugin/wit/impl.wit @@ -5,7 +5,9 @@ world impl { include transact:plugin/imports; import auth-sig:plugin/keyvault; include accounts:plugin/imports; + include auth-invite:plugin/imports; export inviter; export invitee; + export advanced; } diff --git a/services/user/Invite/plugin/wit/world.wit b/services/user/Invite/plugin/wit/world.wit index 6caa60409..22f016ad2 100644 --- a/services/user/Invite/plugin/wit/world.wit +++ b/services/user/Invite/plugin/wit/world.wit @@ -8,7 +8,7 @@ interface types { rejected } - /// This is the information encoded into an invite URL. + /// This is the information tracked about an invite. /// /// Fields: /// * `inviter` - The account responsible for generating the invite @@ -16,7 +16,6 @@ interface types { /// * `state` - The state of the invite /// * `actor` - The name of the account to most recently accept or reject the invite /// * `expiry` - The UTC timestamp of the invite's expiration - /// * `callback` - The link to which the user should be returned after responding to the invite /// /// Other notes: /// `actor` is useful primarily for displaying information about an invite that has already been @@ -27,11 +26,10 @@ interface types { state: invite-state, actor: string, expiry: string, - callback: string, } type url = string; - type invite-id = string; + type invite-token = string; } // interface admin { @@ -54,30 +52,25 @@ interface types { /// Functionality exposed for the inviter interface inviter { use host:common/types.{error}; - use types.{url}; + use types.{invite-token}; - /// Allows a user to generate an invite link that can be used to - /// invite someone (with or without a preexisting account) to use - /// an app. - /// - /// Parameters - /// * `callback-subpath`: This the subpath of the app to which the invited user - /// is redirected if they accept the invite (e.g. "/welcome-page") + /// Allows a user to generate an invite token that can be used to + /// invite someone (with or without a preexisting account) to an + /// application. /// /// Successful return value: - /// * A URL containing the ID of the invite that was created. This URL - /// can be sent to a user to invite them to the app. - generate-invite: func(callback-subpath: string) -> result; + /// * The invite token that was created. This token can + /// be sent to a user to invite them to the app. + generate-invite: func() -> result; /// Used by the creator of an invite to delete it. Deleted invites are removed /// from the database. An invite can be deleted regardless of whether it has been /// accepted, rejected, or is still pending. /// /// Parameters - /// * `invite-public-key`: The key that is publicly associated with the private key - /// embedded in the invite link. This public key should also exist on chain in a table - /// in the invite service. - delete-invite: func(invite-public-key: list) -> result<_, error>; + /// * `token`: The token that was generated during the creation of the + /// invite. + delete-invite: func(token: invite-token) -> result<_, error>; } @@ -85,33 +78,55 @@ interface inviter { interface invitee { use host:common/types.{error}; - use types.{invite, invite-id}; + use types.{invite, invite-token}; - /// Retrieves the `types.invite` information encoded within the invite ID to - /// provide the invite interface. + /// Retrieves details about the invite from the invite token. /// /// Parameters: - /// * `id`: The value in the query parameter of the invite URL that contains the - /// invite ID. - decode-invite: func(id: invite-id) -> result; + /// * `token`: The token that was generated during the creation of the + /// invite. + decode-invite: func(token: invite-token) -> result; - /// Call to accept an invite with a new account. + /// Creates a new account and accepts the specified invite with it. /// /// Parameters /// * `account`: The name of the new account being created - /// * `id`: The id from the invite URL query parameter - accept-with-new-account: func(account: string, id: invite-id) -> result<_, error>; + /// * `invite-token`: The token of the invite being accepted + accept-with-new-account: func(account: string, token: invite-token) -> result<_, error>; + + /// Accepts an invite with an existing account (the currently logged-in account). + /// + /// Parameters + /// * `token`: The token of the invite being accepted + accept: func(token: invite-token) -> result<_, error>; - /// Called by existing accounts or the system account "invited-sys" to reject - /// an invite. Once an invite is rejected, it cannot be accepted or used to + /// Rejects an invite. Once an invite is rejected, it cannot be accepted or used to /// create a new account. /// /// Parameters - /// * `id`: The id from the invite URL query parameter. - reject: func(id: invite-id) -> result<_, error>; + /// * `invite-token`: The token of the invite being rejected + reject: func(token: invite-token) -> result<_, error>; +} + +interface adv-types { + // DER encoded + record inv-keys { + pub-key: list, + priv-key: list, + } +} + +interface advanced { + use types.{invite-token, invite}; + use host:common/types.{error}; + use adv-types.{inv-keys}; + + /// Deserializes an invite token into just the creator app + private key + deserialize: func(token: invite-token) -> result; } world imports { + import advanced; import inviter; import invitee; } diff --git a/services/user/Invite/plugin_AuthInvite/Cargo.lock b/services/user/Invite/plugin_AuthInvite/Cargo.lock new file mode 100644 index 000000000..d3f4015f6 --- /dev/null +++ b/services/user/Invite/plugin_AuthInvite/Cargo.lock @@ -0,0 +1,2726 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[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 = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + +[[package]] +name = "async-compression" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e614738943d3f68c628ae3dbce7c3daffb196665f82f8c8ea6b65de73c79429" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-graphql" +version = "7.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba6d24703c5adc5ba9116901b92ee4e4c0643c01a56c4fd303f3818638d7449" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-stream", + "async-trait", + "base64 0.22.1", + "bytes", + "fast_chemail", + "fnv", + "futures-timer", + "futures-util", + "handlebars", + "http 1.1.0", + "indexmap", + "mime", + "multer", + "num-traits", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "static_assertions_next", + "tempfile", + "thiserror", +] + +[[package]] +name = "async-graphql-derive" +version = "7.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94c2d176893486bd37cd1b6defadd999f7357bf5804e92f510c08bcf16c538f" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling 0.20.10", + "proc-macro-crate", + "proc-macro2", + "quote", + "strum", + "syn 2.0.79", + "thiserror", +] + +[[package]] +name = "async-graphql-parser" +version = "7.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79272bdbf26af97866e149f05b2b546edb5c00e51b5f916289931ed233e208ad" +dependencies = [ + "async-graphql-value", + "pest", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "7.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5ec94176a12a8cbe985cd73f2e54dc9c702c88c766bdef12f1f3a67cedbee1" +dependencies = [ + "bytes", + "indexmap", + "serde", + "serde_json", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "auth-invite" +version = "0.12.0" +dependencies = [ + "psibase", + "wit-bindgen-rt", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[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.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "clap" +version = "4.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[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-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cryptoki" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9123ecc6a29329cd3f852e6e6814f302ed777820e1eb60b098b89aee0eb91b" +dependencies = [ + "bitflags 1.3.2", + "cryptoki-sys", + "libloading", + "log", + "paste", + "secrecy", +] + +[[package]] +name = "cryptoki-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "750380200f47d4ff677be725b6e0d78b590e1d0343573dcd4b62147f25dc6efa" +dependencies = [ + "libloading", +] + +[[package]] +name = "custom_error" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8a51dd197fa6ba5b4dc98a990a43cc13693c23eb0089ebb0fcc1f04152bca6" + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core 0.20.10", + "darling_macro 0.20.10", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.79", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core 0.20.10", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[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 = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +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 = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fracpack" +version = "0.13.0" +dependencies = [ + "custom_error", + "indexmap", + "psibase_macros", + "serde", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "handlebars" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.11", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.11", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.11", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jwt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +dependencies = [ + "base64 0.13.1", + "crypto-common", + "digest", + "hmac", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[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 = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[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 = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.1.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "object" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "partial_ref" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f728bc9b1479656e40cba507034904a8c44027c0efdbbaf6a4bdc5f2d3a910c" +dependencies = [ + "partial_ref_derive", +] + +[[package]] +name = "partial_ref_derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e1d2cb5b898b5a5342e994e0d0c367dbfe69cbf717cd307045ec9fb057581" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "pest_meta" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[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-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psibase" +version = "0.13.0" +dependencies = [ + "anyhow", + "async-graphql", + "async-trait", + "chrono", + "clap", + "const_format", + "cryptoki", + "cryptoki-sys", + "custom_error", + "der", + "flate2", + "fracpack", + "futures", + "getrandom", + "hmac", + "include_dir", + "indexmap", + "indicatif", + "jwt", + "mime_guess", + "pem", + "percent-encoding", + "pkcs8", + "psibase_macros", + "psibase_names", + "regex", + "reqwest", + "ripemd", + "rpassword", + "scopeguard", + "sec1", + "secp256k1", + "serde", + "serde-aux", + "serde-wasm-bindgen", + "serde_bytes", + "serde_json", + "sha2", + "spki", + "subprocess", + "tempfile", + "tokio", + "toml", + "url", + "varisat", + "wasm-bindgen", + "zip", +] + +[[package]] +name = "psibase_macros" +version = "0.13.0" +dependencies = [ + "darling 0.14.4", + "proc-macro-error", + "proc-macro2", + "psibase_names", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "psibase_names" +version = "0.13.0" +dependencies = [ + "seahash", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "async-compression", + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-aux" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95" +dependencies = [ + "chrono", + "serde", + "serde_json", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa 1.0.11", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.11", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions_next" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.79", +] + +[[package]] +name = "subprocess" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[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.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +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.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "varisat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe609851d1e9196674ac295f656bd8601200a1077343d22b345013497807caf" +dependencies = [ + "anyhow", + "itoa 0.4.8", + "leb128", + "log", + "ordered-float", + "partial_ref", + "rustc-hash", + "serde", + "thiserror", + "varisat-checker", + "varisat-dimacs", + "varisat-formula", + "varisat-internal-macros", + "varisat-internal-proof", + "vec_mut_scan", +] + +[[package]] +name = "varisat-checker" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135c977c5913ed6e98f6b81b8e4d322211303b7d40dae773caef7ad1de6c763b" +dependencies = [ + "anyhow", + "log", + "partial_ref", + "rustc-hash", + "smallvec", + "thiserror", + "varisat-dimacs", + "varisat-formula", + "varisat-internal-proof", +] + +[[package]] +name = "varisat-dimacs" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1dee4e21be1f04c0a939f7ae710cced47233a578de08a1b3c7d50848402636" +dependencies = [ + "anyhow", + "itoa 0.4.8", + "thiserror", + "varisat-formula", +] + +[[package]] +name = "varisat-formula" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395c5543b9bfd9076d6d3af49d6c34a4b91b0b355998c0a5ec6ed7265d364520" + +[[package]] +name = "varisat-internal-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602ece773543d066aa7848455486c6c0422a3f214da7a2b899100f3c4f12408d" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "varisat-internal-proof" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6163bb7bc9018af077b76d64f976803d141c36a27d640f1437dddc4fd527d207" +dependencies = [ + "anyhow", + "varisat-formula", +] + +[[package]] +name = "vec_mut_scan" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ed610a8d5e63d9c0e31300e8fdb55104c5f21e422743a9dc74848fa8317fd2" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[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-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[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", + "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_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] diff --git a/services/user/Invite/plugin_AuthInvite/Cargo.toml b/services/user/Invite/plugin_AuthInvite/Cargo.toml new file mode 100644 index 000000000..40344e292 --- /dev/null +++ b/services/user/Invite/plugin_AuthInvite/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "auth-invite" +version = "0.12.0" +edition = "2021" + +[dependencies] +wit-bindgen-rt = { version = "0.33.0", features = ["bitflags"] } +psibase = { path = "../../../../rust/psibase/" } + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "s" +debug = false +strip = true +lto = true + +[package.metadata.component.target] +world = "impl" + +[package.metadata.component] +package = "auth-invite:plugin" + +[package.metadata.component.target.dependencies] +"host:common" = { path = "../../CommonApi/common/packages/wit/host-common.wit" } +"invite:plugin" = { path = "../../Invite/plugin/wit/world.wit" } +"clientdata:plugin" = { path = "../../ClientData/plugin/wit/world.wit" } +"accounts:smart-auth" = { path = "../../../system/Accounts/plugin/smartAuth/export.wit" } +"auth-sig:plugin" = { path = "../../../system/AuthSig/plugin/wit/world.wit" } +"transact:plugin" = { path = "../../../system/Transact/plugin/wit/world.wit" } diff --git a/services/user/Invite/plugin_AuthInvite/src/db.rs b/services/user/Invite/plugin_AuthInvite/src/db.rs new file mode 100644 index 000000000..ae6057242 --- /dev/null +++ b/services/user/Invite/plugin_AuthInvite/src/db.rs @@ -0,0 +1,27 @@ +use crate::bindings::clientdata::plugin::keyvalue as Keyvalue; +use crate::bindings::invite::plugin::advanced::InvKeys; + +// Database operation wrapper +pub struct InviteKeys {} +impl InviteKeys { + const KEY_PUB: &'static str = "pub_key"; + const KEY_PRV: &'static str = "prv_key"; + + pub fn add(keys: &InvKeys) { + Keyvalue::set(Self::KEY_PUB, &keys.pub_key).expect("Failed to set pub key"); + Keyvalue::set(Self::KEY_PRV, &keys.priv_key).expect("Failed to set priv key"); + } + + pub fn delete() { + Keyvalue::delete(Self::KEY_PUB); + Keyvalue::delete(Self::KEY_PRV); + } + + pub fn get_public_key() -> Vec { + Keyvalue::get(Self::KEY_PUB).expect("Failed to get pub key") + } + + pub fn get_private_key() -> Vec { + Keyvalue::get(Self::KEY_PRV).expect("Failed to get priv key") + } +} diff --git a/services/user/Invite/plugin_AuthInvite/src/errors.rs b/services/user/Invite/plugin_AuthInvite/src/errors.rs new file mode 100644 index 000000000..02b091bc2 --- /dev/null +++ b/services/user/Invite/plugin_AuthInvite/src/errors.rs @@ -0,0 +1,31 @@ +use crate::bindings::host::common::types::{Error, PluginId}; + +#[derive(PartialEq, Eq, Hash)] +pub enum ErrorType { + Unauthorized, + DecodeInviteError, +} + +fn my_plugin_id() -> PluginId { + return PluginId { + service: "auth-invite".to_string(), + plugin: "plugin".to_string(), + }; +} + +impl ErrorType { + pub fn err(self, msg: &str) -> Error { + match self { + ErrorType::Unauthorized => Error { + code: self as u32, + producer: my_plugin_id(), + message: format!("Unauthorized: {}", msg), + }, + ErrorType::DecodeInviteError => Error { + code: self as u32, + producer: my_plugin_id(), + message: format!("Decode invite error: {}", msg), + }, + } + } +} diff --git a/services/user/Invite/plugin_AuthInvite/src/lib.rs b/services/user/Invite/plugin_AuthInvite/src/lib.rs new file mode 100644 index 000000000..e1eccb9ad --- /dev/null +++ b/services/user/Invite/plugin_AuthInvite/src/lib.rs @@ -0,0 +1,69 @@ +#[allow(warnings)] +mod bindings; +mod errors; +use errors::ErrorType::*; +mod db; +use db::*; + +// Other plugins +use bindings::auth_invite::plugin::types::InviteToken; +use bindings::auth_sig::plugin::keyvault as KeyVault; +use bindings::host::common::{client as Client, types::Error}; +use bindings::invite::plugin::advanced::deserialize; +use bindings::transact::plugin::auth_notifier as Transact; + +// Exported interfaces/types +use bindings::accounts::smart_auth::types::{Action, Claim, Proof}; +use bindings::exports::accounts::smart_auth::smart_auth::Guest as SmartAuth; + +use bindings::exports::auth_invite::plugin::intf::Guest as Intf; + +fn from_transact() -> bool { + Client::get_sender_app().app.map_or(false, |app| { + app == psibase::services::transact::SERVICE.to_string() + }) +} + +struct AuthInvite; + +impl Intf for AuthInvite { + fn notify(token: InviteToken) -> Result<(), Error> { + if let Some(app) = Client::get_sender_app().app { + if app != psibase::services::invite::SERVICE.to_string() { + return Err(Unauthorized.err("notify")); + } + } + + let inv_keys = deserialize(&token).map_err(|_| DecodeInviteError.err("notify"))?; + + InviteKeys::add(&inv_keys); + + Transact::notify(); + + Ok(()) + } +} + +impl SmartAuth for AuthInvite { + fn get_claims(_: String, _: Vec) -> Result, Error> { + Ok(vec![Claim { + verify_service: psibase::services::auth_invite::VERIFY_SERVICE.to_string(), + raw_data: InviteKeys::get_public_key(), + }]) + } + + fn get_proofs(_account_name: String, transaction_hash: Vec) -> Result, Error> { + if !from_transact() { + return Err(Unauthorized.err("get_proofs")); + } + + let signature = KeyVault::sign(&transaction_hash, &InviteKeys::get_private_key())?; + + // Free the local storage space + InviteKeys::delete(); + + Ok(vec![Proof { signature }]) + } +} + +bindings::export!(AuthInvite with_types_in bindings); diff --git a/services/user/Invite/plugin_AuthInvite/wit/impl.wit b/services/user/Invite/plugin_AuthInvite/wit/impl.wit new file mode 100644 index 000000000..0933aa89b --- /dev/null +++ b/services/user/Invite/plugin_AuthInvite/wit/impl.wit @@ -0,0 +1,13 @@ +package auth-invite:plugin; + +world impl { + include host:common/imports; + include clientdata:plugin/imports; + include invite:plugin/imports; + include auth-sig:plugin/imports; + import accounts:smart-auth/types; + import transact:plugin/auth-notifier; + + export accounts:smart-auth/smart-auth; + export intf; +} \ No newline at end of file diff --git a/services/user/Invite/plugin_AuthInvite/wit/world.wit b/services/user/Invite/plugin_AuthInvite/wit/world.wit new file mode 100644 index 000000000..a0c58b09f --- /dev/null +++ b/services/user/Invite/plugin_AuthInvite/wit/world.wit @@ -0,0 +1,35 @@ +/// This plugin facilitates the creation of the transaction that can be used to +/// submit an action to accept or reject an invite when the user has no pre-existing account. +/// +/// The way it works is that when a third-party app interacts with the invite plugin to accept or +/// reject an invite, it also notifies this auth service of the invite token that is being consumed +/// by the interaction. This auth plugin in turn notifies the transact plugin that it has a claim/proof +/// to attach to the transaction currently being constructed. +/// +/// At the time of transaction construction/submission, the transact plugin will call +/// into this auth service plugin for the claims/proofs (just like any other auth service). +/// +/// This auth service specifies the claim as the public key corresponding to the private key +/// in the invite, and the proof is the signature on the transaction using that same private key. +/// +/// The corresponding auth service to this plugin verifies that the transaction claims to be +/// signed by a key that corresponds to a known and unused invite public key. And the verify service +/// is the same verify service used by typical p256 key verification, and it checks that the signature +/// provided was generated with the private key corresponding to the public key specified in the claim. +package auth-invite:plugin; + +interface types { + type invite-token = string; +} + +interface intf { + use host:common/types.{error}; + use types.{invite-token}; + + // Notifies this plugin that it should use the given invite token on this transaction. + notify: func(token: invite-token) -> result<_, error>; +} + +world imports { + import intf; +} diff --git a/services/user/Supervisor/ui/src/component-loading/host-api.js b/services/user/Supervisor/ui/src/component-loading/host-api.js index 04f560471..5bc947eae 100644 --- a/services/user/Supervisor/ui/src/component-loading/host-api.js +++ b/services/user/Supervisor/ui/src/component-loading/host-api.js @@ -1,3 +1,11 @@ +class PluginRef { + constructor(name) { + this.name = name; + } +} + +export const types = { PluginRef }; + export const server = { postGraphqlGetJson(graphQL) { return host.postGraphqlGetJson(graphQL); @@ -13,22 +21,10 @@ export const server = { }; export const client = { - loginTemp(appOrigin, user) { - return host.loginTemp(appOrigin, user); - }, - getSenderApp() { return host.getSenderApp(); }, - getLoggedInUser() { - return host.getLoggedInUser(); - }, - - isLoggedIn() { - return host.isLoggedIn(); - }, - myServiceAccount() { return host.myServiceAccount(); }, diff --git a/services/user/Supervisor/ui/src/component-loading/loader.ts b/services/user/Supervisor/ui/src/component-loading/loader.ts index 26eb60bad..1bbe825fe 100644 --- a/services/user/Supervisor/ui/src/component-loading/loader.ts +++ b/services/user/Supervisor/ui/src/component-loading/loader.ts @@ -4,6 +4,7 @@ import { plugin } from "./index.js"; const wasiShimURL = new URL("./shims/wasip2-shim.js", import.meta.url); import hostShimCode from "./host-api.js?raw"; +import privilegedShimCode from "./privileged-api.js?raw"; import { HostInterface } from "../hostInterface.js"; import { ComponentAPI, Functions } from "../witExtraction.js"; import { assert } from "../utils.js"; @@ -138,6 +139,21 @@ async function getHostImports(): Promise { }; } +async function getPrivilegedImports(): Promise { + const privilegedShimName = "./privileged-api.js"; // internal name used by bundler + const privileged_importMap: Array<[PkgId, FilePath]> = [ + [`host:privileged/*`, `${privilegedShimName}#*`], + ]; + const privileged_ShimFile: [FilePath, Code] = [ + privilegedShimName, + privilegedShimCode, + ]; + return { + importMap: privileged_importMap, + files: [privileged_ShimFile], + }; +} + function mergeImports(importDetails: ImportDetails[]): ImportDetails { const importMap: Array<[PkgId, FilePath]> = importDetails.flatMap( (detail) => detail.importMap, @@ -157,6 +173,7 @@ export async function loadPlugin( await Promise.all([ getWasiImports(), getHostImports(), + getPrivilegedImports(), getProxiedImports(api.importedFuncs), getNonstandardWasiImports(), ]), @@ -164,7 +181,7 @@ export async function loadPlugin( const pluginModule = await load( wasmBytes, imports, - `${pluginHost.myServiceAccount()} plugin`, + `${pluginHost.myServiceAccount()}.plugin.js`, ); pluginModule.__setHost(pluginHost); @@ -174,7 +191,7 @@ export async function loadPlugin( async function load( wasmBytes: Uint8Array, imports: ImportDetails, - debugName: string, + debugFileName: string, ) { const name = "component"; const opts = { @@ -198,12 +215,14 @@ async function load( const bundleCode: string = await rollup({ input: name + ".js", - plugins: [plugin([...files_2, ...imports.files], true, debugName)], + plugins: [plugin([...files_2, ...imports.files], true, debugFileName)], }) .then((bundle) => bundle.generate({ format: "es" })) .then(({ output }) => output[0].code); - const blob = new Blob([bundleCode], { type: "text/javascript" }); + const namedBundleCode = `${bundleCode}\n//# sourceURL=${debugFileName}`; + + const blob = new Blob([namedBundleCode], { type: "text/javascript" }); const url = URL.createObjectURL(blob); const mod = await import(/* @vite-ignore */ url); @@ -215,7 +234,7 @@ async function load( // Not sufficient for plugins, which require other direct host exports, but can be used // to run various other utilities in the browser that have been compiled into wasm // components. -export async function loadBasic(wasmBytes: Uint8Array, debugName: string) { +export async function loadBasic(wasmBytes: Uint8Array, debugFileName: string) { const wasiImports = await getWasiImports(); const name = "component"; const opts = { @@ -232,12 +251,13 @@ export async function loadBasic(wasmBytes: Uint8Array, debugName: string) { const bundleCode: string = await rollup({ input: name + ".js", - plugins: [plugin(files_2, false, debugName)], + plugins: [plugin(files_2, false, debugFileName)], }) .then((bundle) => bundle.generate({ format: "es" })) .then(({ output }) => output[0].code); - const blob = new Blob([bundleCode], { type: "text/javascript" }); + const namedBundleCode = `${bundleCode}\n//# sourceURL=${debugFileName}`; + const blob = new Blob([namedBundleCode], { type: "text/javascript" }); const url = URL.createObjectURL(blob); const mod = await import(/* @vite-ignore */ url); diff --git a/services/user/Supervisor/ui/src/component-loading/privileged-api.js b/services/user/Supervisor/ui/src/component-loading/privileged-api.js new file mode 100644 index 000000000..356a068a1 --- /dev/null +++ b/services/user/Supervisor/ui/src/component-loading/privileged-api.js @@ -0,0 +1,5 @@ +export const intf = { + getActiveAppDomain() { + return host.getActiveAppDomain(); + }, +}; diff --git a/services/user/Supervisor/ui/src/component-loading/proxy/proxyPackage.ts b/services/user/Supervisor/ui/src/component-loading/proxy/proxyPackage.ts index df37f8ea6..3a232ef79 100644 --- a/services/user/Supervisor/ui/src/component-loading/proxy/proxyPackage.ts +++ b/services/user/Supervisor/ui/src/component-loading/proxy/proxyPackage.ts @@ -1,22 +1,72 @@ import { Code, FilePath, ImportDetails, PkgId } from "../importDetails"; +import { FuncShape } from "../../witExtraction"; + +const col = (col: number): string => { + return " ".repeat(col * 4); +}; class Func { name: string; - - constructor(name: string) { - this.name = name; + service: string; + plugin: string; + intfName: string; + isDynamic: boolean; + + constructor( + service: string, + plugin: string, + intfName: string, + name: string, + isDynamic: boolean, + ) { + // Name might look like `[constructor]classname` or `[method]classname.func-name` + if (name.includes("[method]")) { + this.name = name.split("]")[1].split(".")[1]; + } else if (name.includes("[constructor]")) { + this.name = "constructor"; + } else { + this.name = name; + } + + this.service = service; + this.plugin = plugin; + this.intfName = intfName; + this.isDynamic = isDynamic; } + private proxy = (): string => { + let service = `"${this.service}"`; + let plugin = `"${this.plugin}"`; + + if (this.isDynamic) { + service = `plugin_ref.name`; + plugin = `"plugin"`; + } + return [ + `${col(2)}return host.syncCall({`, + `${col(3)}service: ${service},`, + `${col(3)}plugin: ${plugin},`, + `${col(3)}intf: "${this.intfName}",`, + `${col(3)}method: "${this.name}",`, + `${col(3)}params: args`, + `${col(2)}});`, + ``, + ].join("\n"); + }; + private pre = (): string => { - return ` ${this.name}(...args) {\n`; + if (!this.isDynamic) { + return `${col(1)}${this.name}(...args) {\n`; + } + return `${col(1)}${this.name}(plugin_ref, ...args) {\n`; }; private post = (): string => { return ` },\n`; }; - code = (proxy: string): string => { - return this.pre() + proxy + this.post(); + code = (): string => { + return this.pre() + this.proxy() + this.post(); }; } @@ -24,9 +74,20 @@ class Intf { name: string; funcs: Func[]; - constructor(name: string, funcs: string[]) { - this.name = name; - this.funcs = funcs.map((f) => new Func(f)); + constructor( + service: string, + plugin: string, + intfName: string, + funcs: FuncShape[], + ) { + // Todo (Resource support): Split funcs array into subsets if there are resources + // with grouped methods. + // Functions that are directly part of an interface can be added as normal, + // but resource methods should be added in their own resource object. + this.name = intfName; + this.funcs = funcs.map( + (f) => new Func(service, plugin, intfName, f.name, f.dynamicLink), + ); } private pre = (): string => { @@ -37,10 +98,8 @@ class Intf { return `};\n`; }; - code = (proxy: (intfName: string, funcName: string) => string): string => { - const functions: string[] = this.funcs.map((f) => - f.code(proxy(this.name, f.name)), - ); + code = (): string => { + const functions: string[] = this.funcs.map((f) => f.code()); return this.pre() + functions.join("\n") + this.post(); }; } @@ -69,24 +128,6 @@ export class ProxyPkg { return `./proxy-shim_${this.namespace}_${this.id}.js`; }; - private proxy = (): ((intfName: string, funcName: string) => string) => { - const col = (col: number): string => { - return " ".repeat(col * 4); - }; - return (intfName: string, funcName: string): string => { - return [ - `${col(2)}return host.syncCall({`, - `${col(3)}service: "${this.namespace}",`, - `${col(3)}plugin: "${this.id}",`, - `${col(3)}intf: "${intfName}",`, - `${col(3)}method: "${funcName}",`, - `${col(3)}params: args`, - `${col(2)}});`, - ``, - ].join("\n"); - }; - }; - private pre = (): string => { return `\n/////// SHIM FILE FOR ${this.namespace}:${this.id} ///////////\n\n`; }; @@ -95,8 +136,8 @@ export class ProxyPkg { return `\n\n`; }; - add = (intf: string, funcs: string[]) => { - this.interfaces.push(new Intf(intf, funcs)); + add = (intf: string, funcs: FuncShape[]) => { + this.interfaces.push(new Intf(this.namespace, this.id, intf, funcs)); }; getImportDetails = (): ImportDetails => { @@ -106,9 +147,7 @@ export class ProxyPkg { `${this.getShimName()}#*`, ]); - const interfaces: string[] = this.interfaces.map((i) => - i.code(this.proxy()), - ); + const interfaces: string[] = this.interfaces.map((i) => i.code()); const code = this.pre() + interfaces.join("\n") + this.post(); const shimFile: [FilePath, Code] = [this.getShimName(), code]; diff --git a/services/user/Supervisor/ui/src/hostInterface.ts b/services/user/Supervisor/ui/src/hostInterface.ts index 071bd5969..4eebb5e86 100644 --- a/services/user/Supervisor/ui/src/hostInterface.ts +++ b/services/user/Supervisor/ui/src/hostInterface.ts @@ -31,17 +31,8 @@ export interface HostInterface { ) => Result; // Client interface - loginTemp: ( - appOrigin: string, - user: string, - ) => Result; - getSenderApp: () => OriginationData; - getLoggedInUser: () => Result; - - isLoggedIn: () => boolean; - myServiceAccount: () => string; myServiceOrigin: () => string; diff --git a/services/user/Supervisor/ui/src/plugin/plugin.ts b/services/user/Supervisor/ui/src/plugin/plugin.ts index 71aa5325d..a0af8199d 100644 --- a/services/user/Supervisor/ui/src/plugin/plugin.ts +++ b/services/user/Supervisor/ui/src/plugin/plugin.ts @@ -36,10 +36,10 @@ export class Plugin { const foundInterface = exportedFuncs.interfaces.find( (i) => i.name === intf, ); - return foundInterface?.funcs.some((f) => f === method); + return foundInterface?.funcs.some((f) => f.name === method); } - return exportedFuncs.funcs.some((f) => f === method); + return exportedFuncs.funcs.some((f) => f.name === method); } private async doFetchPlugin(): Promise { @@ -64,12 +64,20 @@ export class Plugin { } async getDependencies(): Promise { - const api = await this.parsed; - - return api.importedFuncs.interfaces.map((intf) => ({ - service: intf.namespace, - plugin: intf.package, - })); + let api: ComponentAPI | undefined; + try { + api = await this.parsed; + return api.importedFuncs.interfaces.map((intf) => ({ + service: intf.namespace, + plugin: intf.package, + })); + } catch (e: any) { + if (e instanceof PluginDownloadFailed) { + return []; + } else { + throw e; + } + } } private async doReady(): Promise { diff --git a/services/user/Supervisor/ui/src/pluginHost.ts b/services/user/Supervisor/ui/src/plugin/pluginHost.ts similarity index 90% rename from services/user/Supervisor/ui/src/pluginHost.ts rename to services/user/Supervisor/ui/src/plugin/pluginHost.ts index 246b00857..286dcb1d6 100644 --- a/services/user/Supervisor/ui/src/pluginHost.ts +++ b/services/user/Supervisor/ui/src/plugin/pluginHost.ts @@ -1,12 +1,12 @@ import { QualifiedFunctionCallArgs } from "@psibase/common-lib"; -import { HostInterface, PluginPostDetails, Result } from "./hostInterface"; -import { Supervisor } from "./supervisor"; +import { HostInterface, PluginPostDetails, Result } from "../hostInterface"; +import { Supervisor } from "../supervisor"; import { assertTruthy, OriginationData, QualifiedOriginationData, -} from "./utils"; -import { RecoverableErrorPayload } from "./plugin/errors"; +} from "../utils"; +import { RecoverableErrorPayload } from "./errors"; interface HttpRequest { uri: string; @@ -180,26 +180,15 @@ export class PluginHost implements HostInterface { return res; } - // Client interface - loginTemp( - appOrigin: string, - user: string, - ): Result { - this.supervisor.loginTemp(appOrigin, user, this.self); + getActiveAppDomain(): string { + return this.supervisor.getActiveAppDomain(this.self); } + // Client interface getSenderApp(): OriginationData { return this.supervisor.getCaller(this.self); } - getLoggedInUser(): Result { - return this.supervisor.getLoggedInUser(this.self.app); - } - - isLoggedIn(): boolean { - return this.supervisor.isLoggedIn(); - } - myServiceAccount(): string { return this.self.app; } diff --git a/services/user/Supervisor/ui/src/plugin/pluginLoader.ts b/services/user/Supervisor/ui/src/plugin/pluginLoader.ts new file mode 100644 index 000000000..a97b92f86 --- /dev/null +++ b/services/user/Supervisor/ui/src/plugin/pluginLoader.ts @@ -0,0 +1,194 @@ +import { isEqual, QualifiedPluginId } from "@psibase/common-lib"; +import { Plugins } from "./plugins"; +import { assertTruthy } from "../utils"; + +class PluginIdSet { + private items: Set = new Set(); + + add(item: QualifiedPluginId): void { + const key = JSON.stringify(item); + this.items.add(key); + } + + has(item: QualifiedPluginId): boolean { + const key = JSON.stringify(item); + return this.items.has(key); + } + + delete(item: QualifiedPluginId): boolean { + const key = JSON.stringify(item); + return this.items.delete(key); + } + + clear(): void { + this.items.clear(); + } + + values(): QualifiedPluginId[] { + return Array.from(this.items).map((item) => JSON.parse(item)); + } + + size(): number { + return this.items.size; + } +} +export class PluginLoader { + private plugins: Plugins; + private dynamicPlugins: Array< + [QualifiedPluginId, () => QualifiedPluginId | null] + > = []; + + private allLoadedPlugins: PluginIdSet = new PluginIdSet(); + private processedDeferred: PluginIdSet = new PluginIdSet(); + private deferred: PluginIdSet = new PluginIdSet(); + private currentPlugins: QualifiedPluginId[] = []; + + private reset() { + this.allLoadedPlugins.clear(); + this.processedDeferred.clear(); + this.deferred.clear(); + this.currentPlugins = []; + } + + /** + * Marks deferred plugins as processed after mapping them + */ + private mapDeferredPlugins() { + let mappedPlugins: QualifiedPluginId[] = []; + + let deferred = this.deferred.values(); + for (const d of deferred) { + if (!this.processedDeferred.has(d)) { + mappedPlugins.push(this.resolve(d)); + this.processedDeferred.add(d); + } + } + this.deferred.clear(); + + this.currentPlugins = mappedPlugins; + } + + private async getAllDependencies(): Promise { + if (this.currentPlugins.length === 0) { + return []; + } + + const imports = await Promise.all( + this.currentPlugins.map(async (pluginId) => { + // Download and parse the plugin, read its imports to determine dependencies + const p = this.plugins.getPlugin(pluginId).plugin; + return await p.getDependencies(); + }), + ); + + // Flatten and deduplicate + let dependencies = imports + .flat() + .reduce((acc: QualifiedPluginId[], current: QualifiedPluginId) => { + if (!acc.some((obj) => isEqual(obj, current))) { + acc.push(current); + } + return acc; + }, []); + + return dependencies; + } + + private removeDeferredPlugins() { + // Defer loading any dynamic plugins + let deferred: QualifiedPluginId[] = []; + let remaining = this.currentPlugins.filter((id) => { + if (this.isDynamic(id)) { + if (!this.processedDeferred.has(id)) { + deferred.push(id); + } + } else return true; + }); + + deferred.forEach((p) => this.deferred.add(p)); + this.currentPlugins = remaining; + } + + private removeHostPlugins() { + // Plugins at these namespace are built into the supervisor host itself and + // are not pulled from the chain. + this.currentPlugins = this.currentPlugins.filter( + (id) => id.service !== "wasi" && id.service !== "host", + ); + } + + private removeAlreadyAddedPlugins() { + this.currentPlugins = this.currentPlugins.filter( + (plugin) => !this.allLoadedPlugins.has(plugin), + ); + } + + private isDynamic(pluginId: QualifiedPluginId): boolean { + return this.dynamicPlugins.some(([p]) => isEqual(p, pluginId)); + } + + private resolve(pluginId: QualifiedPluginId): QualifiedPluginId { + const dynPlugin = this.dynamicPlugins.find(([p]) => + isEqual(p, pluginId), + ); + if (!dynPlugin) { + return pluginId; + } + + const resolver = dynPlugin[1]; + assertTruthy(resolver, "Dynamic plugin has no resolver"); + const resolved = resolver(); + assertTruthy(resolved, "Dynamic plugin resolver returned null"); + + return resolved; + } + + constructor(plugins: Plugins) { + this.plugins = plugins; + } + + public registerDynamic( + plugin: QualifiedPluginId, + resolver: () => QualifiedPluginId | null, + ) { + this.dynamicPlugins.push([plugin, resolver]); + } + + public trackPlugins(plugins: QualifiedPluginId[]) { + this.reset(); + this.currentPlugins = plugins; + } + + public async processPlugins() { + while (this.currentPlugins.length > 0) { + // Prune the currentPlugins list + this.removeHostPlugins(); + this.removeDeferredPlugins(); + this.removeAlreadyAddedPlugins(); + + // Load the plugins + for (const p of this.currentPlugins) { + this.plugins.getPlugin(p); + this.allLoadedPlugins.add(p); + } + + // Prepare to load all dependencies + this.currentPlugins = await this.getAllDependencies(); + } + } + + public async processDeferred() { + while (this.deferred.size() > 0) { + this.mapDeferredPlugins(); + await this.processPlugins(); + } + } + + public async awaitReady() { + await Promise.all( + this.allLoadedPlugins + .values() + .map((p) => this.plugins.getPlugin(p).plugin.ready), + ); + } +} diff --git a/services/user/Supervisor/ui/src/plugin/plugins.ts b/services/user/Supervisor/ui/src/plugin/plugins.ts new file mode 100644 index 000000000..96e36e14a --- /dev/null +++ b/services/user/Supervisor/ui/src/plugin/plugins.ts @@ -0,0 +1,31 @@ +import { Supervisor } from "../supervisor"; +import { LoadedPlugin, ServiceContext } from "./serviceContext"; +import { PluginHost } from "./pluginHost"; +import { QualifiedPluginId, siblingUrl } from "@psibase/common-lib"; + +export class Plugins { + private supervisor: Supervisor; + + private serviceContexts: { [service: string]: ServiceContext } = {}; + + private getServiceContext(service: string): ServiceContext { + if (!this.serviceContexts[service]) { + this.serviceContexts[service] = new ServiceContext( + service, + new PluginHost(this.supervisor, { + app: service, + origin: siblingUrl(null, service), + }), + ); + } + return this.serviceContexts[service]; + } + + constructor(supervisor: Supervisor) { + this.supervisor = supervisor; + } + + public getPlugin(plugin: QualifiedPluginId): LoadedPlugin { + return this.getServiceContext(plugin.service).loadPlugin(plugin.plugin); + } +} diff --git a/services/user/Supervisor/ui/src/serviceContext.ts b/services/user/Supervisor/ui/src/plugin/serviceContext.ts similarity index 93% rename from services/user/Supervisor/ui/src/serviceContext.ts rename to services/user/Supervisor/ui/src/plugin/serviceContext.ts index 44aff3364..3a4bafa03 100644 --- a/services/user/Supervisor/ui/src/serviceContext.ts +++ b/services/user/Supervisor/ui/src/plugin/serviceContext.ts @@ -1,5 +1,5 @@ -import { Plugin } from "./plugin/plugin"; -import { HostInterface } from "./hostInterface"; +import { Plugin } from "./plugin"; +import { HostInterface } from "../hostInterface"; export interface LoadedPlugin { plugin: Plugin; diff --git a/services/user/Supervisor/ui/src/supervisor.ts b/services/user/Supervisor/ui/src/supervisor.ts index 6eb56afb8..9bc08c541 100644 --- a/services/user/Supervisor/ui/src/supervisor.ts +++ b/services/user/Supervisor/ui/src/supervisor.ts @@ -8,7 +8,6 @@ import { } from "@psibase/common-lib"; import { AppInterface } from "./appInterace"; -import { ServiceContext } from "./serviceContext"; import { OriginationData, assert, @@ -16,32 +15,37 @@ import { parser, serviceFromOrigin, } from "./utils"; -import { Plugin } from "./plugin/plugin"; import { CallContext } from "./callContext"; -import { PluginHost } from "./pluginHost"; -import { isRecoverableError, PluginDownloadFailed } from "./plugin/errors"; +import { isRecoverableError } from "./plugin/errors"; import { getCallArgs } from "@psibase/common-lib/messaging/FunctionCallRequest"; -import { isEqual } from "@psibase/common-lib/messaging/PluginId"; +import { pluginId } from "@psibase/common-lib/messaging/PluginId"; +import { Plugins } from "./plugin/plugins"; +import { PluginLoader } from "./plugin/pluginLoader"; const supervisorDomain = siblingUrl(null, "supervisor"); const supervisorOrigination = { app: serviceFromOrigin(supervisorDomain), origin: supervisorDomain, }; + +// System plugins are always loaded, even if they are not used +// in a given call context. const systemPlugins: Array = [ - { - service: "accounts", - plugin: "plugin", - }, - { - service: "transact", - plugin: "plugin", - }, + pluginId("accounts", "plugin"), + pluginId("transact", "plugin"), ]; +interface Account { + accountNum: string; + authService: string; + resourceBalance?: number; +} + // The supervisor facilitates all communication export class Supervisor implements AppInterface { - private serviceContexts: { [service: string]: ServiceContext } = {}; + private plugins: Plugins; + + private loader: PluginLoader; private context: CallContext | undefined; @@ -67,116 +71,133 @@ export class Supervisor implements AppInterface { }; } - private loadContext(service: string): ServiceContext { - if (!this.serviceContexts[service]) { - this.serviceContexts[service] = new ServiceContext( - service, - new PluginHost(this, { - app: service, - origin: siblingUrl(null, service), - }), - ); + private async preload(plugins: QualifiedPluginId[]) { + if (plugins.length === 0) { + return; } - return this.serviceContexts[service]; + + this.loader.trackPlugins([...systemPlugins, ...plugins]); + + // Loading dynamic plugins may require calling into the standard plugins + // to look up information to know what plugin to load. Therefore, + // loading happens in two phases: Load all regular plugins, then load + // all dynamic plugins. + + // Phase 1: Loads all regular plugins + await this.loader.processPlugins(); + await this.loader.awaitReady(); + + // Phase 2: Loads all dynamic plugins + await this.loader.processDeferred(); + await this.loader.awaitReady(); } - // A valid plugin ID implies that it should be loaded as a distinct plugin - // Therefore we should not validate wasi interfaces or direct host exports - private validated(id: QualifiedPluginId): boolean { - return id.service !== "wasi" && id.service !== "host"; + private replyToParent(id: string, call: FunctionCallArgs, result: any) { + assertTruthy(this.parentOrigination, "Unknown reply target"); + window.parent.postMessage( + buildFunctionCallResponse(id, call, result), + this.parentOrigination.origin, + ); } - private async getDependencies( - plugin: Plugin, - ): Promise { + private supervisorCall(callArgs: QualifiedFunctionCallArgs): any { + let newContext = false; + if (!this.context) { + newContext = true; + this.context = new CallContext(); + } + + let ret: any; try { - const dependencies = await plugin.getDependencies(); - return dependencies.filter((id) => this.validated(id)); - } catch (e: any) { - if (e instanceof PluginDownloadFailed) { - return []; - } else { - throw e; + ret = this.call(supervisorOrigination, callArgs); + } finally { + if (newContext) { + this.context = undefined; } } - } - private async preload(callerOrigin: string, plugins: QualifiedPluginId[]) { - this.setParentOrigination(callerOrigin); - await Promise.all([this.loadPlugins(plugins)]); + return ret; } - private async loadPlugins(plugins: QualifiedPluginId[]): Promise { - if (plugins.length === 0) return; - - const addedPlugins: Plugin[] = []; - plugins.forEach(({ service, plugin }) => { - const c = this.loadContext(service); - const loaded = c.loadPlugin(plugin); - if (loaded.new) { - addedPlugins.push(loaded.plugin); - } - }); - - if (addedPlugins.length === 0) return; + private getLoggedInUser(): string | undefined { + assertTruthy(this.parentOrigination, "Parent origination corrupted"); - const imports = await Promise.all( - addedPlugins.map((plugin) => this.getDependencies(plugin)), + let getLoggedInUser = getCallArgs( + "accounts", + "plugin", + "accounts", + "getLoggedInUser", + [], ); + return this.supervisorCall(getLoggedInUser); + } + + private getAccount(user: string): Account | undefined { + assertTruthy(this.parentOrigination, "Parent origination corrupted"); - const dependencies = imports - .flat() - .reduce((acc: QualifiedPluginId[], current: QualifiedPluginId) => { - if (!acc.some((obj) => isEqual(obj, current))) { - acc.push(current); - } - return acc; - }, []); - - const pluginsReady = Promise.all( - addedPlugins.map((plugin) => plugin.ready), + const getAccount = getCallArgs( + "accounts", + "plugin", + "accounts", + "getAccount", + [user], ); - return Promise.all([pluginsReady, this.loadPlugins(dependencies)]); + let account: Account | undefined = this.supervisorCall(getAccount); + return account; } - private replyToParent(id: string, call: FunctionCallArgs, result: any) { - assertTruthy(this.parentOrigination, "Unknown reply target"); - window.parent.postMessage( - buildFunctionCallResponse(id, call, result), - this.parentOrigination.origin, + private logout() { + assertTruthy(this.parentOrigination, "Parent origination corrupted"); + + const logout = getCallArgs( + "accounts", + "plugin", + "accounts", + "logout", + [], ); - } - private supervisorCall(callArgs: QualifiedFunctionCallArgs): any { - return this.call(supervisorOrigination, callArgs); + this.supervisorCall(logout); } constructor() { this.parser = parser(); + + this.plugins = new Plugins(this); + + this.loader = new PluginLoader(this.plugins); + this.loader.registerDynamic(pluginId("accounts", "smart-auth"), () => { + let user = this.getLoggedInUser(); + if (user === undefined) { + return pluginId("auth-invite", "plugin"); + } + const account = this.getAccount(user); + if (account === undefined) { + console.warn( + `Invalid user account '${user}' detected. Automatically logging out.`, + ); + this.logout(); + return pluginId("auth-invite", "plugin"); + } + // Temporary limitation: the auth service plugin must be called "plugin" (":plugin") + // Or, we could just eliminate the possiblity of storing multiple plugins per namespace (and *everything* + // would have to be named "plugin") + return pluginId(account.authService, "plugin"); + }); } - // Temporary function to allow apps to directly log a user in - loginTemp(appOrigin: string, user: string, sender: OriginationData) { + getActiveAppDomain(sender: OriginationData): string { assertTruthy(this.parentOrigination, "Parent origination corrupted"); assertTruthy( sender.app, - "[supervisor:loginTemp] only callable by Accounts plugin", + "[supervisor:getActiveAppDomain] Unauthorized - only callable by Accounts plugin", ); - assert( sender.app === "accounts", - "[supervisor:loginTemp] Unauthorized", - ); - assert( - appOrigin === this.parentOrigination.origin, - "[supervisor:loginTemp] Unauthorized. Can only login to top-level app.", + "[supervisor:getActiveAppDomain] Unauthorized - Only callable by Accounts plugin", ); - let login = getCallArgs("accounts", "plugin", "admin", "forceLogin", [ - appOrigin, - user, - ]); - this.supervisorCall(login); + return this.parentOrigination.origin; } // Called by the current plugin looking to identify its caller @@ -195,23 +216,6 @@ export class Supervisor implements AppInterface { return frame.caller; } - getLoggedInUser(caller_app: string): string | undefined { - assertTruthy(this.parentOrigination, "Parent origination corrupted"); - - let getLoggedInUser = getCallArgs( - "accounts", - "plugin", - "admin", - "getLoggedInUser", - [caller_app, this.parentOrigination.origin], - ); - return this.supervisorCall(getLoggedInUser); - } - - isLoggedIn(): boolean { - return this.getLoggedInUser("supervisor") !== undefined; - } - // Manages callstack and calls plugins call(sender: OriginationData, args: QualifiedFunctionCallArgs): any { assertTruthy(this.context, "Uninitialized call context"); @@ -232,15 +236,16 @@ export class Supervisor implements AppInterface { ); } + // Load the plugin const { service, plugin, intf, method, params } = args; - const p = this.loadContext(service).loadPlugin(plugin); + const p = this.plugins.getPlugin({ service, plugin }); assert( p.new === false, `Tried to call plugin ${service}:${plugin} before initialization`, ); + // Manage the callstack and call the plugin this.context.stack.push(sender, args); - let ret: any; try { ret = p.plugin.call(intf, method, params); @@ -256,7 +261,8 @@ export class Supervisor implements AppInterface { // which accelerates the responsiveness of the plugins for subsequent calls. async preloadPlugins(callerOrigin: string, plugins: QualifiedPluginId[]) { try { - this.preload(callerOrigin, [...plugins, ...systemPlugins]); + this.setParentOrigination(callerOrigin); + await this.preload(plugins); } catch (e) { console.error("TODO: Return an error to the caller."); console.error(e); @@ -270,17 +276,18 @@ export class Supervisor implements AppInterface { args: QualifiedFunctionCallArgs, ): Promise { try { + this.setParentOrigination(callerOrigin); + // Wait to load the full plugin tree (a plugin and all its dependencies, recursively). // This is the time-intensive step. It includes: downloading, parsing, generating import fills, // transpiling the component, bundling with rollup, and importing & instantiating the es module // in memory. // UIs should use `preloadPlugins` to decouple this task from the actual call to the plugin. - await this.preload(callerOrigin, [ + await this.preload([ { service: args.service, plugin: args.plugin, }, - ...systemPlugins, ]); this.context = new CallContext(); diff --git a/services/user/Supervisor/ui/src/utils.ts b/services/user/Supervisor/ui/src/utils.ts index e1b0a5bd4..06545400d 100644 --- a/services/user/Supervisor/ui/src/utils.ts +++ b/services/user/Supervisor/ui/src/utils.ts @@ -59,7 +59,7 @@ export const parser = (): Promise => { "/common/component_parser.wasm", ); modulePromise = wasmFromUrl(url).then((bytes) => - loadBasic(bytes, "Component parser"), + loadBasic(bytes, "component-parser.js"), ); } return modulePromise; diff --git a/services/user/Supervisor/ui/src/witExtraction.ts b/services/user/Supervisor/ui/src/witExtraction.ts index e268f0bf6..3d2fff2d0 100644 --- a/services/user/Supervisor/ui/src/witExtraction.ts +++ b/services/user/Supervisor/ui/src/witExtraction.ts @@ -1,15 +1,20 @@ +export interface FuncShape { + name: string; + dynamicLink: boolean; +} + export interface Interface { namespace: string; package: string; name: string; - funcs: string[]; + funcs: FuncShape[]; } export interface Functions { namespace: string; package: string; interfaces: Interface[]; - funcs: string[]; + funcs: FuncShape[]; } export interface ComponentAPI { diff --git a/services/user/Tokens/plugin/Cargo.lock b/services/user/Tokens/plugin/Cargo.lock index 9e066eee8..0ce0a2aa9 100644 --- a/services/user/Tokens/plugin/Cargo.lock +++ b/services/user/Tokens/plugin/Cargo.lock @@ -1105,9 +1105,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128"