15 Commits

Author SHA1 Message Date
a8e4a710f1 Merge pull request 'security(server): bind grant revocation state (revoked_at) to integrity hash' (#83) from security-hash-revoke_at into main
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-lint Pipeline was successful
ci/woodpecker/push/server-test Pipeline was successful
ci/woodpecker/push/useragent-analyze Pipeline failed
Reviewed-on: #83
2026-06-11 09:44:28 +00:00
CleverWild
d99c87c473 fix: lints
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-06-09 21:07:01 +02:00
CleverWild
303120c9ac Merge branch 'main' into security-hash-revoke_at
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-06-09 20:58:20 +02:00
CleverWild
32f317384d security(evm): remove client-controlled wallet_access_id from grant revocation
Some checks failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-06-09 19:36:44 +02:00
CleverWild
4bb2c062dc feat(evm): add wallet_access_id to grant deletion requests and revocation logic
Some checks failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-06-09 19:16:21 +02:00
CleverWild
b0a3f37cea refactor(evm): implement revoke_grant method for grant revocation 2026-06-09 19:11:39 +02:00
CleverWild
58a72da46c Merge branch 'security-hash-revoke_at' of ssh://git.markettakers.org:22222/MarketTakers/arbiter into security-hash-revoke_at
Some checks failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-06-09 19:10:57 +02:00
CleverWild
e287459b10 revert(server): bind grant revocation state (revoked_at) to integrity hash 2026-06-09 18:45:30 +02:00
CleverWild
3c482da917 fix(smlang::statemachine): macro invocation requires inner types to be public
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
2026-06-08 18:00:52 +02:00
Skipper
3f801abdff housekeeping(server): deps upgrade + diesel migration to AsyncFnOnce
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
2026-05-01 11:22:40 +02:00
Skipper
2b44570ab4 fix(server): MacOS build version
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
2026-04-19 13:47:47 +02:00
Skipper
1f9b253433 housekeeping(server): removed unused deps 2026-04-19 13:46:49 +02:00
Skipper
a1c3ffd2d1 refactor: rename to to better reflect meaning
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
2026-04-19 13:41:50 +02:00
Skipper
fd25de32a1 docs: move to folder and update to new challenge payload 2026-04-18 15:17:18 +02:00
CleverWild
5a34463228 security(server): bind grant revocation state (revoked_at) to integrity hash
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-04-08 12:09:54 +02:00
77 changed files with 1186 additions and 1368 deletions

2
.gitignore vendored
View File

@@ -3,4 +3,4 @@ scripts/__pycache__/
.DS_Store .DS_Store
.cargo/config.toml .cargo/config.toml
.vscode/ .vscode/
docs/ docs/superpowers

View File

@@ -6,7 +6,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
Arbiter is a **permissioned signing service** for cryptocurrency wallets. It consists of: Arbiter is a **permissioned signing service** for cryptocurrency wallets. It consists of:
- **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies - **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies
- **`useragent/`** — Flutter desktop app (macOS/Windows) with a Rust backend via Rinf - **`operator/`** — Flutter desktop app (macOS/Windows) with a Rust backend via Rinf
- **`protobufs/`** — Protocol Buffer definitions shared between server and client - **`protobufs/`** — Protocol Buffer definitions shared between server and client
The vault never exposes key material; it only produces signatures when requests satisfy configured policies. The vault never exposes key material; it only produces signatures when requests satisfy configured policies.
@@ -28,7 +28,7 @@ Key versions: Rust 1.93.0 (with clippy), Flutter 3.38.9-stable, protoc 29.6, die
|---|---| |---|---|
| `arbiter-proto` | Generated gRPC stubs + protobuf types; compiled from `protobufs/*.proto` via `tonic-prost-build` | | `arbiter-proto` | Generated gRPC stubs + protobuf types; compiled from `protobufs/*.proto` via `tonic-prost-build` |
| `arbiter-server` | Main daemon — actors, DB, EVM policy engine, gRPC service implementation | | `arbiter-server` | Main daemon — actors, DB, EVM policy engine, gRPC service implementation |
| `arbiter-useragent` | Rust client library for the user agent side of the gRPC protocol | | `arbiter-operator` | Rust client library for the operator side of the gRPC protocol |
| `arbiter-client` | Rust client library for SDK clients | | `arbiter-client` | Rust client library for SDK clients |
### Common Commands ### Common Commands
@@ -67,10 +67,10 @@ The server is actor-based using the **kameo** crate. All long-lived state lives
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run. - **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
- **`Vault`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell. - **`Vault`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
- **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients. - **`FlowCoordinator`** — Coordinates cross-connection flow between operators and SDK clients.
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing. - **`EvmActor`** — Handles EVM transaction policy enforcement and signing.
Per-connection actors live under `actors/user_agent/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules. Per-connection actors live under `actors/operator/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules.
**Database:** SQLite via `diesel-async` + `bb8` connection pool. Schema managed by embedded Diesel migrations in `crates/arbiter-server/migrations/`. DB file lives at `~/.arbiter/arbiter.sqlite`. Tests use a temp-file DB via `db::create_test_pool()`. **Database:** SQLite via `diesel-async` + `bb8` connection pool. Schema managed by embedded Diesel migrations in `crates/arbiter-server/migrations/`. DB file lives at `~/.arbiter/arbiter.sqlite`. Tests use a temp-file DB via `db::create_test_pool()`.
@@ -121,20 +121,20 @@ pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool
This forces callers to either use the return value or explicitly ignore it with `let _ = ...;`, preventing silent failures. This forces callers to either use the return value or explicitly ignore it with `let _ = ...;`, preventing silent failures.
## User Agent (Flutter + Rinf at `useragent/`) ## Operator (Flutter + Rinf at `operator/`)
The Flutter app uses [Rinf](https://rinf.cunarist.org) to call Rust code. The Rust logic lives in `useragent/native/hub/` as a separate crate that uses `arbiter-useragent` for the gRPC client. The Flutter app uses [Rinf](https://rinf.cunarist.org) to call Rust code. The Rust logic lives in `operator/native/hub/` as a separate crate that uses `arbiter-operator` for the gRPC client.
Communication between Dart and Rust uses typed **signals** defined in `useragent/native/hub/src/signals/`. After modifying signal structs, regenerate Dart bindings: Communication between Dart and Rust uses typed **signals** defined in `operator/native/hub/src/signals/`. After modifying signal structs, regenerate Dart bindings:
```sh ```sh
cd useragent && rinf gen cd operator && rinf gen
``` ```
### Common Commands ### Common Commands
```sh ```sh
cd useragent cd operator
# Run the app (macOS or Windows) # Run the app (macOS or Windows)
flutter run flutter run
@@ -146,4 +146,4 @@ rinf gen
flutter analyze flutter analyze
``` ```
The Rinf Rust entry point is `useragent/native/hub/src/lib.rs`. It spawns actors defined in `useragent/native/hub/src/actors/` which handle Dart↔server communication via signals. The Rinf Rust entry point is `operator/native/hub/src/lib.rs`. It spawns actors defined in `operator/native/hub/src/actors/` which handle Dart↔server communication via signals.

150
CLAUDE.md
View File

@@ -1,149 +1 @@
# CLAUDE.md Refer to @AGENTS.md for instructions.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Arbiter is a **permissioned signing service** for cryptocurrency wallets. It consists of:
- **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies
- **`useragent/`** — Flutter desktop app (macOS/Windows) with a Rust backend via Rinf
- **`protobufs/`** — Protocol Buffer definitions shared between server and client
The vault never exposes key material; it only produces signatures when requests satisfy configured policies.
## Toolchain Setup
Tools are managed via [mise](https://mise.jdx.dev/). Install all required tools:
```sh
mise install
```
Key versions: Rust 1.93.0 (with clippy), Flutter 3.38.9-stable, protoc 29.6, diesel_cli 2.3.6 (sqlite).
## Server (Rust workspace at `server/`)
### Crates
| Crate | Purpose |
|---|---|
| `arbiter-proto` | Generated gRPC stubs + protobuf types; compiled from `protobufs/*.proto` via `tonic-prost-build` |
| `arbiter-server` | Main daemon — actors, DB, EVM policy engine, gRPC service implementation |
| `arbiter-useragent` | Rust client library for the user agent side of the gRPC protocol |
| `arbiter-client` | Rust client library for SDK clients |
### Common Commands
```sh
cd server
# Build
cargo build
# Run the server daemon
cargo run -p arbiter-server
# Run all tests (preferred over cargo test)
cargo nextest run
# Run a single test
cargo nextest run <test_name>
# Lint
cargo clippy
# Security audit
cargo audit
# Check unused dependencies
cargo shear
# Run snapshot tests and update snapshots
cargo insta review
```
### Architecture
The server is actor-based using the **kameo** crate. All long-lived state lives in `GlobalActors`:
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
- **`Vault`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
- **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients.
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.
Per-connection actors live under `actors/user_agent/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules.
**Database:** SQLite via `diesel-async` + `bb8` connection pool. Schema managed by embedded Diesel migrations in `crates/arbiter-server/migrations/`. DB file lives at `~/.arbiter/arbiter.sqlite`. Tests use a temp-file DB via `db::create_test_pool()`.
**Cryptography:**
- Authentication: ed25519 (challenge-response, nonce-tracked per peer)
- Encryption at rest: XChaCha20-Poly1305 (versioned via `scheme` field for transparent migration on unseal)
- Password KDF: Argon2
- Unseal transport: X25519 ephemeral key exchange
- TLS: self-signed certificate (aws-lc-rs backend), fingerprint distributed via `ArbiterUrl`
**Protocol:** gRPC with Protocol Buffers. The `ArbiterUrl` type encodes host, port, CA cert, and bootstrap token into a single shareable string (printed to console on first run).
### Proto Regeneration
When `.proto` files in `protobufs/` change, rebuild to regenerate:
```sh
cd server && cargo build -p arbiter-proto
```
### Database Migrations
```sh
# Create a new migration
diesel migration generate <name> --migration-dir crates/arbiter-server/migrations
# Run migrations manually (server also runs them on startup)
diesel migration run --migration-dir crates/arbiter-server/migrations
```
### Code Conventions
**`#[must_use]` Attribute:**
Apply the `#[must_use]` attribute to return types of functions where the return value is critical and should not be accidentally ignored. This is commonly used for:
- Methods that return `bool` indicating success/failure or validation state
- Any function where ignoring the return value indicates a logic error
Do not apply `#[must_use]` redundantly to items (types or functions) that are already annotated with `#[must_use]`.
Example:
```rust
#[must_use]
pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool {
// verification logic
}
```
This forces callers to either use the return value or explicitly ignore it with `let _ = ...;`, preventing silent failures.
## User Agent (Flutter + Rinf at `useragent/`)
The Flutter app uses [Rinf](https://rinf.cunarist.org) to call Rust code. The Rust logic lives in `useragent/native/hub/` as a separate crate that uses `arbiter-useragent` for the gRPC client.
Communication between Dart and Rust uses typed **signals** defined in `useragent/native/hub/src/signals/`. After modifying signal structs, regenerate Dart bindings:
```sh
cd useragent && rinf gen
```
### Common Commands
```sh
cd useragent
# Run the app (macOS or Windows)
flutter run
# Regenerate Rust↔Dart signal bindings
rinf gen
# Analyze Dart code
flutter analyze
```
The Rinf Rust entry point is `useragent/native/hub/src/lib.rs`. It spawns actors defined in `useragent/native/hub/src/actors/` which handle Dart↔server communication via signals.

View File

@@ -4,7 +4,7 @@
## Security warning ## Security warning
Arbiter can't meaningfully protect against host compromise. Potential attack flow: Arbiter can't meaningfully protect against host compromise. Potential attack flow:
- Attacker steals TLS keys from database - Attacker steals TLS keys from database
- Pretends to be server; just accepts user agent challenge solutions - Pretends to be server; just accepts operator challenge solutions
- Pretend to be in sealed state and performing DH with client - Pretend to be in sealed state and performing DH with client
- Steals user password and derives seal key - Steals user password and derives seal key

View File

@@ -9,7 +9,7 @@ Arbiter is a permissioned signing service for cryptocurrency wallets. It runs as
Arbiter distinguishes two kinds of peers: Arbiter distinguishes two kinds of peers:
- **User Agent** — A client application used by the owner to manage the vault (create wallets, approve SDK clients, configure policies). - **Operator** — A client application used by the owner to manage the vault (create wallets, approve SDK clients, configure policies).
- **SDK Client** — A consumer of signing capabilities, typically an automation tool. In the future, this could include a browser-based wallet. - **SDK Client** — A consumer of signing capabilities, typically an automation tool. In the future, this could include a browser-based wallet.
- **Recovery Operator** — A dormant recovery participant with narrowly scoped authority used only for custody recovery and operator replacement. - **Recovery Operator** — A dormant recovery participant with narrowly scoped authority used only for custody recovery and operator replacement.
@@ -22,30 +22,32 @@ Arbiter distinguishes two kinds of peers:
All peers authenticate via public-key cryptography using a challenge-response protocol: All peers authenticate via public-key cryptography using a challenge-response protocol:
1. The peer sends its public key and requests a challenge. 1. The peer sends its public key and requests a challenge.
2. The server looks up the key in its database. If found, it increments the nonce and returns a challenge (replay-attack protection). 2. The server looks up the key in its database. If found, it generates a fresh challenge from random bytes plus the current timestamp.
3. The peer signs the challenge with its private key and sends the signature back. 3. The peer signs the canonical challenge payload with its private key and sends the signature back.
4. The server verifies the signature: 4. The server verifies the signature:
- **Pass:** The connection is considered authenticated. - **Pass:** The connection is considered authenticated.
- **Fail:** The server closes the connection. - **Fail:** The server closes the connection.
### 2.2 User Agent Bootstrap Authentication challenges are per-connection, ephemeral values. They are not persisted in the peer tables, and peer records store no challenge state.
On first run — when no User Agents are registered — the server generates a one-time bootstrap token. It is made available in two ways: ### 2.2 Operator Bootstrap
- **Local setup:** Written to `~/.arbiter/bootstrap_token` for automatic discovery by a co-located User Agent. On first run — when no Operators are registered — the server generates a one-time bootstrap token. It is made available in two ways:
- **Local setup:** Written to `~/.arbiter/bootstrap_token` for automatic discovery by a co-located Operator.
- **Remote setup:** Printed to the server's console output. - **Remote setup:** Printed to the server's console output.
The first User Agent must present this token alongside the standard challenge-response to complete registration. The first Operator must present this token alongside the standard challenge-response to complete registration.
### 2.3 SDK Client Registration ### 2.3 SDK Client Registration
There is no bootstrap mechanism for SDK clients. They must be explicitly approved by an already-registered User Agent. There is no bootstrap mechanism for SDK clients. They must be explicitly approved by an already-registered Operator.
--- ---
## 3. Multi-Operator Governance ## 3. Multi-Operator Governance
When more than one User Agent is registered, the vault is treated as having multiple operators. In that mode, sensitive actions are governed by voting rather than by a single operator decision. When more than one Operator is registered, the vault is treated as having multiple operators. In that mode, sensitive actions are governed by voting rather than by a single operator decision.
### 3.1 Voting Rules ### 3.1 Voting Rules
@@ -163,13 +165,13 @@ In both cases, committee formation is a coordinated process. Arbiter does not al
When an unbootstrapped vault is initialized as a multi-operator vault, the setup proceeds as follows: When an unbootstrapped vault is initialized as a multi-operator vault, the setup proceeds as follows:
1. An operator connects to the unbootstrapped vault using a User Agent and the bootstrap token. 1. An operator connects to the unbootstrapped vault using an Operator and the bootstrap token.
2. During bootstrap setup, that operator declares: 2. During bootstrap setup, that operator declares:
- the total number of ordinary operators - the total number of ordinary operators
- the total number of Recovery Operators - the total number of Recovery Operators
3. The vault enters **multi-bootstrap mode**. 3. The vault enters **multi-bootstrap mode**.
4. While in multi-bootstrap mode: 4. While in multi-bootstrap mode:
- every ordinary operator must connect with a User Agent using the bootstrap token - every ordinary operator must connect with an Operator using the bootstrap token
- every Recovery Operator must also connect using the bootstrap token - every Recovery Operator must also connect using the bootstrap token
- each participant is registered individually - each participant is registered individually
- each participant's share is created and protected with that participant's credentials - each participant's share is created and protected with that participant's credentials
@@ -191,8 +193,8 @@ The server proves its identity using TLS with a self-signed certificate. The TLS
Peers verify the server by its **public key fingerprint**: Peers verify the server by its **public key fingerprint**:
- **User Agent (local):** Receives the fingerprint automatically through the bootstrap token. - **Operator (local):** Receives the fingerprint automatically through the bootstrap token.
- **User Agent (remote) / SDK Client:** Must receive the fingerprint out-of-band. - **Operator (remote) / SDK Client:** Must receive the fingerprint out-of-band.
> A streamlined setup mechanism using a single connection string is planned but not yet implemented. > A streamlined setup mechanism using a single connection string is planned but not yet implemented.
@@ -229,11 +231,11 @@ On boot, the root key is encrypted and the server cannot perform any signing ope
### 6.2 Unseal Flow ### 6.2 Unseal Flow
To transition to the **Unsealed** state, a User Agent must provide the password: To transition to the **Unsealed** state, an Operator must provide the password:
1. The User Agent initiates an unseal request. 1. The Operator initiates an unseal request.
2. The server generates a one-time key pair and returns the public key. 2. The server generates a one-time key pair and returns the public key.
3. The User Agent encrypts the user's password with this one-time public key and sends the ciphertext to the server. 3. The Operator encrypts the user's password with this one-time public key and sends the ciphertext to the server.
4. The server decrypts and verifies the password: 4. The server decrypts and verifies the password:
- **Success:** The root key is decrypted and placed into a hardened memory cell. The server transitions to `Unsealed`. Any entries pending encryption scheme migration are re-encrypted. - **Success:** The root key is decrypted and placed into a hardened memory cell. The server transitions to `Unsealed`. Any entries pending encryption scheme migration are re-encrypted.
- **Failure:** The server returns an error indicating the password is incorrect. - **Failure:** The server returns an error indicating the password is incorrect.
@@ -255,7 +257,7 @@ See [IMPLEMENTATION.md](IMPLEMENTATION.md) for the current and planned memory pr
### 7.1 Fundamental Rules ### 7.1 Fundamental Rules
- SDK clients have **no access by default**. - SDK clients have **no access by default**.
- Access is granted **explicitly** by a User Agent. - Access is granted **explicitly** by an Operator.
- Grants are scoped to **specific wallets** and governed by **policies**. - Grants are scoped to **specific wallets** and governed by **policies**.
Each blockchain requires its own policy system due to differences in static transaction analysis. Currently, only EVM is supported; Solana support is planned. Each blockchain requires its own policy system due to differences in static transaction analysis. Currently, only EVM is supported; Solana support is planned.
@@ -275,19 +277,19 @@ sequenceDiagram
autonumber autonumber
actor SDK as SDK Client actor SDK as SDK Client
participant Server participant Server
participant UA as User Agent participant operator as Operator
SDK->>Server: SignTransactionRequest SDK->>Server: SignTransactionRequest
Server->>Server: Resolve wallet and wallet visibility Server->>Server: Resolve wallet and wallet visibility
alt Visibility approval required alt Visibility approval required
Server->>UA: Ask for wallet visibility approval Server->>operator: Ask for wallet visibility approval
UA-->>Server: Vote result operator-->>Server: Vote result
end end
Server->>Server: Evaluate transaction Server->>Server: Evaluate transaction
Server->>Server: Load grant and limits context Server->>Server: Load grant and limits context
alt Grant approval required alt Grant approval required
Server->>UA: Ask for execution / grant approval Server->>operator: Ask for execution / grant approval
UA-->>Server: Vote result operator-->>Server: Vote result
opt Create persistent grant opt Create persistent grant
Server->>Server: Create and store grant Server->>Server: Create and store grant
end end

View File

@@ -8,10 +8,10 @@ This document covers concrete technology choices and dependencies. For the archi
### Authentication Result Semantics ### Authentication Result Semantics
Authentication no longer uses an implicit success-only response shape. Both `client` and `user-agent` return explicit auth status enums over the wire. Authentication no longer uses an implicit success-only response shape. Both `client` and `operator` return explicit auth status enums over the wire.
- **Client:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `APPROVAL_DENIED`, `NO_USER_AGENTS_ONLINE`, or `INTERNAL` - **Client:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `APPROVAL_DENIED`, `NO_OPERATORS_ONLINE`, or `INTERNAL`
- **User-agent:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `BOOTSTRAP_REQUIRED`, `TOKEN_INVALID`, or `INTERNAL` - **Operator:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `BOOTSTRAP_REQUIRED`, `TOKEN_INVALID`, or `INTERNAL`
This makes transport-level failures and actor/domain-level auth failures distinct: This makes transport-level failures and actor/domain-level auth failures distinct:
@@ -22,7 +22,7 @@ Clients are expected to handle these status codes directly and present the concr
### New Client Approval ### New Client Approval
When a client whose public key is not yet in the database connects, all connected user agents are asked to approve the connection. The first agent to respond determines the outcome; remaining requests are cancelled via a watch channel. When a client whose public key is not yet in the database connects, all connected operators are asked to approve the connection. The first operator to respond determines the outcome; remaining requests are cancelled via a watch channel.
```mermaid ```mermaid
flowchart TD flowchart TD
@@ -31,10 +31,10 @@ flowchart TD
C -- yes --> G[Generate AuthChallenge] C -- yes --> G[Generate AuthChallenge]
C -- no --> E[Ask all UserAgents:\nClientConnectionRequest] C -- no --> E[Ask all Operators:\nClientConnectionRequest]
E --> F{First response} E --> F{First response}
F -- denied --> Z([Reject connection]) F -- denied --> Z([Reject connection])
F -- approved --> F2[Cancel remaining\nUserAgent requests] F -- approved --> F2[Cancel remaining\nOperator requests]
F2 --> F3[INSERT client] F2 --> F3[INSERT client]
F3 --> G F3 --> G
@@ -45,7 +45,13 @@ flowchart TD
K -- yes --> J([Session started]) K -- yes --> J([Session started])
``` ```
Auth challenges are generated from fresh random bytes plus a timestamp. They are signed as the canonical challenge payload and are not persisted in `program_client`. Auth challenges are generated from fresh random bytes plus a nanosecond timestamp. The server keeps the issued challenge only in the in-flight authentication state for that connection, then verifies the signature against the same canonical challenge payload.
The authentication schema stores peer identity, not replay counters:
- `program_client` stores the SDK client's public key, metadata binding, and timestamps.
- `operator_client` stores the Operator public key and timestamps.
- Neither table stores an authentication nonce, and challenge generation does not update either table.
--- ---
@@ -56,7 +62,7 @@ Auth challenges are generated from fresh random bytes plus a timestamp. They are
### User-Agent Authentication ### User-Agent Authentication
User-agent authentication supports multiple signature schemes because platform-provided "hardware-bound" keys do not expose a uniform algorithm across operating systems and hardware. Operator authentication supports multiple signature schemes because platform-provided "hardware-bound" keys do not expose a uniform algorithm across operating systems and hardware.
- **Supported schemes:** ML-DSA - **Supported schemes:** ML-DSA
- **Why:** Secure Enclave (MacOS) support them natively, on other platforms we could emulate while they roll-out - **Why:** Secure Enclave (MacOS) support them natively, on other platforms we could emulate while they roll-out
@@ -80,7 +86,7 @@ User-agent authentication supports multiple signature schemes because platform-p
### Request Multiplexing ### Request Multiplexing
Both `client` and `user-agent` connections support multiple in-flight requests over one gRPC bidi stream. Both `client` and `operator` connections support multiple in-flight requests over one gRPC bidi stream.
- Every request carries a monotonically increasing request ID - Every request carries a monotonically increasing request ID
- Every normal response echoes the request ID it corresponds to - Every normal response echoes the request ID it corresponds to
@@ -135,7 +141,7 @@ flowchart TD
L -- Yes --> M[Check grant limits] L -- Yes --> M[Check grant limits]
L -- No --> N[Start execution or grant voting flow] L -- No --> N[Start execution or grant voting flow]
N --> O{User-agent decision} N --> O{Operator decision}
O -- Reject --> Z4[Return no matching grant error] O -- Reject --> Z4[Return no matching grant error]
O -- Allow once --> M O -- Allow once --> M
O -- Create grant --> P[Create grant with user-selected limits] O -- Create grant --> P[Create grant with user-selected limits]

View File

@@ -111,7 +111,7 @@ String shortAddress(List<int> bytes) {
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/utils.dart cd operator && dart analyze lib/screens/dashboard/evm/grants/create/utils.dart
``` ```
Expected: no errors. Expected: no errors.
@@ -168,7 +168,7 @@ class GrantCreation extends _$GrantCreation {
- [ ] **Step 2: Run code generator** - [ ] **Step 2: Run code generator**
```sh ```sh
cd useragent && dart run build_runner build --delete-conflicting-outputs cd operator && dart run build_runner build --delete-conflicting-outputs
``` ```
Expected: generates `provider.freezed.dart` and `provider.g.dart`, no errors. Expected: generates `provider.freezed.dart` and `provider.g.dart`, no errors.
@@ -176,7 +176,7 @@ Expected: generates `provider.freezed.dart` and `provider.g.dart`, no errors.
- [ ] **Step 3: Verify** - [ ] **Step 3: Verify**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/provider.dart cd operator && dart analyze lib/screens/dashboard/evm/grants/create/provider.dart
``` ```
Expected: no errors. Expected: no errors.
@@ -204,7 +204,7 @@ jj describe -m "feat(grants): add GrantCreation provider (client selection + gra
```dart ```dart
// lib/screens/dashboard/evm/grants/create/fields/client_picker_field.dart // lib/screens/dashboard/evm/grants/create/fields/client_picker_field.dart
import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:arbiter/proto/operator.pb.dart';
import 'package:arbiter/providers/sdk_clients/list.dart'; import 'package:arbiter/providers/sdk_clients/list.dart';
import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart'; import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -246,7 +246,7 @@ class ClientPickerField extends ConsumerWidget {
```dart ```dart
// lib/screens/dashboard/evm/grants/create/fields/wallet_access_picker_field.dart // lib/screens/dashboard/evm/grants/create/fields/wallet_access_picker_field.dart
import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:arbiter/proto/operator.pb.dart';
import 'package:arbiter/providers/evm/evm.dart'; import 'package:arbiter/providers/evm/evm.dart';
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart'; import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart'; import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart';
@@ -522,7 +522,7 @@ class TransactionRateLimitField extends StatelessWidget {
- [ ] **Step 8: Verify all field widgets** - [ ] **Step 8: Verify all field widgets**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/fields/ cd operator && dart analyze lib/screens/dashboard/evm/grants/create/fields/
``` ```
Expected: no errors. Expected: no errors.
@@ -585,7 +585,7 @@ class SharedGrantFields extends StatelessWidget {
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/shared_grant_fields.dart cd operator && dart analyze lib/screens/dashboard/evm/grants/create/shared_grant_fields.dart
``` ```
Expected: no errors. Expected: no errors.
@@ -978,7 +978,7 @@ class _TokenVolumeLimitRow extends HookWidget {
- [ ] **Step 4: Run code generator for token_transfer_grant.g.dart** - [ ] **Step 4: Run code generator for token_transfer_grant.g.dart**
```sh ```sh
cd useragent && dart run build_runner build --delete-conflicting-outputs cd operator && dart run build_runner build --delete-conflicting-outputs
``` ```
Expected: generates `token_transfer_grant.g.dart`, no errors. Expected: generates `token_transfer_grant.g.dart`, no errors.
@@ -986,7 +986,7 @@ Expected: generates `token_transfer_grant.g.dart`, no errors.
- [ ] **Step 5: Verify** - [ ] **Step 5: Verify**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/grants/ cd operator && dart analyze lib/screens/dashboard/evm/grants/create/grants/
``` ```
Expected: no errors. Expected: no errors.
@@ -1265,7 +1265,7 @@ String _formatError(Object error) {
- [ ] **Step 2: Verify the full create/ directory** - [ ] **Step 2: Verify the full create/ directory**
```sh ```sh
cd useragent && dart analyze lib/screens/dashboard/evm/grants/create/ cd operator && dart analyze lib/screens/dashboard/evm/grants/create/
``` ```
Expected: no errors. Expected: no errors.

View File

@@ -14,24 +14,24 @@
| File | Action | Responsibility | | File | Action | Responsibility |
|---|---|---| |---|---|---|
| `useragent/lib/theme/palette.dart` | Modify | Add `Palette.token` (indigo accent for token-transfer cards) | | `operator/lib/theme/palette.dart` | Modify | Add `Palette.token` (indigo accent for token-transfer cards) |
| `useragent/lib/features/connection/evm/wallet_access.dart` | Modify | Add `listAllWalletAccesses()` function | | `operator/lib/features/connection/evm/wallet_access.dart` | Modify | Add `listAllWalletAccesses()` function |
| `useragent/lib/providers/sdk_clients/wallet_access_list.dart` | Create | `WalletAccessListProvider` — fetches full wallet access list with IDs | | `operator/lib/providers/sdk_clients/wallet_access_list.dart` | Create | `WalletAccessListProvider` — fetches full wallet access list with IDs |
| `useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart` | Create | `GrantCard` widget — watches enrichment providers + revoke mutation; one card per grant | | `operator/lib/screens/dashboard/evm/grants/widgets/grant_card.dart` | Create | `GrantCard` widget — watches enrichment providers + revoke mutation; one card per grant |
| `useragent/lib/screens/dashboard/evm/grants/grants.dart` | Create | `EvmGrantsScreen` — watches `evmGrantsProvider`; handles loading/error/empty/data states; renders `GrantCard` list | | `operator/lib/screens/dashboard/evm/grants/grants.dart` | Create | `EvmGrantsScreen` — watches `evmGrantsProvider`; handles loading/error/empty/data states; renders `GrantCard` list |
| `useragent/lib/router.dart` | Modify | Register `EvmGrantsRoute` in dashboard children | | `operator/lib/router.dart` | Modify | Register `EvmGrantsRoute` in dashboard children |
| `useragent/lib/screens/dashboard.dart` | Modify | Add Grants entry to `routes` list and `NavigationDestination` list | | `operator/lib/screens/dashboard.dart` | Modify | Add Grants entry to `routes` list and `NavigationDestination` list |
--- ---
## Task 1: Add `Palette.token` ## Task 1: Add `Palette.token`
**Files:** **Files:**
- Modify: `useragent/lib/theme/palette.dart` - Modify: `operator/lib/theme/palette.dart`
- [ ] **Step 1: Add the color** - [ ] **Step 1: Add the color**
Replace the contents of `useragent/lib/theme/palette.dart` with: Replace the contents of `operator/lib/theme/palette.dart` with:
```dart ```dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -48,7 +48,7 @@ class Palette {
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && flutter analyze lib/theme/palette.dart cd operator && flutter analyze lib/theme/palette.dart
``` ```
Expected: no issues. Expected: no issues.
@@ -65,20 +65,20 @@ jj new
## Task 2: Add `listAllWalletAccesses` feature function ## Task 2: Add `listAllWalletAccesses` feature function
**Files:** **Files:**
- Modify: `useragent/lib/features/connection/evm/wallet_access.dart` - Modify: `operator/lib/features/connection/evm/wallet_access.dart`
`readClientWalletAccess` (existing) filters the list to one client's wallet IDs and returns `Set<int>`. This new function returns the complete unfiltered list with row IDs so the grant cards can resolve wallet_access_id → wallet + client. `readClientWalletAccess` (existing) filters the list to one client's wallet IDs and returns `Set<int>`. This new function returns the complete unfiltered list with row IDs so the grant cards can resolve wallet_access_id → wallet + client.
- [ ] **Step 1: Append function** - [ ] **Step 1: Append function**
Add at the bottom of `useragent/lib/features/connection/evm/wallet_access.dart`: Add at the bottom of `operator/lib/features/connection/evm/wallet_access.dart`:
```dart ```dart
Future<List<SdkClientWalletAccess>> listAllWalletAccesses( Future<List<SdkClientWalletAccess>> listAllWalletAccesses(
Connection connection, Connection connection,
) async { ) async {
final response = await connection.ask( final response = await connection.ask(
UserAgentRequest(listWalletAccess: Empty()), OperatorRequest(listWalletAccess: Empty()),
); );
if (!response.hasListWalletAccessResponse()) { if (!response.hasListWalletAccessResponse()) {
throw Exception( throw Exception(
@@ -97,7 +97,7 @@ Each returned `SdkClientWalletAccess` has:
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && flutter analyze lib/features/connection/evm/wallet_access.dart cd operator && flutter analyze lib/features/connection/evm/wallet_access.dart
``` ```
Expected: no issues. Expected: no issues.
@@ -114,18 +114,18 @@ jj new
## Task 3: Create `WalletAccessListProvider` ## Task 3: Create `WalletAccessListProvider`
**Files:** **Files:**
- Create: `useragent/lib/providers/sdk_clients/wallet_access_list.dart` - Create: `operator/lib/providers/sdk_clients/wallet_access_list.dart`
- Generated: `useragent/lib/providers/sdk_clients/wallet_access_list.g.dart` - Generated: `operator/lib/providers/sdk_clients/wallet_access_list.g.dart`
Mirrors the structure of `EvmGrants` in `providers/evm/evm_grants.dart` — class-based `@riverpod` with a `refresh()` method. Mirrors the structure of `EvmGrants` in `providers/evm/evm_grants.dart` — class-based `@riverpod` with a `refresh()` method.
- [ ] **Step 1: Write the provider** - [ ] **Step 1: Write the provider**
Create `useragent/lib/providers/sdk_clients/wallet_access_list.dart`: Create `operator/lib/providers/sdk_clients/wallet_access_list.dart`:
```dart ```dart
import 'package:arbiter/features/connection/evm/wallet_access.dart'; import 'package:arbiter/features/connection/evm/wallet_access.dart';
import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:arbiter/proto/operator.pb.dart';
import 'package:arbiter/providers/connection/connection_manager.dart'; import 'package:arbiter/providers/connection/connection_manager.dart';
import 'package:mtcore/markettakers.dart'; import 'package:mtcore/markettakers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -165,15 +165,15 @@ class WalletAccessList extends _$WalletAccessList {
- [ ] **Step 2: Run code generation** - [ ] **Step 2: Run code generation**
```sh ```sh
cd useragent && dart run build_runner build --delete-conflicting-outputs cd operator && dart run build_runner build --delete-conflicting-outputs
``` ```
Expected: `useragent/lib/providers/sdk_clients/wallet_access_list.g.dart` created. No errors. Expected: `operator/lib/providers/sdk_clients/wallet_access_list.g.dart` created. No errors.
- [ ] **Step 3: Verify** - [ ] **Step 3: Verify**
```sh ```sh
cd useragent && flutter analyze lib/providers/sdk_clients/ cd operator && flutter analyze lib/providers/sdk_clients/
``` ```
Expected: no issues. Expected: no issues.
@@ -190,26 +190,26 @@ jj new
## Task 4: Create `GrantCard` widget ## Task 4: Create `GrantCard` widget
**Files:** **Files:**
- Create: `useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart` - Create: `operator/lib/screens/dashboard/evm/grants/widgets/grant_card.dart`
This widget owns all per-card logic: enrichment lookups, revoke action, and rebuild scope. The screen only passes it a `GrantEntry` — the card fetches everything else itself. This widget owns all per-card logic: enrichment lookups, revoke action, and rebuild scope. The screen only passes it a `GrantEntry` — the card fetches everything else itself.
**Key types:** **Key types:**
- `GrantEntry` (from `proto/evm.pb.dart`): `.id`, `.shared.walletAccessId`, `.shared.chainId`, `.specific.whichGrant()` - `GrantEntry` (from `proto/evm.pb.dart`): `.id`, `.shared.walletAccessId`, `.shared.chainId`, `.specific.whichGrant()`
- `SpecificGrant_Grant.etherTransfer` / `.tokenTransfer` — enum values for the oneof - `SpecificGrant_Grant.etherTransfer` / `.tokenTransfer` — enum values for the oneof
- `SdkClientWalletAccess` (from `proto/user_agent.pb.dart`): `.id`, `.access.walletId`, `.access.sdkClientId` - `SdkClientWalletAccess` (from `proto/operator.pb.dart`): `.id`, `.access.walletId`, `.access.sdkClientId`
- `WalletEntry` (from `proto/evm.pb.dart`): `.id`, `.address` (List<int>) - `WalletEntry` (from `proto/evm.pb.dart`): `.id`, `.address` (List<int>)
- `SdkClientEntry` (from `proto/user_agent.pb.dart`): `.id`, `.info.name` - `SdkClientEntry` (from `proto/operator.pb.dart`): `.id`, `.info.name`
- `revokeEvmGrantMutation``Mutation<void>` (global; all revoke buttons disable together while any revoke is in flight) - `revokeEvmGrantMutation``Mutation<void>` (global; all revoke buttons disable together while any revoke is in flight)
- `executeRevokeEvmGrant(ref, grantId: int)``Future<void>` - `executeRevokeEvmGrant(ref, grantId: int)``Future<void>`
- [ ] **Step 1: Write the widget** - [ ] **Step 1: Write the widget**
Create `useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart`: Create `operator/lib/screens/dashboard/evm/grants/widgets/grant_card.dart`:
```dart ```dart
import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:arbiter/proto/operator.pb.dart';
import 'package:arbiter/providers/evm/evm.dart'; import 'package:arbiter/providers/evm/evm.dart';
import 'package:arbiter/providers/evm/evm_grants.dart'; import 'package:arbiter/providers/evm/evm_grants.dart';
import 'package:arbiter/providers/sdk_clients/list.dart'; import 'package:arbiter/providers/sdk_clients/list.dart';
@@ -438,7 +438,7 @@ class GrantCard extends ConsumerWidget {
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && flutter analyze lib/screens/dashboard/evm/grants/widgets/grant_card.dart cd operator && flutter analyze lib/screens/dashboard/evm/grants/widgets/grant_card.dart
``` ```
Expected: no issues. Expected: no issues.
@@ -455,13 +455,13 @@ jj new
## Task 5: Create `EvmGrantsScreen` ## Task 5: Create `EvmGrantsScreen`
**Files:** **Files:**
- Create: `useragent/lib/screens/dashboard/evm/grants/grants.dart` - Create: `operator/lib/screens/dashboard/evm/grants/grants.dart`
The screen watches only `evmGrantsProvider` for top-level state (loading / error / no connection / empty / data). When there is data it renders a list of `GrantCard` widgets — each card manages its own enrichment subscriptions. The screen watches only `evmGrantsProvider` for top-level state (loading / error / no connection / empty / data). When there is data it renders a list of `GrantCard` widgets — each card manages its own enrichment subscriptions.
- [ ] **Step 1: Write the screen** - [ ] **Step 1: Write the screen**
Create `useragent/lib/screens/dashboard/evm/grants/grants.dart`: Create `operator/lib/screens/dashboard/evm/grants/grants.dart`:
```dart ```dart
import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/proto/evm.pb.dart';
@@ -702,7 +702,7 @@ class EvmGrantsScreen extends ConsumerWidget {
- [ ] **Step 2: Verify** - [ ] **Step 2: Verify**
```sh ```sh
cd useragent && flutter analyze lib/screens/dashboard/evm/grants/ cd operator && flutter analyze lib/screens/dashboard/evm/grants/
``` ```
Expected: no issues. Expected: no issues.
@@ -719,13 +719,13 @@ jj new
## Task 6: Wire router and dashboard tab ## Task 6: Wire router and dashboard tab
**Files:** **Files:**
- Modify: `useragent/lib/router.dart` - Modify: `operator/lib/router.dart`
- Modify: `useragent/lib/screens/dashboard.dart` - Modify: `operator/lib/screens/dashboard.dart`
- Regenerated: `useragent/lib/router.gr.dart` - Regenerated: `operator/lib/router.gr.dart`
- [ ] **Step 1: Add route to `router.dart`** - [ ] **Step 1: Add route to `router.dart`**
Replace the contents of `useragent/lib/router.dart` with: Replace the contents of `operator/lib/router.dart` with:
```dart ```dart
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@@ -759,7 +759,7 @@ class Router extends RootStackRouter {
- [ ] **Step 2: Update `dashboard.dart`** - [ ] **Step 2: Update `dashboard.dart`**
In `useragent/lib/screens/dashboard.dart`, replace the `routes` constant: In `operator/lib/screens/dashboard.dart`, replace the `routes` constant:
```dart ```dart
final routes = [ final routes = [
@@ -800,7 +800,7 @@ destinations: const [
- [ ] **Step 3: Regenerate router** - [ ] **Step 3: Regenerate router**
```sh ```sh
cd useragent && dart run build_runner build --delete-conflicting-outputs cd operator && dart run build_runner build --delete-conflicting-outputs
``` ```
Expected: `lib/router.gr.dart` updated, `EvmGrantsRoute` now available, no errors. Expected: `lib/router.gr.dart` updated, `EvmGrantsRoute` now available, no errors.
@@ -808,7 +808,7 @@ Expected: `lib/router.gr.dart` updated, `EvmGrantsRoute` now available, no error
- [ ] **Step 4: Full project verify** - [ ] **Step 4: Full project verify**
```sh ```sh
cd useragent && flutter analyze cd operator && flutter analyze
``` ```
Expected: no issues. Expected: no issues.

View File

@@ -4,7 +4,7 @@
## Overview ## Overview
Add a "Grants" dashboard tab to the Flutter user-agent app that displays all EVM grants as a card-based grid. Each card shows a compact summary (type, chain, wallet address, client name) with a revoke action. The tab integrates into the existing `AdaptiveScaffold` navigation alongside Wallets, Clients, and About. Add a "Grants" dashboard tab to the Flutter operator app that displays all EVM grants as a card-based grid. Each card shows a compact summary (type, chain, wallet address, client name) with a revoke action. The tab integrates into the existing `AdaptiveScaffold` navigation alongside Wallets, Clients, and About.
## Scope ## Scope
@@ -23,7 +23,7 @@ Add a "Grants" dashboard tab to the Flutter user-agent app that displays all EVM
### `walletAccessListProvider` ### `walletAccessListProvider`
**File:** `useragent/lib/providers/sdk_clients/wallet_access_list.dart` **File:** `operator/lib/providers/sdk_clients/wallet_access_list.dart`
- `@riverpod` class, watches `connectionManagerProvider.future` - `@riverpod` class, watches `connectionManagerProvider.future`
- Returns `List<SdkClientWalletAccess>?` (null when not connected) - Returns `List<SdkClientWalletAccess>?` (null when not connected)
@@ -85,7 +85,7 @@ NavigationDestination(
## Screen: `EvmGrantsScreen` ## Screen: `EvmGrantsScreen`
**File:** `useragent/lib/screens/dashboard/evm/grants/grants.dart` **File:** `operator/lib/screens/dashboard/evm/grants/grants.dart`
``` ```
Scaffold Scaffold

View File

@@ -1,51 +1,51 @@
# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html # @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
[[tools.ast-grep]] [[tools.ast-grep]]
version = "0.42.0" version = "0.42.1"
backend = "aqua:ast-grep/ast-grep" backend = "aqua:ast-grep/ast-grep"
[tools.ast-grep."platforms.linux-arm64"] [tools.ast-grep."platforms.linux-arm64"]
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836" checksum = "sha256:3ba383839044cf9817929435f5ce0027f91d06931e8efb32d942e58d73d92be5"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-aarch64-unknown-linux-gnu.zip"
[tools.ast-grep."platforms.linux-arm64-musl"] [tools.ast-grep."platforms.linux-arm64-musl"]
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836" checksum = "sha256:3ba383839044cf9817929435f5ce0027f91d06931e8efb32d942e58d73d92be5"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-aarch64-unknown-linux-gnu.zip"
[tools.ast-grep."platforms.linux-x64"] [tools.ast-grep."platforms.linux-x64"]
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651" checksum = "sha256:5de8b87cba67fc8dc3e239d54b6484802ad745a7ae3de76be4fe89661dc52657"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-x86_64-unknown-linux-gnu.zip"
[tools.ast-grep."platforms.linux-x64-musl"] [tools.ast-grep."platforms.linux-x64-musl"]
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651" checksum = "sha256:5de8b87cba67fc8dc3e239d54b6484802ad745a7ae3de76be4fe89661dc52657"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-x86_64-unknown-linux-gnu.zip"
[tools.ast-grep."platforms.macos-arm64"] [tools.ast-grep."platforms.macos-arm64"]
checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620" checksum = "sha256:c3961d8e8a4ee0ce2d0d98c7beeb168bb331cdc766b53630118a7b6c4fd39015"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-aarch64-apple-darwin.zip"
[tools.ast-grep."platforms.macos-x64"] [tools.ast-grep."platforms.macos-x64"]
checksum = "sha256:979ffe611327056f4730a1ae71b0209b3b830f58b22c6ed194cda34f55400db2" checksum = "sha256:a038965bfd7fe44257c771cdf8918dc3467dd8ec0eef673b8b14f639b144cdbd"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-apple-darwin.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-x86_64-apple-darwin.zip"
[tools.ast-grep."platforms.windows-x64"] [tools.ast-grep."platforms.windows-x64"]
checksum = "sha256:55836fa1b2c65dc7d61615a4d9368622a0d2371a76d28b9a165e5a3ab6ae32a4" checksum = "sha256:fe34f631bb24c08ad146f92ca2a92971a53d179461b509fd8d32dc863bff9f83"
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-pc-windows-msvc.zip" url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.1/app-x86_64-pc-windows-msvc.zip"
[[tools."cargo:cargo-audit"]] [[tools."cargo:cargo-audit"]]
version = "0.22.1" version = "0.22.1"
backend = "cargo:cargo-audit" backend = "cargo:cargo-audit"
[[tools."cargo:cargo-edit"]] [[tools."cargo:cargo-edit"]]
version = "0.13.9" version = "0.13.10"
backend = "cargo:cargo-edit" backend = "cargo:cargo-edit"
[[tools."cargo:cargo-features-manager"]] [[tools."cargo:cargo-features-manager"]]
version = "0.11.1" version = "0.12.0"
backend = "cargo:cargo-features-manager" backend = "cargo:cargo-features-manager"
[[tools."cargo:cargo-insta"]] [[tools."cargo:cargo-insta"]]
version = "1.46.3" version = "1.47.2"
backend = "cargo:cargo-insta" backend = "cargo:cargo-insta"
[[tools."cargo:cargo-mutants"]] [[tools."cargo:cargo-mutants"]]
@@ -53,7 +53,7 @@ version = "27.0.0"
backend = "cargo:cargo-mutants" backend = "cargo:cargo-mutants"
[[tools."cargo:cargo-nextest"]] [[tools."cargo:cargo-nextest"]]
version = "0.9.126" version = "0.9.133"
backend = "cargo:cargo-nextest" backend = "cargo:cargo-nextest"
[[tools."cargo:cargo-shear"]] [[tools."cargo:cargo-shear"]]
@@ -65,7 +65,7 @@ version = "0.10.2"
backend = "cargo:cargo-vet" backend = "cargo:cargo-vet"
[[tools."cargo:diesel_cli"]] [[tools."cargo:diesel_cli"]]
version = "2.3.6" version = "2.3.7"
backend = "cargo:diesel_cli" backend = "cargo:diesel_cli"
[tools."cargo:diesel_cli".options] [tools."cargo:diesel_cli".options]
@@ -77,7 +77,7 @@ version = "2.12.0"
backend = "cargo:flutter_rust_bridge_codegen" backend = "cargo:flutter_rust_bridge_codegen"
[[tools.flutter]] [[tools.flutter]]
version = "3.38.9-stable" version = "3.41.7-stable"
backend = "asdf:flutter" backend = "asdf:flutter"
[[tools.protoc]] [[tools.protoc]]
@@ -113,44 +113,44 @@ checksum = "sha256:1ebd7c87baffb9f1c47169b640872bf5fb1e4408079c691af527be9561d8f
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-win64.zip" url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-win64.zip"
[[tools.python]] [[tools.python]]
version = "3.14.3" version = "3.14.4"
backend = "core:python" backend = "core:python"
[tools.python."platforms.linux-arm64"] [tools.python."platforms.linux-arm64"]
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be" checksum = "sha256:b8b597fdb2f8dccdc502c11947b60a4b65eb6bce79cfa60c7ccf9b6e8352c60a"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.linux-arm64-musl"] [tools.python."platforms.linux-arm64-musl"]
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be" checksum = "sha256:b8b597fdb2f8dccdc502c11947b60a4b65eb6bce79cfa60c7ccf9b6e8352c60a"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.linux-x64"] [tools.python."platforms.linux-x64"]
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39" checksum = "sha256:fe9a9c32d13870af632cbac3dfc7528ae53597e94472aa4c7d6a42e8166136cd"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.linux-x64-musl"] [tools.python."platforms.linux-x64-musl"]
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39" checksum = "sha256:fe9a9c32d13870af632cbac3dfc7528ae53597e94472aa4c7d6a42e8166136cd"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.macos-arm64"] [tools.python."platforms.macos-arm64"]
checksum = "sha256:c43aecde4a663aebff99b9b83da0efec506479f1c3f98331442f33d2c43501f9" checksum = "blake3:0314ec66e0f33ec04959583b5900bc8edae371a396aa96b8874e750d1fe936e6"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-apple-darwin-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-aarch64-apple-darwin-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.macos-x64"] [tools.python."platforms.macos-x64"]
checksum = "sha256:9ab41dbc2f100a2a45d1833b9c11165f51051c558b5213eda9a9731d5948a0c0" checksum = "sha256:d51250a32fa5d9f0799c7bcb71720c27b10a3afd4a7de288120f96085d508a5a"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-apple-darwin-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-x86_64-apple-darwin-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[tools.python."platforms.windows-x64"] [tools.python."platforms.windows-x64"]
checksum = "sha256:bbe19034b35b0267176a7442575ae7dc6343480fd4d35598cb7700173d431e09" checksum = "sha256:a976991dcd085c1bb5d9a8084823a6bc8b7f9b079d8c432574a6ddd68c3a6fe1"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-pc-windows-msvc-install_only_stripped.tar.gz" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.14.4+20260414-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
provenance = "github-attestations" provenance = "github-attestations"
[[tools.rust]] [[tools.rust]]
version = "1.93.0" version = "1.95.0"
backend = "core:rust" backend = "core:rust"

View File

@@ -1,17 +1,17 @@
[tools] [tools]
"cargo:diesel_cli" = { version = "2.3.6", features = "sqlite,sqlite-bundled", default-features = false } "cargo:diesel_cli" = { version = "2.3.7", features = "sqlite,sqlite-bundled", default-features = "false" }
"cargo:cargo-audit" = "0.22.1" "cargo:cargo-audit" = "0.22.1"
"cargo:cargo-vet" = "0.10.2" "cargo:cargo-vet" = "0.10.2"
flutter = "3.38.9-stable" flutter = "3.41.7-stable"
protoc = "29.6" protoc = "29.6"
"rust" = {version = "1.93.0", components = "clippy,rust-analyzer"} rust = { version = "1.95.0", components = "clippy,rust-analyzer" }
"cargo:cargo-features-manager" = "0.11.1" "cargo:cargo-features-manager" = "0.12.0"
"cargo:cargo-nextest" = "0.9.126" "cargo:cargo-nextest" = "0.9.133"
"cargo:cargo-shear" = "latest" "cargo:cargo-shear" = "latest"
"cargo:cargo-insta" = "1.46.3" "cargo:cargo-insta" = "1.47.2"
python = "3.14.3" python = "3.14.4"
ast-grep = "0.42.0" ast-grep = "0.42.1"
"cargo:cargo-edit" = "0.13.9" "cargo:cargo-edit" = "0.13.10"
"cargo:cargo-mutants" = "27.0.0" "cargo:cargo-mutants" = "27.0.0"
"cargo:flutter_rust_bridge_codegen" = "2.12.0" "cargo:flutter_rust_bridge_codegen" = "2.12.0"

View File

@@ -3,7 +3,7 @@ syntax = "proto3";
package arbiter; package arbiter;
import "client.proto"; import "client.proto";
import "user_agent.proto"; import "operator.proto";
message ServerInfo { message ServerInfo {
string version = 1; string version = 1;
@@ -12,5 +12,5 @@ message ServerInfo {
service ArbiterService { service ArbiterService {
rpc Client(stream arbiter.client.ClientRequest) returns (stream arbiter.client.ClientResponse); rpc Client(stream arbiter.client.ClientRequest) returns (stream arbiter.client.ClientResponse);
rpc UserAgent(stream arbiter.user_agent.UserAgentRequest) returns (stream arbiter.user_agent.UserAgentResponse); rpc Operator(stream arbiter.operator.OperatorRequest) returns (stream arbiter.operator.OperatorResponse);
} }

View File

@@ -24,7 +24,7 @@ enum AuthResult {
AUTH_RESULT_INVALID_KEY = 2; AUTH_RESULT_INVALID_KEY = 2;
AUTH_RESULT_INVALID_SIGNATURE = 3; AUTH_RESULT_INVALID_SIGNATURE = 3;
AUTH_RESULT_APPROVAL_DENIED = 4; AUTH_RESULT_APPROVAL_DENIED = 4;
AUTH_RESULT_NO_USER_AGENTS_ONLINE = 5; AUTH_RESULT_NO_OPERATORS_ONLINE = 5;
AUTH_RESULT_INTERNAL = 6; AUTH_RESULT_INTERNAL = 6;
} }

View File

@@ -75,7 +75,7 @@ message SpecificGrant {
} }
} }
// --- UserAgent grant management --- // --- Operator grant management ---
message EvmGrantCreateRequest { message EvmGrantCreateRequest {
SharedSettings shared = 1; SharedSettings shared = 1;
SpecificGrant specific = 2; SpecificGrant specific = 2;

View File

@@ -1,13 +1,13 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent; package arbiter.operator;
import "user_agent/auth.proto"; import "operator/auth.proto";
import "user_agent/evm.proto"; import "operator/evm.proto";
import "user_agent/sdk_client.proto"; import "operator/sdk_client.proto";
import "user_agent/vault/vault.proto"; import "operator/vault/vault.proto";
message UserAgentRequest { message OperatorRequest {
int32 id = 16; int32 id = 16;
oneof payload { oneof payload {
auth.Request auth = 1; auth.Request auth = 1;
@@ -17,7 +17,7 @@ message UserAgentRequest {
} }
} }
message UserAgentResponse { message OperatorResponse {
optional int32 id = 16; optional int32 id = 16;
oneof payload { oneof payload {
auth.Response auth = 1; auth.Response auth = 1;

View File

@@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.auth; package arbiter.operator.auth;
message AuthChallengeRequest { message AuthChallengeRequest {
bytes pubkey = 1; bytes pubkey = 1;

View File

@@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.evm; package arbiter.operator.evm;
import "evm.proto"; import "evm.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";

View File

@@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.sdk_client; package arbiter.operator.sdk_client;
import "shared/client.proto"; import "shared/client.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";

View File

@@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.vault.bootstrap; package arbiter.operator.vault.bootstrap;
message BootstrapEncryptedKey { message BootstrapEncryptedKey {
bytes nonce = 1; bytes nonce = 1;

View File

@@ -1,6 +1,6 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.vault.unseal; package arbiter.operator.vault.unseal;
message UnsealStart { message UnsealStart {
bytes client_pubkey = 1; bytes client_pubkey = 1;

View File

@@ -1,11 +1,11 @@
syntax = "proto3"; syntax = "proto3";
package arbiter.user_agent.vault; package arbiter.operator.vault;
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "shared/vault.proto"; import "shared/vault.proto";
import "user_agent/vault/bootstrap.proto"; import "operator/vault/bootstrap.proto";
import "user_agent/vault/unseal.proto"; import "operator/vault/unseal.proto";
message Request { message Request {
oneof payload { oneof payload {

View File

@@ -0,0 +1,2 @@
[env]
MACOSX_DEPLOYMENT_TARGET = "26.3"

631
server/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,30 +6,27 @@ resolver = "3"
[workspace.dependencies] [workspace.dependencies]
alloy = "2.0.0" alloy = "2.0.4"
async-trait = "0.1.89" async-trait = "0.1.89"
base64 = "0.22.1" base64 = "0.22.1"
chrono = { version = "0.4.44", features = ["serde"] } chrono = { version = "0.4.44", features = ["serde"] }
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
futures = "0.3.32" futures = "0.3.32"
k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] } k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
kameo = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"} kameo = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"}
kameo_actors = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"} kameo_actors = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"}
hmac = "0.13.0" hmac = "0.13.0"
miette = { version = "7.6.0", features = ["fancy", "serde"] } miette = { version = "7.6.0", features = ["fancy", "serde"] }
ml-dsa = { version = "0.1.0-rc.8", features = ["zeroize"] } ml-dsa = { version = "0.1.0-rc.9", features = ["zeroize"] }
mutants = "0.0.4" mutants = "0.0.4"
prost = "0.14.3" prost = "0.14.3"
prost-types = { version = "0.14.3", features = ["chrono"] } prost-types = { version = "0.14.3", features = ["chrono"] }
rand = "0.10.1" rand = "0.10.1"
rcgen = { version = "0.14.7", features = [ "aws_lc_rs", "pem", "x509-parser", "zeroize" ], default-features = false } rcgen = { version = "0.14.7", features = [ "aws_lc_rs", "pem", "x509-parser", "zeroize" ], default-features = false }
rsa = { version = "0.9", features = ["sha2"] }
rstest = "0.26.1" rstest = "0.26.1"
rustls = { version = "0.23.38", features = ["aws-lc-rs", "logging", "prefer-post-quantum", "std"], default-features = false } rustls = { version = "0.23.40", features = ["aws-lc-rs", "logging", "prefer-post-quantum", "std"], default-features = false }
rustls-pki-types = "1.14.0" rustls-pki-types = "1.14.1"
sha2 = "0.11" sha2 = "0.11"
smlang = "0.8.0" smlang = "0.8.0"
spki = "0.8"
thiserror = "2.0.18" thiserror = "2.0.18"
tokio = { version = "1.52.1", features = ["full"] } tokio = { version = "1.52.1", features = ["full"] }
tokio-stream = { version = "0.1.18", features = ["full"] } tokio-stream = { version = "0.1.18", features = ["full"] }
@@ -79,6 +76,7 @@ needless_pass_by_ref_mut = "allow"
pub_underscore_fields = "allow" pub_underscore_fields = "allow"
redundant_pub_crate = "allow" redundant_pub_crate = "allow"
uninhabited_references = "allow" # safe with unsafe_code = "forbid" and standard uninhabited pattern (match *self {}) uninhabited_references = "allow" # safe with unsafe_code = "forbid" and standard uninhabited pattern (match *self {})
too-many-lines = "allow" # this is a very common pattern in server code, and it's not always possible to break it down into smaller modules without hurting readability
# restriction lints # restriction lints
alloc_instead_of_core = "warn" alloc_instead_of_core = "warn"

View File

@@ -21,7 +21,9 @@ tokio.workspace = true
tokio-stream.workspace = true tokio-stream.workspace = true
thiserror.workspace = true thiserror.workspace = true
http = "1.4.0" http = "1.4.0"
rustls-webpki = { version = "0.103.12", features = ["aws-lc-rs"] } rustls-webpki = { version = "0.103.13", features = ["aws-lc-rs"] }
async-trait.workspace = true async-trait.workspace = true
rand.workspace = true
chrono.workspace = true chrono.workspace = true
[lib]
doctest = false

View File

@@ -26,13 +26,13 @@ use chrono::DateTime;
pub enum AuthError { pub enum AuthError {
#[error("Server sent invalid auth challenge")] #[error("Server sent invalid auth challenge")]
InvalidChallenge, InvalidChallenge,
#[error("Client approval denied by User Agent")] #[error("Client approval denied by Operator")]
ApprovalDenied, ApprovalDenied,
#[error("Auth challenge was not returned by server")] #[error("Auth challenge was not returned by server")]
MissingAuthChallenge, MissingAuthChallenge,
#[error("No User Agents online to approve client")] #[error("No Operators online to approve client")]
NoUserAgentsOnline, NoOperatorsOnline,
#[error("Signing key storage error")] #[error("Signing key storage error")]
Storage(#[from] StorageError), Storage(#[from] StorageError),
@@ -44,7 +44,7 @@ pub enum AuthError {
fn map_auth_result(code: i32) -> AuthError { fn map_auth_result(code: i32) -> AuthError {
match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) { match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
AuthResult::ApprovalDenied => AuthError::ApprovalDenied, AuthResult::ApprovalDenied => AuthError::ApprovalDenied,
AuthResult::NoUserAgentsOnline => AuthError::NoUserAgentsOnline, AuthResult::NoOperatorsOnline => AuthError::NoOperatorsOnline,
AuthResult::Unspecified AuthResult::Unspecified
| AuthResult::Success | AuthResult::Success
| AuthResult::InvalidKey | AuthResult::InvalidKey
@@ -100,7 +100,7 @@ async fn send_auth_challenge_solution(
key: &SigningKey, key: &SigningKey,
challenge: AuthChallenge, challenge: AuthChallenge,
) -> Result<(), AuthError> { ) -> Result<(), AuthError> {
let timestamp = DateTime::from_timestamp_nanos(challenge.timestamp_nanos as i64); let timestamp = DateTime::from_timestamp_nanos(challenge.timestamp_nanos.cast_signed());
let challenge = authn::AuthChallenge { let challenge = authn::AuthChallenge {
nonce: *challenge nonce: *challenge
.random .random

View File

@@ -20,3 +20,6 @@ workspace = true
default = ["authn", "safecell"] default = ["authn", "safecell"]
authn = ["dep:ml-dsa", "dep:rand"] authn = ["dep:ml-dsa", "dep:rand"]
safecell = ["dep:memsafe"] safecell = ["dep:memsafe"]
[lib]
doctest = false

View File

@@ -7,7 +7,7 @@ use ml_dsa::{
use rand::RngExt; use rand::RngExt;
pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client"; pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client";
pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent"; pub static OPERATOR_CONTEXT: &[u8] = b"arbiter_operator";
const NONCE_SIZE: usize = 32; const NONCE_SIZE: usize = 32;
@@ -192,7 +192,7 @@ mod tests {
use crate::authn::AuthChallenge; use crate::authn::AuthChallenge;
use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT}; use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, OPERATOR_CONTEXT};
#[test] #[test]
fn public_key_round_trip_decodes() { fn public_key_round_trip_decodes() {
@@ -227,7 +227,7 @@ mod tests {
.expect("signature should be created"); .expect("signature should be created");
assert!(public_key.verify(&challenge, CLIENT_CONTEXT, &signature)); assert!(public_key.verify(&challenge, CLIENT_CONTEXT, &signature));
assert!(!public_key.verify(&challenge, USERAGENT_CONTEXT, &signature)); assert!(!public_key.verify(&challenge, OPERATOR_CONTEXT, &signature));
} }
#[test] #[test]

View File

@@ -5,6 +5,7 @@ edition = "2024"
[lib] [lib]
proc-macro = true proc-macro = true
doctest = false
[dependencies] [dependencies]
proc-macro2 = "1.0" proc-macro2 = "1.0"

View File

@@ -9,7 +9,6 @@ license = "Apache-2.0"
tonic.workspace = true tonic.workspace = true
tokio.workspace = true tokio.workspace = true
futures.workspace = true futures.workspace = true
hex = "0.4.3"
tonic-prost = "0.14.5" tonic-prost = "0.14.5"
prost.workspace = true prost.workspace = true
kameo.workspace = true kameo.workspace = true
@@ -19,18 +18,18 @@ thiserror.workspace = true
rustls-pki-types.workspace = true rustls-pki-types.workspace = true
base64.workspace = true base64.workspace = true
prost-types.workspace = true prost-types.workspace = true
tracing.workspace = true
async-trait.workspace = true async-trait.workspace = true
tokio-stream.workspace = true tokio-stream.workspace = true
[build-dependencies] [build-dependencies]
tonic-prost-build = "0.14.5" tonic-prost-build = "0.14.5"
protoc-bin-vendored = "3"
[dev-dependencies] [dev-dependencies]
rstest.workspace = true rstest.workspace = true
rand.workspace = true
rcgen.workspace = true rcgen.workspace = true
[lib]
doctest = false
[package.metadata.cargo-shear] [package.metadata.cargo-shear]
ignored = ["tonic-prost", "prost", "kameo"] ignored = ["tonic-prost", "prost"]

View File

@@ -10,7 +10,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.compile_protos( .compile_protos(
&[ &[
format!("{}/arbiter.proto", PROTOBUF_DIR), format!("{}/arbiter.proto", PROTOBUF_DIR),
format!("{}/user_agent.proto", PROTOBUF_DIR), format!("{}/operator.proto", PROTOBUF_DIR),
format!("{}/client.proto", PROTOBUF_DIR), format!("{}/client.proto", PROTOBUF_DIR),
format!("{}/evm.proto", PROTOBUF_DIR), format!("{}/evm.proto", PROTOBUF_DIR),
], ],

View File

@@ -12,30 +12,30 @@ pub mod proto {
} }
} }
pub mod user_agent { pub mod operator {
tonic::include_proto!("arbiter.user_agent"); tonic::include_proto!("arbiter.operator");
pub mod auth { pub mod auth {
tonic::include_proto!("arbiter.user_agent.auth"); tonic::include_proto!("arbiter.operator.auth");
} }
pub mod evm { pub mod evm {
tonic::include_proto!("arbiter.user_agent.evm"); tonic::include_proto!("arbiter.operator.evm");
} }
pub mod sdk_client { pub mod sdk_client {
tonic::include_proto!("arbiter.user_agent.sdk_client"); tonic::include_proto!("arbiter.operator.sdk_client");
} }
pub mod vault { pub mod vault {
tonic::include_proto!("arbiter.user_agent.vault"); tonic::include_proto!("arbiter.operator.vault");
pub mod bootstrap { pub mod bootstrap {
tonic::include_proto!("arbiter.user_agent.vault.bootstrap"); tonic::include_proto!("arbiter.operator.vault.bootstrap");
} }
pub mod unseal { pub mod unseal {
tonic::include_proto!("arbiter.user_agent.vault.unseal"); tonic::include_proto!("arbiter.operator.vault.unseal");
} }
} }
} }

View File

@@ -9,8 +9,8 @@ license = "Apache-2.0"
workspace = true workspace = true
[dependencies] [dependencies]
diesel = { version = "2.3.7", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] } diesel = { version = "2.3.9", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] }
diesel-async = { version = "0.8.0", features = [ diesel-async = { version = "0.9.0", features = [
"bb8", "bb8",
"migrations", "migrations",
"sqlite", "sqlite",
@@ -27,17 +27,12 @@ tokio.workspace = true
rustls.workspace = true rustls.workspace = true
smlang.workspace = true smlang.workspace = true
thiserror.workspace = true thiserror.workspace = true
fatality = "0.1.1" diesel_migrations = { version = "2.3.2", features = ["sqlite"] }
diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
async-trait.workspace = true async-trait.workspace = true
secrecy = "0.10.3"
futures.workspace = true
tokio-stream.workspace = true tokio-stream.workspace = true
dashmap = "6.1.0"
rand.workspace = true rand.workspace = true
rcgen.workspace = true rcgen.workspace = true
chrono.workspace = true chrono.workspace = true
zeroize = { version = "1.8.2", features = ["std", "simd"] }
kameo.workspace = true kameo.workspace = true
chacha20poly1305 = { version = "0.10.1", features = ["std"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] }
argon2 = { version = "0.5.3", features = ["zeroize"] } argon2 = { version = "0.5.3", features = ["zeroize"] }
@@ -46,23 +41,21 @@ strum = { version = "0.28.0", features = ["derive"] }
pem = "3.0.6" pem = "3.0.6"
sha2.workspace = true sha2.workspace = true
hmac.workspace = true hmac.workspace = true
spki.workspace = true
alloy.workspace = true alloy.workspace = true
prost-types.workspace = true prost-types.workspace = true
prost.workspace = true
arbiter-tokens-registry.path = "../arbiter-tokens-registry" arbiter-tokens-registry.path = "../arbiter-tokens-registry"
anyhow = "1.0.102" anyhow = "1.0.102"
serde_with = "3.18.0"
mutants.workspace = true mutants.workspace = true
subtle = "2.6.1" subtle = "2.6.1"
ml-dsa.workspace = true
ed25519-dalek.workspace = true
x25519-dalek.workspace = true x25519-dalek.workspace = true
k256.workspace = true k256.workspace = true
kameo_actors.workspace = true kameo_actors.workspace = true
[dev-dependencies] [dev-dependencies]
insta = "1.47.2"
proptest = "1.11.0" proptest = "1.11.0"
rstest.workspace = true rstest.workspace = true
test-log = { version = "0.2", default-features = false, features = ["trace"] } test-log = { version = "0.2", default-features = false, features = ["trace"] }
ml-dsa.workspace = true
[lib]
doctest = false

View File

@@ -43,13 +43,13 @@ create table if not exists arbiter_settings (
insert into arbiter_settings (id) values (1) on conflict do nothing; insert into arbiter_settings (id) values (1) on conflict do nothing;
-- ensure singleton row exists -- ensure singleton row exists
create table if not exists useragent_client ( create table if not exists operator_client (
id integer not null primary key, id integer not null primary key,
public_key blob not null, public_key blob not null,
created_at integer not null default(unixepoch ('now')), created_at integer not null default(unixepoch ('now')),
updated_at integer not null default(unixepoch ('now')) updated_at integer not null default(unixepoch ('now'))
) STRICT; ) STRICT;
create unique index if not exists uniq_useragent_client_public_key on useragent_client (public_key); create unique index if not exists uniq_operator_client_public_key on operator_client (public_key);
create table if not exists client_metadata ( create table if not exists client_metadata (
id integer not null primary key, id integer not null primary key,

View File

@@ -48,7 +48,7 @@ impl Bootstrapper {
let row_count: i64 = { let row_count: i64 = {
let mut conn = db.get().await?; let mut conn = db.get().await?;
schema::useragent_client::table schema::operator_client::table
.count() .count()
.get_result(&mut conn) .get_result(&mut conn)
.await? .await?

View File

@@ -134,7 +134,7 @@ impl EvmActor {
#[messages] #[messages]
impl EvmActor { impl EvmActor {
#[message] #[message]
pub async fn useragent_create_grant( pub async fn operator_create_grant(
&mut self, &mut self,
basic: SharedGrantSettings, basic: SharedGrantSettings,
grant: SpecificGrant, grant: SpecificGrant,
@@ -160,33 +160,18 @@ impl EvmActor {
} }
#[message] #[message]
#[expect(clippy::unused_async, reason = "reserved for impl")] pub async fn useragent_delete_grant(
pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> { &mut self,
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?; grant_id: i32,
// let vault = self.vault.clone(); ) -> Result<(), Error> {
self.engine
// diesel_async::AsyncConnection::transaction(&mut conn, |conn| { .revoke_grant(grant_id)
// Box::pin(async move { .await
// diesel::update(schema::evm_basic_grant::table) .map_err(Error::from)
// .filter(schema::evm_basic_grant::id.eq(grant_id))
// .set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
// .execute(conn)
// .await?;
// let signed = integrity::evm::load_signed_grant_by_basic_id(conn, grant_id).await?;
// diesel::result::QueryResult::Ok(())
// })
// })
// .await
// .map_err(DatabaseError::from)?;
// Ok(())
todo!()
} }
#[message] #[message]
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> { pub async fn operator_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
match self.engine.list_all_grants().await { match self.engine.list_all_grants().await {
Ok(grants) => Ok(grants), Ok(grants) => Ok(grants),
Err(ListError::Database(db_err)) => Err(Error::Database(db_err)), Err(ListError::Database(db_err)) => Err(Error::Database(db_err)),

View File

@@ -2,7 +2,7 @@ use crate::{
actors::flow_coordinator::ApprovalError, actors::flow_coordinator::ApprovalError,
peers::{ peers::{
client::ClientProfile, client::ClientProfile,
user_agent::{UserAgentSession, session::BeginNewClientApproval}, operator::{OperatorSession, session::BeginNewClientApproval},
}, },
}; };
@@ -15,12 +15,12 @@ use std::ops::ControlFlow;
pub struct Args { pub struct Args {
pub client: ClientProfile, pub client: ClientProfile,
pub user_agents: Vec<ActorRef<UserAgentSession>>, pub operators: Vec<ActorRef<OperatorSession>>,
pub reply: ReplySender<Result<bool, ApprovalError>>, pub reply: ReplySender<Result<bool, ApprovalError>>,
} }
pub struct ClientApprovalController { pub struct ClientApprovalController {
/// Number of UAs that have not yet responded (approval or denial) or died. /// Number of operators that have not yet responded (approval or denial) or died.
pending: usize, pending: usize,
/// Number of approvals received so far. /// Number of approvals received so far.
approved: usize, approved: usize,
@@ -42,21 +42,21 @@ impl Actor for ClientApprovalController {
async fn on_start( async fn on_start(
Args { Args {
client, client,
user_agents, operators,
reply, reply,
}: Self::Args, }: Self::Args,
actor_ref: ActorRef<Self>, actor_ref: ActorRef<Self>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let this = Self { let this = Self {
pending: user_agents.len(), pending: operators.len(),
approved: 0, approved: 0,
reply: Some(reply), reply: Some(reply),
}; };
for user_agent in user_agents { for operator in operators {
actor_ref.link(&user_agent).await; actor_ref.link(&operator).await;
let _ = user_agent let _ = operator
.tell(BeginNewClientApproval { .tell(BeginNewClientApproval {
client: client.clone(), client: client.clone(),
controller: actor_ref.clone(), controller: actor_ref.clone(),
@@ -73,10 +73,10 @@ impl Actor for ClientApprovalController {
_: ActorId, _: ActorId,
_: ActorStopReason, _: ActorStopReason,
) -> Result<ControlFlow<ActorStopReason>, Self::Error> { ) -> Result<ControlFlow<ActorStopReason>, Self::Error> {
// A linked UA died before responding — counts as a non-approval. // A linked operator died before responding — counts as a non-approval.
self.pending = self.pending.saturating_sub(1); self.pending = self.pending.saturating_sub(1);
if self.pending == 0 { if self.pending == 0 {
// At least one UA didn't approve: deny. // At least one operator didn't approve: deny.
self.send_reply(Ok(false)); self.send_reply(Ok(false));
return Ok(ControlFlow::Break(ActorStopReason::Normal)); return Ok(ControlFlow::Break(ActorStopReason::Normal));
} }
@@ -99,7 +99,7 @@ impl ClientApprovalController {
self.pending = self.pending.saturating_sub(1); self.pending = self.pending.saturating_sub(1);
if self.pending == 0 { if self.pending == 0 {
// Every connected UA approved. // Every connected operator approved.
self.send_reply(Ok(true)); self.send_reply(Ok(true));
ctx.stop(); ctx.stop();
} }

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
actors::{ actors::{
flow_coordinator::client_connect_approval::ClientApprovalController, flow_coordinator::client_connect_approval::ClientApprovalController,
useragent_registry::{GetConnected, UserAgentRegistry}, operator_registry::{GetConnected, OperatorRegistry},
}, },
peers::client::{ClientProfile, session::ClientSession}, peers::client::{ClientProfile, session::ClientSession},
}; };
@@ -20,14 +20,14 @@ pub mod client_connect_approval;
pub struct FlowCoordinator { pub struct FlowCoordinator {
pub clients: HashMap<ActorId, ActorRef<ClientSession>>, pub clients: HashMap<ActorId, ActorRef<ClientSession>>,
useragent_registry: ActorRef<UserAgentRegistry>, operator_registry: ActorRef<OperatorRegistry>,
} }
impl FlowCoordinator { impl FlowCoordinator {
pub fn new(useragent_registry: ActorRef<UserAgentRegistry>) -> Self { pub fn new(operator_registry: ActorRef<OperatorRegistry>) -> Self {
Self { Self {
clients: HashMap::default(), clients: HashMap::default(),
useragent_registry, operator_registry,
} }
} }
} }
@@ -66,8 +66,8 @@ impl Actor for FlowCoordinator {
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq, Hash)] #[derive(Debug, thiserror::Error, Clone, PartialEq, Eq, Hash)]
pub enum ApprovalError { pub enum ApprovalError {
#[error("No user agents connected")] #[error("No operators connected")]
NoUserAgentsConnected, NoOperatorsConnected,
} }
#[messages] #[messages]
@@ -93,19 +93,19 @@ impl FlowCoordinator {
unreachable!("Expected `request_client_approval` to have callback channel"); unreachable!("Expected `request_client_approval` to have callback channel");
}; };
let Ok(refs) = self.useragent_registry.ask(GetConnected).await else { let Ok(refs) = self.operator_registry.ask(GetConnected).await else {
reply_sender.send(Err(ApprovalError::NoUserAgentsConnected)); reply_sender.send(Err(ApprovalError::NoOperatorsConnected));
return reply; return reply;
}; };
if refs.is_empty() { if refs.is_empty() {
reply_sender.send(Err(ApprovalError::NoUserAgentsConnected)); reply_sender.send(Err(ApprovalError::NoOperatorsConnected));
return reply; return reply;
} }
ClientApprovalController::spawn(client_connect_approval::Args { ClientApprovalController::spawn(client_connect_approval::Args {
client, client,
user_agents: refs, operators: refs,
reply: reply_sender, reply: reply_sender,
}); });

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
actors::{ actors::{
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator, bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator,
useragent_registry::UserAgentRegistry, vault::Vault, operator_registry::OperatorRegistry, vault::Vault,
}, },
db, db,
}; };
@@ -13,7 +13,7 @@ use thiserror::Error;
pub mod bootstrap; pub mod bootstrap;
pub mod evm; pub mod evm;
pub mod flow_coordinator; pub mod flow_coordinator;
pub mod useragent_registry; pub mod operator_registry;
pub mod vault; pub mod vault;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@@ -31,7 +31,7 @@ pub struct GlobalActors {
pub vault: ActorRef<Vault>, pub vault: ActorRef<Vault>,
pub bootstrapper: ActorRef<Bootstrapper>, pub bootstrapper: ActorRef<Bootstrapper>,
pub flow_coordinator: ActorRef<FlowCoordinator>, pub flow_coordinator: ActorRef<FlowCoordinator>,
pub useragent_registry: ActorRef<UserAgentRegistry>, pub operator_registry: ActorRef<OperatorRegistry>,
pub evm: ActorRef<EvmActor>, pub evm: ActorRef<EvmActor>,
pub events: ActorRef<MessageBus>, pub events: ActorRef<MessageBus>,
} }
@@ -44,15 +44,15 @@ impl GlobalActors {
pub async fn spawn(db: db::DatabasePool) -> Result<Self, SpawnError> { pub async fn spawn(db: db::DatabasePool) -> Result<Self, SpawnError> {
let message_bus = Self::spawn_message_bus(); let message_bus = Self::spawn_message_bus();
let key_holder = Vault::spawn(Vault::new(db.clone(), message_bus.clone()).await?); let key_holder = Vault::spawn(Vault::new(db.clone(), message_bus.clone()).await?);
let useragent_registry = UserAgentRegistry::spawn(UserAgentRegistry::default()); let operator_registry = OperatorRegistry::spawn(OperatorRegistry::default());
Ok(Self { Ok(Self {
bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?), bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?),
evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)), evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)),
vault: key_holder, vault: key_holder,
flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::new( flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::new(
useragent_registry.clone(), operator_registry.clone(),
)), )),
useragent_registry, operator_registry,
events: message_bus, events: message_bus,
}) })
} }

View File

@@ -1,4 +1,4 @@
use crate::peers::user_agent::UserAgentSession; use crate::peers::operator::OperatorSession;
use kameo::{ use kameo::{
Actor, Actor,
@@ -11,11 +11,11 @@ use std::{collections::HashMap, ops::ControlFlow};
use tracing::info; use tracing::info;
#[derive(Default)] #[derive(Default)]
pub struct UserAgentRegistry { pub struct OperatorRegistry {
connected: HashMap<ActorId, ActorRef<UserAgentSession>>, connected: HashMap<ActorId, ActorRef<OperatorSession>>,
} }
impl Actor for UserAgentRegistry { impl Actor for OperatorRegistry {
type Args = Self; type Args = Self;
type Error = Infallible; type Error = Infallible;
@@ -33,8 +33,8 @@ impl Actor for UserAgentRegistry {
if self.connected.remove(&id).is_some() { if self.connected.remove(&id).is_some() {
info!( info!(
?id, ?id,
actor = "UserAgentRegistry", actor = "OperatorRegistry",
event = "useragent.disconnected" event = "operator.disconnected"
); );
} }
Ok(ControlFlow::Continue(())) Ok(ControlFlow::Continue(()))
@@ -42,20 +42,20 @@ impl Actor for UserAgentRegistry {
} }
#[messages] #[messages]
impl UserAgentRegistry { impl OperatorRegistry {
#[message(ctx)] #[message(ctx)]
pub async fn connect_useragent( pub async fn connect_operator(
&mut self, &mut self,
actor: ActorRef<UserAgentSession>, actor: ActorRef<OperatorSession>,
ctx: &mut Context<Self, ()>, ctx: &mut Context<Self, ()>,
) { ) {
info!(id = %actor.id(), actor = "UserAgentRegistry", event = "useragent.connected"); info!(id = %actor.id(), actor = "OperatorRegistry", event = "operator.connected");
ctx.actor_ref().link(&actor).await; ctx.actor_ref().link(&actor).await;
self.connected.insert(actor.id(), actor); self.connected.insert(actor.id(), actor);
} }
#[message] #[message]
pub fn get_connected(&self) -> Vec<ActorRef<UserAgentSession>> { pub fn get_connected(&self) -> Vec<ActorRef<OperatorSession>> {
self.connected.values().cloned().collect() self.connected.values().cloned().collect()
} }
} }

View File

@@ -119,31 +119,29 @@ impl Vault {
let mut conn = pool.get().await?; let mut conn = pool.get().await?;
let nonce = conn let nonce = conn
.exclusive_transaction(|conn| { .exclusive_transaction(async |conn| {
Box::pin(async move { let current_nonce: Vec<u8> = schema::root_key_history::table
let current_nonce: Vec<u8> = schema::root_key_history::table .filter(schema::root_key_history::id.eq(root_key_id))
.filter(schema::root_key_history::id.eq(root_key_id)) .select(schema::root_key_history::data_encryption_nonce)
.select(schema::root_key_history::data_encryption_nonce) .first(&mut *conn)
.first(conn) .await?;
.await?;
let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|()| { let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|()| {
error!( error!(
"Broken database: invalid nonce for root key history id={}", "Broken database: invalid nonce for root key history id={}",
root_key_id root_key_id
); );
Error::BrokenDatabase Error::BrokenDatabase
})?; })?;
nonce.increment(); nonce.increment();
update(schema::root_key_history::table) update(schema::root_key_history::table)
.filter(schema::root_key_history::id.eq(root_key_id)) .filter(schema::root_key_history::id.eq(root_key_id))
.set(schema::root_key_history::data_encryption_nonce.eq(nonce.to_vec())) .set(schema::root_key_history::data_encryption_nonce.eq(nonce.to_vec()))
.execute(conn) .execute(&mut *conn)
.await?; .await?;
Result::<_, Error>::Ok(nonce) Result::<_, Error>::Ok(nonce)
})
}) })
.await?; .await?;
@@ -185,28 +183,26 @@ impl Vault {
let data_encryption_nonce_bytes = data_encryption_nonce.to_vec(); let data_encryption_nonce_bytes = data_encryption_nonce.to_vec();
let root_key_history_id = conn let root_key_history_id = conn
.transaction(|conn| { .transaction(async |conn| {
Box::pin(async move { let root_key_history_id: i32 = insert_into(schema::root_key_history::table)
let root_key_history_id: i32 = insert_into(schema::root_key_history::table) .values(&models::NewRootKeyHistory {
.values(&models::NewRootKeyHistory { ciphertext: root_key_ciphertext.clone(),
ciphertext: root_key_ciphertext, tag: v1::ROOT_KEY_TAG.to_vec(),
tag: v1::ROOT_KEY_TAG.to_vec(), root_key_encryption_nonce: root_key_nonce.to_vec(),
root_key_encryption_nonce: root_key_nonce.to_vec(), data_encryption_nonce: data_encryption_nonce_bytes.clone(),
data_encryption_nonce: data_encryption_nonce_bytes, schema_version: 1,
schema_version: 1, salt: salt.to_vec(),
salt: salt.to_vec(), })
}) .returning(schema::root_key_history::id)
.returning(schema::root_key_history::id) .get_result(&mut *conn)
.get_result(conn) .await?;
.await?;
update(schema::arbiter_settings::table) update(schema::arbiter_settings::table)
.set(schema::arbiter_settings::root_key_id.eq(root_key_history_id)) .set(schema::arbiter_settings::root_key_id.eq(root_key_history_id))
.execute(conn) .execute(&mut *conn)
.await?; .await?;
Result::<_, diesel::result::Error>::Ok(root_key_history_id) Result::<_, diesel::result::Error>::Ok(root_key_history_id)
})
}) })
.await?; .await?;
@@ -350,12 +346,10 @@ impl Vault {
root_key_history_id, root_key_history_id,
} = Self::expect_unsealed(&mut self.state)?; } = Self::expect_unsealed(&mut self.state)?;
let mut hmac = root_key let mut hmac = root_key.0.read_inline(|k| {
.0 HmacSha256::new_from_slice(k)
.read_inline(|k| match HmacSha256::new_from_slice(k) { .unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size"))
Ok(v) => v, });
Err(_) => unreachable!("HMAC accepts keys of any size"),
});
hmac.update(&root_key_history_id.to_be_bytes()); hmac.update(&root_key_history_id.to_be_bytes());
hmac.update(&mac_input); hmac.update(&mac_input);
@@ -379,12 +373,10 @@ impl Vault {
return Ok(false); return Ok(false);
} }
let mut hmac = root_key let mut hmac = root_key.0.read_inline(|k| {
.0 HmacSha256::new_from_slice(k)
.read_inline(|k| match HmacSha256::new_from_slice(k) { .unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size"))
Ok(v) => v, });
Err(_) => unreachable!("HMAC accepts keys of any size"),
});
hmac.update(&key_version.to_be_bytes()); hmac.update(&key_version.to_be_bytes());
hmac.update(&mac_input); hmac.update(&mac_input);
@@ -427,12 +419,13 @@ mod tests {
async fn nonce_monotonic_even_when_nonce_allocation_interleaves() { async fn nonce_monotonic_even_when_nonce_allocation_interleaves() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = bootstrapped_actor(&db).await; let mut actor = bootstrapped_actor(&db).await;
let root_key_history_id = match actor.state {
State::Unsealed(Unsealed { let State::Unsealed(Unsealed {
root_key_history_id, root_key_history_id,
.. ..
}) => root_key_history_id, }) = actor.state
_ => panic!("expected unsealed state"), else {
panic!("expected unsealed state")
}; };
let n1 = Vault::get_new_nonce(&db, root_key_history_id) let n1 = Vault::get_new_nonce(&db, root_key_history_id)
@@ -444,8 +437,8 @@ mod tests {
assert!(n2.to_vec() > n1.to_vec(), "nonce must increase"); assert!(n2.to_vec() > n1.to_vec(), "nonce must increase");
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let root_row: models::RootKeyHistory = schema::root_key_history::table let root_row: RootKeyHistory = schema::root_key_history::table
.select(models::RootKeyHistory::as_select()) .select(RootKeyHistory::as_select())
.first(&mut conn) .first(&mut conn)
.await .await
.unwrap(); .unwrap();

View File

@@ -174,28 +174,26 @@ impl TlsManager {
{ {
let mut conn = db.get().await?; let mut conn = db.get().await?;
conn.transaction(|conn| { conn.transaction(async |conn| {
Box::pin(async { let new_tls_history = NewTlsHistory {
let new_tls_history = NewTlsHistory { cert: new_cert.cert.pem(),
cert: new_cert.cert.pem(), cert_key: new_cert.cert_key.serialize_pem(),
cert_key: new_cert.cert_key.serialize_pem(), ca_cert: encode_cert_to_pem(&ca.cert),
ca_cert: encode_cert_to_pem(&ca.cert), ca_key: ca.issuer.key().serialize_pem(),
ca_key: ca.issuer.key().serialize_pem(), };
};
let inserted_tls_history: i32 = diesel::insert_into(tls_history::table) let inserted_tls_history: i32 = diesel::insert_into(tls_history::table)
.values(&new_tls_history) .values(&new_tls_history)
.returning(tls_history::id) .returning(tls_history::id)
.get_result(conn) .get_result(&mut *conn)
.await?; .await?;
diesel::update(arbiter_settings::table) diesel::update(arbiter_settings::table)
.set(arbiter_settings::tls_id.eq(inserted_tls_history)) .set(arbiter_settings::tls_id.eq(inserted_tls_history))
.execute(conn) .execute(&mut *conn)
.await?; .await?;
Result::<_, diesel::result::Error>::Ok(()) Result::<_, diesel::result::Error>::Ok(())
})
}) })
.await?; .await?;
} }

View File

@@ -248,8 +248,8 @@ pub struct ProgramClient {
} }
#[derive(Queryable, Debug)] #[derive(Queryable, Debug)]
#[diesel(table_name = schema::useragent_client, check_for_backend(Sqlite))] #[diesel(table_name = schema::operator_client, check_for_backend(Sqlite))]
pub struct UseragentClient { pub struct OperatorClient {
pub id: i32, pub id: i32,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,

View File

@@ -186,7 +186,7 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
useragent_client (id) { operator_client (id) {
id -> Integer, id -> Integer,
public_key -> Binary, public_key -> Binary,
created_at -> Integer, created_at -> Integer,
@@ -233,5 +233,5 @@ diesel::allow_tables_to_appear_in_same_query!(
program_client, program_client,
root_key_history, root_key_history,
tls_history, tls_history,
useragent_client, operator_client,
); );

View File

@@ -1,28 +1,34 @@
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::actor::ActorRef;
use crate::{ use crate::{
actors::vault::Vault, actors::vault::Vault,
crypto::integrity, crypto::integrity,
db::{ db::{
self, DatabaseError, self, DatabaseError,
models::{ models::{
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp, EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget,
EvmEtherTransferLimit, EvmTokenTransferGrant, EvmTokenTransferVolumeLimit,
EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
}, },
schema::{self, evm_transaction_log}, schema::{self, evm_transaction_log},
}, },
evm::policies::{ evm::policies::{
CombinedSettings, DatabaseID, EvalContext, EvalViolation, Grant, Policy, CombinedSettings, DatabaseID, EvalContext, EvalViolation, Grant, Policy,
SharedGrantSettings, SpecificGrant, SpecificMeaning, ether_transfer::EtherTransfer, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
token_transfers::TokenTransfer, ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
}, },
}; };
use alloy::{ use alloy::{
consensus::TxEip1559, consensus::TxEip1559,
primitives::{TxKind, U256}, primitives::{Address, TxKind, U256},
}; };
use chrono::Utc; use chrono::Utc;
use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite}; use diesel::{
use diesel_async::{AsyncConnection, RunQueryDsl}; ExpressionMethods as _, OptionalExtension, QueryDsl as _, QueryResult, SelectableHelper,
use kameo::actor::ActorRef; insert_into, sqlite::Sqlite, update,
};
pub mod abi; pub mod abi;
pub mod safe_signer; pub mod safe_signer;
@@ -179,24 +185,22 @@ impl Engine {
} }
if run_kind == RunKind::Execution { if run_kind == RunKind::Execution {
conn.transaction(|conn| { conn.transaction(async |conn| {
Box::pin(async move { let log_id: i32 = insert_into(evm_transaction_log::table)
let log_id: i32 = insert_into(evm_transaction_log::table) .values(&NewEvmTransactionLog {
.values(&NewEvmTransactionLog { grant_id: grant.common_settings_id,
grant_id: grant.common_settings_id, wallet_access_id: context.target.id,
wallet_access_id: context.target.id, chain_id: context.chain.into(),
chain_id: context.chain.into(), eth_value: utils::u256_to_bytes(context.value).to_vec(),
eth_value: utils::u256_to_bytes(context.value).to_vec(), signed_at: Utc::now().into(),
signed_at: Utc::now().into(), })
}) .returning(evm_transaction_log::id)
.returning(evm_transaction_log::id) .get_result(&mut *conn)
.get_result(conn) .await?;
.await?;
P::record_transaction(&context, meaning, log_id, &grant, conn).await?; P::record_transaction(&context, meaning, log_id, &grant, &mut *conn).await?;
QueryResult::Ok(()) QueryResult::Ok(())
})
}) })
.await .await
.map_err(DatabaseError::from)?; .map_err(DatabaseError::from)?;
@@ -222,60 +226,203 @@ impl Engine {
let vault = self.vault.clone(); let vault = self.vault.clone();
let id = conn let id = conn
.transaction(|conn| { .transaction(async |conn| {
Box::pin(async move { use schema::evm_basic_grant;
use schema::evm_basic_grant;
#[expect( #[expect(
clippy::cast_possible_truncation, clippy::cast_possible_truncation,
clippy::cast_possible_wrap, clippy::cast_possible_wrap,
clippy::as_conversions, clippy::as_conversions,
reason = "fixme! #86" reason = "fixme! #86"
)] )]
let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table) let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table)
.values(&NewEvmBasicGrant { .values(&NewEvmBasicGrant {
chain_id: full_grant.shared.chain.into(), chain_id: full_grant.shared.chain.into(),
wallet_access_id: full_grant.shared.wallet_access_id, wallet_access_id: full_grant.shared.wallet_access_id,
valid_from: full_grant.shared.valid_from.map(SqliteTimestamp), valid_from: full_grant.shared.valid_from.map(SqliteTimestamp),
valid_until: full_grant.shared.valid_until.map(SqliteTimestamp), valid_until: full_grant.shared.valid_until.map(SqliteTimestamp),
max_gas_fee_per_gas: full_grant max_gas_fee_per_gas: full_grant
.shared .shared
.max_gas_fee_per_gas .max_gas_fee_per_gas
.map(|fee| utils::u256_to_bytes(fee).to_vec()), .map(|fee| utils::u256_to_bytes(fee).to_vec()),
max_priority_fee_per_gas: full_grant max_priority_fee_per_gas: full_grant
.shared .shared
.max_priority_fee_per_gas .max_priority_fee_per_gas
.map(|fee| utils::u256_to_bytes(fee).to_vec()), .map(|fee| utils::u256_to_bytes(fee).to_vec()),
rate_limit_count: full_grant rate_limit_count: full_grant
.shared .shared
.rate_limit .rate_limit
.as_ref() .as_ref()
.map(|rl| rl.count as i32), .map(|rl| rl.count as i32),
rate_limit_window_secs: full_grant rate_limit_window_secs: full_grant
.shared .shared
.rate_limit .rate_limit
.as_ref() .as_ref()
.map(|rl| rl.window.num_seconds() as i32), .map(|rl| rl.window.num_seconds() as i32),
revoked_at: None, revoked_at: None,
}) })
.returning(evm_basic_grant::all_columns) .returning(evm_basic_grant::all_columns)
.get_result(conn) .get_result(&mut *conn)
.await?; .await?;
P::create_grant(&basic_grant, &full_grant.specific, conn).await?; P::create_grant(&basic_grant, &full_grant.specific, &mut *conn).await?;
integrity::sign_entity(conn, &vault, &full_grant, basic_grant.id) integrity::sign_entity(&mut *conn, &vault, &full_grant, basic_grant.id)
.await .await
.map_err(|_| diesel::result::Error::RollbackTransaction)?; .map_err(|_| diesel::result::Error::RollbackTransaction)?;
QueryResult::Ok(basic_grant.id) QueryResult::Ok(basic_grant.id)
})
}) })
.await?; .await?;
Ok(id) Ok(id)
} }
pub async fn revoke_grant(
&self,
basic_grant_id: i32,
) -> Result<(), DatabaseError> {
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
let vault = self.vault.clone();
conn.transaction(async move |conn| {
use crate::db::schema::{
evm_basic_grant, evm_ether_transfer_grant, evm_ether_transfer_grant_target,
evm_ether_transfer_limit, evm_token_transfer_grant,
evm_token_transfer_volume_limit,
};
update(evm_basic_grant::table)
.filter(evm_basic_grant::id.eq(basic_grant_id))
.set(evm_basic_grant::revoked_at.eq(SqliteTimestamp(Utc::now())))
.execute(&mut *conn)
.await?;
let basic_grant: EvmBasicGrant = evm_basic_grant::table
.filter(evm_basic_grant::id.eq(basic_grant_id))
.select(EvmBasicGrant::as_select())
.first(&mut *conn)
.await?;
let shared = SharedGrantSettings::try_from_model(basic_grant)?;
if let Some(ether_grant) = evm_ether_transfer_grant::table
.filter(evm_ether_transfer_grant::basic_grant_id.eq(basic_grant_id))
.select(EvmEtherTransferGrant::as_select())
.first(&mut *conn)
.await
.optional()?
{
let target_rows: Vec<EvmEtherTransferGrantTarget> =
evm_ether_transfer_grant_target::table
.filter(evm_ether_transfer_grant_target::grant_id.eq(ether_grant.id))
.select(EvmEtherTransferGrantTarget::as_select())
.load(&mut *conn)
.await?;
let targets: Vec<Address> = target_rows
.into_iter()
.filter_map(|target| {
let arr: [u8; 20] = target.address.try_into().ok()?;
Some(Address::from(arr))
})
.collect();
let limit: EvmEtherTransferLimit = evm_ether_transfer_limit::table
.filter(evm_ether_transfer_limit::id.eq(ether_grant.limit_id))
.select(EvmEtherTransferLimit::as_select())
.first(&mut *conn)
.await?;
let settings = CombinedSettings {
shared: shared.clone(),
specific: policies::ether_transfer::Settings {
target: targets,
limit: VolumeRateLimit {
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(
|err| {
diesel::result::Error::DeserializationError(Box::new(err))
},
)?,
window: chrono::Duration::seconds(limit.window_secs.into()),
},
},
};
integrity::sign_entity(&mut *conn, &vault, &settings, basic_grant_id)
.await
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
return QueryResult::Ok(());
}
if let Some(token_grant) = evm_token_transfer_grant::table
.filter(evm_token_transfer_grant::basic_grant_id.eq(basic_grant_id))
.select(EvmTokenTransferGrant::as_select())
.first(&mut *conn)
.await
.optional()?
{
let volume_limit_rows: Vec<EvmTokenTransferVolumeLimit> =
evm_token_transfer_volume_limit::table
.filter(evm_token_transfer_volume_limit::grant_id.eq(token_grant.id))
.select(EvmTokenTransferVolumeLimit::as_select())
.load(&mut *conn)
.await?;
let volume_limits: Vec<VolumeRateLimit> = volume_limit_rows
.into_iter()
.map(|row| {
Ok(VolumeRateLimit {
max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(
|err| {
diesel::result::Error::DeserializationError(Box::new(err))
},
)?,
window: chrono::Duration::seconds(row.window_secs.into()),
})
})
.collect::<QueryResult<Vec<_>>>()?;
let target: Option<Address> = match token_grant.receiver {
None => None,
Some(bytes) => {
let arr: [u8; 20] = bytes.try_into().map_err(|_| {
diesel::result::Error::DeserializationError(
"Invalid receiver address length".into(),
)
})?;
Some(Address::from(arr))
}
};
let token_contract: [u8; 20] =
token_grant.token_contract.clone().try_into().map_err(|_| {
diesel::result::Error::DeserializationError(
"Invalid token contract address length".into(),
)
})?;
let settings = CombinedSettings {
shared,
specific: policies::token_transfers::Settings {
token_contract: Address::from(token_contract),
target,
volume_limits,
},
};
integrity::sign_entity(&mut *conn, &vault, &settings, basic_grant_id)
.await
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
return QueryResult::Ok(());
}
Err(diesel::result::Error::NotFound)
})
.await
.map_err(DatabaseError::from)
}
async fn list_one_kind<Kind: Policy, Y>( async fn list_one_kind<Kind: Policy, Y>(
&self, &self,
conn: &mut impl AsyncConnection<Backend = Sqlite>, conn: &mut impl AsyncConnection<Backend = Sqlite>,
@@ -355,11 +502,15 @@ impl Engine {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use alloy::primitives::{Address, Bytes, U256, address}; use alloy::primitives::{Address, Bytes, U256, address};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use diesel::{SelectableHelper, insert_into}; use diesel::{SelectableHelper, insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use kameo::{actor::ActorRef, prelude::Spawn};
use rstest::rstest; use rstest::rstest;
use crate::actors::{GlobalActors, vault::{Bootstrap, Vault}};
use crate::crypto::integrity;
use crate::db::{ use crate::db::{
self, DatabaseConnection, self, DatabaseConnection,
models::{ models::{
@@ -367,8 +518,10 @@ mod tests {
}, },
schema::{evm_basic_grant, evm_transaction_log}, schema::{evm_basic_grant, evm_transaction_log},
}; };
use crate::evm::policies::ether_transfer::EtherTransfer;
use crate::evm::policies::{ use crate::evm::policies::{
EvalContext, EvalViolation, SharedGrantSettings, TransactionRateLimit, CombinedSettings, EvalContext, EvalViolation, Policy, SharedGrantSettings,
TransactionRateLimit, VolumeRateLimit,
}; };
use super::check_shared_constraints; use super::check_shared_constraints;
@@ -400,6 +553,7 @@ mod tests {
chain: CHAIN_ID, chain: CHAIN_ID,
valid_from: None, valid_from: None,
valid_until: None, valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None, max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None, max_priority_fee_per_gas: None,
rate_limit: None, rate_limit: None,
@@ -608,4 +762,115 @@ mod tests {
assert!(violations.is_empty()); assert!(violations.is_empty());
} }
} }
async fn bootstrapped_vault(db: &db::DatabasePool) -> ActorRef<Vault> {
let actor = Vault::spawn(
Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap(),
);
actor
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"integrity-test-seal-key".to_vec()),
})
.await
.unwrap();
actor
}
#[tokio::test]
async fn revoke_grant_preserves_revoked_integrity() {
use crate::db::schema::evm_basic_grant;
use diesel::ExpressionMethods as _;
let db = db::create_test_pool().await;
let vault = bootstrapped_vault(&db).await;
let engine = super::Engine::new(db.clone(), vault.clone());
let full_grant = CombinedSettings {
shared: SharedGrantSettings {
wallet_access_id: WALLET_ACCESS_ID,
chain: CHAIN_ID,
valid_from: None,
valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None,
rate_limit: None,
},
specific: super::policies::ether_transfer::Settings {
target: vec![RECIPIENT],
limit: VolumeRateLimit {
max_volume: U256::from(100u64),
window: Duration::hours(1),
},
},
};
let grant_id = engine
.create_grant::<EtherTransfer>(full_grant)
.await
.unwrap();
engine.revoke_grant(grant_id).await.unwrap();
let mut conn = db.get().await.unwrap();
diesel::update(evm_basic_grant::table)
.filter(evm_basic_grant::id.eq(grant_id))
.set(evm_basic_grant::revoked_at.eq::<Option<SqliteTimestamp>>(None))
.execute(&mut conn)
.await
.unwrap();
let wallet_access = EvmWalletAccess {
id: WALLET_ACCESS_ID,
wallet_id: 10,
client_id: 20,
created_at: SqliteTimestamp(Utc::now()),
};
let context = EvalContext {
target: wallet_access,
chain: CHAIN_ID,
to: RECIPIENT,
value: U256::ONE,
calldata: Bytes::new(),
max_fee_per_gas: 1,
max_priority_fee_per_gas: 1,
};
let grant = EtherTransfer::try_find_grant(
&context, &mut conn,
)
.await
.unwrap()
.unwrap();
let result =
integrity::verify_entity(&mut conn, &vault, &grant.settings, grant.id).await;
assert!(matches!(
result,
Err(integrity::Error::MacMismatch { .. })
));
}
#[test]
fn shared_settings_hash_changes_when_revoked_at_changes() {
use arbiter_crypto::hashing::Hashable;
use sha2::Digest;
let active = shared_settings();
let revoked = SharedGrantSettings {
revoked_at: Some(Utc::now()),
..shared_settings()
};
let mut active_hash = sha2::Sha256::new();
active.hash(&mut active_hash);
let mut revoked_hash = sha2::Sha256::new();
revoked.hash(&mut revoked_hash);
assert_ne!(active_hash.finalize(), revoked_hash.finalize());
}
} }

View File

@@ -144,6 +144,7 @@ pub struct SharedGrantSettings {
pub valid_from: Option<DateTime<Utc>>, pub valid_from: Option<DateTime<Utc>>,
pub valid_until: Option<DateTime<Utc>>, pub valid_until: Option<DateTime<Utc>>,
pub revoked_at: Option<DateTime<Utc>>,
pub max_gas_fee_per_gas: Option<U256>, pub max_gas_fee_per_gas: Option<U256>,
pub max_priority_fee_per_gas: Option<U256>, pub max_priority_fee_per_gas: Option<U256>,
@@ -158,6 +159,7 @@ impl SharedGrantSettings {
chain: model.chain_id.into(), chain: model.chain_id.into(),
valid_from: model.valid_from.map(Into::into), valid_from: model.valid_from.map(Into::into),
valid_until: model.valid_until.map(Into::into), valid_until: model.valid_until.map(Into::into),
revoked_at: model.revoked_at.map(Into::into),
max_gas_fee_per_gas: model max_gas_fee_per_gas: model
.max_gas_fee_per_gas .max_gas_fee_per_gas
.map(|b| utils::try_bytes_to_u256(&b)) .map(|b| utils::try_bytes_to_u256(&b))

View File

@@ -79,6 +79,7 @@ fn shared() -> SharedGrantSettings {
chain: CHAIN_ID, chain: CHAIN_ID,
valid_from: None, valid_from: None,
valid_until: None, valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None, max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None, max_priority_fee_per_gas: None,
rate_limit: None, rate_limit: None,

View File

@@ -98,6 +98,7 @@ fn shared() -> SharedGrantSettings {
chain: CHAIN_ID, chain: CHAIN_ID,
valid_from: None, valid_from: None,
valid_until: None, valid_until: None,
revoked_at: None,
max_gas_fee_per_gas: None, max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None, max_priority_fee_per_gas: None,
rate_limit: None, rate_limit: None,

View File

@@ -176,8 +176,8 @@ impl Convert for auth::Error {
InvalidChallengeSolution => ProtoAuthResult::InvalidSignature, InvalidChallengeSolution => ProtoAuthResult::InvalidSignature,
ApproveError(auth::ApproveError::Denied) => ProtoAuthResult::ApprovalDenied, ApproveError(auth::ApproveError::Denied) => ProtoAuthResult::ApprovalDenied,
ApproveError(auth::ApproveError::Upstream( ApproveError(auth::ApproveError::Upstream(
crate::actors::flow_coordinator::ApprovalError::NoUserAgentsConnected, crate::actors::flow_coordinator::ApprovalError::NoOperatorsConnected,
)) => ProtoAuthResult::NoUserAgentsOnline, )) => ProtoAuthResult::NoOperatorsOnline,
ApproveError(auth::ApproveError::Internal) ApproveError(auth::ApproveError::Internal)
| DatabasePoolUnavailable | DatabasePoolUnavailable
| DatabaseOperationFailed | DatabaseOperationFailed
@@ -200,7 +200,7 @@ impl Convert for auth::Outbound {
.timestamp .timestamp
.timestamp_nanos_opt() .timestamp_nanos_opt()
.expect("timestamp within range") .expect("timestamp within range")
as u64, .cast_unsigned(),
random: challenge.nonce.to_vec(), random: challenge.nonce.to_vec(),
}) })
} }

View File

@@ -1,8 +1,8 @@
use crate::peers::{client::ClientConnection, user_agent::UserAgentConnection}; use crate::peers::{client::ClientConnection, operator::OperatorConnection};
use arbiter_proto::{ use arbiter_proto::{
proto::{ proto::{
client::{ClientRequest, ClientResponse}, client::{ClientRequest, ClientResponse},
user_agent::{UserAgentRequest, UserAgentResponse}, operator::{OperatorRequest, OperatorResponse},
}, },
transport::grpc::GrpcBi, transport::grpc::GrpcBi,
}; };
@@ -14,7 +14,7 @@ use tracing::info;
mod request_tracker; mod request_tracker;
pub mod client; pub mod client;
pub mod user_agent; pub mod operator;
mod common; mod common;
@@ -33,7 +33,7 @@ pub trait TryConvert {
#[async_trait] #[async_trait]
impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Server { impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Server {
type UserAgentStream = ReceiverStream<Result<UserAgentResponse, Status>>; type OperatorStream = ReceiverStream<Result<OperatorResponse, Status>>;
type ClientStream = ReceiverStream<Result<ClientResponse, Status>>; type ClientStream = ReceiverStream<Result<ClientResponse, Status>>;
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
@@ -52,23 +52,23 @@ impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Ser
} }
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
async fn user_agent( async fn operator(
&self, &self,
request: Request<tonic::Streaming<UserAgentRequest>>, request: Request<tonic::Streaming<OperatorRequest>>,
) -> Result<Response<Self::UserAgentStream>, Status> { ) -> Result<Response<Self::OperatorStream>, Status> {
let req_stream = request.into_inner(); let req_stream = request.into_inner();
let (bi, rx) = GrpcBi::from_bi_stream(req_stream); let (bi, rx) = GrpcBi::from_bi_stream(req_stream);
tokio::spawn(user_agent::start( tokio::spawn(operator::start(
UserAgentConnection { OperatorConnection {
db: self.context.db.clone(), db: self.context.db.clone(),
actors: self.context.actors.clone(), actors: self.context.actors.clone(),
}, },
bi, bi,
)); ));
info!(event = "connection established", "grpc.user_agent"); info!(event = "connection established", "grpc.operator");
Ok(Response::new(rx)) Ok(Response::new(rx))
} }

View File

@@ -1,12 +1,12 @@
use crate::{ use crate::{
grpc::request_tracker::RequestTracker, grpc::request_tracker::RequestTracker,
peers::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession}, peers::operator::{OutOfBand, OperatorConnection, OperatorSession},
}; };
use arbiter_proto::{ use arbiter_proto::{
proto::user_agent::{ proto::operator::{
UserAgentRequest, UserAgentResponse, OperatorRequest, OperatorResponse,
user_agent_request::Payload as UserAgentRequestPayload, operator_request::Payload as OperatorRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
}, },
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi}, transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
}; };
@@ -38,8 +38,8 @@ impl Sender<OutOfBand> for OutOfBandAdapter {
} }
async fn dispatch_loop( async fn dispatch_loop(
mut bi: GrpcBi<UserAgentRequest, UserAgentResponse>, mut bi: GrpcBi<OperatorRequest, OperatorResponse>,
actor: ActorRef<UserAgentSession>, actor: ActorRef<OperatorSession>,
mut receiver: mpsc::Receiver<OutOfBand>, mut receiver: mpsc::Receiver<OutOfBand>,
mut request_tracker: RequestTracker, mut request_tracker: RequestTracker,
) { ) {
@@ -53,7 +53,7 @@ async fn dispatch_loop(
let payload = sdk_client::out_of_band_payload(oob); let payload = sdk_client::out_of_band_payload(oob);
if bi.send(Ok(UserAgentResponse { id: None, payload: Some(payload) })).await.is_err() { if bi.send(Ok(OperatorResponse { id: None, payload: Some(payload) })).await.is_err() {
return; return;
} }
} }
@@ -64,7 +64,7 @@ async fn dispatch_loop(
let conn = match message { let conn = match message {
Ok(conn) => conn, Ok(conn) => conn,
Err(err) => { Err(err) => {
warn!(error = ?err, "Failed to receive user agent request"); warn!(error = ?err, "Failed to receive operator request");
return; return;
} }
}; };
@@ -78,13 +78,13 @@ async fn dispatch_loop(
}; };
let Some(payload) = conn.payload else { let Some(payload) = conn.payload else {
let _ = bi.send(Err(Status::invalid_argument("Missing user-agent request payload"))).await; let _ = bi.send(Err(Status::invalid_argument("Missing operator request payload"))).await;
return; return;
}; };
match dispatch_inner(&actor, payload).await { match dispatch_inner(&actor, payload).await {
Ok(Some(response)) => { Ok(Some(response)) => {
if bi.send(Ok(UserAgentResponse { if bi.send(Ok(OperatorResponse {
id: Some(request_id), id: Some(request_id),
payload: Some(response), payload: Some(response),
})).await.is_err() { })).await.is_err() {
@@ -93,7 +93,7 @@ async fn dispatch_loop(
} }
Ok(None) => {} Ok(None) => {}
Err(status) => { Err(status) => {
error!(?status, "Failed to process user agent request"); error!(?status, "Failed to process operator request");
let _ = bi.send(Err(status)).await; let _ = bi.send(Err(status)).await;
return; return;
} }
@@ -104,23 +104,23 @@ async fn dispatch_loop(
} }
async fn dispatch_inner( async fn dispatch_inner(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
payload: UserAgentRequestPayload, payload: OperatorRequestPayload,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
match payload { match payload {
UserAgentRequestPayload::Vault(req) => vault::dispatch(actor, req).await, OperatorRequestPayload::Vault(req) => vault::dispatch(actor, req).await,
UserAgentRequestPayload::Evm(req) => evm::dispatch(actor, req).await, OperatorRequestPayload::Evm(req) => evm::dispatch(actor, req).await,
UserAgentRequestPayload::SdkClient(req) => sdk_client::dispatch(actor, req).await, OperatorRequestPayload::SdkClient(req) => sdk_client::dispatch(actor, req).await,
UserAgentRequestPayload::Auth(..) => { OperatorRequestPayload::Auth(..) => {
warn!("Unsupported post-auth user agent auth request"); warn!("Unsupported post-auth operator auth request");
Err(Status::invalid_argument("Unsupported user-agent request")) Err(Status::invalid_argument("Unsupported operator request"))
} }
} }
} }
pub async fn start( pub async fn start(
mut conn: UserAgentConnection, mut conn: OperatorConnection,
mut bi: GrpcBi<UserAgentRequest, UserAgentResponse>, mut bi: GrpcBi<OperatorRequest, OperatorResponse>,
) { ) {
let mut request_tracker = RequestTracker::default(); let mut request_tracker = RequestTracker::default();
@@ -129,16 +129,16 @@ pub async fn start(
let actor = { let actor = {
let transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker); let transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker);
match crate::peers::user_agent::start(&mut conn, transport, Box::new(oob_adapter)).await { match crate::peers::operator::start(&mut conn, transport, Box::new(oob_adapter)).await {
Ok(actor) => actor, Ok(actor) => actor,
Err(e) => { Err(e) => {
warn!(error = ?e, "User agent connection failed"); warn!(error = ?e, "Operator connection failed");
return; return;
} }
} }
}; };
info!("User agent session established"); info!("Operator session established");
dispatch_loop(bi, actor.clone(), oob_receiver, request_tracker).await; dispatch_loop(bi, actor.clone(), oob_receiver, request_tracker).await;
actor.kill(); actor.kill();

View File

@@ -1,16 +1,16 @@
use crate::{grpc::request_tracker::RequestTracker, peers::user_agent::auth}; use crate::{grpc::request_tracker::RequestTracker, peers::operator::auth};
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_proto::{ use arbiter_proto::{
proto::user_agent::{ proto::operator::{
UserAgentRequest, UserAgentResponse, OperatorRequest, OperatorResponse,
auth::{ auth::{
self as proto_auth, AuthChallenge as ProtoAuthChallenge, self as proto_auth, AuthChallenge as ProtoAuthChallenge,
AuthChallengeRequest as ProtoAuthChallengeRequest, AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
request::Payload as AuthRequestPayload, response::Payload as AuthResponsePayload, request::Payload as AuthRequestPayload, response::Payload as AuthResponsePayload,
}, },
user_agent_request::Payload as UserAgentRequestPayload, operator_request::Payload as OperatorRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
}, },
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
}; };
@@ -20,13 +20,13 @@ use tonic::Status;
use tracing::warn; use tracing::warn;
pub(super) struct AuthTransportAdapter<'a> { pub(super) struct AuthTransportAdapter<'a> {
pub(super) bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>, pub(super) bi: &'a mut GrpcBi<OperatorRequest, OperatorResponse>,
pub(super) request_tracker: &'a mut RequestTracker, pub(super) request_tracker: &'a mut RequestTracker,
} }
impl<'a> AuthTransportAdapter<'a> { impl<'a> AuthTransportAdapter<'a> {
pub(super) const fn new( pub(super) const fn new(
bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>, bi: &'a mut GrpcBi<OperatorRequest, OperatorResponse>,
request_tracker: &'a mut RequestTracker, request_tracker: &'a mut RequestTracker,
) -> Self { ) -> Self {
Self { Self {
@@ -35,7 +35,7 @@ impl<'a> AuthTransportAdapter<'a> {
} }
} }
pub(super) const fn bi_mut(&mut self) -> &mut GrpcBi<UserAgentRequest, UserAgentResponse> { pub(super) const fn bi_mut(&mut self) -> &mut GrpcBi<OperatorRequest, OperatorResponse> {
self.bi self.bi
} }
@@ -45,21 +45,21 @@ impl<'a> AuthTransportAdapter<'a> {
pub(super) async fn send_response_payload( pub(super) async fn send_response_payload(
&mut self, &mut self,
payload: UserAgentResponsePayload, payload: OperatorResponsePayload,
) -> Result<(), TransportError> { ) -> Result<(), TransportError> {
self.bi self.bi
.send(Ok(UserAgentResponse { .send(Ok(OperatorResponse {
id: Some(self.request_tracker.current_request_id()), id: Some(self.request_tracker.current_request_id()),
payload: Some(payload), payload: Some(payload),
})) }))
.await .await
} }
async fn send_user_agent_response( async fn send_operator_response(
&mut self, &mut self,
payload: AuthResponsePayload, payload: AuthResponsePayload,
) -> Result<(), TransportError> { ) -> Result<(), TransportError> {
self.send_response_payload(UserAgentResponsePayload::Auth(proto_auth::Response { self.send_response_payload(OperatorResponsePayload::Auth(proto_auth::Response {
payload: Some(payload), payload: Some(payload),
})) }))
.await .await
@@ -80,7 +80,7 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
.timestamp .timestamp
.timestamp_nanos_opt() .timestamp_nanos_opt()
.expect("timestamp within range") .expect("timestamp within range")
as u64, .cast_unsigned(),
random: challenge.nonce.to_vec(), random: challenge.nonce.to_vec(),
}) })
} }
@@ -107,7 +107,7 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
} }
}; };
self.send_user_agent_response(payload).await self.send_operator_response(payload).await
} }
} }
@@ -117,7 +117,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
let request = match self.bi.recv().await? { let request = match self.bi.recv().await? {
Ok(request) => request, Ok(request) => request,
Err(error) => { Err(error) => {
warn!(error = ?error, "Failed to receive user agent auth request"); warn!(error = ?error, "Failed to receive operator auth request");
return None; return None;
} }
}; };
@@ -133,16 +133,16 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
let Some(payload) = request.payload else { let Some(payload) = request.payload else {
warn!( warn!(
event = "received request with empty payload", event = "received request with empty payload",
"grpc.useragent.auth_adapter" "grpc.operator.auth_adapter"
); );
return None; return None;
}; };
let UserAgentRequestPayload::Auth(auth_request) = payload else { let OperatorRequestPayload::Auth(auth_request) = payload else {
let _ = self let _ = self
.bi .bi
.send(Err(Status::invalid_argument( .send(Err(Status::invalid_argument(
"Unsupported user-agent auth request", "Unsupported operator auth request",
))) )))
.await; .await;
return None; return None;
@@ -151,7 +151,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
let Some(payload) = auth_request.payload else { let Some(payload) = auth_request.payload else {
warn!( warn!(
event = "received auth request with empty payload", event = "received auth request with empty payload",
"grpc.useragent.auth_adapter" "grpc.operator.auth_adapter"
); );
return None; return None;
}; };
@@ -164,7 +164,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else { let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else {
warn!( warn!(
event = "received request with invalid public key", event = "received request with invalid public key",
"grpc.useragent.auth_adapter" "grpc.operator.auth_adapter"
); );
return None; return None;
}; };

View File

@@ -3,8 +3,8 @@ use crate::{
Convert, TryConvert, Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction}, common::inbound::{RawEvmAddress, RawEvmTransaction},
}, },
peers::user_agent::{ peers::operator::{
UserAgentSession, OperatorSession,
session::handlers::{ session::handlers::{
GrantMutationError, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate, GrantMutationError, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate,
HandleGrantDelete, HandleGrantList, HandleSignTransaction, HandleGrantDelete, HandleGrantList, HandleSignTransaction,
@@ -24,12 +24,12 @@ use arbiter_proto::proto::{
wallet_create_response::Result as WalletCreateResult, wallet_create_response::Result as WalletCreateResult,
wallet_list_response::Result as WalletListResult, wallet_list_response::Result as WalletListResult,
}, },
user_agent::{ operator::{
evm::{ evm::{
self as proto_evm, SignTransactionRequest as ProtoSignTransactionRequest, self as proto_evm, SignTransactionRequest as ProtoSignTransactionRequest,
request::Payload as EvmRequestPayload, response::Payload as EvmResponsePayload, request::Payload as EvmRequestPayload, response::Payload as EvmResponsePayload,
}, },
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
}, },
}; };
@@ -37,16 +37,16 @@ use kameo::actor::ActorRef;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
const fn wrap_evm_response(payload: EvmResponsePayload) -> UserAgentResponsePayload { const fn wrap_evm_response(payload: EvmResponsePayload) -> OperatorResponsePayload {
UserAgentResponsePayload::Evm(proto_evm::Response { OperatorResponsePayload::Evm(proto_evm::Response {
payload: Some(payload), payload: Some(payload),
}) })
} }
pub(super) async fn dispatch( pub(super) async fn dispatch(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: proto_evm::Request, req: proto_evm::Request,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let Some(payload) = req.payload else { let Some(payload) = req.payload else {
return Err(Status::invalid_argument("Missing EVM request payload")); return Err(Status::invalid_argument("Missing EVM request payload"));
}; };
@@ -62,8 +62,8 @@ pub(super) async fn dispatch(
} }
async fn handle_wallet_create( async fn handle_wallet_create(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let result = match actor.ask(HandleEvmWalletCreate {}).await { let result = match actor.ask(HandleEvmWalletCreate {}).await {
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry { Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
id: wallet_id, id: wallet_id,
@@ -82,8 +82,8 @@ async fn handle_wallet_create(
} }
async fn handle_wallet_list( async fn handle_wallet_list(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let result = match actor.ask(HandleEvmWalletList {}).await { let result = match actor.ask(HandleEvmWalletList {}).await {
Ok(wallets) => WalletListResult::Wallets(WalletList { Ok(wallets) => WalletListResult::Wallets(WalletList {
wallets: wallets wallets: wallets
@@ -107,8 +107,8 @@ async fn handle_wallet_list(
} }
async fn handle_grant_list( async fn handle_grant_list(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let result = match actor.ask(HandleGrantList {}).await { let result = match actor.ask(HandleGrantList {}).await {
Ok(grants) => EvmGrantListResult::Grants(EvmGrantList { Ok(grants) => EvmGrantListResult::Grants(EvmGrantList {
grants: grants grants: grants
@@ -134,9 +134,9 @@ async fn handle_grant_list(
} }
async fn handle_grant_create( async fn handle_grant_create(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: EvmGrantCreateRequest, req: EvmGrantCreateRequest,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let basic = req let basic = req
.shared .shared
.ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))? .ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))?
@@ -164,9 +164,9 @@ async fn handle_grant_create(
} }
async fn handle_grant_delete( async fn handle_grant_delete(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: EvmGrantDeleteRequest, req: EvmGrantDeleteRequest,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let result = match actor let result = match actor
.ask(HandleGrantDelete { .ask(HandleGrantDelete {
grant_id: req.grant_id, grant_id: req.grant_id,
@@ -190,9 +190,9 @@ async fn handle_grant_delete(
} }
async fn handle_sign_transaction( async fn handle_sign_transaction(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: ProtoSignTransactionRequest, req: ProtoSignTransactionRequest,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let request = req let request = req
.request .request
.ok_or_else(|| Status::invalid_argument("Missing sign transaction request"))?; .ok_or_else(|| Status::invalid_argument("Missing sign transaction request"))?;

View File

@@ -14,7 +14,7 @@ use arbiter_proto::{
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
specific_grant::Grant as ProtoSpecificGrantType, specific_grant::Grant as ProtoSpecificGrantType,
}, },
proto::user_agent::sdk_client::{WalletAccess, WalletAccessEntry as SdkClientWalletAccess}, proto::operator::sdk_client::{WalletAccess, WalletAccessEntry as SdkClientWalletAccess},
}; };
use alloy::primitives::{Address, U256}; use alloy::primitives::{Address, U256};
@@ -87,6 +87,7 @@ impl TryConvert for ProtoSharedSettings {
.valid_until .valid_until
.map(ProtoTimestamp::try_convert) .map(ProtoTimestamp::try_convert)
.transpose()?, .transpose()?,
revoked_at: None,
max_gas_fee_per_gas: self max_gas_fee_per_gas: self
.max_gas_fee_per_gas .max_gas_fee_per_gas
.as_deref() .as_deref()

View File

@@ -10,7 +10,7 @@ use arbiter_proto::proto::{
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
specific_grant::Grant as ProtoSpecificGrantType, specific_grant::Grant as ProtoSpecificGrantType,
}, },
user_agent::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess}, operator::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess},
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};

View File

@@ -1,8 +1,8 @@
use crate::{ use crate::{
db::models::NewEvmWalletAccess, db::models::NewEvmWalletAccess,
grpc::Convert, grpc::Convert,
peers::user_agent::{ peers::operator::{
OutOfBand, UserAgentSession, OutOfBand, OperatorSession,
session::handlers::{ session::handlers::{
HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove, HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove,
HandleRevokeEvmWalletAccess, HandleSdkClientList, HandleRevokeEvmWalletAccess, HandleSdkClientList,
@@ -12,7 +12,7 @@ use crate::{
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_proto::proto::{ use arbiter_proto::proto::{
shared::ClientInfo as ProtoClientMetadata, shared::ClientInfo as ProtoClientMetadata,
user_agent::{ operator::{
sdk_client::{ sdk_client::{
self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel, self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel,
ConnectionRequest as ProtoSdkClientConnectionRequest, ConnectionRequest as ProtoSdkClientConnectionRequest,
@@ -24,7 +24,7 @@ use arbiter_proto::proto::{
request::Payload as SdkClientRequestPayload, request::Payload as SdkClientRequestPayload,
response::Payload as SdkClientResponsePayload, response::Payload as SdkClientResponsePayload,
}, },
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
}, },
}; };
@@ -32,13 +32,13 @@ use kameo::actor::ActorRef;
use tonic::Status; use tonic::Status;
use tracing::{info, warn}; use tracing::{info, warn};
const fn wrap_sdk_client_response(payload: SdkClientResponsePayload) -> UserAgentResponsePayload { const fn wrap_sdk_client_response(payload: SdkClientResponsePayload) -> OperatorResponsePayload {
UserAgentResponsePayload::SdkClient(proto_sdk_client::Response { OperatorResponsePayload::SdkClient(proto_sdk_client::Response {
payload: Some(payload), payload: Some(payload),
}) })
} }
pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload { pub(super) fn out_of_band_payload(oob: OutOfBand) -> OperatorResponsePayload {
match oob { match oob {
OutOfBand::ClientConnectionRequest { profile } => wrap_sdk_client_response( OutOfBand::ClientConnectionRequest { profile } => wrap_sdk_client_response(
SdkClientResponsePayload::ConnectionRequest(ProtoSdkClientConnectionRequest { SdkClientResponsePayload::ConnectionRequest(ProtoSdkClientConnectionRequest {
@@ -59,9 +59,9 @@ pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload {
} }
pub(super) async fn dispatch( pub(super) async fn dispatch(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: proto_sdk_client::Request, req: proto_sdk_client::Request,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let Some(payload) = req.payload else { let Some(payload) = req.payload else {
return Err(Status::invalid_argument( return Err(Status::invalid_argument(
"Missing SDK client request payload", "Missing SDK client request payload",
@@ -87,9 +87,9 @@ pub(super) async fn dispatch(
} }
async fn handle_connection_response( async fn handle_connection_response(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
resp: ProtoSdkClientConnectionResponse, resp: ProtoSdkClientConnectionResponse,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let pubkey = authn::PublicKey::try_from(resp.pubkey.as_slice()) let pubkey = authn::PublicKey::try_from(resp.pubkey.as_slice())
.map_err(|()| Status::invalid_argument("Invalid ML-DSA public key"))?; .map_err(|()| Status::invalid_argument("Invalid ML-DSA public key"))?;
@@ -108,8 +108,8 @@ async fn handle_connection_response(
} }
async fn handle_list( async fn handle_list(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let result = match actor.ask(HandleSdkClientList {}).await { let result = match actor.ask(HandleSdkClientList {}).await {
Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList { Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList {
clients: clients clients: clients
@@ -144,9 +144,9 @@ async fn handle_list(
} }
async fn handle_grant_wallet_access( async fn handle_grant_wallet_access(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: ProtoSdkClientGrantWalletAccess, req: ProtoSdkClientGrantWalletAccess,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let entries: Vec<NewEvmWalletAccess> = req.accesses.into_iter().map(Convert::convert).collect(); let entries: Vec<NewEvmWalletAccess> = req.accesses.into_iter().map(Convert::convert).collect();
match actor.ask(HandleGrantEvmWalletAccess { entries }).await { match actor.ask(HandleGrantEvmWalletAccess { entries }).await {
Ok(()) => { Ok(()) => {
@@ -161,9 +161,9 @@ async fn handle_grant_wallet_access(
} }
async fn handle_revoke_wallet_access( async fn handle_revoke_wallet_access(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: ProtoSdkClientRevokeWalletAccess, req: ProtoSdkClientRevokeWalletAccess,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
match actor match actor
.ask(HandleRevokeEvmWalletAccess { .ask(HandleRevokeEvmWalletAccess {
entries: req.accesses, entries: req.accesses,
@@ -182,8 +182,8 @@ async fn handle_revoke_wallet_access(
} }
async fn handle_list_wallet_access( async fn handle_list_wallet_access(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
match actor.ask(HandleListWalletAccess {}).await { match actor.ask(HandleListWalletAccess {}).await {
Ok(accesses) => Ok(Some(wrap_sdk_client_response( Ok(accesses) => Ok(Some(wrap_sdk_client_response(
SdkClientResponsePayload::ListWalletAccess(ListWalletAccessResponse { SdkClientResponsePayload::ListWalletAccess(ListWalletAccessResponse {

View File

@@ -1,11 +1,11 @@
use crate::{ use crate::{
actors::vault::VaultState, actors::vault::VaultState,
peers::user_agent::{UserAgentSession, session::handlers::HandleQueryVaultState}, peers::operator::{OperatorSession, session::handlers::HandleQueryVaultState},
}; };
use arbiter_proto::{ use arbiter_proto::{
proto::shared::VaultState as ProtoVaultState, proto::shared::VaultState as ProtoVaultState,
proto::user_agent::{ proto::operator::{
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
vault::{ vault::{
self as proto_vault, request::Payload as VaultRequestPayload, self as proto_vault, request::Payload as VaultRequestPayload,
response::Payload as VaultResponsePayload, response::Payload as VaultResponsePayload,
@@ -17,16 +17,16 @@ use kameo::actor::ActorRef;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
const fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload { const fn wrap_vault_response(payload: VaultResponsePayload) -> OperatorResponsePayload {
UserAgentResponsePayload::Vault(proto_vault::Response { OperatorResponsePayload::Vault(proto_vault::Response {
payload: Some(payload), payload: Some(payload),
}) })
} }
pub(super) async fn dispatch( pub(super) async fn dispatch(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
req: proto_vault::Request, req: proto_vault::Request,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let Some(payload) = req.payload else { let Some(payload) = req.payload else {
return Err(Status::invalid_argument("Missing vault request payload")); return Err(Status::invalid_argument("Missing vault request payload"));
}; };
@@ -42,8 +42,8 @@ pub(super) async fn dispatch(
} }
async fn handle_query_vault_state( async fn handle_query_vault_state(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<OperatorSession>,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<OperatorResponsePayload>, Status> {
let state = match actor.ask(HandleQueryVaultState {}).await { let state = match actor.ask(HandleQueryVaultState {}).await {
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
Ok(VaultState::Sealed) => ProtoVaultState::Sealed, Ok(VaultState::Sealed) => ProtoVaultState::Sealed,

View File

@@ -1,7 +1,7 @@
use super::auth::AuthTransportAdapter; use super::auth::AuthTransportAdapter;
use crate::{ use crate::{
grpc::TryConvert, grpc::TryConvert,
peers::user_agent::vault_gate::{self as vault_gate}, peers::operator::vault_gate::{self as vault_gate},
}; };
use arbiter_proto::transport::{Bi, Error as TransportError, Receiver, Sender}; use arbiter_proto::transport::{Bi, Error as TransportError, Receiver, Sender};
@@ -20,7 +20,7 @@ impl Receiver<vault_gate::Inbound> for AuthTransportAdapter<'_> {
Err(error) => { Err(error) => {
warn!( warn!(
?error, ?error,
"Failed to receive user agent request during vault gate" "Failed to receive operator request during vault gate"
); );
return None; return None;
} }

View File

@@ -1,11 +1,11 @@
use crate::{ use crate::{
grpc::{Convert, TryConvert}, grpc::{Convert, TryConvert},
peers::user_agent::vault_gate::{ peers::operator::vault_gate::{
self as vault_gate, HandleBootstrapEncryptedKey, HandleHandshake, HandleUnsealEncryptedKey, self as vault_gate, HandleBootstrapEncryptedKey, HandleHandshake, HandleUnsealEncryptedKey,
}, },
}; };
use arbiter_proto::proto::user_agent::{ use arbiter_proto::proto::operator::{
user_agent_request::Payload as UserAgentRequestPayload, operator_request::Payload as OperatorRequestPayload,
vault::{ vault::{
self as proto_vault, self as proto_vault,
bootstrap::{self as proto_bootstrap}, bootstrap::{self as proto_bootstrap},
@@ -16,7 +16,7 @@ use arbiter_proto::proto::user_agent::{
use tonic::Status; use tonic::Status;
impl TryConvert for UserAgentRequestPayload { impl TryConvert for OperatorRequestPayload {
type Output = vault_gate::Inbound; type Output = vault_gate::Inbound;
type Error = Status; type Error = Status;

View File

@@ -1,12 +1,12 @@
use crate::{ use crate::{
actors::vault::VaultState, actors::vault::VaultState,
grpc::{Convert, TryConvert}, grpc::{Convert, TryConvert},
peers::user_agent::vault_gate::{self as vault_gate}, peers::operator::vault_gate::{self as vault_gate},
}; };
use arbiter_proto::proto::{ use arbiter_proto::proto::{
shared::VaultState as ProtoVaultState, shared::VaultState as ProtoVaultState,
user_agent::{ operator::{
user_agent_response::Payload as UserAgentResponsePayload, operator_response::Payload as OperatorResponsePayload,
vault::{ vault::{
self as proto_vault, self as proto_vault,
bootstrap::{self as proto_bootstrap, BootstrapResult as ProtoBootstrapResult}, bootstrap::{self as proto_bootstrap, BootstrapResult as ProtoBootstrapResult},
@@ -22,28 +22,28 @@ use arbiter_proto::proto::{
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
const fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload { const fn wrap_vault_response(payload: VaultResponsePayload) -> OperatorResponsePayload {
UserAgentResponsePayload::Vault(proto_vault::Response { OperatorResponsePayload::Vault(proto_vault::Response {
payload: Some(payload), payload: Some(payload),
}) })
} }
const fn wrap_unseal_response(payload: UnsealResponsePayload) -> UserAgentResponsePayload { const fn wrap_unseal_response(payload: UnsealResponsePayload) -> OperatorResponsePayload {
wrap_vault_response(VaultResponsePayload::Unseal(proto_unseal::Response { wrap_vault_response(VaultResponsePayload::Unseal(proto_unseal::Response {
payload: Some(payload), payload: Some(payload),
})) }))
} }
fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> UserAgentResponsePayload { fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> OperatorResponsePayload {
wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response { wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response {
result: result.into(), result: result.into(),
})) }))
} }
impl Convert for VaultState { impl Convert for VaultState {
type Output = UserAgentResponsePayload; type Output = OperatorResponsePayload;
fn convert(self) -> UserAgentResponsePayload { fn convert(self) -> OperatorResponsePayload {
let proto_state = match self { let proto_state = match self {
Self::Unbootstrapped => ProtoVaultState::Unbootstrapped, Self::Unbootstrapped => ProtoVaultState::Unbootstrapped,
Self::Sealed => ProtoVaultState::Sealed, Self::Sealed => ProtoVaultState::Sealed,
@@ -54,9 +54,9 @@ impl Convert for VaultState {
} }
impl Convert for vault_gate::HandshakeResponse { impl Convert for vault_gate::HandshakeResponse {
type Output = UserAgentResponsePayload; type Output = OperatorResponsePayload;
fn convert(self) -> UserAgentResponsePayload { fn convert(self) -> OperatorResponsePayload {
wrap_unseal_response(UnsealResponsePayload::Start( wrap_unseal_response(UnsealResponsePayload::Start(
proto_unseal::UnsealStartResponse { proto_unseal::UnsealStartResponse {
server_pubkey: self.server_pubkey.as_bytes().to_vec(), server_pubkey: self.server_pubkey.as_bytes().to_vec(),
@@ -66,10 +66,10 @@ impl Convert for vault_gate::HandshakeResponse {
} }
impl TryConvert for vault_gate::Outbound { impl TryConvert for vault_gate::Outbound {
type Output = UserAgentResponsePayload; type Output = OperatorResponsePayload;
type Error = Status; type Error = Status;
fn try_convert(self) -> Result<UserAgentResponsePayload, Status> { fn try_convert(self) -> Result<OperatorResponsePayload, Status> {
match self { match self {
Self::HandleVaultState(result) => result Self::HandleVaultState(result) => result
.map_err(|err| { .map_err(|err| {

View File

@@ -54,7 +54,7 @@ impl From<diesel::result::Error> for Error {
pub enum ApproveError { pub enum ApproveError {
#[error("Internal error")] #[error("Internal error")]
Internal, Internal,
#[error("Client connection denied by user agents")] #[error("Client connection denied by operators")]
Denied, Denied,
#[error("Upstream error: {0}")] #[error("Upstream error: {0}")]
Upstream(flow_coordinator::ApprovalError), Upstream(flow_coordinator::ApprovalError),
@@ -171,46 +171,42 @@ async fn insert_client(
Error::DatabasePoolUnavailable Error::DatabasePoolUnavailable
})?; })?;
conn.exclusive_transaction(|conn| { conn.exclusive_transaction(async |conn| {
let vault = vault.clone(); let metadata_id = insert_into(client_metadata::table)
let pubkey = pubkey.clone(); .values((
Box::pin(async move { client_metadata::name.eq(&metadata.name),
let metadata_id = insert_into(client_metadata::table) client_metadata::description.eq(&metadata.description),
.values(( client_metadata::version.eq(&metadata.version),
client_metadata::name.eq(&metadata.name), ))
client_metadata::description.eq(&metadata.description), .returning(client_metadata::id)
client_metadata::version.eq(&metadata.version), .get_result::<i32>(&mut *conn)
)) .await?;
.returning(client_metadata::id)
.get_result::<i32>(conn)
.await?;
let client_id = insert_into(program_client::table) let client_id = insert_into(program_client::table)
.values(( .values((
program_client::public_key.eq(pubkey.to_bytes()), program_client::public_key.eq(pubkey.to_bytes()),
program_client::metadata_id.eq(metadata_id), program_client::metadata_id.eq(metadata_id),
)) ))
.on_conflict_do_nothing() .on_conflict_do_nothing()
.returning(program_client::id) .returning(program_client::id)
.get_result::<i32>(conn) .get_result::<i32>(&mut *conn)
.await?; .await?;
integrity::sign_entity( integrity::sign_entity(
conn, &mut *conn,
&vault, vault,
&ClientCredentials { &ClientCredentials {
pubkey: pubkey.clone(), pubkey: pubkey.clone(),
}, },
client_id, client_id,
) )
.await .await
.map_err(|e| { .map_err(|e| {
error!(error = ?e, "Failed to sign integrity tag for new client key"); error!(error = ?e, "Failed to sign integrity tag for new client key");
Error::DatabaseOperationFailed Error::DatabaseOperationFailed
})?; })?;
Ok(client_id) Ok(client_id)
})
}) })
.await .await
} }
@@ -229,55 +225,51 @@ async fn sync_client_metadata(
Error::DatabasePoolUnavailable Error::DatabasePoolUnavailable
})?; })?;
conn.exclusive_transaction(|conn| { conn.exclusive_transaction(async |conn| {
let metadata = metadata.clone(); let (current_metadata_id, current): (i32, ProgramClientMetadata) = program_client::table
Box::pin(async move { .find(client_id)
let (current_metadata_id, current): (i32, ProgramClientMetadata) = .inner_join(client_metadata::table)
program_client::table .select((
.find(client_id) program_client::metadata_id,
.inner_join(client_metadata::table) ProgramClientMetadata::as_select(),
.select(( ))
program_client::metadata_id, .first(&mut *conn)
ProgramClientMetadata::as_select(), .await?;
))
.first(conn)
.await?;
let unchanged = current.name == metadata.name let unchanged = current.name == metadata.name
&& current.description == metadata.description && current.description == metadata.description
&& current.version == metadata.version; && current.version == metadata.version;
if unchanged { if unchanged {
return Ok(()); return Ok(());
} }
insert_into(client_metadata_history::table) insert_into(client_metadata_history::table)
.values(( .values((
client_metadata_history::metadata_id.eq(current_metadata_id), client_metadata_history::metadata_id.eq(current_metadata_id),
client_metadata_history::client_id.eq(client_id), client_metadata_history::client_id.eq(client_id),
)) ))
.execute(conn) .execute(&mut *conn)
.await?; .await?;
let metadata_id = insert_into(client_metadata::table) let metadata_id = insert_into(client_metadata::table)
.values(( .values((
client_metadata::name.eq(&metadata.name), client_metadata::name.eq(&metadata.name),
client_metadata::description.eq(&metadata.description), client_metadata::description.eq(&metadata.description),
client_metadata::version.eq(&metadata.version), client_metadata::version.eq(&metadata.version),
)) ))
.returning(client_metadata::id) .returning(client_metadata::id)
.get_result::<i32>(conn) .get_result::<i32>(&mut *conn)
.await?; .await?;
update(program_client::table.find(client_id)) update(program_client::table.find(client_id))
.set(( .set((
program_client::metadata_id.eq(metadata_id), program_client::metadata_id.eq(metadata_id),
program_client::updated_at.eq(now), program_client::updated_at.eq(now),
)) ))
.execute(conn) .execute(&mut *conn)
.await?; .await?;
Ok::<(), diesel::result::Error>(()) Ok::<(), diesel::result::Error>(())
})
}) })
.await .await
.map_err(|e| { .map_err(|e| {
@@ -306,7 +298,7 @@ where
let signature = expect_message(transport, |req: Inbound| match req { let signature = expect_message(transport, |req: Inbound| match req {
Inbound::AuthChallengeSolution { signature } => Some(signature), Inbound::AuthChallengeSolution { signature } => Some(signature),
_ => None, Inbound::AuthChallengeRequest { .. } => None,
}) })
.await .await
.map_err(|e| { .map_err(|e| {

View File

@@ -1,2 +1,2 @@
pub mod client; pub mod client;
pub mod user_agent; pub mod operator;

View File

@@ -1,4 +1,4 @@
use super::{Credentials, UserAgentConnection}; use super::{Credentials, OperatorConnection};
use arbiter_crypto::authn::{self, AuthChallenge}; use arbiter_crypto::authn::{self, AuthChallenge};
use arbiter_proto::transport::Bi; use arbiter_proto::transport::Bi;
@@ -69,7 +69,7 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
} }
pub async fn authenticate<T>( pub async fn authenticate<T>(
props: &mut UserAgentConnection, props: &mut OperatorConnection,
transport: &mut T, transport: &mut T,
) -> Result<Credentials, Error> ) -> Result<Credentials, Error>
where where

View File

@@ -1,13 +1,13 @@
use super::{ use super::{
super::{Credentials, UserAgentConnection}, super::{Credentials, OperatorConnection},
Error, Error,
}; };
use crate::{ use crate::{
actors::bootstrap::ConsumeToken, actors::bootstrap::ConsumeToken,
db::{DatabasePool, schema::useragent_client}, db::{DatabasePool, schema::operator_client},
peers::user_agent::auth::Outbound, peers::operator::auth::Outbound,
}; };
use arbiter_crypto::authn::{self, AuthChallenge, USERAGENT_CONTEXT}; use arbiter_crypto::authn::{self, AuthChallenge, OPERATOR_CONTEXT};
use arbiter_proto::transport::Bi; use arbiter_proto::transport::Bi;
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl}; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl};
@@ -19,7 +19,7 @@ pub(super) struct ChallengeRequest {
pub(super) bootstrap_token: Option<String>, pub(super) bootstrap_token: Option<String>,
} }
pub(super) struct ChallengeContext { pub struct ChallengeContext {
pub(super) challenge: AuthChallenge, pub(super) challenge: AuthChallenge,
pub(super) pubkey: authn::PublicKey, pub(super) pubkey: authn::PublicKey,
pub(super) bootstrap_token: Option<String>, pub(super) bootstrap_token: Option<String>,
@@ -44,9 +44,9 @@ async fn get_client_id(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<O
Error::internal("Database unavailable") Error::internal("Database unavailable")
})?; })?;
useragent_client::table operator_client::table
.filter(useragent_client::public_key.eq(pubkey.to_bytes())) .filter(operator_client::public_key.eq(pubkey.to_bytes()))
.select(useragent_client::id) .select(operator_client::id)
.first::<i32>(&mut conn) .first::<i32>(&mut conn)
.await .await
.optional() .optional()
@@ -63,9 +63,9 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
Error::internal("Database unavailable") Error::internal("Database unavailable")
})?; })?;
let id: i32 = diesel::insert_into(useragent_client::table) let id: i32 = diesel::insert_into(operator_client::table)
.values((useragent_client::public_key.eq(pubkey_bytes),)) .values((operator_client::public_key.eq(pubkey_bytes),))
.returning(useragent_client::id) .returning(operator_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.map_err(|e| { .map_err(|e| {
@@ -77,12 +77,12 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
} }
pub(super) struct AuthContext<'a, T: ?Sized> { pub(super) struct AuthContext<'a, T: ?Sized> {
pub(super) conn: &'a mut UserAgentConnection, pub(super) conn: &'a mut OperatorConnection,
pub(super) transport: &'a mut T, pub(super) transport: &'a mut T,
} }
impl<'a, T: ?Sized> AuthContext<'a, T> { impl<'a, T: ?Sized> AuthContext<'a, T> {
pub(super) const fn new(conn: &'a mut UserAgentConnection, transport: &'a mut T) -> Self { pub(super) const fn new(conn: &'a mut OperatorConnection, transport: &'a mut T) -> Self {
Self { conn, transport } Self { conn, transport }
} }
} }
@@ -127,8 +127,6 @@ where
}) })
} }
#[allow(missing_docs)]
#[allow(clippy::unused_unit)]
async fn verify_solution( async fn verify_solution(
&mut self, &mut self,
ChallengeContext { ChallengeContext {
@@ -143,7 +141,7 @@ where
Error::InvalidChallengeSolution Error::InvalidChallengeSolution
})?; })?;
let valid = pubkey.verify(challenge, USERAGENT_CONTEXT, &signature); let valid = pubkey.verify(challenge, OPERATOR_CONTEXT, &signature);
if !valid { if !valid {
self.transport self.transport

View File

@@ -17,7 +17,7 @@ use tokio::sync::oneshot;
use tracing::{error, warn}; use tracing::{error, warn};
pub use auth::authenticate; pub use auth::authenticate;
pub use session::UserAgentSession; pub use session::OperatorSession;
pub mod auth; pub mod auth;
pub mod session; pub mod session;
@@ -30,10 +30,10 @@ pub struct Credentials {
} }
impl Integrable for Credentials { impl Integrable for Credentials {
const KIND: &'static str = "useragent_credentials"; const KIND: &'static str = "operator_credentials";
} }
// Messages, sent by user agent to connection client without having a request // Messages, sent by operator to connection client without having a request
#[derive(Debug)] #[derive(Debug)]
pub enum OutOfBand { pub enum OutOfBand {
ClientConnectionRequest { profile: ClientProfile }, ClientConnectionRequest { profile: ClientProfile },
@@ -41,12 +41,12 @@ pub enum OutOfBand {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct UserAgentConnection { pub struct OperatorConnection {
pub(crate) db: DatabasePool, pub(crate) db: DatabasePool,
pub(crate) actors: GlobalActors, pub(crate) actors: GlobalActors,
} }
impl UserAgentConnection { impl OperatorConnection {
pub const fn new(db: DatabasePool, actors: GlobalActors) -> Self { pub const fn new(db: DatabasePool, actors: GlobalActors) -> Self {
Self { db, actors } Self { db, actors }
} }
@@ -106,7 +106,7 @@ async fn should_run_gate(vault: &ActorRef<Vault>) -> Result<bool, Error> {
} }
async fn run_vault_gate<T>( async fn run_vault_gate<T>(
props: &UserAgentConnection, props: &OperatorConnection,
transport: &mut T, transport: &mut T,
auth_creds: Credentials, auth_creds: Credentials,
) -> Result<(), Error> ) -> Result<(), Error>
@@ -160,10 +160,10 @@ where
} }
pub async fn start<T>( pub async fn start<T>(
props: &mut UserAgentConnection, props: &mut OperatorConnection,
mut transport: T, mut transport: T,
oob_sender: Box<dyn Sender<OutOfBand>>, oob_sender: Box<dyn Sender<OutOfBand>>,
) -> Result<ActorRef<UserAgentSession>, Error> ) -> Result<ActorRef<OperatorSession>, Error>
where where
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send, T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send,
T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send, T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send,
@@ -178,7 +178,7 @@ where
// checking the integrity // checking the integrity
verify_integrity(&props.db, &props.actors.vault, &creds).await?; verify_integrity(&props.db, &props.actors.vault, &creds).await?;
Ok(UserAgentSession::spawn(UserAgentSession::new( Ok(OperatorSession::spawn(OperatorSession::new(
props.clone(), props.clone(),
oob_sender, oob_sender,
))) )))

View File

@@ -1,8 +1,8 @@
use super::{Error, UserAgentSession}; use super::{Error, OperatorSession};
use crate::{ use crate::{
actors::evm::{ actors::evm::{
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, ClientSignTransaction, Generate, ListWallets, OperatorCreateGrant, OperatorListGrants,
UseragentCreateGrant, UseragentListGrants, SignTransactionError as EvmSignError,
}, },
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer, actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
actors::vault::VaultState, actors::vault::VaultState,
@@ -36,7 +36,7 @@ pub enum GrantMutationError {
} }
#[messages] #[messages]
impl UserAgentSession { impl OperatorSession {
#[message] #[message]
pub(crate) async fn handle_query_vault_state(&mut self) -> Result<VaultState, Error> { pub(crate) async fn handle_query_vault_state(&mut self) -> Result<VaultState, Error> {
use crate::actors::vault::GetState; use crate::actors::vault::GetState;
@@ -44,7 +44,7 @@ impl UserAgentSession {
let vault_state = match self.props.actors.vault.ask(GetState {}).await { let vault_state = match self.props.actors.vault.ask(GetState {}).await {
Ok(state) => state, Ok(state) => state,
Err(err) => { Err(err) => {
error!(?err, actor = "useragent", "vault.query.failed"); error!(?err, actor = "operator", "vault.query.failed");
return Err(Error::internal("Vault is in broken state")); return Err(Error::internal("Vault is in broken state"));
} }
}; };
@@ -54,7 +54,7 @@ impl UserAgentSession {
} }
#[messages] #[messages]
impl UserAgentSession { impl OperatorSession {
#[message] #[message]
pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> { pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> {
match self.props.actors.evm.ask(Generate {}).await { match self.props.actors.evm.ask(Generate {}).await {
@@ -82,10 +82,10 @@ impl UserAgentSession {
} }
#[messages] #[messages]
impl UserAgentSession { impl OperatorSession {
#[message] #[message]
pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> { pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
match self.props.actors.evm.ask(UseragentListGrants {}).await { match self.props.actors.evm.ask(OperatorListGrants {}).await {
Ok(grants) => Ok(grants), Ok(grants) => Ok(grants),
Err(err) => { Err(err) => {
error!(?err, "EVM grant list failed"); error!(?err, "EVM grant list failed");
@@ -104,7 +104,7 @@ impl UserAgentSession {
.props .props
.actors .actors
.evm .evm
.ask(UseragentCreateGrant { basic, grant }) .ask(OperatorCreateGrant { basic, grant })
.await .await
{ {
Ok(grant_id) => Ok(grant_id), Ok(grant_id) => Ok(grant_id),
@@ -121,7 +121,7 @@ impl UserAgentSession {
// .props // .props
// .actors // .actors
// .evm // .evm
// .ask(UseragentDeleteGrant { grant_id }) // .ask(OperatorDeleteGrant { grant_id })
// .await // .await
// { // {
// Ok(()) => Ok(()), // Ok(()) => Ok(()),
@@ -157,7 +157,7 @@ impl UserAgentSession {
Err(SignTransactionError::Vet(vet_error)) Err(SignTransactionError::Vet(vet_error))
} }
Err(err) => { Err(err) => {
error!(?err, "EVM sign transaction failed in user-agent session"); error!(?err, "EVM sign transaction failed in operator session");
Err(SignTransactionError::Internal) Err(SignTransactionError::Internal)
} }
} }
@@ -169,20 +169,18 @@ impl UserAgentSession {
entries: Vec<NewEvmWalletAccess>, entries: Vec<NewEvmWalletAccess>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut conn = self.props.db.get().await?; let mut conn = self.props.db.get().await?;
conn.transaction(|conn| { conn.transaction(async |conn| {
Box::pin(async move { use crate::db::schema::evm_wallet_access;
use crate::db::schema::evm_wallet_access;
for entry in entries { for entry in entries {
diesel::insert_into(evm_wallet_access::table) diesel::insert_into(evm_wallet_access::table)
.values(&entry) .values(&entry)
.on_conflict_do_nothing() .on_conflict_do_nothing()
.execute(conn) .execute(&mut *conn)
.await?; .await?;
} }
Result::<_, Error>::Ok(()) Result::<_, Error>::Ok(())
})
}) })
.await?; .await?;
Ok(()) Ok(())
@@ -194,18 +192,16 @@ impl UserAgentSession {
entries: Vec<i32>, entries: Vec<i32>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut conn = self.props.db.get().await?; let mut conn = self.props.db.get().await?;
conn.transaction(|conn| { conn.transaction(async |conn| {
Box::pin(async move { use crate::db::schema::evm_wallet_access;
use crate::db::schema::evm_wallet_access; for entry in entries {
for entry in entries { diesel::delete(evm_wallet_access::table)
diesel::delete(evm_wallet_access::table) .filter(evm_wallet_access::wallet_id.eq(entry))
.filter(evm_wallet_access::wallet_id.eq(entry)) .execute(&mut *conn)
.execute(conn) .await?;
.await?; }
}
Result::<_, Error>::Ok(()) Result::<_, Error>::Ok(())
})
}) })
.await?; .await?;
Ok(()) Ok(())
@@ -216,8 +212,7 @@ impl UserAgentSession {
&mut self, &mut self,
) -> Result<Vec<EvmWalletAccess>, Error> { ) -> Result<Vec<EvmWalletAccess>, Error> {
let mut conn = self.props.db.get().await?; let mut conn = self.props.db.get().await?;
use crate::db::schema::evm_wallet_access; let access_entries = crate::db::schema::evm_wallet_access::table
let access_entries = evm_wallet_access::table
.select(EvmWalletAccess::as_select()) .select(EvmWalletAccess::as_select())
.load::<_>(&mut conn) .load::<_>(&mut conn)
.await?; .await?;
@@ -226,7 +221,7 @@ impl UserAgentSession {
} }
#[messages] #[messages]
impl UserAgentSession { impl OperatorSession {
#[message(ctx)] #[message(ctx)]
pub(crate) async fn handle_new_client_approve( pub(crate) async fn handle_new_client_approve(
&mut self, &mut self,

View File

@@ -1,8 +1,8 @@
use super::{OutOfBand, UserAgentConnection}; use super::{OutOfBand, OperatorConnection};
use crate::{ use crate::{
actors::{ actors::{
flow_coordinator::client_connect_approval::ClientApprovalController, flow_coordinator::client_connect_approval::ClientApprovalController,
useragent_registry::ConnectUseragent, operator_registry::ConnectOperator,
}, },
peers::client::ClientProfile, peers::client::ClientProfile,
}; };
@@ -49,8 +49,8 @@ pub struct PendingClientApproval {
controller: ActorRef<ClientApprovalController>, controller: ActorRef<ClientApprovalController>,
} }
pub struct UserAgentSession { pub struct OperatorSession {
props: UserAgentConnection, props: OperatorConnection,
sender: Box<dyn Sender<OutOfBand>>, sender: Box<dyn Sender<OutOfBand>>,
pending_client_approvals: HashMap<Vec<u8>, PendingClientApproval>, pending_client_approvals: HashMap<Vec<u8>, PendingClientApproval>,
@@ -58,18 +58,18 @@ pub struct UserAgentSession {
pub mod handlers; pub mod handlers;
impl UserAgentSession { impl OperatorSession {
pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self { pub(crate) fn new(props: OperatorConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self {
Self { Self {
props, props,
sender, sender,
pending_client_approvals: Default::default(), pending_client_approvals: HashMap::default(),
} }
} }
} }
#[messages] #[messages]
impl UserAgentSession { impl OperatorSession {
#[message] #[message]
pub async fn begin_new_client_approval( pub async fn begin_new_client_approval(
&mut self, &mut self,
@@ -85,7 +85,7 @@ impl UserAgentSession {
{ {
error!( error!(
?e, ?e,
actor = "user_agent", actor = "operator",
event = "failed to announce new client connection" event = "failed to announce new client connection"
); );
return; return;
@@ -101,7 +101,7 @@ impl UserAgentSession {
} }
} }
impl Actor for UserAgentSession { impl Actor for OperatorSession {
type Args = Self; type Args = Self;
type Error = Error; type Error = Error;
@@ -109,17 +109,17 @@ impl Actor for UserAgentSession {
async fn on_start(args: Self::Args, this: ActorRef<Self>) -> Result<Self, Self::Error> { async fn on_start(args: Self::Args, this: ActorRef<Self>) -> Result<Self, Self::Error> {
args.props args.props
.actors .actors
.useragent_registry .operator_registry
.ask(ConnectUseragent { .ask(ConnectOperator {
actor: this.clone(), actor: this.clone(),
}) })
.await .await
.map_err(|err| { .map_err(|err| {
error!( error!(
?err, ?err,
"Failed to register user agent connection with user agent registry" "Failed to register operator connection with operator registry"
); );
Error::internal("Failed to register user agent connection with user agent registry") Error::internal("Failed to register operator connection with operator registry")
})?; })?;
Ok(args) Ok(args)
} }
@@ -149,7 +149,7 @@ impl Actor for UserAgentSession {
{ {
error!( error!(
?e, ?e,
actor = "user_agent", actor = "operator",
event = "failed to announce client connection cancellation" event = "failed to announce client connection cancellation"
); );
} }

View File

@@ -79,22 +79,22 @@ fn sign_client_challenge(key: &SigningKey<MlDsa87>, challenge: &AuthChallenge) -
.into() .into()
} }
async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) { async fn insert_bootstrap_sentinel_operator(db: &db::DatabasePool) {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let sentinel_key = verifying_key(&MlDsa87::key_gen(&mut rand::rng())) let sentinel_key = verifying_key(&MlDsa87::key_gen(&mut rand::rng()))
.encode() .encode()
.0 .0
.to_vec(); .to_vec();
insert_into(schema::useragent_client::table) insert_into(schema::operator_client::table)
.values((schema::useragent_client::public_key.eq(sentinel_key),)) .values((schema::operator_client::public_key.eq(sentinel_key),))
.execute(&mut conn) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();
} }
async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors { async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
insert_bootstrap_sentinel_useragent(db).await; insert_bootstrap_sentinel_operator(db).await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors actors

View File

@@ -0,0 +1,6 @@
mod common;
#[path = "operator/auth.rs"]
mod auth;
#[path = "operator/unseal.rs"]
mod unseal;

View File

@@ -1,6 +1,6 @@
use super::common::ChannelTransport; use super::common::ChannelTransport;
use arbiter_crypto::{ use arbiter_crypto::{
authn::{self, AuthChallenge, USERAGENT_CONTEXT}, authn::{self, AuthChallenge, OPERATOR_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _}, safecell::{SafeCell, SafeCellHandle as _},
}; };
use arbiter_proto::transport::{Error as TransportError, Receiver, Sender}; use arbiter_proto::transport::{Error as TransportError, Receiver, Sender};
@@ -8,7 +8,7 @@ use arbiter_server::{
actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap}, actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap},
crypto::integrity, crypto::integrity,
db::{self, schema}, db::{self, schema},
peers::user_agent::{self, Credentials, UserAgentConnection, auth, vault_gate}, peers::operator::{self, Credentials, OperatorConnection, auth, vault_gate},
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -21,13 +21,13 @@ fn verifying_key(key: &SigningKey<MlDsa87>) -> VerifyingKey<MlDsa87> {
<SigningKey<MlDsa87> as Keypair>::verifying_key(key) <SigningKey<MlDsa87> as Keypair>::verifying_key(key)
} }
fn sign_useragent_challenge( fn sign_operator_challenge(
key: &SigningKey<MlDsa87>, key: &SigningKey<MlDsa87>,
challenge: &AuthChallenge, challenge: &AuthChallenge,
) -> authn::Signature { ) -> authn::Signature {
let challenge = challenge.format(); let challenge = challenge.format();
key.signing_key() key.signing_key()
.sign_deterministic(&challenge, USERAGENT_CONTEXT) .sign_deterministic(&challenge, OPERATOR_CONTEXT)
.unwrap() .unwrap()
.into() .into()
} }
@@ -41,8 +41,8 @@ fn tamper_challenge(challenge: &AuthChallenge) -> AuthChallenge {
struct NullOobSender; struct NullOobSender;
#[async_trait] #[async_trait]
impl Sender<user_agent::OutOfBand> for NullOobSender { impl Sender<operator::OutOfBand> for NullOobSender {
async fn send(&mut self, _item: user_agent::OutOfBand) -> Result<(), TransportError> { async fn send(&mut self, _item: operator::OutOfBand) -> Result<(), TransportError> {
Ok(()) Ok(())
} }
} }
@@ -166,7 +166,7 @@ pub async fn bootstrap_token_auth() {
let (mut server_transport, mut test_transport) = ChannelTransport::new(); let (mut server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = OperatorConnection::new(db_for_task, actors);
auth::authenticate(&mut props, &mut server_transport).await auth::authenticate(&mut props, &mut server_transport).await
}); });
@@ -188,7 +188,7 @@ pub async fn bootstrap_token_auth() {
other => panic!("Expected AuthChallenge, got {other:?}"), other => panic!("Expected AuthChallenge, got {other:?}"),
}; };
let signature = sign_useragent_challenge(&new_key, &challenge); let signature = sign_operator_challenge(&new_key, &challenge);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {
@@ -206,8 +206,8 @@ pub async fn bootstrap_token_auth() {
task.await.unwrap().unwrap(); task.await.unwrap().unwrap();
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let stored_pubkey: Vec<u8> = schema::useragent_client::table let stored_pubkey: Vec<u8> = schema::operator_client::table
.select(schema::useragent_client::public_key) .select(schema::operator_client::public_key)
.first::<Vec<u8>>(&mut conn) .first::<Vec<u8>>(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -223,7 +223,7 @@ pub async fn bootstrap_invalid_token_auth() {
let (mut server_transport, mut test_transport) = ChannelTransport::new(); let (mut server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = OperatorConnection::new(db_for_task, actors);
auth::authenticate(&mut props, &mut server_transport).await auth::authenticate(&mut props, &mut server_transport).await
}); });
@@ -245,7 +245,7 @@ pub async fn bootstrap_invalid_token_auth() {
other => panic!("Expected AuthChallenge, got {other:?}"), other => panic!("Expected AuthChallenge, got {other:?}"),
}; };
let signature = sign_useragent_challenge(&new_key, &challenge); let signature = sign_operator_challenge(&new_key, &challenge);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {
signature: signature.to_bytes(), signature: signature.to_bytes(),
@@ -259,7 +259,7 @@ pub async fn bootstrap_invalid_token_auth() {
)); ));
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let count: i64 = schema::useragent_client::table let count: i64 = schema::operator_client::table
.count() .count()
.get_result::<i64>(&mut conn) .get_result::<i64>(&mut conn)
.await .await
@@ -285,9 +285,9 @@ pub async fn challenge_auth() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::useragent_client::table) let id: i32 = insert_into(schema::operator_client::table)
.values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),))
.returning(schema::useragent_client::id) .returning(schema::operator_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -307,7 +307,7 @@ pub async fn challenge_auth() {
let (mut server_transport, mut test_transport) = ChannelTransport::new(); let (mut server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = OperatorConnection::new(db_for_task, actors);
auth::authenticate(&mut props, &mut server_transport).await auth::authenticate(&mut props, &mut server_transport).await
}); });
@@ -331,7 +331,7 @@ pub async fn challenge_auth() {
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
let signature = sign_useragent_challenge(&new_key, &challenge); let signature = sign_operator_challenge(&new_key, &challenge);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {
@@ -371,8 +371,8 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
insert_into(schema::useragent_client::table) insert_into(schema::operator_client::table)
.values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),))
.execute(&mut conn) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -381,8 +381,8 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
let (server_transport, mut test_transport) = start_transport_pair(); let (server_transport, mut test_transport) = start_transport_pair();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = OperatorConnection::new(db_for_task, actors);
user_agent::start(&mut props, server_transport, Box::new(NullOobSender)).await operator::start(&mut props, server_transport, Box::new(NullOobSender)).await
}); });
test_transport test_transport
@@ -400,12 +400,12 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
let challenge = match response { let challenge = match response {
Ok(resp) => match resp { Ok(resp) => match resp {
auth::Outbound::AuthChallenge { challenge } => challenge, auth::Outbound::AuthChallenge { challenge } => challenge,
other => panic!("Expected AuthChallenge, got {other:?}"), other @ auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got {other:?}"),
}, },
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
let signature = sign_useragent_challenge(&new_key, &challenge); let signature = sign_operator_challenge(&new_key, &challenge);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {
@@ -422,7 +422,7 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
assert!(matches!( assert!(matches!(
task.await.unwrap(), task.await.unwrap(),
Err(user_agent::Error::Internal(_)) Err(operator::Error::Internal(_))
)); ));
} }
@@ -444,9 +444,9 @@ pub async fn challenge_auth_rejects_invalid_signature() {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::useragent_client::table) let id: i32 = insert_into(schema::operator_client::table)
.values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),)) .values((schema::operator_client::public_key.eq(pubkey_bytes.clone()),))
.returning(schema::useragent_client::id) .returning(schema::operator_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -466,7 +466,7 @@ pub async fn challenge_auth_rejects_invalid_signature() {
let (mut server_transport, mut test_transport) = ChannelTransport::new(); let (mut server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = OperatorConnection::new(db_for_task, actors);
auth::authenticate(&mut props, &mut server_transport).await auth::authenticate(&mut props, &mut server_transport).await
}); });
@@ -490,7 +490,7 @@ pub async fn challenge_auth_rejects_invalid_signature() {
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
let signature = sign_useragent_challenge(&new_key, &tamper_challenge(&challenge)); let signature = sign_operator_challenge(&new_key, &tamper_challenge(&challenge));
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {

View File

@@ -8,7 +8,7 @@ use arbiter_server::{
vault::{Bootstrap, Seal}, vault::{Bootstrap, Seal},
}, },
db, db,
peers::user_agent::{ peers::operator::{
Credentials, Credentials,
vault_gate::{ vault_gate::{
Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate, Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate,

View File

@@ -1,6 +0,0 @@
mod common;
#[path = "user_agent/auth.rs"]
mod auth;
#[path = "user_agent/unseal.rs"]
mod unseal;

View File

@@ -14,7 +14,7 @@ use diesel_async::RunQueryDsl;
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_bootstrap() { async fn bootstrap() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await .await
@@ -39,7 +39,7 @@ async fn test_bootstrap() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_bootstrap_rejects_double() { async fn bootstrap_rejects_double() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
@@ -50,7 +50,7 @@ async fn test_bootstrap_rejects_double() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_create_new_before_bootstrap_fails() { async fn create_new_before_bootstrap_fails() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus()) let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await .await
@@ -65,7 +65,7 @@ async fn test_create_new_before_bootstrap_fails() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_decrypt_before_bootstrap_fails() { async fn decrypt_before_bootstrap_fails() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = Vault::new(db, GlobalActors::spawn_message_bus()) let mut actor = Vault::new(db, GlobalActors::spawn_message_bus())
.await .await
@@ -77,7 +77,7 @@ async fn test_decrypt_before_bootstrap_fails() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_new_restores_sealed_state() { async fn new_restores_sealed_state() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actor = common::bootstrapped_vault(&db).await; let actor = common::bootstrapped_vault(&db).await;
drop(actor); drop(actor);
@@ -91,7 +91,7 @@ async fn test_new_restores_sealed_state() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_unseal_correct_password() { async fn unseal_correct_password() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
@@ -114,7 +114,7 @@ async fn test_unseal_correct_password() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_unseal_wrong_then_correct_password() { async fn unseal_wrong_then_correct_password() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;

View File

@@ -12,7 +12,7 @@ use std::collections::HashSet;
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_create_decrypt_roundtrip() { async fn create_decrypt_roundtrip() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
@@ -28,7 +28,7 @@ async fn test_create_decrypt_roundtrip() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_decrypt_nonexistent_returns_not_found() { async fn decrypt_nonexistent_returns_not_found() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
@@ -38,7 +38,7 @@ async fn test_decrypt_nonexistent_returns_not_found() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_ciphertext_differs_across_entries() { async fn ciphertext_differs_across_entries() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;
@@ -76,7 +76,7 @@ async fn test_ciphertext_differs_across_entries() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_nonce_never_reused() { async fn nonce_never_reused() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let mut actor = common::bootstrapped_vault(&db).await; let mut actor = common::bootstrapped_vault(&db).await;

View File

@@ -5,3 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
alloy.workspace = true alloy.workspace = true
[lib]
test = false
doctest = false