diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5985f6e..9eb3bc9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,7 +28,7 @@ jobs:
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- - run: cargo clippy --workspace --all-targets --all-features -- -D warnings
+ - run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
test:
name: test
@@ -37,7 +37,14 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- - run: cargo test --workspace --all-features
+ # The integration tests dial macula-go's in-process stations
+ # (tests/teststation), built to target/teststation first.
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: tests/teststation/go.mod
+ cache-dependency-path: tests/teststation/go.sum
+ - run: ./scripts/build-teststation.sh
+ - run: cargo test --workspace --all-features --locked
doc:
name: docs (deny broken links)
diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml
index 09e48de..f34d20b 100644
--- a/.github/workflows/release-core.yml
+++ b/.github/workflows/release-core.yml
@@ -24,18 +24,20 @@ jobs:
exit 1
fi
- # Rebuilds and re-runs the offline suite here rather than trusting
- # an artifact from ci.yml's own run -- this workflow triggers off a
- # tag push, a separate event from the branch push ci.yml already
- # validated, so there's no prior run's output to reuse. Deliberately
- # NOT --locked anywhere below: this repo doesn't commit Cargo.lock
- # (library crate; a committed lock silently caps what a loose
- # constraint resolves to on every later run -- see the workspace's
- # own lockfile-hygiene convention), so a fresh checkout has no lock
- # to be strict against.
- - run: cargo build --workspace --all-targets
- - run: cargo test --workspace --all-features
- - run: cargo clippy --workspace --all-targets --all-features -- -D warnings
+ # Rebuilds and re-runs the suite here rather than trusting an artifact
+ # from ci.yml's run: a tag push is a separate event from the branch
+ # push ci.yml validated, so there is no prior run to reuse. --locked:
+ # the committed Cargo.lock is what was tested.
+ # The integration tests dial macula-go's in-process stations
+ # (tests/teststation), built to target/teststation first.
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: tests/teststation/go.mod
+ cache-dependency-path: tests/teststation/go.sum
+ - run: ./scripts/build-teststation.sh
+ - run: cargo build --workspace --all-targets --locked
+ - run: cargo test --workspace --all-features --locked
+ - run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
- run: cargo fmt --all -- --check
# Dry-run needs no registry auth -- it only verifies metadata,
diff --git a/.github/workflows/release-ffi.yml b/.github/workflows/release-ffi.yml
index 981e81a..4758ffb 100644
--- a/.github/workflows/release-ffi.yml
+++ b/.github/workflows/release-ffi.yml
@@ -28,19 +28,25 @@ jobs:
exit 1
fi
- # Same reasoning as release-core.yml: rebuild+retest from scratch
+ # Same reasoning as release-core.yml: rebuild and retest from scratch
# (a tag push is a separate event from the branch push ci.yml
- # already validated), no --locked anywhere (no committed Cargo.lock
- # in this repo).
- - run: cargo build --workspace --all-targets
- - run: cargo test --workspace --all-features
- - run: cargo clippy --workspace --all-targets --all-features -- -D warnings
+ # validated), against the committed Cargo.lock.
+ # The integration tests dial macula-go's in-process stations
+ # (tests/teststation), built to target/teststation first.
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: tests/teststation/go.mod
+ cache-dependency-path: tests/teststation/go.sum
+ - run: ./scripts/build-teststation.sh
+ - run: cargo build --workspace --all-targets --locked
+ - run: cargo test --workspace --all-features --locked
+ - run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
- run: cargo fmt --all -- --check
# Dry-run needs no registry auth -- it only verifies metadata,
# compresses the package, and checks the result, never uploads.
# This is also the step that proves macula-rust-ffi's `macula-rust
- # = { path = "..", version = "0.2" }` dependency actually resolves
+ # = { path = "..", version = "0.4" }` dependency actually resolves
# against the REAL published macula-rust on crates.io, not just the
# local workspace path -- `cargo publish` downloads and rebuilds
# against the registry version during verification, confirmed
diff --git a/.gitignore b/.gitignore
index 60db30f..c17da7f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,2 @@
/target
-Cargo.lock
Cargo.lock.bak
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc53d63..04f985c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,164 +15,64 @@ usually touches both, but their version numbers don't move in lockstep.
### [0.4.0] - Unreleased
+The macula 12 wire. **Breaking throughout**: a 0.3 node cannot reach a
+macula 12 station, and nothing of the 0.3 API carries over. See the README's
+"Coming from 0.3 and earlier".
+
+#### Added
+
+- `node_key::NodeKey`: macula 12 identities, ML-DSA-87 (`pq_pure`) or the
+ LAMPS composite id-MLDSA87-RSA4096-PSS-SHA512 (`pq_hybrid`, the fleet's),
+ RSA-PSS-4096 from aws-lc-rs. Node and key ids, the admission puzzle
+ (difficulty 8), owner-only key files in the seed form, the platform secure
+ store through `keystore::KeyStore`, and `load_or_create`. The composite is
+ held to the draft's own vector, and signatures crossed both ways with macula
+ 12.8.0 (`scripts/cross-verify-macula.sh`).
+- `cbor`: macula 12's decoding rule v1 (depth 64, 131,072 elements, integers
+ within ±2^63, text or integer map keys, no duplicates, finite floats, `null`
+ the only simple value), with its refusal reasons.
+- `binding`, `signed_object`: TLS and CONNECT bindings, status statements, and
+ signed and held objects.
+- `transport`: QUIC on macula-pqc 0.3, ML-KEM hybrid key exchange with the
+ AES-256 Initial suite, every station pinned by its node_id through its TLS
+ binding.
+- `handshake`: the v4 handshake (opener, challenge, CONNECT with its member
+ endorsement, HELLO, status).
+- `frame`: version-2 frames: signed requests, replies, relay errors,
+ publications and stream frames, and neighbour signatures with their seq in
+ pq_hybrid.
+- `record`: the DHT's records and storage keys, D25 authorization (a realm's
+ org directory and an org's delegation) and a node's own namespace
+ `~/`.
+- `statement_issuer`: the CONNECT key bound to the identity key, its status
+ statement reissued every 15 minutes, and the key rotated every 5 days.
+- `station_link::Link`: one station, ported from macula-go v0.12.0's
+ `stationlink`. Status statements both ways, a liveness probe, calls, the
+ DHT, pubsub with per-node dedup, serving with request admission, and
+ streaming sessions released on every path.
+- `pool::Pool`: a node's links, ported from macula-go v0.12.0's `pool`. Pinned
+ seeds redialed and given back their subscriptions and served procedures.
+ Calls and streams reach a provider at its own station, trusted only under
+ the pinned realm key. Publications are signed once, and records go through
+ the pool.
+- Tests against macula-go's in-process stations (`tests/teststation`, built by
+ `scripts/build-teststation.sh`, which CI runs), and `tests/live.rs` against
+ one real station, ignored unless asked.
+- Examples: `quickstart`, `serve`, `publish_subscribe`.
+
+#### Removed
+
+- The 10.x wire and everything on it: `identity::KeyPair` (Ed25519),
+ `connection::Session` and its telemetry facts, the 10.x `frame`, `dht`,
+ `stream` and `pool`, `direct_dial`, `cert`, `cert_chain`, `ucan`, `bolt4`,
+ `content` and `manifest`, and WebPki trust.
+- `plans/`: the 10.x wire spec and leak survey, which describe code that is
+ gone.
+
#### Changed
-- **Breaking on the wire: every dial now uses POST-QUANTUM KEY EXCHANGE, and
- nothing else.** Each TLS configuration starts from
- [`macula-pqc`](https://crates.io/crates/macula-pqc) 0.1's `client_builder()`:
- `SecP384r1MLKEM1024`, then `SecP256r1MLKEM768`, and no classical group,
- where the crate used rustls's `ring` defaults (X25519, P-256, P-384, all
- classical). A station on macula 11.5.0 or earlier offers only those and
- **cannot be reached**; a station on macula's `macula-pqc` QUIC NIF
- negotiates `SecP384r1MLKEM1024`. Every trust mode is covered, and
- `PubkeyPinVerifier` and `SkipServerVerification` verify with the same
- provider. Key exchange only: certificates are still classically signed.
-
-- **Direct dial tries every authorized provider.** `direct_dial::call`,
- `call_with_ucan`, `call_with_cert_chain`, `open_stream_direct`,
- `open_stream_direct_with_cert_chain` and `get_direct` try each advertised
- provider in the order the DHT returns them, instead of only the first. A
- provider that can't be reached before the request is sent is skipped for
- the next one, and a request that has been sent is never sent again.
- `get_direct` also retries while no provider has announced the content
- yet, and a DHT lookup that fails is retried within the timeout instead of
- ending the call.
-- **Breaking: the timeout bounds the whole call**, finding the provider
- included. A timeout sized for the request alone can now run out during
- resolution. `resolve` and `resolve_with_cert_chain` give up after 10
- seconds, and `put_direct`'s timeout covers the endpoint lookup and the
- dial.
-- **Breaking: new `GetDirectError::Timeout { last }`.** It reports a
- `get_direct` whose timeout ran out during a transfer, where `last`, also
- its `source()`, carries the failure before it, or before any provider
- lookup was answered. An exhaustive `match` on `GetDirectError` needs the
- new arm.
-- **Breaking: new `ResolveError::Timeout`, and a call reports what it
- observed.** At its timeout a direct-dial call returns the last candidate
- failure, else why an answered DHT lookup found nothing, else a failed
- lookup's error, and `ResolveError::Timeout` only when nothing was
- observed at all, where it used to report `ProcedureNotAdvertised` or
- `StationEndpointNotFound`. A `station_endpoint` lookup follows the same
- rule, reporting `StationEndpointNotFound` only when a lookup was
- answered, and retries a lookup that fails within its budget. A record
- that names no dialable address is looked up again too, and when it is
- the latest answer the lookup reports the new
- `ResolveError::MalformedStationEndpoint`. An exhaustive `match` on
- `ResolveError` needs both new arms.
-- **Breaking: direct dial reuses a session this process already has open
- to the provider's station under the same identity.** A station keeps one
- connection per identity and closes the older one when a newer one
- arrives, so a second dial used to close `resolve_via` or a `Pool` link.
- `open_stream_direct`, `open_stream_direct_with_cert_chain`, `put_direct`
- and `get_direct` now run on that open session, on a dedicated QUIC
- stream, and leave it open. The stream functions return
- `direct_dial::OpenedStream` (`stream`, `lease`) instead of a
- `(Session, StreamHandle)` tuple; release its `SessionLease` once the
- stream is done. `call`, `call_with_ucan` and `call_with_cert_chain` run
- on an open session the same way.
-- **A direct call whose CALL was not sent tries the next candidate.** A call
- whose session had ended, or whose turn to write didn't come in time, moves
- on to the next candidate, and its station may be tried again on a later
- pass. A call that was or may have been sent is returned as before.
-- **A CALL handler receives its caller.** A map payload reaches the handler
- with the caller's 32-byte node id under `"caller"`, the caller the CALL's
- signature was verified against, replacing any `"caller"` the sender put in
- the payload. A payload that isn't a map reaches the handler unchanged and
- carries no caller.
-- **Breaking: `StreamHandle::accept` refuses a STREAM_OPEN not signed by its
- caller.** Stream handlers previously received the STREAM_OPEN's caller
- field unverified; upgrade if a stream handler relies on it. A stream whose
- first frame doesn't verify against the caller it names, has no signature
- or caller, is of another type, or doesn't decode is aborted in both
- directions with application error code 2 (`stream::REFUSED_STREAM`),
- with nothing written, and accept waits for the next stream within its
- timeout. `AcceptError::Parse` is removed. A provider that accepts a
- stream and won't serve it refuses it with `StreamHandle::refuse`, which
- writes a STREAM_ERROR, finishes the send half and stops reading with
- code 2.
-- **A stream handler receives its caller.** Map args of an accepted
- STREAM_OPEN carry the verified caller under `"caller"`, replacing any
- `"caller"` the opener put there, as a CALL handler's payload does.
-- **Drop warnings.** A session logs a dropped CALL (`dropped_call`), a
- RESULT or ERROR for no pending call (`dropped_reply`) and a refused stream
- (`refused_stream_open`) with its `count`, `reason`, and `procedure` or
- `call_id`. The first of a kind in an interval is logged at once, and the
- rest are counted into one closing line when the interval ends.
- `Session::set_drop_warning_interval` sets the interval, 60 seconds by
- default.
-- **A frame that doesn't decode ends a session as `SessionEndReason::Malformed`**,
- where it used to end as `StreamFailed`. An exhaustive `match` on
- `SessionEndReason` needs the new arm.
-- **A session direct dial dialed is shared until its last request is done.**
- A direct-dial request that finds it open uses it too, holding a lease of
- its own, and the session closes when the last lease is released instead
- of when the request that dialed it finishes. It is not reused once it is
- closing.
-- **Breaking: a UCAN-gated procedure binds the token to its caller.**
- `ucan::Policy::check` takes the CALL's `caller` as well as its token, and
- a `Policy::required` procedure accepts a token only when its `aud` is
- that caller's 32-byte node id as lowercase hex, with no `did:` prefix. A
- token with another or no audience is refused as `unauthorized`
- (`UcanError::WrongAudience`, and `UcanError::NoCaller` when `check` gets
- no 32-byte caller; both new). Mint tokens for gated procedures with that
- audience.
-- **An inbound CALL must be signed by the caller it names.**
- `Session::serve_one_call` and `serve_one_call_gated` drop a CALL whose
- signature doesn't verify against its `caller` field, without a reply and
- before any policy or handler runs, matching the Erlang station link.
-- **Breaking: a `Session` is a cloneable handle with one reader.** Its
- methods take `&self`, so calls, subscriptions, publishing and serving on
- one session run at the same time. A reader task routes each RESULT or
- ERROR to its call by call id, each EVENT to the subscriptions it matches,
- and each inbound CALL to a queue of 64 that `serve_one_call` and
- `serve_one_call_gated` take from. A slow subscriber never delays a call's
- reply. The functions in `dht`, `content`, `stream` and `direct_dial` take
- `&Session` instead of `&mut Session`.
-- **Breaking: `Session::subscribe` returns a `Subscription`** with its own
- queue of 256 events, read with `recv_event(timeout)` and ended with
- `close()`. A topic matches segment by segment on `/`, where `*` is exactly
- one segment, and the realm must be equal. Closing the last subscription
- for a realm and topic sends UNSUBSCRIBE. A subscription that falls more
- than 256 events behind returns its queued events and then
- `RecvEventError::Overflow`, and stays subscribed at the station until it
- is closed. `Session::recv_event`, `unsubscribe`, `recv_frame`,
- `recv_frame_timeout` and `leftover_bytes` are removed, and
- `run_subscriber` runs on a `Subscription`.
-- **Breaking: new error types.** `Session::call` and `call_with_ucan`
- return `CallError`: `Timeout { write_started }`,
- `SessionEnded { reason, write_started }`, `SendTimeout`, `Encode`,
- `Write` or `MalformedReply`, where `not_sent()` tells whether the CALL
- can safely be sent again. `publish`, `advertise`, `unadvertise` and
- `subscribe` return `SendError`, `RecvEventError` is `Timeout`, `Overflow`
- or `SessionEnded`, `serve_one_call` returns `ServeCallError`, and
- `FrameStream`'s call error is renamed `StreamCallError`.
-- **Writes are bounded.** A caller waits for its turn to write no longer
- than its deadline, a call's timeout or else 30 seconds. A write that
- takes longer than 30 seconds ends the session. The reader never waits on
- a write: an inbound CALL that finds the queue full is answered with
- `temporary_relay_failure` through a separate queue of 64 frames, and
- serving carries on.
-- **How a session ends.** A GOODBYE, a HELLO or CONNECT after the handshake,
- a frame that doesn't decode, a stalled write or the end of the control
- stream ends the session: its pending calls fail with `SessionEnded`, its
- connection closes, direct dial no longer reuses it, and the end is logged
- once through the `log` crate, as a warning when the station or connection
- ended it and as info when it was closed here, with both node ids.
- `Session::end_reason` and `ended` report it. Frames no route claims are
- counted by type in `unrouted_frame_counts`, with a log line at most once
- a minute.
-- **`Pool::call` publishes no RPC facts.** A pooled call goes through the
- link's session without the `rpc.sent_v1` and `rpc.completed_v1` facts
- that `Session::call` publishes.
-- **Breaking: `Pool::call` tries another link only when the CALL was not
- sent.** It moves on to the next connected link only while a call fails
- before its CALL was written, so no CALL runs twice. A call that timed out
- after its write started, and an ERROR reply, are returned as they are.
- `PoolCallError::AllFailed` is replaced by `PoolCallError::Call`, the
- failure that stopped the call.
-- **A pool link is dialed again when its session ends**, instead of when a
- call or publish on it fails, so a call that times out on a link that is
- still up no longer drops that link.
+- `rust-version` is 1.89, the least the dependencies build with.
+- `Cargo.lock` is committed and CI tests with `--locked`.
### [0.3.0] - 2026-09-05
@@ -442,30 +342,35 @@ did, but the two have moved at different paces ever since).
### [ffi-0.4.0] - Unreleased
+**Breaking throughout**: rewritten on `macula-rust` 0.4's pool, the macula 12
+wire.
+
+#### Added
+
+- `FfiNodeKey`: `generate`, `load`, `load_or_create`, `save`,
+ `load_from_keystore` and `save_to_keystore`, in `FfiProfile::PqPure` or
+ `PqHybrid`.
+- `FfiPool`: `connect` with pinned `FfiSeed`s and `FfiPoolOptions` (realm
+ trust, timeouts, bounds). It offers `call`, `providers`, `publish`,
+ `subscribe`, `serve`, `serve_stream`, `open_stream`, the DHT's
+ `find_record`, `find_records`, `find_records_by_type` and `put_record`,
+ `status` and `close`.
+- `FfiCallHandler` and `FfiStreamHandler`, implemented by the app
+ (`suspend fun` in Kotlin, `async throws` in Swift). A handler's thrown
+ `FfiError` reaches the caller as a handler_error.
+- `FfiSubscription` (`next` with a timeout, `unsubscribe`), `FfiStream` on
+ either side, `FfiServed`, and `own_procedure`.
+- `FfiError` maps the pool's errors, a provider's error with its code, and a
+ foreign handler's unexpected throw.
+
+#### Removed
+
+- `FfiKeyPair`, `FfiSession`, `FfiTrust`, the UCAN functions, direct-dial and
+ cert-chain calls, content transfer, and the live tests that used them.
+
#### Changed
-- Builds on `macula-rust` 0.4, with the dependency requirement moved to
- `"0.4"`. The direct-dial calls on `FfiSession` therefore try every
- authorized provider, and their `timeout_ms` now bounds finding the
- provider as well. A `get_direct` whose transfer the timeout cuts off
- reports `FfiError::Content` with the earlier failure in its reason.
-- `FfiSession::serve_one_call_gated` refuses a UCAN token whose `aud` isn't
- the calling node's id as lowercase hex, and both serve calls drop a CALL
- that isn't signed by its caller. Mint tokens for gated procedures with
- `ucan_create` using that audience.
-- **Breaking: `FfiOpenedDirectStream` carries a `lease` instead of a
- `session`.** The direct-dial stream and content calls on `FfiSession` run
- on a session this process already has open to the provider's station
- under the same identity, instead of dialing a second one that would close
- it. Call `FfiSessionLease::release` once the stream is done: a session
- direct dial dialed closes when no other direct-dial request still uses it.
-- **Breaking: `FfiSession::subscribe` returns an `FfiSubscription`**, read
- with `recv_event(timeout_ms)` and ended with `close()`. Each subscription
- has its own queue of 256 events and receives only the events its topic
- and realm match. `FfiSession::recv_event` and `unsubscribe` are removed.
-- Methods on one `FfiSession` no longer wait for each other:
- `serve_one_call`, `accept_stream`, calls and subscriptions on the same
- session run at the same time.
+- `rust-version` is 1.91, the least the dependencies build with.
### [ffi-0.3.1] - 2026-09-05
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..27d8431
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,2929 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aes"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32"
+dependencies = [
+ "cipher",
+ "cpubits",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android-native-keyring-store"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c6349ddff23194f8fdce2ea8849380f5a4868c1648965b70e801e104cba9b3"
+dependencies = [
+ "base64 0.22.1",
+ "jni 0.21.1",
+ "keyring-core",
+ "log",
+ "ndk-context",
+ "regex",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.21",
+ "tracing",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "apple-native-keyring-store"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b350bfd03649e07aa05c0a81b3e15934374e585c98204a57e20b9d49f49bb9a"
+dependencies = [
+ "keyring-core",
+ "log",
+ "security-framework",
+]
+
+[[package]]
+name = "askama"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6024d73179f43f15ccd2b881bfea6fee7f3a46ec53f33b52210dea749ebebaa4"
+dependencies = [
+ "askama_macros",
+ "itoa",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "askama_derive"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071ee5ebf2138e3ad180e0aacf6940c2cab5e6d8333741d9925c7bee2b153f39"
+dependencies = [
+ "askama_parser",
+ "basic-toml",
+ "glob",
+ "memchr",
+ "proc-macro2",
+ "quote",
+ "rustc-hash",
+ "serde",
+ "serde_derive",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "askama_macros"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "643e1c7cbb6aec1d920332fe51a7c0d8219e273dcb8602db03f5263e4d16487b"
+dependencies = [
+ "askama_derive",
+]
+
+[[package]]
+name = "askama_parser"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c5ae75772275d268b03ab8bdccdd12117b6169ee23256942b34e46c9f476583"
+dependencies = [
+ "rustc-hash",
+ "serde",
+ "serde_derive",
+ "unicode-ident",
+ "winnow",
+]
+
+[[package]]
+name = "asn1-rs"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror 2.0.21",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-compat"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c97d7ff3c25d6c10d64170c12acaf5d4245e76dece3779c1d92b153a64f11df"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "once_cell",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"
+dependencies = [
+ "aws-lc-sys",
+ "untrusted 0.7.1",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "pkg-config",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
+[[package]]
+name = "basic-toml"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
+
+[[package]]
+name = "block-buffer"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "camino"
+version = "1.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.21",
+]
+
+[[package]]
+name = "cbc"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "cc"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chacha20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "rand_core",
+]
+
+[[package]]
+name = "cipher"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
+dependencies = [
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
+
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cmov"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
+
+[[package]]
+name = "combine"
+version = "4.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpubits"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
+
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "ctutils"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
+dependencies = [
+ "cmov",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
+
+[[package]]
+name = "der-parser"
+version = "10.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "ctutils",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+dependencies = [
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastbloom"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8"
+dependencies = [
+ "foldhash",
+ "libm",
+ "portable-atomic",
+ "siphasher",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "fs-err"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-io"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi",
+ "rand_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
+name = "goblin"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
+dependencies = [
+ "log",
+ "plain",
+ "scroll",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "hybrid-array"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "inout"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
+dependencies = [
+ "block-padding",
+ "hybrid-array",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys 0.3.1",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.21",
+ "walkdir",
+ "windows-link",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
+dependencies = [
+ "getrandom 0.4.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "keyring"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2270074a3d26bcac93c1dc5d2845eb4c089e8d761ccf6e0ea266a16004640627"
+dependencies = [
+ "android-native-keyring-store",
+ "apple-native-keyring-store",
+ "keyring-core",
+ "windows-native-keyring-store",
+ "zbus-secret-service-keyring-store",
+]
+
+[[package]]
+name = "keyring-core"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "linux-keyutils"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590"
+dependencies = [
+ "bitflags",
+ "libc",
+]
+
+[[package]]
+name = "linux-keyutils-keyring-store"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39fbed79f71dc21eb21d3d07c0e908a3c58ff9a1fdbf5cf44230fb3deb6d994b"
+dependencies = [
+ "keyring-core",
+ "linux-keyutils",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
+
+[[package]]
+name = "macula-keccak"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d99a9ef94be3261f1debe3b04bebcafb992f7f29fb990a87e080365a810567bb"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "macula-mldsa"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e846a49c6be27e4e9abf33a88ac1edcfc4e0c3308e2bdb92f12e101c05856ebb"
+dependencies = [
+ "getrandom 0.4.3",
+ "macula-keccak",
+ "zeroize",
+]
+
+[[package]]
+name = "macula-mlkem"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1dde77fd0a48f76533836a342cd4c2cf844f63e2d534330b182a52b85b3471"
+dependencies = [
+ "getrandom 0.4.3",
+ "macula-keccak",
+ "zeroize",
+]
+
+[[package]]
+name = "macula-pqc"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bbd8755ca194c2d381cb15f49f0ca2022c86129e8c6b8c9bd685d3c77b728af"
+dependencies = [
+ "macula-mldsa",
+ "macula-pqc-kx",
+ "rcgen",
+ "rustls",
+]
+
+[[package]]
+name = "macula-pqc-kx"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "151b72a3953fe9bfa0046421046b13b7cd360133d8cd1caf39b468ac25f98456"
+dependencies = [
+ "macula-mlkem",
+ "rustls",
+]
+
+[[package]]
+name = "macula-rust"
+version = "0.4.0"
+dependencies = [
+ "apple-native-keyring-store",
+ "aws-lc-rs",
+ "hex",
+ "keyring",
+ "keyring-core",
+ "linux-keyutils-keyring-store",
+ "macula-mldsa",
+ "macula-pqc",
+ "quinn",
+ "rcgen",
+ "rustix",
+ "rustls",
+ "serde_json",
+ "sha2",
+ "tempfile",
+ "tokio",
+]
+
+[[package]]
+name = "macula-rust-ffi"
+version = "0.4.0"
+dependencies = [
+ "async-trait",
+ "hex",
+ "macula-rust",
+ "serde_json",
+ "tempfile",
+ "thiserror 2.0.21",
+ "tokio",
+ "uniffi",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "mio"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-integer"
+version = "0.1.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "oid-registry"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
+dependencies = [
+ "asn1-rs",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "pem"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120"
+dependencies = [
+ "base64 0.23.1",
+ "serde_core",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.21",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
+dependencies = [
+ "bytes",
+ "fastbloom",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
+ "slab",
+ "thiserror 2.0.21",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rcgen"
+version = "0.14.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8"
+dependencies = [
+ "aws-lc-rs",
+ "pem",
+ "ring",
+ "rustls-pki-types",
+ "time",
+ "x509-parser",
+ "yasna",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted 0.9.0",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
+dependencies = [
+ "aws-lc-rs",
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
+dependencies = [
+ "core-foundation",
+ "core-foundation-sys",
+ "jni 0.22.4",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "scroll"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
+dependencies = [
+ "scroll_derive",
+]
+
+[[package]]
+name = "scroll_derive"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "secret-service"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5107b24b91445dd2aa449a258a1807b63240942157292354dc5bfdbeb8bc6db8"
+dependencies = [
+ "aes",
+ "cbc",
+ "futures-util",
+ "getrandom 0.4.3",
+ "hkdf",
+ "hybrid-array",
+ "num",
+ "once_cell",
+ "serde",
+ "sha2",
+ "zbus",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "siphasher"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
+
+[[package]]
+name = "smawk"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.16.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49"
+dependencies = [
+ "smawk",
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
+dependencies = [
+ "thiserror-impl 2.0.21",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
+
+[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.6+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.15+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "uniffi"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76407f5f396a2c949a069eff4a9fca9ce462410eb872bffef4c2b7b89804a77a"
+dependencies = [
+ "anyhow",
+ "camino",
+ "cargo_metadata",
+ "clap",
+ "uniffi_bindgen",
+ "uniffi_core",
+ "uniffi_macros",
+ "uniffi_pipeline",
+]
+
+[[package]]
+name = "uniffi_bindgen"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3df3ced8f0eda3de99f6d50820e8628f443da8e4f447f477d1403ceba29dacbd"
+dependencies = [
+ "anyhow",
+ "askama",
+ "camino",
+ "cargo_metadata",
+ "fs-err",
+ "glob",
+ "goblin",
+ "heck",
+ "indexmap",
+ "once_cell",
+ "serde",
+ "tempfile",
+ "textwrap",
+ "toml",
+ "uniffi_internal_macros",
+ "uniffi_meta",
+ "uniffi_pipeline",
+ "uniffi_udl",
+]
+
+[[package]]
+name = "uniffi_core"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7530ae8efeaaa488622865966763fe0294832779fff80508c36d69c6a874a6a"
+dependencies = [
+ "anyhow",
+ "async-compat",
+ "bytes",
+ "once_cell",
+ "static_assertions",
+]
+
+[[package]]
+name = "uniffi_internal_macros"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f4bad017164375d99450d70495014810d15a84ba9da6ed14b9628b1424223ba"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "uniffi_macros"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b855a58da153739ce6e863f6e296edc3716a093768c4d3d957f17f5e4317c60"
+dependencies = [
+ "camino",
+ "fs-err",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.119",
+ "toml",
+ "uniffi_meta",
+]
+
+[[package]]
+name = "uniffi_meta"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af0c88664a9cb9559856f5a250283c9708923b303170e2e5c7db8f4ad65e9459"
+dependencies = [
+ "anyhow",
+ "siphasher",
+ "uniffi_internal_macros",
+ "uniffi_pipeline",
+]
+
+[[package]]
+name = "uniffi_pipeline"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d00f5c044b4a3dca0b1226c92ddaeea626f337470e22180dd182160c87a06b0b"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "tempfile",
+ "uniffi_internal_macros",
+]
+
+[[package]]
+name = "uniffi_udl"
+version = "0.32.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55dfd9c778d6b39cb5acd541d9ff63431974035d2bad83c897b766a6ee3152ac"
+dependencies = [
+ "anyhow",
+ "textwrap",
+ "uniffi_meta",
+ "weedle2",
+]
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "uuid"
+version = "1.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
+dependencies = [
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.129"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "weedle2"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-native-keyring-store"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8"
+dependencies = [
+ "byteorder",
+ "keyring-core",
+ "regex",
+ "windows-sys 0.61.2",
+ "zeroize",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "x509-parser"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
+dependencies = [
+ "asn1-rs",
+ "aws-lc-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom",
+ "oid-registry",
+ "ring",
+ "rusticata-macros",
+ "thiserror 2.0.21",
+ "time",
+]
+
+[[package]]
+name = "yasna"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282"
+dependencies = [
+ "bit-vec",
+ "time",
+]
+
+[[package]]
+name = "zbus"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus-secret-service-keyring-store"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74801d001b9e7729adb4f1825b67b398185fed424749aa3d8bacf70417137d9a"
+dependencies = [
+ "keyring-core",
+ "secret-service",
+ "zbus",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
+dependencies = [
+ "serde",
+ "winnow",
+ "zvariant",
+]
+
+[[package]]
+name = "zcheapstr"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+
+[[package]]
+name = "zvariant"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow",
+ "zcheapstr",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 3.0.6",
+ "winnow",
+]
diff --git a/Cargo.toml b/Cargo.toml
index c2f835f..2e5fb44 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,14 +5,14 @@ members = ["macula-rust-ffi"]
name = "macula-rust"
version = "0.4.0"
edition = "2021"
-rust-version = "1.85"
+rust-version = "1.89"
authors = ["Macula "]
-description = "Rust port of macula's SDK (client/leaf) wire protocol — mobile first, not mobile-only. See plans/PLAN_WIRE_PROTOCOL.md."
+description = "Rust SDK for the macula 12 mesh: ML-DSA-87 and LAMPS composite node keys, a post-quantum QUIC transport — mobile first, not mobile-only."
license = "Apache-2.0"
repository = "https://github.com/macula-io/macula-rust"
homepage = "https://github.com/macula-io/macula-rust"
readme = "README.md"
-keywords = ["mesh-network", "quic", "p2p", "ed25519", "decentralized"]
+keywords = ["mesh-network", "quic", "p2p", "post-quantum", "decentralized"]
categories = ["network-programming", "cryptography"]
[lints.rust]
@@ -22,37 +22,23 @@ unsafe_code = "forbid"
all = { level = "deny", priority = -1 }
[dependencies]
-ed25519-dalek = { version = "3.0", features = ["rand_core"] }
-rand = "0.10"
sha2 = "0.11"
quinn = "0.11"
# The key exchange for every connection this crate dials: every TLS
# configuration starts from macula-pqc's client_builder(), which offers
-# SecP384r1MLKEM1024 then SecP256r1MLKEM768 and nothing classical. So
-# rustls selects no crypto provider of its own here. See transport.rs.
-macula-pqc = "0.1"
-rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12"] }
-webpki-roots = "1.0"
-x509-parser = "0.18"
-# cert_chain.rs: pure X.509 path validation (no hostname/SAN check, unlike
-# rustls's own ServerCertVerifier machinery in cert.rs) to a caller-supplied
-# realm CA — already transitively pulled in by rustls, declared directly
-# here since cert_chain.rs uses its EndEntityCert::verify_for_usage API.
-rustls-webpki = { version = "0.103", features = ["ring"] }
+# SecP384r1MLKEM1024 then SecP256r1MLKEM768 and nothing classical, and whose
+# KeyPossessionVerifier accepts one ML-DSA-87 station certificate. So rustls
+# selects no crypto provider of its own here. See transport.rs.
+macula-pqc = "0.3"
+# ML-DSA-87 for every node key signature (profile.rs, node_key.rs): the same
+# implementation macula-pqc signs TLS with.
+macula-mldsa = "0.3"
+# The RSA-PSS-4096 half of pq_hybrid's LAMPS composite: already linked through
+# rustls for the key exchange, constant-time, and with a FIPS path.
+aws-lc-rs = "1"
+rustls = { version = "0.23", default-features = false, features = ["logging", "std"] }
tokio = { version = "1", features = ["full"] }
-uuid = { version = "1", features = ["v7"] }
-# control_channel.rs: a session end and dropped unrouted frames are reported
-# through the `log` facade, silent unless the application installs a logger.
-# Already in the graph through rustls's `logging` feature.
-log = "0.4"
-blake3 = "1.5"
-# ucan.rs: JWT-shaped token (de)serialization, matching the reference
-# macula_ucan_nif's own dependency choices exactly (its Cargo.toml has no
-# UCAN-spec crate either, only these same generic primitives).
-serde = { version = "1", features = ["derive"] }
-serde_json = "1"
-base64 = "0.23"
-# keystore.rs: platform-native secure storage for a persisted seed
+# keystore.rs: platform-native secure storage for a node key
# (Keychain via Security.framework on macOS/iOS, Secret Service via D-Bus
# on Linux, Credential Manager on Windows, Keystore via JNI on Android —
# each selected automatically per target by keyring's own Cargo.toml
@@ -91,25 +77,18 @@ linux-keyutils-keyring-store = "1"
[target.'cfg(target_os = "ios")'.dependencies]
apple-native-keyring-store = { version = "1", features = ["protected"] }
+[target.'cfg(unix)'.dependencies]
+# node_key/key_file.rs: a key file must belong to the effective user, and is
+# opened without blocking; std has neither geteuid nor O_NONBLOCK without libc.
+rustix = { version = "1", features = ["fs", "process"] }
+
[dev-dependencies]
hex = "0.4"
tempfile = "3"
-# Test-only: generates synthetic Ed25519 certs to unit-test
-# PubkeyPinVerifier's matching logic without needing a live station that
-# happens to present that cert type — see src/cert.rs's tests. Not a
-# runtime dependency of the crate itself (see src/cert.rs's module doc:
-# a dialing client never generates or presents a cert of its own).
-
-# Test-only, cert_chain.rs's own tests: signed_by() needs a SubjectPublicKeyInfo
-# constructed from a raw advertiser Ed25519 pubkey rather than a freshly
-# rcgen-generated one (the leaf must bind the SAME key that signed the
-# advertisement) — SubjectPublicKeyInfo::from_der needs this feature.
-rcgen = { version = "0.14", default-features = false, features = ["pem", "ring", "x509-parser"] }
-# Test-only, transport.rs's own tests: the stations the dialler must still
-# reach, or must refuse, are built from aws-lc-rs's own groups.
+serde_json = "1"
+# Test-only, transport.rs's own tests: a station with a classical certificate,
+# which a dial must refuse.
+rcgen = { version = "0.14", default-features = false, features = ["pem", "ring"] }
+# Test-only, transport.rs's own tests: the stations the dialler must refuse
+# are built from aws-lc-rs's own groups.
rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs"] }
-# Test-only, cert_chain.rs's own tests: rcgen's CertificateParams
-# not_before/not_after fields take this crate's OffsetDateTime directly;
-# not re-exported by rcgen, so declared explicitly (already transitively
-# present via rcgen itself).
-time = "0.3"
diff --git a/README.md b/README.md
index 223ed3c..dfc866d 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://github.com/macula-io/macula-rust/actions/workflows/ci.yml)
[](#license)
-[](https://www.rust-lang.org)
+[](https://www.rust-lang.org)
[](https://github.com/rust-secure-code/safety-dance/)
[](https://github.com/sponsors/rgfaber)
@@ -14,211 +14,159 @@
- Rust port of the Macula SDK wire protocol — mobile first, not mobile-only
+ Rust SDK for the Macula mesh, with Kotlin and Swift bindings
---
-> **Status, 2026-08-30:** feature-complete for a leaf/edge client —
-> the client/leaf side of the wire protocol is built and
-> **live-verified against the production station fleet**
-> (`station-de-frankfurt.macula.io`) — handshake (pinned or WebPki
-> trust), unary RPC, PubSub, content transfer, and streaming RPC, every
-> primitive in both caller and provider roles, plus direct-dial
-> (DHT resolve/publish, both plain and cert-chain-authorized), periodic
-> re-advertise, UCAN (mint/verify/introspect — policy-gated serving's
-> live-network behavior needs a closer look, see Known limitations), a
-> supervised PubSub pair, RPC telemetry auto-facts, and an
-> overridable-per-platform `KeyStore` for identity persistence. Mobile
-> bindings (Kotlin + Swift, via UniFFI) wrap almost the entire surface,
-> generated and CI-checked on every push. See [Status](#status) for
-> what's deliberately out of scope vs. genuinely separate future work,
-> and [Known limitations](#known-limitations) for one real external bug
-> this crate can't fix.
+> **Status, 2026-09-26:** on the **macula 12** wire. That means post-quantum
+> ML-DSA-87 identities (in pq_hybrid, the fleet's profile, the ML-DSA-87 +
+> RSA-PSS-4096 composite), ML-KEM hybrid key exchange, and signed requests.
+> Calls and streams by direct dial, serving (under an org or in a node's own
+> namespace), publish/subscribe and the DHT are tested against in-process
+> macula 12 stations on every `cargo test`, and live against the fleet. Not
+> here yet: UCAN-gated calls and node-served content; see [Not yet
+> implemented](#not-yet-implemented). Releases before 0.4.0 speak the retired
+> 10.x wire and cannot reach the current fleet.
## What is this?
-A ground-up Rust implementation of the client half of Macula's wire
-protocol — the same protocol [`macula-io/macula`](https://github.com/macula-io/macula)
-(the Erlang/OTP SDK) speaks, extracted directly from that source and
-tracked in [`plans/PLAN_WIRE_PROTOCOL.md`](plans/PLAN_WIRE_PROTOCOL.md).
-Macula is a federated mesh for sovereign, end-to-end-encrypted
-application networks; a **station** is the relay/DHT node, and this crate
-is what a **leaf** — a phone, a desktop app, a CLI, anything that isn't
-itself a station — uses to join it.
+A native Rust implementation of a Macula node: its identity key, a pool of
+links to the stations it pins by node_id, calls and streams that reach a
+provider at its own station, serving procedures, publish/subscribe, and the
+DHT. It speaks the same wire as [macula](https://github.com/macula-io/macula)
+(the Erlang/OTP reference) and [macula-go](https://github.com/macula-io/macula-go),
+over QUIC ([quinn](https://github.com/quinn-rs/quinn)) with the post-quantum
+TLS of [macula-pqc](https://crates.io/crates/macula-pqc), ML-DSA-87 from
+[macula-mldsa](https://crates.io/crates/macula-mldsa), and RSA-PSS-4096 from
+aws-lc-rs.
-Mobile is the flagship consumer driving the work (hence the UniFFI
-crate), not a ceiling on it: the core crate has zero UniFFI dependency
-and zero FFI-shaped types, so it's exactly as usable from plain Rust, a
-CLI, or WASM as any other Rust SDK.
-
-## Features
-
-| Primitive | Caller | Provider | Notes |
-|---|---|---|---|
-| Handshake (CONNECT/HELLO) | ✅ | — | Ed25519 identity, S/Kademlia puzzle-hardened |
-| One session, many uses | ✅ | ✅ | `Session` is a cloneable handle with one reader: calls, subscriptions and serving on it run at the same time, and a slow consumer never stalls a call's reply |
-| Unary RPC (CALL/RESULT/ERROR) | ✅ | ✅ | `Session::serve_one_call`, BOLT#4 error mapping live-verified; a call that times out says whether its frame was sent |
-| PubSub (PUBLISH/SUBSCRIBE/EVENT) | ✅ | ✅ | `Session::subscribe` returns a `Subscription` with its own queue of 256 events; a subscriber gets its own publish, verified live |
-| Content transfer (single-block + chunked) | ✅ | ✅ | Content-addressed, BLAKE3/SHA-256 |
-| Streaming RPC (STREAM_OPEN/DATA/END/REPLY) | ✅ | ✅ | Both roles live-verified against the real fleet; `ClientStream` mode's reply path is SDK-correct but currently blocked by a `macula-station` bug — see [Known limitations](#known-limitations) |
-| RPC advertise/unadvertise | ✅ | — | |
-| Direct-dial (DHT resolve/publish) | ✅ | ✅ | `direct_dial::{resolve,call,advertise_direct}` — reaches a service without depending on advertise-gossip having propagated a route; plain + cert-chain-authorized (`*_with_cert_chain`) |
-| Direct-dial streaming/content | ✅ | ✅ | `direct_dial::{open_stream_direct,put_direct,get_direct}` — runs on a session already open to the same station under the same identity instead of dialing a second one, which the station would answer by closing the first; `get_direct` is correct but currently unreachable, see [Known limitations](#known-limitations) |
-| Periodic re-advertise | — | ✅ | `Session::keep_advertised` / `direct_dial::keep_advertised_direct` — a ctx-cancellable loop, since a station's registration doesn't survive the connection that sent it being replaced |
-| UCAN (mint/verify/introspect) | ✅ | ✅ | `ucan::{create,verify,decode,get_*}` are pure functions; `Session::call_with_ucan`/`serve_one_call_gated` live-verified end-to-end; a gated provider accepts a token only when its `aud` is the calling node's id as lowercase hex, and drops a CALL not signed by its caller (see [`examples/ucan.rs`](examples/ucan.rs) and [Known limitations](#known-limitations) for the resolved investigation) |
-| Cert-chain (org/realm authorization) | ✅ | ✅ | `cert_chain::verify_advertisement_cert_chain` + `direct_dial::*_with_cert_chain` — opt-in, the plain direct-dial path is unaffected |
-| Supervised PubSub pair | ✅ | ✅ | `Session::run_publisher`/`run_subscriber` — addressable/cancellable wrappers over bare publish/subscribe, auto-publishing `pubsub.publish_*_v1` facts |
-| RPC telemetry auto-facts | ✅ | ✅ | `rpc.sent_v1`/`rpc.completed_v1` (caller), `rpc.received_v1`/`rpc.replied_v1` (provider) — always-on, fire-and-forget, fired automatically by `call`/`serve_one_call_gated` |
-| Overridable `KeyStore` | ✅ | — | `keystore::KeyStore` trait + `KeyringStore`/`LinuxKeyutilsStore` — `KeyPair::save_to_keystore`/`load_from_keystore`; the raw-file `KeyPair::save` stays as a testing/parity convenience |
-| Mobile bindings (Kotlin, Swift) | ✅ | ✅ | Via [UniFFI](#mobile-bindings-uniffi) — provider role serves via `FfiCallHandler`, a foreign-implemented async trait (`suspend fun`/`async throws`), not a closure. Covers direct-dial, UCAN, cert-chain, content/stream direct-dial reuse, and `KeyStore`; deliberately NOT `keep_advertised`/`run_subscriber` (see the FFI crate's own module doc for why) |
-| Pubkey-pinned trust | ✅ | — | `Trust::Pinned` / `FfiTrust.Pinned` — the only mode that works at all for a station without a CA-issued cert |
-| Post-quantum key exchange | ✅ | — | Every dial, in every trust mode, via [`macula-pqc`](https://crates.io/crates/macula-pqc): `SecP384r1MLKEM1024`, then `SecP256r1MLKEM768`, nothing classical. A station on macula 11.5.0 or earlier offers only classical groups and cannot be reached. Key exchange only: certificates are still classically signed |
-
-`unsafe_code = "forbid"` at the crate level — the only unsafe in this
-workspace lives inside its dependencies (`quinn`, `ring`, `aws-lc-rs`),
-not here.
+[`macula-rust-ffi`](#mobile-bindings-kotlin-and-swift) wraps it for Kotlin and
+Swift. The core crate has no FFI dependency and no FFI-shaped types.
## Quick start
-Also lives as a runnable example — `cargo run --example quickstart`.
-Advertises and calls its own trivial echo procedure (two identities, a
-provider and a caller, since a station kicks a connection the instant a
-second one arrives under the same identity) rather than depending on any
-particular procedure already being advertised on the fleet:
+```toml
+[dependencies]
+macula-rust = "0.4"
+tokio = { version = "1", features = ["full"] }
+```
+
+A node needs a station to link to, **pinned by its node_id**, and the key of
+each realm it trusts, which the realm publishes. Its own key is created on
+first use and kept in a file its owner alone can read.
```rust
-use std::time::{Duration, SystemTime, UNIX_EPOCH};
-use macula_rust::{
- cbor::Value,
- connection::{self, BoxFuture, CallHandler},
- frame::AdvertiseSpec,
- identity::KeyPair,
- transport::Trust,
-};
-
-#[tokio::main]
-async fn main() -> Result<(), Box> {
- // Puzzle-hardened identities — required. An unhardened identity fails
- // the handshake silently (QUIC/TLS looks healthy, HELLO never accepts).
- let provider_identity = KeyPair::generate_with_default_puzzle();
- let caller_identity = KeyPair::generate_with_default_puzzle();
-
- let provider_session = connection::connect(
- "station-de-frankfurt.macula.io",
- 4433,
- Trust::WebPki,
- &provider_identity,
- )
- .await?;
- let caller_session = connection::connect(
- "station-de-frankfurt.macula.io",
- 4433,
- Trust::WebPki,
- &caller_identity,
- )
+use std::collections::HashMap;
+use std::path::Path;
+use std::sync::Arc;
+
+use macula_rust::cbor::Value;
+use macula_rust::node_key::NodeKey;
+use macula_rust::pool::{Call, Opts, Pool, Seed};
+use macula_rust::profile::Profile;
+use macula_rust::station_link::Publication;
+
+let key = NodeKey::load_or_create(Path::new("node.key"), Profile::PqHybrid)?;
+let mut opts = Opts::new(Arc::new(key));
+opts.realm_trust = HashMap::from([(realm, realm_key)]);
+let pool = Pool::connect(
+ vec![Seed { host: "station-fi-helsinki.macula.io".into(), port: 4433, node_id: station_id }],
+ opts,
+)
+.await?;
+
+// A call reaches a provider by direct dial: its advertisement from the DHT,
+// trusted only when the realm key authorizes it, and its station dialed.
+let answer = pool
+ .call(Call { realm, procedure: "mcl-echo/echo".into(), payload: Value::text("hello"), ..Call::default() })
.await?;
- let realm = [0u8; 32];
- // Unique per run — reusing a fixed procedure name across rapid
- // repeated runs can hit stale DHT routing state from the prior run's
- // now-dead advertiser.
- let procedure = format!(
- "macula_rust.quickstart_echo.{}",
- SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos()
- );
-
- let advertise_spec = AdvertiseSpec::new(realm, procedure.clone(), provider_identity.node_id());
- provider_session
- .advertise(&advertise_spec, &provider_identity)
- .await?;
- tokio::time::sleep(Duration::from_millis(500)).await; // ADVERTISE is fire-and-forget; give it a moment to land
-
- let target_procedure = procedure.clone();
- let lookup = move |_realm: &[u8; 32], proc: &str| -> Option {
- if proc != target_procedure {
- return None;
- }
- let handler: CallHandler = std::sync::Arc::new(|payload: Value| {
- Box::pin(async move { Ok(payload) }) as BoxFuture<'static, Result>
- });
- Some(handler)
- };
-
- let serve_task = tokio::spawn(async move {
- let result = provider_session
- .serve_one_call(lookup, &provider_identity, Duration::from_secs(10))
- .await;
- // Close explicitly instead of letting provider_session drop when
- // this task ends — see Session's own doc for why: dropping the
- // last handle closes the connection at once, which gives quinn's
- // send-scheduling no guarantee the RESULT just sent actually
- // reached the peer first.
- provider_session
- .close(
- "normal",
- Some("quickstart provider done"),
- &provider_identity,
- )
- .await;
- result
- });
-
- let now_ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i128;
- let response = caller_session
- .call(
- &procedure,
- realm,
- Value::Text("hello".into()),
- now_ms + 5_000, // deadline_ms
- &caller_identity,
- Duration::from_secs(5),
- )
- .await?;
-
- serve_task.await??;
- caller_session
- .close("normal", Some("quickstart caller done"), &caller_identity)
- .await;
-
- println!("{response:?}");
- Ok(())
-}
+// Publish and subscribe; topics name a kind of fact, ids go in the payload.
+let mut sub = pool.subscribe(&realm, "acme/demo/greeting_sent_v1").await?;
+pool.publish(Publication {
+ realm,
+ topic: "acme/demo/greeting_sent_v1".into(),
+ payload: Value::Map(vec![(Value::text("text"), Value::text("hi"))]),
+ ttl_ms: None,
+})
+.await?;
+let event = sub.recv().await;
+
+pool.close().await;
```
-## Mobile bindings (UniFFI)
-
-`macula-rust-ffi` is a separate crate — not code bolted onto the
-core one — wrapping every application primitive (`FfiSession::connect`/
-`call`/`serve_one_call`/`publish`/`subscribe`/`content_put`/
-`content_get`/`stream_open`/`advertise`/`accept_stream`) for Kotlin and
-Swift, in the modern proc-macro UniFFI style (`#[uniffi::export]`,
-native `async`/`await` and Kotlin coroutines, no `.udl` file). CI
-rebuilds the `cdylib` and regenerates both language bindings on every
-push as a codegen smoke test.
+Serving a procedure in the node's own namespace needs no org and no realm key:
-Serving an RPC from Kotlin or Swift means implementing `FfiCallHandler`
-— a **foreign trait** (`#[uniffi::export(foreign)]`), not a callback
-closure (UniFFI foreign traits can't carry a plain closure, so
-`handle` receives the full inbound call and does its own procedure
-routing if a session serves more than one):
-
-```kotlin
-class Doubler : FfiCallHandler {
- override suspend fun handle(procedure: String, realm: ByteArray, payload: FfiValue): FfiValue {
- val n = (payload as FfiValue.Int).v1
- return FfiValue.Int(n * 2)
- }
-}
+```rust
+use macula_rust::pool::Offer;
+use macula_rust::record::own_procedure;
+use macula_rust::station_link::handler;
-session.advertise("math.double", realm, identity)
-session.serveOneCall(Doubler(), timeoutMs = 30_000u, identity)
+let ring = own_procedure(&pool.node_id(), "ring"); // ~/ring
+let served = pool
+ .serve(Offer::unary(realm, &ring, handler(|request| async move { Ok(request.payload) })))
+ .await?;
```
-(`FfiValue` currently covers `Null`/`Int`/`Bytes`/`Text`/`Float` — see
-this crate's own module doc for why `List`/`Map` aren't there yet; a
-handler needing a structured payload should encode it as `Bytes`
-today.)
+Runnable versions are in [`examples/`](examples): `quickstart`, `serve` and
+`publish_subscribe`, each reading the environment described at the top of
+[`examples/common/mod.rs`](examples/common/mod.rs).
+
+### Coming from 0.3 and earlier
+
+Everything moved to the macula 12 wire, and the API with it. There is no
+compatibility layer.
+
+- **New identities.** A macula 12 node_id derives from an ML-DSA-87 key (or
+ the LAMPS composite in `pq_hybrid`), so no Ed25519 identity carries over.
+ `NodeKey::load_or_create` makes a new key file. **Re-join your realms and
+ re-trust your agents**: anything that named your old node_id must be redone
+ with the new one.
+- `identity::KeyPair` is now `node_key::NodeKey`; `connection::Session` is
+ `pool::Pool` (or `station_link::Link` for one station), whose seeds carry
+ the station's node_id and whose `realm_trust` pins realm keys;
+ `direct_dial::call` is simply `Pool::call`; `resolve` is `Pool::providers`;
+ `serve_one_call` is `Pool::serve` with a handler; `Trust::WebPki` is gone:
+ every station is pinned by its node_id.
+- `ucan`, `cert_chain` and the content-transfer modules are gone until
+ macula 12's own arrive (see [Not yet implemented](#not-yet-implemented)).
+- Serving an org procedure needs the realm's org directory and the org's
+ delegation to your node in the DHT: a realm admits orgs through a human.
+
+## What's implemented
+
+| Primitive | Caller | Provider | Notes |
+|---|---|---|---|
+| Node keys (`node_key::NodeKey`) | ✅ | ✅ | `pq_hybrid` (the fleet's) or `pq_pure`; key files readable by the owner only, or the platform's secure store (`keystore`); pq_hybrid checked against the LAMPS draft's own vector and cross-verified with macula 12.8.0 |
+| Pool of station links (`pool::Pool`) | ✅ | ✅ | Seeds pinned by node_id; realm keys pinned; links redialed with subscriptions and served procedures replayed |
+| One station link (`station_link::Link`) | ✅ | ✅ | The v4 handshake, status statements both ways, neighbour signatures in pq_hybrid, a liveness probe |
+| Calls by direct dial (`call`, `providers`) | ✅ | ✅ | Candidates tried freshest first; errors arrive as `LinkError::Provider` / `LinkError::Relay` |
+| A node's own namespace (`record::own_procedure`) | ✅ | ✅ | `~/`: served and called with no org and no realm key |
+| Streams (`open_stream`, `Offer::stream`) | ✅ | ✅ | Server, client and bidi; a QUIC stream per session, released on every path |
+| Publish/subscribe | ✅ | ✅ | Signed publications, delivered once across links |
+| DHT (`find_record`, `find_records`, `find_records_by_type`, `put_record`) | ✅ | — | Records verified before they are handed on |
+| Mobile bindings (Kotlin, Swift) | ✅ | ✅ | `macula-rust-ffi`, below |
+
+The link and the pool are ported from macula-go v0.12.0's `stationlink` and
+`pool`, and every wire format is checked against macula-go's and macula's own
+vectors (`tests/vectors/`). `unsafe_code = "forbid"` holds across the
+workspace; the unsafe code is inside dependencies (quinn, aws-lc-rs).
+
+## Payloads
+
+A payload is what macula's wire CBOR carries: `Value::Null`, `Int`, `Float`,
+`Text`, `Bytes`, `List` and `Map`. **There is no boolean**: write 1 or 0. A
+decoded payload obeys macula 12's decoding rule (depth 64, 131,072 elements,
+integers within ±2^63, text or integer map keys, no duplicates).
+
+## Mobile bindings (Kotlin and Swift)
+
+`macula-rust-ffi` wraps the pool with [UniFFI](https://mozilla.github.io/uniffi-rs/)
+proc macros: `FfiNodeKey`, `FfiPool`, `FfiSubscription`, `FfiStream`, and two
+handlers the app implements, `FfiCallHandler` and `FfiStreamHandler`
+(`suspend fun` in Kotlin, `async throws` in Swift). Every 32-byte id crosses as
+bytes and is checked.
```bash
cargo build -p macula-rust-ffi --release
@@ -227,216 +175,74 @@ cargo run -p macula-rust-ffi --release --bin uniffi-bindgen -- generate \
--language kotlin --out-dir bindings-kotlin
```
-### Connecting and a basic call
-
-Signatures cross-checked against real generated bindings (`uniffi-bindgen generate`, both languages), not guessed — `call` takes no separate deadline, only a timeout. Calls `math.double`, the procedure the [`Doubler`](#mobile-bindings-uniffi) example above this one advertises and serves — this SDK's own, not a fleet-wide service, so it only resolves while that example (or an equivalent provider) is actually running:
-
```kotlin
-val identity = FfiKeyPair.generate()
-val session = FfiSession.connect("station-de-frankfurt.macula.io", 4433.toUShort(), FfiTrust.WebPki, identity)
-val response = session.call("math.double", realm, FfiValue.Int(21), 5_000uL, identity)
-```
+class Echo : FfiCallHandler {
+ override suspend fun handle(request: FfiRequest): FfiValue = request.payload
+}
-```swift
-let identity = FfiKeyPair.generate()
-let session = try await FfiSession.connect(host: "station-de-frankfurt.macula.io", port: 4433, trust: .webPki, identity: identity)
-let response = try await session.call(procedure: "math.double", realm: realm, payload: .int(21), timeoutMs: 5_000, identity: identity)
+val key = try {
+ FfiNodeKey.loadFromKeystore("io.macula.myapp", "node-identity", FfiProfile.PQ_HYBRID)
+} catch (e: FfiException.KeystoreNotFound) {
+ FfiNodeKey.generate(FfiProfile.PQ_HYBRID).also { it.saveToKeystore("io.macula.myapp", "node-identity") }
+}
+val pool = FfiPool.connect(key, listOf(FfiSeed(host, 4433.toUShort(), stationId)),
+ FfiPoolOptions(realmTrust = listOf(FfiRealmKey(realm, realmKey))))
+pool.serve(realm, ownProcedure(pool.nodeId(), "ring"), Echo())
+val answer = pool.call(realm, "mcl-echo/echo", FfiValue.Text("hello"), null, 5_000uL)
```
-### Persisting identity via platform secure storage
+On Android the platform keystore needs one call at app start,
+`Keyring.initializeNdkContext(applicationContext)`; see the `keystore`
+module's documentation. iOS needs nothing extra.
-Real, working usage — this is `macula-apps/macula-cam2me`'s actual
-Android identity persistence, not a contrived snippet. Android needs one
-extra one-time call at app startup (Keystore has no NDK surface, so the
-`android-native-keyring-store` crate ships its own JNI init export); iOS
-needs nothing extra, since `apple-native-keyring-store` covers both
-macOS and iOS as one backend. `saveToKeystore`/`loadFromKeystore` are
-plain blocking calls, not `suspend`/`async` — note the `FfiError`
-variant name is `KeystoreNotFound` (capitalized, mirroring the Rust
-error type directly) in both languages, unlike `FfiTrust`/`FfiValue`'s
-ordinary lower-camelCase Swift cases (`.webPki`, `.text`) — a real,
-confirmed UniFFI codegen quirk, not a typo.
-
-```kotlin
-// Once, in Application.onCreate or MainActivity.onCreate:
-Keyring.initializeNdkContext(applicationContext)
+CI generates both bindings on every push to master and every pull request;
+the apps that use them compile them. The FFI crate needs Rust 1.91, the core
+crate 1.89.
-// Then anywhere:
-val identity = try {
- FfiKeyPair.loadFromKeystore("io.macula.myapp", "node-identity")
-} catch (e: FfiException.KeystoreNotFound) {
- FfiKeyPair.generate().also { it.saveToKeystore("io.macula.myapp", "node-identity") }
-}
-```
+## Not yet implemented
-```swift
-// No extra init needed on iOS.
-let identity: FfiKeyPair
-do {
- identity = try FfiKeyPair.loadFromKeystore(service: "io.macula.myapp", account: "node-identity")
-} catch FfiError.KeystoreNotFound {
- identity = FfiKeyPair.generate()
- try identity.saveToKeystore(service: "io.macula.myapp", account: "node-identity")
-}
-```
+- **UCAN-gated calls and serving.** macula 12 uses post-quantum UCANs; calls
+ carry no token yet, and a gated procedure cannot be served.
+- **Node-served content** (macula 12's D27): planned for 0.5.0.
+- **Station discovery beyond the seeds.** macula's discovery call is not
+ served by the fleet today (macula-io/macula#31); give the pool its seeds.
## Testing
```bash
-cargo test --workspace --all-features
+./scripts/build-teststation.sh # macula-go's in-process stations, to target/teststation
+cargo test --workspace
```
-100+ tests across the workspace, plus a separate live-verification suite
-(`tests/live_station.rs`) that dials the real production fleet —
-`#[ignore]`d by default since it depends on infrastructure this crate
-doesn't control:
+The integration tests (`tests/station_link.rs`, `tests/pool.rs`,
+`macula-rust-ffi/tests/pool_ffi.rs`) run against `tests/teststation`, a Go
+helper around macula-go's `teststation`. It starts in-process macula 12
+stations, realms and orgs as each test asks, and reports what a station sees
+(who is connected, what is advertised or subscribed, how many streams it
+relays). A test fails, not skips, when the helper is missing. No network is
+needed. Go ≥ 1.27 builds the helper.
+
+`tests/live.rs` runs against one real station and is ignored unless asked:
```bash
-cargo test --test live_station -- --ignored --nocapture
+MACULA_RUST_LIVE_SEED=station-fi-helsinki.macula.io:4433 \
+MACULA_RUST_LIVE_STATION_ID=<64 hex> MACULA_RUST_LIVE_REALM=<64 hex> \
+MACULA_RUST_LIVE_REALM_KEY= cargo test --test live -- --ignored
```
-## Status
-
-**Live-verified, 2026-08-28 — full parity, both directions:** handshake,
-CALL/RESULT/ERROR as both caller (`Session::call`) and provider
-(`Session::serve_one_call`, BOLT#4 error mapping — `unknown_next_peer`
-on a lookup miss, `temporary_relay_failure` on a handler panic (caught
-via `tokio::spawn`, one task per call, the same shape
-`macula_station_link.erl`'s one-process-per-call already uses),
-`unknown_error` with detail on a handler-returned error, all ported
-field-for-field from that module's `handle_inbound_call/2`), PUBLISH/
-SUBSCRIBE/EVENT (a subscriber does receive its own publish), content
-transfer, and streaming RPC in both the caller and provider roles — all
-against `station-de-frankfurt.macula.io`, the real fleet, not a local
-mock. Two independent connections to the same station (one advertising
-and serving, the other calling in) is the pattern behind every
-provider-role test — see `tests/live_station.rs`'s
-`unary_call_provider_round_trip_against_the_real_fleet` for the unary
-case. Three real protocol bugs were caught by differential-vector tests
-before ever touching production.
-
-Unary-RPC provider dispatch was the one gap left after the streaming
-and content-transfer provider roles landed — a service built on this
-crate could call RPCs and serve streams, but couldn't serve a
-request/response procedure at all. It's now built here and in
-[`macula-go`](https://github.com/macula-io/macula-go) in the
-same pass, so both SDKs serve RPCs, not just call them, and wrapped in
-the FFI layer the same day: [`FfiCallHandler`](#mobile-bindings-uniffi)
-is a **foreign trait** (`#[uniffi::export(foreign)]`), not a callback
-closure — UniFFI doesn't support passing a bare closure across the
-boundary, so `handle` receives the full inbound call and a Kotlin/Swift
-implementation does its own procedure routing if a session serves more
-than one. Verified past "it compiles": rebuilt the release `cdylib`,
-regenerated both Kotlin and Swift, and inspected the actual generated
-code — `FfiCallHandler.handle` renders as `suspend fun ... : FfiValue`
-in Kotlin and `func handle(...) async throws -> FfiValue` in Swift,
-`FfiSession.serveOneCall`/`serveOneCall` takes it as a parameter in
-both, not just as an exit-code smoke test.
-
-Pubkey-pinned trust reached the FFI layer the same day too: `connect`
-now takes an `FfiTrust` (`Pinned { node_id }` or `WebPki`) instead of
-hardcoding WebPki. Not a nice-to-have — WebPki has no chain to validate
-against a self-hosted station outside the public demo fleet, so a real
-deployment off `station-de-frankfurt.macula.io` needs pinning to
-connect at all. `Trust::Insecure` stays deliberately unexposed at the
-FFI boundary (dev/diagnostic only in the core crate; a shipped mobile
-app should never be able to select "skip TLS verification").
-
-**2026-08-30: direct-dial, UCAN, cert-chain, periodic re-advertise, a
-supervised PubSub pair, RPC telemetry facts, and an overridable
-`KeyStore` all landed, live-verified, and FFI-wrapped the same day.**
-Direct-dial exists because ordinary advertise/gossip routing depends on
-a route having already propagated between the caller's and the
-service's station — this fleet's gossip is best-effort and often hasn't,
-so direct-dial resolves a signed DHT record naming the serving station
-and dials it in one hop instead. `KeyStore` closes a real gap this
-crate's own `KeyPair::save` doc comment had flagged since it was
-written: raw-file persistence is fine for tests, but a real mobile app
-needs Keychain/Keystore-backed storage — `KeyringStore` covers macOS,
-iOS, Linux (D-Bus secret service) and Windows via one `keyring`-crate
-backend (confirmed via its own `Cargo.toml`: `apple-native-keyring-store`
-covers macOS *and* iOS with a single backend, no per-platform bridge
-needed), `LinuxKeyutilsStore` is a second backend for sandboxes with no
-secret-service daemon running. `macula-apps/macula-cam2me`'s Android app
-migrated to it the same day (`NodeKeyPair.kt`), the first real consumer.
-
-**This crate is feature-complete for its stated purpose — a leaf
-client dialing a known macula-station — in both the core crate and the
-FFI layer.** What's genuinely still outstanding is a different kind of
-thing entirely, not an SDK gap:
-- DHT/HyParView/Plumtree gossip primitives — deliberately **not**
- leaf-client scope; they're how *stations* gossip membership and
- broadcast to each other (§6.5-§6.7 say so explicitly). A leaf never
- needs them, so this was never a completeness gap to begin with.
-- The actual Android demo app — real Kotlin/Android work outside this
- crate, needing a device/emulator and toolchain this repo's own CI
- doesn't have. The SDK surface it needs (`advertise`/`acceptStream`/
- `FfiStream`/`serveOneCall`, both pull and push streaming modes) is
- already complete and live-verified; nothing here is blocking it.
-- Additional language ports (C#, Python) — a separate initiative, not
- a gap in this crate.
-
-See [`plans/PLAN_WIRE_PROTOCOL.md`](plans/PLAN_WIRE_PROTOCOL.md) for the
-full wire-format spec this crate is built against, section by section,
-traced directly to the Erlang SDK's source.
-
-## Known limitations
-
-- **`direct_dial::get_direct` can only resolve a `content_announcement`
- that something has actually published** — and nothing in this
- ecosystem currently does, since only a station/relay can legitimately
- publish one (a `content_announcement`'s endpoint is dialed with no
- relay indirection, unlike a `procedure_advertisement`, so a leaf SDK
- identity can't pass its own trust check). Correct but currently
- unreachable, not a bug.
-- **RESOLVED**: an earlier draft of this section reported
- `call_direct_with_cert_chain` timing out waiting for a reply after a
- successful resolve+dial, narrowed but not root-caused across several
- investigation rounds. Root-caused: the same premature-`Session`-drop
- race as the `serve_one_call_gated` finding below — the FFI test's
- `serve_task` dropped the provider `Session` the instant
- `serve_until_procedure` returned, closing the QUIC connection before
- the reply frame reached the peer. Fixed by keeping the session alive
- 300ms after the last reply, matching the identical fix already applied
- there. Confirmed with 5 consecutive clean passes (was failing reliably
- before). No SDK defect — the cert-chain mechanism itself was never
- broken. See `macula-rust-ffi/tests/live_cert_chain_direct_dial.rs`'s
- own comments for the ruled-out theories from the earlier rounds.
-- The demo fleet's `station_endpoint` DHT records carry a short TTL and
- are not always freshly republished, so a station's record can be stale
- for a while. Direct dial tries every advertised provider in turn and
- keeps re-querying within the call's `timeout`; only when no provider's
- station has a usable record before it runs out does the call return
- `StationEndpointNotFound`. This is fleet infrastructure state, not a
- code defect.
-- **RESOLVED**: an earlier draft of this section reported
- `serve_one_call_gated`/`call_with_ucan` failing 100% of live attempts
- while `serve_one_call` succeeded reliably in the same window, and left
- it as an open, unconfirmed question. Root-caused: it was a test-harness
- bug, not a real difference between gated and plain serving. The failing
- harness spawned the provider's `Session` into a task that dropped it
- the instant `serve_one_call`/`serve_one_call_gated` returned; dropping
- the last `Session` handle closes the underlying QUIC connection, which
- can happen before the just-sent reply frame is flushed to the peer — the exact
- same class of race already documented on [`Session::close`], just
- never hit by drop instead of an explicit close before now. Confirmed
- by direct A/B: 8/8 plain AND 8/8 gated calls succeeded once the
- provider session was kept alive briefly after serving, interleaved on
- the same station in the same window; the pre-existing
- `unary_call_provider_round_trip_against_the_real_fleet` test also
- passed 3/3 at the same moment, ruling out the fleet-degradation theory
- entirely for this specific finding. **Practical takeaway for any
- caller**: don't let a `Session` drop immediately after `serve_one_call`/
- `publish`/any send-then-return call — keep it alive briefly (or call
- [`Session::close`] explicitly) so in-flight writes have time to reach
- the wire. See `examples/ucan.rs` for a real, live-verified gated-serving
- example built once this was root-caused.
-
-## Related projects
-
-| Project | Description |
+With a key generated for the run and never saved, it reads the DHT, calls
+`mcl-echo/echo` by direct dial and hears its own publication.
+`scripts/cross-verify-macula.sh` renews the pq_hybrid signatures that crossed
+both ways with macula (`tests/vectors/identity/macula_12_cross`).
+
+## Sibling SDKs
+
+| Repo | Approach |
|---|---|
-| [macula](https://github.com/macula-io/macula) | The reference SDK (Erlang/OTP) — the protocol this crate ports |
+| [macula](https://github.com/macula-io/macula) | The reference SDK (Erlang/OTP) |
+| [macula-go](https://github.com/macula-io/macula-go) | Go port; this crate's link and pool follow it |
+| [macula-ts](https://github.com/macula-io/macula-ts) | FFI binding over macula-go, for Node.js |
+| [macula-php](https://github.com/macula-io/macula-php) | FFI binding over macula-go, for PHP |
| [macula-station](https://github.com/macula-io/macula-station) | The station: DHT, SWIM, routing, peering |
| [macula-realm](https://github.com/macula-io/macula-realm) | Managed-realm identity + certificate authority |
diff --git a/examples/common/mod.rs b/examples/common/mod.rs
new file mode 100644
index 0000000..b30ad95
--- /dev/null
+++ b/examples/common/mod.rs
@@ -0,0 +1,89 @@
+//! What every example joins the mesh with, from the environment:
+//!
+//! - `MACULA_SEED`: the station, host:port (`[v6]:port` for IPv6)
+//! - `MACULA_STATION_ID`: its node_id, 64 hex: the station must prove it
+//! - `MACULA_REALM`: the realm id, 64 hex
+//! - `MACULA_REALM_KEY`: the realm's key as carried, hex (the realm publishes it)
+//! - `MACULA_KEY`: this node's key file, created on first use (`node.key`)
+//! - `MACULA_PROFILE`: `pq_hybrid` (the fleet's, the default) or `pq_pure`
+
+#![allow(dead_code)]
+
+use std::collections::HashMap;
+use std::path::Path;
+use std::sync::Arc;
+
+use macula_rust::node_key::NodeKey;
+use macula_rust::pool::{Opts, Pool, Seed};
+use macula_rust::profile::Profile;
+
+pub fn env(name: &str) -> String {
+ match std::env::var(name) {
+ Ok(v) if !v.is_empty() => v,
+ _ => {
+ eprintln!("set {name} (see the top of examples/common/mod.rs)");
+ std::process::exit(2);
+ }
+ }
+}
+
+pub fn hex32(name: &str) -> [u8; 32] {
+ let bytes = hex_decode(&env(name));
+ bytes.try_into().unwrap_or_else(|_| {
+ eprintln!("{name} must be 64 hex characters");
+ std::process::exit(2);
+ })
+}
+
+pub fn realm() -> [u8; 32] {
+ hex32("MACULA_REALM")
+}
+
+/// A pool on the seed, as the key in `key_file` (or `MACULA_KEY`, or
+/// `node.key`), made on first use, trusting the realm.
+pub async fn connect(key_file: Option<&str>) -> Pool {
+ let seed = env("MACULA_SEED");
+ let Some((host, port)) = seed.rsplit_once(':') else {
+ eprintln!("MACULA_SEED must be host:port");
+ std::process::exit(2);
+ };
+ let profile = std::env::var("MACULA_PROFILE")
+ .ok()
+ .and_then(|p| Profile::parse(&p).ok())
+ .unwrap_or(Profile::PqHybrid);
+ let key_file = key_file
+ .map(str::to_string)
+ .or_else(|| std::env::var("MACULA_KEY").ok())
+ .unwrap_or_else(|| "node.key".into());
+ let key = NodeKey::load_or_create(Path::new(&key_file), profile).expect("the node's key");
+ let mut opts = Opts::new(Arc::new(key));
+ opts.realm_trust = HashMap::from([(realm(), hex_decode(&env("MACULA_REALM_KEY")))]);
+ Pool::connect(
+ vec![Seed {
+ host: host
+ .trim_start_matches('[')
+ .trim_end_matches(']')
+ .to_string(),
+ port: port.parse().expect("MACULA_SEED's port"),
+ node_id: hex32("MACULA_STATION_ID"),
+ }],
+ opts,
+ )
+ .await
+ .expect("a link to the seed")
+}
+
+pub fn hex(bytes: &[u8]) -> String {
+ bytes.iter().map(|b| format!("{b:02x}")).collect()
+}
+
+fn hex_decode(text: &str) -> Vec {
+ (0..text.len())
+ .step_by(2)
+ .map(|i| u8::from_str_radix(text.get(i..i + 2).unwrap_or("zz"), 16))
+ .collect::>()
+ .unwrap_or_else(|_| {
+ eprintln!("not hex: {text}");
+ std::process::exit(2);
+ })
+}
diff --git a/examples/cross_verify_sign.rs b/examples/cross_verify_sign.rs
new file mode 100644
index 0000000..851190b
--- /dev/null
+++ b/examples/cross_verify_sign.rs
@@ -0,0 +1,29 @@
+//! This crate's half of scripts/cross-verify-macula.sh: a pq_hybrid key made
+//! for the run and never saved signs a message; the message, the public key
+//! as carried and the signature go to the directory named on the command line
+//! for macula to verify.
+
+use macula_rust::node_key::{verify, NodeKey, Purpose};
+use macula_rust::profile::Profile;
+
+fn main() -> Result<(), Box> {
+ let dir = std::path::PathBuf::from(
+ std::env::args()
+ .nth(1)
+ .ok_or("usage: cross_verify_sign ")?,
+ );
+ let key = NodeKey::generate(Purpose::Identity, Profile::PqHybrid)?;
+ let message = b"signed by macula-rust";
+ let signature = key.sign(message)?;
+ if !verify(message, &signature, &key.public_key(), Profile::PqHybrid) {
+ return Err("macula-rust does not verify its own composite".into());
+ }
+ std::fs::write(dir.join("m.bin"), message)?;
+ std::fs::write(dir.join("pk.bin"), key.public_key())?;
+ std::fs::write(dir.join("s.bin"), &signature)?;
+ println!(
+ "rust_signed: {}-byte composite by macula-rust written",
+ signature.len()
+ );
+ Ok(())
+}
diff --git a/examples/publish_subscribe.rs b/examples/publish_subscribe.rs
new file mode 100644
index 0000000..b7f7d9b
--- /dev/null
+++ b/examples/publish_subscribe.rs
@@ -0,0 +1,42 @@
+//! Subscribes to a topic and publishes to it. A topic names a kind of fact,
+//! with a business verb, and ids go in the payload. There is no boolean on
+//! the wire: write 1 or 0.
+//!
+//! Run: `cargo run --example publish_subscribe`, with the environment
+//! examples/common/mod.rs reads.
+
+mod common;
+
+use std::time::Duration;
+
+use macula_rust::cbor::Value;
+use macula_rust::station_link::Publication;
+
+const TOPIC: &str = "acme/demo/greeting_sent_v1";
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let pool = common::connect(None).await;
+ let mut sub = pool.subscribe(&common::realm(), TOPIC).await?;
+ tokio::time::sleep(Duration::from_millis(300)).await;
+ pool.publish(Publication {
+ realm: common::realm(),
+ topic: TOPIC.into(),
+ payload: Value::Map(vec![
+ (Value::text("text"), Value::text("hi")),
+ (Value::text("urgent"), Value::Int(0)),
+ ]),
+ ttl_ms: None,
+ })
+ .await?;
+ while let Ok(Some(event)) = tokio::time::timeout(Duration::from_secs(2), sub.recv()).await {
+ println!(
+ "{} published {:?}",
+ common::hex(&event.publisher),
+ event.payload
+ );
+ }
+ sub.unsubscribe().await?;
+ pool.close().await;
+ Ok(())
+}
diff --git a/examples/quickstart.rs b/examples/quickstart.rs
index 932d3c5..aba410d 100644
--- a/examples/quickstart.rs
+++ b/examples/quickstart.rs
@@ -1,123 +1,35 @@
-//! Minimal end-to-end example: connect to a station, advertise a
-//! trivial echo procedure, and call it. Dials the real fleet, so this
-//! isn't run by CI — see README.md's "Quick start" section, which this
-//! file backs (kept compiling by `cargo build --examples` in CI, run
-//! manually with `cargo run --example quickstart`).
+//! Connects to a macula 12 station and calls mcl-echo/echo, which runs on
+//! another station: the pool finds its trusted advertisement in the DHT and
+//! dials the station it serves from.
//!
-//! Two identities are used (a provider and a caller) because a station
-//! kicks a connection the instant a second one arrives under the same
-//! identity — the same reason this crate's own live tests use separate
-//! identities for each role (see `tests/live_station.rs`'s
-//! `unary_call_provider_round_trip_against_the_real_fleet`). The
-//! procedure name is unique per run (a station's DHT can hold stale
-//! routing state for a fixed name from a prior run's now-dead
-//! advertiser) — and it's this crate's own procedure, not a shared
-//! fleet service, so this example never depends on anything else being
-//! deployed.
-//!
-//! The provider `Session` is moved back OUT of its `tokio::spawn` task
-//! and closed explicitly, rather than let it drop when the task ends --
-//! see [`macula_rust::connection::Session`]'s own doc for why: dropping
-//! the last handle closes the connection at once, which gives quinn's
-//! send-scheduling no guarantee the RESULT this example just sent
-//! actually reached the peer first. Confirmed live 2026-09-05:
-//! under `#[tokio::main]`'s default multi-threaded runtime, a spawned
-//! task with nothing after `serve_one_call().await` can complete (and
-//! drop the session) within microseconds of the write, losing the reply
-//! deterministically -- `tests/live_station.rs`'s own
-//! `unary_call_provider_round_trip_multi_thread_runtime` reproduces this
-//! and confirms the fix.
-use std::time::{Duration, SystemTime, UNIX_EPOCH};
+//! Run: `cargo run --example quickstart`, with the environment
+//! examples/common/mod.rs reads.
+
+mod common;
-use macula_rust::{
- cbor::Value,
- connection::{self, BoxFuture, CallHandler},
- frame::AdvertiseSpec,
- identity::KeyPair,
- transport::Trust,
-};
+use macula_rust::cbor::Value;
+use macula_rust::pool::Call;
#[tokio::main]
async fn main() -> Result<(), Box> {
- // Puzzle-hardened identities — required. An unhardened identity fails
- // the handshake silently (QUIC/TLS looks healthy, HELLO never accepts).
- let provider_identity = KeyPair::generate_with_default_puzzle();
- let caller_identity = KeyPair::generate_with_default_puzzle();
-
- let provider_session = connection::connect(
- "station-de-frankfurt.macula.io",
- 4433,
- Trust::WebPki,
- &provider_identity,
- )
- .await?;
- let caller_session = connection::connect(
- "station-de-frankfurt.macula.io",
- 4433,
- Trust::WebPki,
- &caller_identity,
- )
- .await?;
-
- let realm = [0u8; 32];
- // Unique per run — reusing a fixed procedure name across rapid
- // repeated runs can hit stale DHT routing state from the prior run's
- // now-dead advertiser.
- let procedure = format!(
- "macula_rust.quickstart_echo.{}",
- SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos()
- );
-
- let advertise_spec = AdvertiseSpec::new(realm, procedure.clone(), provider_identity.node_id());
- provider_session
- .advertise(&advertise_spec, &provider_identity)
+ let pool = common::connect(None).await;
+ println!("node {}", common::hex(&pool.node_id()));
+ for provider in pool.providers(&common::realm(), "mcl-echo/echo").await? {
+ println!(
+ "provider {} at station {}",
+ common::hex(&provider.node),
+ common::hex(&provider.station)
+ );
+ }
+ let answered = pool
+ .call(Call {
+ realm: common::realm(),
+ procedure: "mcl-echo/echo".into(),
+ payload: Value::text("hello"),
+ ..Call::default()
+ })
.await?;
- tokio::time::sleep(Duration::from_millis(500)).await; // ADVERTISE is fire-and-forget; give it a moment to land
-
- let target_procedure = procedure.clone();
- let lookup = move |_realm: &[u8; 32], proc: &str| -> Option {
- if proc != target_procedure {
- return None;
- }
- let handler: CallHandler = std::sync::Arc::new(|payload: Value| {
- Box::pin(async move { Ok(payload) }) as BoxFuture<'static, Result>
- });
- Some(handler)
- };
-
- let serve_task = tokio::spawn(async move {
- let result = provider_session
- .serve_one_call(lookup, &provider_identity, Duration::from_secs(10))
- .await;
- // Close explicitly instead of letting provider_session drop when
- // this task ends -- see this file's own doc comment.
- provider_session
- .close(
- "normal",
- Some("quickstart provider done"),
- &provider_identity,
- )
- .await;
- result
- });
-
- let now_ms = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as i128;
- let response = caller_session
- .call(
- &procedure,
- realm,
- Value::Text("hello".into()),
- now_ms + 5_000, // deadline_ms
- &caller_identity,
- Duration::from_secs(5),
- )
- .await?;
-
- serve_task.await??;
- caller_session
- .close("normal", Some("quickstart caller done"), &caller_identity)
- .await;
-
- println!("{response:?}");
+ println!("mcl-echo/echo answered {answered:?}");
+ pool.close().await;
Ok(())
}
diff --git a/examples/serve.rs b/examples/serve.rs
new file mode 100644
index 0000000..cf14600
--- /dev/null
+++ b/examples/serve.rs
@@ -0,0 +1,48 @@
+//! Serves a procedure in this node's own namespace, `~/ring`, which
+//! needs no org and no realm key: the node's signature authorizes it. A
+//! second node, with a key of its own, calls it by direct dial.
+//!
+//! Run: `cargo run --example serve`, with the environment
+//! examples/common/mod.rs reads. The caller's key is `caller.key`.
+
+mod common;
+
+use macula_rust::cbor::Value;
+use macula_rust::pool::{Call, Offer};
+use macula_rust::record;
+use macula_rust::station_link::handler;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let provider = common::connect(None).await;
+ let ring = record::own_procedure(&provider.node_id(), "ring");
+ let served = provider
+ .serve(Offer::unary(
+ common::realm(),
+ &ring,
+ handler(|request| async move {
+ Ok(Value::Map(vec![(
+ Value::text("answered"),
+ Value::Bytes(request.caller.to_vec()),
+ )]))
+ }),
+ ))
+ .await?;
+ println!("serving {ring}");
+
+ let caller = common::connect(Some("caller.key")).await;
+ let answered = caller
+ .call(Call {
+ realm: common::realm(),
+ procedure: ring,
+ payload: Value::Null,
+ ..Call::default()
+ })
+ .await?;
+ println!("{answered:?}");
+
+ served.stop().await?;
+ caller.close().await;
+ provider.close().await;
+ Ok(())
+}
diff --git a/examples/ucan.rs b/examples/ucan.rs
deleted file mode 100644
index 09d0f7f..0000000
--- a/examples/ucan.rs
+++ /dev/null
@@ -1,113 +0,0 @@
-//! UCAN-gated serving: mint a token, gate a served procedure on it, show
-//! both the rejected-without-token and accepted-with-token paths. Dials
-//! the real fleet, so this isn't run by CI — see README.md's "Known
-//! limitations" section for the investigation this example closes out
-//! (kept compiling by `cargo build --examples` in CI, run manually with
-//! `cargo run --example ucan`).
-//!
-//! Keeps the provider `Session` alive for a moment after
-//! `serve_one_call_gated` returns before letting it drop — dropping its
-//! last handle closes the QUIC connection at once, which can discard the
-//! just-sent reply before it reaches the peer (the same race documented
-//! on [`macula_rust::connection::Session::close`]). See this file's own
-//! git history / README for the investigation that found this.
-use std::sync::Arc;
-use std::time::Duration;
-
-use macula_rust::{
- cbor::Value, connection, connection::CallHandler, identity::KeyPair, transport::Trust, ucan,
-};
-
-const HOST: &str = "station-de-frankfurt.macula.io";
-const PORT: u16 = 4433;
-
-#[tokio::main]
-async fn main() -> Result<(), Box> {
- let provider_id = KeyPair::generate_with_default_puzzle();
- let caller_id = KeyPair::generate_with_default_puzzle();
- let authority = KeyPair::generate_with_default_puzzle();
-
- let provider = connection::connect(HOST, PORT, Trust::WebPki, &provider_id).await?;
- let caller = connection::connect(HOST, PORT, Trust::WebPki, &caller_id).await?;
-
- let realm = [0u8; 32];
- let procedure = "macula_rust.examples.ucan_gated";
- let advertise_spec =
- macula_rust::frame::AdvertiseSpec::new(realm, procedure, provider_id.node_id());
- provider.advertise(&advertise_spec, &provider_id).await?;
- tokio::time::sleep(Duration::from_millis(1200)).await;
-
- // Only callers holding a token issued by `authority` may invoke this
- // procedure. A real deployment would use a stable, pre-shared
- // authority identity, not one minted fresh per run.
- let issuer_pub = authority.node_id();
- let handler: CallHandler = Arc::new(|payload: Value| {
- Box::pin(async move { Ok(Value::Text(format!("granted: {payload:?}"))) })
- });
-
- // serve_one_call_gated answers exactly ONE inbound call, then
- // returns -- this example makes two calls (rejected, then granted),
- // so the provider loops twice, once per expected call.
- let serve_task = tokio::spawn(async move {
- for _ in 0..2 {
- let handler = handler.clone();
- provider
- .serve_one_call_gated(
- move |_realm, proc| {
- if proc == procedure {
- Some(handler.clone())
- } else {
- None
- }
- },
- move |_, _| ucan::Policy::required(issuer_pub),
- &provider_id,
- Duration::from_secs(15),
- )
- .await?;
- }
- // Keep the session alive briefly after the last reply -- see
- // this file's module doc for why this matters.
- tokio::time::sleep(Duration::from_millis(300)).await;
- Ok::<(), connection::ServeCallError>(())
- });
-
- // First call: no token at all -- refused before the handler ever runs.
- let rejected = caller
- .call(
- procedure,
- realm,
- Value::Null,
- 0,
- &caller_id,
- Duration::from_secs(5),
- )
- .await;
- println!("call without a token: {rejected:?}");
-
- // Second call: a real token minted by the required authority. It names
- // this caller as its audience (the caller's node id as lowercase hex),
- // the only caller a gated provider accepts it from.
- let token = ucan::create(
- "did:key:example-issuer",
- &hex::encode(caller_id.node_id()),
- vec![],
- &authority,
- ucan::CreateOpts::default(),
- )?;
- let granted = caller
- .call_with_ucan(
- procedure,
- realm,
- Value::Text("hello".into()),
- 0,
- &caller_id,
- Duration::from_secs(5),
- token,
- )
- .await;
- println!("call with a valid token: {granted:?}");
-
- serve_task.await??;
- Ok(())
-}
diff --git a/macula-rust-ffi/Cargo.toml b/macula-rust-ffi/Cargo.toml
index 53b9a4e..1e35d5d 100644
--- a/macula-rust-ffi/Cargo.toml
+++ b/macula-rust-ffi/Cargo.toml
@@ -2,7 +2,7 @@
name = "macula-rust-ffi"
version = "0.4.0"
edition = "2021"
-rust-version = "1.85"
+rust-version = "1.91"
authors = ["Macula "]
description = "UniFFI mobile (Kotlin/Swift) bindings for macula-rust. Wraps the core crate; adds nothing to it."
license = "Apache-2.0"
@@ -34,9 +34,8 @@ thiserror = "2"
async-trait = "0.1"
[dev-dependencies]
-# tests/live_cert_chain_direct_dial.rs's self-issued realm CA/leaf fixture,
-# mirroring ../tests/live_cert_chain.rs's own — versions matched to the
-# core crate's own pins.
-rcgen = { version = "0.14", default-features = false, features = ["pem", "ring", "x509-parser"] }
-time = "0.3"
-base64 = "0.23"
+# tests/pool_ffi.rs drives the core crate's teststation lab
+# (../tests/common), which reads the helper's JSON and hex.
+hex = "0.4"
+serde_json = "1"
+tempfile = "3"
diff --git a/macula-rust-ffi/src/lib.rs b/macula-rust-ffi/src/lib.rs
index a395e86..e664fbe 100644
--- a/macula-rust-ffi/src/lib.rs
+++ b/macula-rust-ffi/src/lib.rs
@@ -1,55 +1,25 @@
-//! UniFFI (Kotlin/Swift) bindings for [`macula_rust`]. A thin wrapper,
-//! not a reimplementation — everything here delegates straight to the
-//! core crate; nothing wire-level lives in this crate at all.
+//! UniFFI (Kotlin/Swift) bindings for [`macula_rust`] on the macula 12 wire.
+//! A thin wrapper, not a reimplementation: everything here delegates to the
+//! core crate's [`macula_rust::pool`], and nothing wire-level lives in this
+//! crate. A separate crate keeps the core free of any UniFFI dependency or
+//! FFI-shaped type, so it stays as usable from plain Rust or a CLI.
//!
-//! Structure mirrors `iroh-ffi`'s relationship to `iroh`: a separate
-//! crate depending on the core one, so the core crate carries zero
-//! UniFFI dependency and zero FFI-shaped types. That separation is what
-//! keeps `macula-rust` itself just as usable from plain Rust, a CLI,
-//! or WASM as it was before this crate existed.
+//! What is wrapped: a node key ([`FfiNodeKey`]: generated in either
+//! profile, kept in a key file or the platform's secure store), and a pool
+//! of station links ([`FfiPool`]) with everything a node does through it:
+//! calls to a provider at its own station, serving a procedure with a
+//! handler the foreign side implements ([`FfiCallHandler`]), pubsub
+//! ([`FfiSubscription`]), streaming sessions on either side ([`FfiStream`],
+//! [`FfiStreamHandler`]), and DHT records.
//!
-//! Every application primitive the core crate has is wrapped: identity,
-//! CONNECT/HELLO (either [`FfiTrust::Pinned`] or [`FfiTrust::WebPki`] —
-//! see that type's own doc for when each applies), CALL/RESULT/ERROR as
-//! both caller AND provider (`call`/[`FfiSession::serve_one_call`]),
-//! UCAN-gated serving ([`FfiSession::serve_one_call_gated`]/
-//! [`FfiSession::call_with_ucan`]) and the standalone `ucan_*` mint/verify/
-//! introspect functions, PUBLISH/SUBSCRIBE/EVENT (including the supervised
-//! [`FfiSession::run_publisher`]), content transfer, streaming RPC — both
-//! the caller/consumer role (§13.1) and the provider role (§13.2/§6.9,
-//! `advertise`/`accept_stream`), direct-dial resolution
-//! ([`FfiSession::resolve_direct`]/[`call_direct`](FfiSession::call_direct)/
-//! [`advertise_direct`](FfiSession::advertise_direct)) and its cert-chain-
-//! authorized variants (`*_with_cert_chain`), and direct-dial streaming/
-//! content transfer ([`FfiSession::open_stream_direct`]/
-//! [`FfiSession::put_direct`]/[`FfiSession::get_direct`]).
+//! [`FfiValue`] mirrors every variant [`macula_rust::cbor::Value`] has,
+//! narrowed only where the FFI boundary forces it: `Int` is `i64`, and an
+//! integer outside it is [`FfiError::UnrepresentableValue`], never
+//! truncated. Every 32-byte id crosses as bytes and is checked here
+//! ([`FfiError::WrongByteLength`]).
//!
-//! Not exposed, each a real, reasoned decision rather than an oversight:
-//! `Trust::Insecure` — see [`FfiTrust`]'s own doc; the core crate's
-//! `keep_advertised`/`keep_advertised_direct` background-loop helpers —
-//! see [`FfiSession::advertise_direct`]'s own doc for why a native
-//! background timer is the wrong shape for a mobile app and what to do
-//! instead; `Session::run_subscriber` — same reasoning as
-//! `keep_advertised` (it takes a generic `stop: impl Future` and
-//! `handler: impl FnMut`, neither of which crosses the UniFFI boundary,
-//! and a native long-lived receive loop fights mobile app-lifecycle
-//! management the same way a background timer does) — its full external
-//! behavior (subscribe once, receive until stopped, unsubscribe when done)
-//! is still achievable on the foreign side with [`FfiSession::subscribe`],
-//! [`FfiSubscription::recv_event`] in a loop that carries on past a
-//! timeout, and [`FfiSubscription::close`] — nothing is lost, only where
-//! that loop lives.
-//!
-//! [`FfiValue`] mirrors every variant [`macula_rust::cbor::Value`]
-//! has, including recursive list/map shapes (`Items`/`Fields`, via
-//! `Vec` — see the type's own doc for why they aren't named `List`/
-//! `Map` like the core type), narrowed only where the FFI boundary
-//! forces it: `Int` is `i64` not `i128` (out-of-range values round-trip
-//! as an [`FfiError::UnrepresentableValue`] rather than silently
-//! truncating).
-//!
-//! Generate the bindings with the `uniffi-bindgen` binary this crate
-//! also builds, e.g.:
+//! Generate the bindings with the `uniffi-bindgen` binary this crate also
+//! builds, e.g.:
//! ```text
//! cargo build -p macula-rust-ffi --release
//! cargo run -p macula-rust-ffi --bin uniffi-bindgen -- generate \
@@ -57,71 +27,151 @@
//! --language kotlin --out-dir bindings/kotlin
//! ```
-uniffi::setup_scaffolding!();
+mod node_key;
+mod pool;
+mod pubsub;
+mod serve;
+mod stream;
-fn now_ms() -> u64 {
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .expect("system clock after epoch")
- .as_millis() as u64
-}
+pub use node_key::{FfiNodeKey, FfiProfile};
+pub use pool::{
+ own_procedure, FfiLinkStatus, FfiPool, FfiPoolOptions, FfiProvider, FfiRealmKey, FfiRecord,
+ FfiSeed,
+};
+pub use pubsub::{FfiEvent, FfiSubscription};
+pub use serve::{FfiCallHandler, FfiRequest, FfiServed};
+pub use stream::{FfiStream, FfiStreamEncoding, FfiStreamEvent, FfiStreamHandler, FfiStreamMode};
+
+use macula_rust::pool::PoolError;
+use macula_rust::station_link::LinkError;
-#[derive(Debug, thiserror::Error, uniffi::Error)]
+uniffi::setup_scaffolding!();
+
+/// Why an operation failed, as Kotlin and Swift see it.
+#[derive(Debug, Clone, PartialEq, thiserror::Error, uniffi::Error)]
pub enum FfiError {
- #[error("connecting to the station: {reason}")]
- Connect { reason: String },
- #[error("the call failed: {reason}")]
- Call { reason: String },
- #[error("sending a frame failed: {reason}")]
- Send { reason: String },
- #[error("receiving failed: {reason}")]
- Recv { reason: String },
- #[error("content operation failed: {reason}")]
- Content { reason: String },
- #[error("a value could not cross the FFI boundary: {reason}")]
- UnrepresentableValue { reason: String },
+ /// An argument outside what the operation takes.
+ #[error("invalid argument: {message}")]
+ InvalidArgument { message: String },
+ /// A byte string of the wrong length, where a 32-byte id belongs.
#[error("expected exactly {expected} bytes, got {actual}")]
WrongByteLength { expected: u32, actual: u32 },
- #[error("this session is already closed")]
- Closed,
- #[error("the call handler failed: {reason}")]
- CallHandlerFailed { reason: String },
- #[error("direct-dial resolution failed: {reason}")]
- Resolve { reason: String },
- #[error("direct-dial trust violation: the dialed peer's proven identity did not match the resolved station (see resolved/dialed fields)")]
- DirectDialTrustViolation { resolved: Vec, dialed: Vec },
- #[error("UCAN operation failed: {reason}")]
- Ucan { reason: String },
- #[error("no seed is stored under this keystore identity")]
+ /// A value the FFI boundary cannot carry, such as an integer outside i64.
+ #[error("a value could not cross the FFI boundary: {message}")]
+ UnrepresentableValue { message: String },
+ /// A node key that could not be made, saved or loaded.
+ #[error("node key: {message}")]
+ Key { message: String },
+ /// Nothing is stored under this keystore identity.
+ #[error("no key is stored under this keystore identity")]
KeystoreNotFound,
- #[error("platform secure store error: {reason}")]
- Keystore { reason: String },
+ /// The platform's secure store failed.
+ #[error("platform secure store: {message}")]
+ Keystore { message: String },
+ /// No station link came up, or none is up to carry the operation.
+ #[error("no station link: {message}")]
+ NoLink { message: String },
+ /// A realm the pool pins no key for: nothing in it is served or trusted.
+ #[error("no realm key is pinned for the realm")]
+ NoRealmKey,
+ /// No trusted provider advertises or answered the procedure.
+ #[error("{message}")]
+ NoProvider { message: String },
+ /// The provider's own ERROR: its code, detail, and who responded.
+ #[error("the provider answered {code}")]
+ Provider {
+ code: String,
+ detail: Option,
+ responded_by: Vec,
+ },
+ /// The connected station could not relay the call.
+ #[error("the station could not relay the call: {code}")]
+ Relay { code: String },
+ /// No answer within the timeout.
+ #[error("timed out")]
+ Timeout,
+ /// A record the DHT does not hold.
+ #[error("record not found")]
+ RecordNotFound,
+ /// A stream ended by an error: the peer's, the station's, or this side's.
+ #[error("stream error {code}: {message}")]
+ Stream { code: String, message: String },
+ /// A stream that ended normally.
+ #[error("end of stream")]
+ EndOfStream,
+ /// A handler the foreign side implements refused, or failed.
+ #[error("handler: {message}")]
+ Handler { message: String },
+ /// An operation on a closed pool, subscription, stream or serving.
+ #[error("closed")]
+ Closed,
+ /// Anything else the core crate reports, as its text.
+ #[error("{message}")]
+ Other { message: String },
+}
+
+impl From for FfiError {
+ /// A foreign handler that threw something other than an FfiError.
+ fn from(e: uniffi::UnexpectedUniFFICallbackError) -> Self {
+ FfiError::Handler { message: e.reason }
+ }
}
-impl From for FfiError {
- fn from(e: macula_rust::ucan::UcanError) -> Self {
- FfiError::Ucan {
- reason: e.to_string(),
+impl From for FfiError {
+ fn from(e: LinkError) -> Self {
+ match e {
+ LinkError::Provider {
+ responded_by,
+ code,
+ detail,
+ } => FfiError::Provider {
+ code,
+ detail,
+ responded_by: responded_by.to_vec(),
+ },
+ LinkError::Relay { code, .. } => FfiError::Relay { code },
+ LinkError::CallTimeout | LinkError::HandshakeTimeout => FfiError::Timeout,
+ LinkError::RecordNotFound => FfiError::RecordNotFound,
+ LinkError::Stream { code, message, .. } => FfiError::Stream { code, message },
+ LinkError::EndOfStream => FfiError::EndOfStream,
+ LinkError::Closed | LinkError::StreamClosed | LinkError::Stopped => FfiError::Closed,
+ other => FfiError::Other {
+ message: other.to_string(),
+ },
}
}
}
-impl From for FfiError {
- fn from(e: macula_rust::keystore::KeyStoreError) -> Self {
+impl From for FfiError {
+ fn from(e: PoolError) -> Self {
match e {
- macula_rust::keystore::KeyStoreError::NotFound => FfiError::KeystoreNotFound,
- other => FfiError::Keystore {
- reason: other.to_string(),
+ PoolError::Link(link) => link.into(),
+ PoolError::NoRealmKey => FfiError::NoRealmKey,
+ PoolError::Closed => FfiError::Closed,
+ e @ PoolError::NoProvider(_) => FfiError::NoProvider {
+ message: e.to_string(),
+ },
+ e @ PoolError::NoLink(_) => FfiError::NoLink {
+ message: e.to_string(),
+ },
+ e @ (PoolError::NoSeeds
+ | PoolError::SeedNotPinned(_)
+ | PoolError::TooManySeeds { .. }
+ | PoolError::RealmTrustInvalid(_)
+ | PoolError::InvalidOpts(_)) => FfiError::InvalidArgument {
+ message: e.to_string(),
+ },
+ other => FfiError::Other {
+ message: other.to_string(),
},
}
}
}
-/// `Vec` -> `[u8; 32]`, with both lengths actually reported on
-/// mismatch — UniFFI has no fixed-size byte array type, so every 32-byte
-/// field (`realm`, node ids) crosses the boundary as `Vec` and gets
-/// validated here.
-fn to_32(bytes: Vec) -> Result<[u8; 32], FfiError> {
+/// `Vec` to `[u8; 32]`, reporting both lengths on a mismatch: UniFFI has
+/// no fixed-size byte array, so every id crosses as bytes and is checked
+/// here.
+pub(crate) fn to_32(bytes: Vec) -> Result<[u8; 32], FfiError> {
let actual = bytes.len() as u32;
bytes.try_into().map_err(|_| FfiError::WrongByteLength {
expected: 32,
@@ -129,15 +179,9 @@ fn to_32(bytes: Vec) -> Result<[u8; 32], FfiError> {
})
}
-/// `Vec` -> `[u8; 34]` — same as [`to_32`], for an MCID
-/// (`<>`, `plans/PLAN_WIRE_PROTOCOL.md`
-/// §12.1).
-fn to_mcid(bytes: Vec) -> Result {
- let actual = bytes.len() as u32;
- bytes.try_into().map_err(|_| FfiError::WrongByteLength {
- expected: 34,
- actual,
- })
+/// A timeout in milliseconds, zero for the core crate's default.
+pub(crate) fn millis(ms: u64) -> std::time::Duration {
+ std::time::Duration::from_millis(ms)
}
/// A mirror of [`macula_rust::cbor::Value`], narrowed only where the
@@ -220,7 +264,7 @@ impl TryFrom for FfiValue {
i64::try_from(n)
.map(FfiValue::Int)
.map_err(|_| FfiError::UnrepresentableValue {
- reason: format!("integer {n} is outside i64 range"),
+ message: format!("integer {n} is outside i64 range"),
})
}
Value::Bytes(b) => Ok(FfiValue::Bytes(b)),
@@ -244,1660 +288,3 @@ impl TryFrom for FfiValue {
}
}
}
-
-/// The result of a CALL: a mirror of
-/// [`macula_rust::frame::CallResponse`].
-#[derive(uniffi::Enum, Debug, Clone)]
-pub enum FfiCallResponse {
- Result {
- payload: FfiValue,
- responded_by: Vec,
- },
- Error {
- code: u8,
- name: String,
- reported_by: Vec,
- detail: Option,
- },
-}
-
-impl TryFrom for FfiCallResponse {
- type Error = FfiError;
-
- fn try_from(r: macula_rust::frame::CallResponse) -> Result {
- use macula_rust::frame::CallResponse;
- match r {
- CallResponse::Result {
- payload,
- responded_by,
- } => Ok(FfiCallResponse::Result {
- payload: FfiValue::try_from(payload)?,
- responded_by: responded_by.to_vec(),
- }),
- CallResponse::Error {
- code,
- name,
- reported_by,
- detail,
- } => Ok(FfiCallResponse::Error {
- code,
- name,
- reported_by: reported_by.to_vec(),
- detail,
- }),
- }
- }
-}
-
-/// A resolved direct-dial target — a mirror of
-/// [`macula_rust::direct_dial::Resolved`]: the station's own node id
-/// (32 bytes) plus its dialable host/port. Returned by
-/// [`FfiSession::resolve_direct`]; [`FfiSession::call_direct`] does this
-/// same resolution internally, so most callers never need this type
-/// directly — it's exposed for a caller that wants to resolve once and
-/// decide what to do with the target itself (e.g. displaying it, or
-/// dialing via a mechanism this crate doesn't cover).
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct FfiResolved {
- pub station: Vec,
- pub host: String,
- pub port: u16,
-}
-
-impl From for FfiResolved {
- fn from(r: macula_rust::direct_dial::Resolved) -> Self {
- FfiResolved {
- station: r.station.to_vec(),
- host: r.host,
- port: r.port,
- }
- }
-}
-
-impl From for FfiError {
- fn from(e: macula_rust::direct_dial::ResolveError) -> Self {
- FfiError::Resolve {
- reason: e.to_string(),
- }
- }
-}
-
-impl From for FfiError {
- fn from(e: macula_rust::direct_dial::CallError) -> Self {
- use macula_rust::direct_dial::CallError;
- match e {
- CallError::Resolve(re) => re.into(),
- CallError::TrustViolation { resolved, dialed } => FfiError::DirectDialTrustViolation {
- resolved: resolved.to_vec(),
- dialed: dialed.to_vec(),
- },
- other => FfiError::Call {
- reason: other.to_string(),
- },
- }
- }
-}
-
-impl From for FfiError {
- fn from(e: macula_rust::direct_dial::AdvertiseDirectError) -> Self {
- FfiError::Send {
- reason: e.to_string(),
- }
- }
-}
-
-/// One entry in a UCAN token's capability list — mirrors
-/// [`macula_rust::ucan::Capability`].
-#[derive(uniffi::Record, Debug, Clone, PartialEq)]
-pub struct FfiCapability {
- pub with: String,
- pub can: String,
-}
-
-impl From for FfiCapability {
- fn from(c: macula_rust::ucan::Capability) -> Self {
- FfiCapability {
- with: c.with,
- can: c.can,
- }
- }
-}
-
-impl From for macula_rust::ucan::Capability {
- fn from(c: FfiCapability) -> Self {
- macula_rust::ucan::Capability {
- with: c.with,
- can: c.can,
- }
- }
-}
-
-/// A UCAN token's decoded claims — a mirror of
-/// [`macula_rust::ucan::Payload`], minus `facts`: the core type's
-/// `facts` field is an arbitrary `serde_json::Value` map, which has no
-/// UniFFI-representable shape (unlike [`FfiValue`], which exists
-/// specifically to give CBOR values one) — the same class of narrowing
-/// [`FfiValue::Int`] already documents for `i128`. A caller needing the
-/// raw `fct` claim can decode the token bytes on the foreign side with any
-/// JSON library.
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct FfiUcanPayload {
- pub issuer: String,
- pub audience: String,
- pub capabilities: Vec,
- pub expires_at: Option,
- pub not_before: Option,
- pub nonce: String,
- pub proofs: Vec,
-}
-
-impl From for FfiUcanPayload {
- fn from(p: macula_rust::ucan::Payload) -> Self {
- FfiUcanPayload {
- issuer: p.issuer,
- audience: p.audience,
- capabilities: p.capabilities.into_iter().map(Into::into).collect(),
- expires_at: p.expires_at,
- not_before: p.not_before,
- nonce: p.nonce,
- proofs: p.proofs,
- }
- }
-}
-
-/// Mints a new UCAN token, self-issued and signed by `identity` — see
-/// [`macula_rust::ucan::create`]'s own doc for the full contract
-/// (`issuer`/`audience` are opaque strings, not validated here). A token
-/// for a UCAN-gated procedure must name the calling node as its `audience`:
-/// that node's id as lowercase hex.
-#[uniffi::export]
-pub fn ucan_create(
- issuer: String,
- audience: String,
- capabilities: Vec,
- identity: &FfiKeyPair,
- expires_at: Option,
- not_before: Option,
-) -> Result, FfiError> {
- let opts = macula_rust::ucan::CreateOpts {
- expires_at,
- not_before,
- ..Default::default()
- };
- macula_rust::ucan::create(
- &issuer,
- &audience,
- capabilities.into_iter().map(Into::into).collect(),
- &identity.0,
- opts,
- )
- .map_err(FfiError::from)
-}
-
-/// Verifies `token`'s signature against `public_key` (32 bytes) and its
-/// `exp`/`nbf` claims against the current time — see
-/// [`macula_rust::ucan::verify`]'s own doc, including its check order.
-/// Only a successful [`ucan_verify`] result should ever back an
-/// authorization decision — [`ucan_decode`] and the `ucan_get_*` getters
-/// below never check the signature.
-#[uniffi::export]
-pub fn ucan_verify(token: Vec, public_key: Vec) -> Result {
- let key = to_32(public_key)?;
- macula_rust::ucan::verify(&token, &key)
- .map(FfiUcanPayload::from)
- .map_err(FfiError::from)
-}
-
-/// Parses `token`'s payload WITHOUT verifying its signature or checking
-/// expiration — see [`ucan_verify`]'s doc for why that distinction matters.
-#[uniffi::export]
-pub fn ucan_decode(token: Vec) -> Result {
- macula_rust::ucan::decode(&token)
- .map(FfiUcanPayload::from)
- .map_err(FfiError::from)
-}
-
-/// `token`'s `iss` claim, unverified — see [`ucan_verify`]'s doc.
-#[uniffi::export]
-pub fn ucan_get_issuer(token: Vec) -> Result {
- macula_rust::ucan::get_issuer(&token).map_err(FfiError::from)
-}
-
-/// `token`'s `aud` claim, unverified — see [`ucan_verify`]'s doc.
-#[uniffi::export]
-pub fn ucan_get_audience(token: Vec) -> Result {
- macula_rust::ucan::get_audience(&token).map_err(FfiError::from)
-}
-
-/// `token`'s `cap` claim, unverified — see [`ucan_verify`]'s doc.
-#[uniffi::export]
-pub fn ucan_get_capabilities(token: Vec) -> Result, FfiError> {
- macula_rust::ucan::get_capabilities(&token)
- .map(|caps| caps.into_iter().map(Into::into).collect())
- .map_err(FfiError::from)
-}
-
-/// `token`'s `exp` claim, unverified — see [`ucan_verify`]'s doc.
-#[uniffi::export]
-pub fn ucan_get_expiration(token: Vec) -> Result