docs(arch): ConnectionCredentials decouples dial from call protocol (ADR-091) + migration plan cleanups

ADR-091: ConnectionCredentials — decouple the dial credential bundle
from the call protocol. CallCredentials (call-protocol-level, carries
auth_token) was being used as the dial's credential type, coupling the
dial to the call protocol. The auth_token is a hub-layer identity
correlation mechanism (browsers, alknet/register), not a transport
credential. ConnectionCredentials (transport-level: local_identity +
remote_identity) is the dial's credential bundle; all three dial
signatures unify on &ConnectionCredentials; dial_iroh's node_id
parameter is derived from remote_identity.fingerprint. CallCredentials
stays in alknet-call with auth_token. The shape is validated by the
future dial_ssh pattern (russh's check_server_key + authenticate_publickey
consume the same two dimensions).

Amends ADR-089 §3 (dial signatures) and §5 (move consequence), ADR-087
(input framing). Updates client/tls/call/core crate specs and the
extraction plan's Phase 0/3/4/5.

Migration plan cleanups (findings.md):
- Remove duplicated ordering-rationale bullets (copy-paste artifact)
- TL;DR: six phases -> seven (Phase 0 promoted); four compilable
  intermediate states -> each phase leaves workspace compilable
- Remove inline 'Wait — that's 10, not 8' self-correction; fix
  Category B header to (10 tests)
- Replace contradictory line ranges in Net Phase 5 test impact with
  name-based references
- Decide quinn feature fate: removed (not no-op)
- Note webpki-roots always-present per ADR-088 §5 in Phase 1 dep list
This commit is contained in:
glm-5.2 committed 2026-07-17 05:56:47 +00:00
1 parent 16ae3cf6c9
commit 613bf680cd
9 files changed
+599 -154

No files matched your search

+5 -4
View File
@@ -210,7 +210,7 @@ adapter location map is now consistent: all HTTP-backed adapters
|----------|--------|-------------| |----------|--------|-------------|
| [overview.md](overview.md) | draft | Workspace-level overview, crate graph (core mono-repo scope per ADR-085), hub/worker model, shared types, design principles | | [overview.md](overview.md) | draft | Workspace-level overview, crate graph (core mono-repo scope per ADR-085), hub/worker model, shared types, design principles |
| [open-questions.md](open-questions.md) | draft | OQ index — theme-grouped tables + Deferred/Blocked section; per-OQ files in [`questions/`](questions/) | | [open-questions.md](open-questions.md) | draft | OQ index — theme-grouped tables + Deferred/Blocked section; per-OQ files in [`questions/`](questions/) |
| [crates/core/README.md](crates/core/README.md) | draft | alknet-core crate index — shared types + auth + config (endpoint extracted to `alknet-endpoint` per ADR-083 Am. 2026-07-15; `CallCredentials`/`RemoteIdentity` moved here from `alknet-call` per ADR-089 §5) | | [crates/core/README.md](crates/core/README.md) | draft | alknet-core crate index — shared types + auth + config (endpoint extracted to `alknet-endpoint` per ADR-083 Am. 2026-07-15; `ConnectionCredentials`/`RemoteIdentity` moved here from `alknet-call` per ADR-091; `CallCredentials` stays in `alknet-call`) |
| [crates/core/core-types.md](crates/core/core-types.md) | draft | ProtocolHandler, HandlerError, Connection (`Box<dyn BidiStreamSource>` — ADR-070), BidiStreamSource trait, BiStream, StreamError | | [crates/core/core-types.md](crates/core/core-types.md) | draft | ProtocolHandler, HandlerError, Connection (`Box<dyn BidiStreamSource>` — ADR-070), BidiStreamSource trait, BiStream, StreamError |
| [crates/core/endpoint.md](crates/core/endpoint.md) | deprecated | Endpoint spec — **moved to `alknet-endpoint`** (ADR-083 Am. 2026-07-15); see [`crates/endpoint/README.md`](crates/endpoint/README.md) | | [crates/core/endpoint.md](crates/core/endpoint.md) | deprecated | Endpoint spec — **moved to `alknet-endpoint`** (ADR-083 Am. 2026-07-15); see [`crates/endpoint/README.md`](crates/endpoint/README.md) |
| [crates/core/auth.md](crates/core/auth.md) | draft | AuthContext (incl. `anonymous` constructor), Identity, IdentityProvider, AuthToken, resolution flow | | [crates/core/auth.md](crates/core/auth.md) | draft | AuthContext (incl. `anonymous` constructor), Identity, IdentityProvider, AuthToken, resolution flow |
@@ -243,7 +243,7 @@ adapter location map is now consistent: all HTTP-backed adapters
| [crates/vault/protocol.md](crates/vault/protocol.md) | stable | DerivedKey redaction, KeyType, serialization behavior | | [crates/vault/protocol.md](crates/vault/protocol.md) | stable | DerivedKey redaction, KeyType, serialization behavior |
| [crates/hub/README.md](crates/hub/README.md) | draft | alknet-hub crate — composes a subset of three endpoint types (web/native/iroh — ADR-086), channels substrate (ADR-079 relay), worker registration flow (OQ-58), identity over transports, aggregated peer env, connection lifecycle, service discovery | | [crates/hub/README.md](crates/hub/README.md) | draft | alknet-hub crate — composes a subset of three endpoint types (web/native/iroh — ADR-086), channels substrate (ADR-079 relay), worker registration flow (OQ-58), identity over transports, aggregated peer env, connection lifecycle, service discovery |
| [crates/tls/README.md](crates/tls/README.md) | reviewed | alknet-tls crate — shared TLS config (`TlsServerConfig` + `TlsClientConfig`) shared across quinn + TCP+TLS + iroh; one cert, one ACME state machine, N transports; split ALPN lists per endpoint type (ADR-086, resolves OQ-62); `FingerprintPinVerifier` moved here from `alknet-call` (ADR-089 §5); `webpki-roots` fallback for empty platform stores (ADR-088 §5); fixes cert-reuse welding in `alknet-core/endpoint.rs` (ADR-082) | | [crates/tls/README.md](crates/tls/README.md) | reviewed | alknet-tls crate — shared TLS config (`TlsServerConfig` + `TlsClientConfig`) shared across quinn + TCP+TLS + iroh; one cert, one ACME state machine, N transports; split ALPN lists per endpoint type (ADR-086, resolves OQ-62); `FingerprintPinVerifier` moved here from `alknet-call` (ADR-089 §5); `webpki-roots` fallback for empty platform stores (ADR-088 §5); fixes cert-reuse welding in `alknet-core/endpoint.rs` (ADR-082) |
| [crates/client/README.md](crates/client/README.md) | draft | alknet-client crate — the native client dial seam (`AlknetClient`), client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); optional SOCKS5 proxy (ADR-090 — UDP ASSOCIATE for QUIC, CONNECT for TCP+TLS, force-relay-only + HTTP-to-SOCKS5 bridge for iroh; OQ-67 resolved); produces `Connection` for `CallClient`/`ChannelClient` take-over; `CallClient::connect`/`ChannelClient::connect_quic` removed (dial centralized here); `alknet/register` named (wire protocol deferred, OQ-66) | | [crates/client/README.md](crates/client/README.md) | draft | alknet-client crate — the native client dial seam (`AlknetClient`), client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key) unified on `&ConnectionCredentials` (ADR-091); optional SOCKS5 proxy (ADR-090 — UDP ASSOCIATE for QUIC, CONNECT for TCP+TLS, force-relay-only + HTTP-to-SOCKS5 bridge for iroh; OQ-67 resolved); produces `Connection` for `CallClient`/`ChannelClient` take-over; `CallClient::connect`/`ChannelClient::connect_quic` removed (dial centralized here); `alknet/register` named (wire protocol deferred, OQ-66) |
| [crates/endpoint/README.md](crates/endpoint/README.md) | draft | alknet-endpoint crate — the server-side accept-loop runner (`AlknetEndpoint`), extracted from `alknet-core` (ADR-083 Am. 2026-07-15); takes pre-built transports via `with_quinn`/`with_iroh`/`with_tcp_tls`; public `dispatch` for SSH/WT; `EndpointError` removed (vestigial); handler crates no longer transitively link quinn/iroh | | [crates/endpoint/README.md](crates/endpoint/README.md) | draft | alknet-endpoint crate — the server-side accept-loop runner (`AlknetEndpoint`), extracted from `alknet-core` (ADR-083 Am. 2026-07-15); takes pre-built transports via `with_quinn`/`with_iroh`/`with_tcp_tls`; public `dispatch` for SSH/WT; `EndpointError` removed (vestigial); handler crates no longer transitively link quinn/iroh |
| [crates/channels/README.md](crates/channels/README.md) | draft | alknet-channels crate — multiplexing proxy, 9-byte chunk format, N channels over one transport stream | | [crates/channels/README.md](crates/channels/README.md) | draft | alknet-channels crate — multiplexing proxy, 9-byte chunk format, N channels over one transport stream |
| [crates/channels/overview.md](crates/channels/overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, transport agnosticism, WASM, relationship to existing crates | | [crates/channels/overview.md](crates/channels/overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, transport agnosticism, WASM, relationship to existing crates |
@@ -343,10 +343,11 @@ adapter location map is now consistent: all HTTP-backed adapters
| [084](decisions/084-aws-lc-rs-crypto-provider.md) | aws-lc-rs as the TLS Crypto Provider | Accepted | | [084](decisions/084-aws-lc-rs-crypto-provider.md) | aws-lc-rs as the TLS Crypto Provider | Accepted |
| [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | Accepted | | [085](decisions/085-workspace-scope-core-vs-consumer-repos.md) | Workspace Scope — Core vs. Consumer Repos | Accepted |
| [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Accepted | | [086](decisions/086-endpoint-types-and-entry-points.md) | Endpoint Types and Entry Points | Accepted |
| [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | Accepted (§5 amended by ADR-089 — `FingerprintPinVerifier` moves to `alknet-tls`; `alknet-call` sheds TLS deps) | | [087](decisions/087-tlsclientconfig-not-blocked-on-dial.md) | `TlsClientConfig` Not Blocked on Dial Seam | Accepted (§5 amended by ADR-089 — `FingerprintPinVerifier` moves to `alknet-tls`; `alknet-call` sheds TLS deps; input framing amended by ADR-091 — `ClientVerifierContext` derived from `ConnectionCredentials`, not `CallCredentials`) |
| [088](decisions/088-tlserror-shape.md) | `TlsError` Shape — Single Enum, Owned by `alknet-tls` | Accepted (§5 added — `webpki-roots` fallback when platform store is empty; §7 references ADR-089 for handshake-error surfacing) | | [088](decisions/088-tlserror-shape.md) | `TlsError` Shape — Single Enum, Owned by `alknet-tls` | Accepted (§5 added — `webpki-roots` fallback when platform store is empty; §7 references ADR-089 for handshake-error surfacing) |
| [089](decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | Accepted (resolves OQ-55; `CallClient::connect` / `ChannelClient::connect_quic` removed; `CallCredentials`/`RemoteIdentity` moved to `alknet-core`; `FingerprintPinVerifier` moved to `alknet-tls`; `ClientError` removed; `alknet-call` sheds TLS deps) | | [089](decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — Native Client Dial Seam | Accepted (resolves OQ-55; `CallClient::connect` / `ChannelClient::connect_quic` removed; §3/§5 amended by ADR-091 — dial takes `ConnectionCredentials`, not `CallCredentials`; `CallCredentials` stays in `alknet-call`; `FingerprintPinVerifier` moved to `alknet-tls`; `ClientError` removed; `alknet-call` sheds TLS deps) |
| [090](decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | Accepted (§5 amended 2026-07-16 — OQ-67 resolved: iroh force-relay-only + HTTP-to-SOCKS5 bridge) | | [090](decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | Accepted (§5 amended 2026-07-16 — OQ-67 resolved: iroh force-relay-only + HTTP-to-SOCKS5 bridge) |
| [091](decisions/091-connectioncredentials-decouple-dial-from-call.md) | `ConnectionCredentials` — Decouple Dial Credentials from Call Protocol | Accepted (amends ADR-089 §3/§5 and ADR-087 input framing; dial takes `ConnectionCredentials` not `CallCredentials`; all three dial signatures unified; `dial_iroh`'s `node_id` derived from `remote_identity`; `auth_token` stays in call-protocol layer) |
## Open Questions ## Open Questions
@@ -246,19 +246,43 @@ attribution, filtered by the calling peer's authorization). See
### Credential sources for connections ### Credential sources for connections
`CallCredentials` (now in `alknet-core`, moved from `alknet-call` per The credential dimensions are split across two layers (ADR-091):
ADR-089 §5 so the dial does not depend on the call protocol) carries
the three credential dimensions (ADR-017 §7). Credentials come from - **`ConnectionCredentials`** (in `alknet-core`, moved from
`Capabilities` (ADR-014), never from environment variables. The three `alknet-call` per ADR-091) — the **transport-level** credential
credential dimensions (ADR-017 §7): bundle, consumed by the dial (`AlknetClient`). Carries the two
transport-identity dimensions: `local_identity` (the local node's
`TlsIdentity`) and `remote_identity` (the expected fingerprint). The
dial does not depend on the call protocol for this type.
- **`CallCredentials`** (stays in `alknet-call`) — the
**call-protocol** credential bundle. The `auth_token` dimension
(ADR-017 §7) is a call-protocol / hub-layer concept: a bearer token
correlated to an identity via `IdentityProvider::resolve_from_token`,
used for browsers (no raw-key support) and `alknet/register` (no
prior peer relationship). It is a per-request field on
`call.requested` payloads, not a transport credential.
Credentials come from `Capabilities` (ADR-014), never from environment
variables. The three credential dimensions (ADR-017 §7):
```rust ```rust
pub struct CallCredentials { // Transport-level (alknet-core, consumed by the dial — ADR-091)
pub tls_identity: Option<TlsIdentity>, // RFC 7250 raw key or X.509 pub struct ConnectionCredentials {
pub auth_token: Option<AuthToken>, // call-protocol-level token pub local_identity: Option<TlsIdentity>, // RFC 7250 raw key or X.509
pub remote_identity: Option<RemoteIdentity>, // expected fingerprint/cert (None = CA path, see below) pub remote_identity: Option<RemoteIdentity>, // expected fingerprint (None = CA path / fail-closed)
} }
// Call-protocol-level (alknet-call — the auth_token stays here)
// The auth_token is set per-request on call.requested payloads, not
// carried by the dial.
```
`CallCredentials` retains `auth_token` (and may assemble from
`ConnectionCredentials` + `auth_token` at the take-over site, or the
caller provides `auth_token` per-request via `call_with_payload`). The
transport dimensions (`local_identity`, `remote_identity`) moved to
`ConnectionCredentials` in `alknet-core` per ADR-091.
/// Expected identity of the remote node (ADR-017 §7, extended by /// Expected identity of the remote node (ADR-017 §7, extended by
/// ADR-034 §2). Carries a fingerprint string the assembly layer /// ADR-034 §2). Carries a fingerprint string the assembly layer
/// derives from `Capabilities` when the local node has a `PeerEntry` /// derives from `Capabilities` when the local node has a `PeerEntry`
+64 -52
View File
@@ -155,7 +155,7 @@ hiding the client's real IP from the hub.
```rust ```rust
impl AlknetClient { impl AlknetClient {
/// QUIC dial. Builds a `TlsClientConfig` from `credentials` /// QUIC dial. Builds a `TlsClientConfig` from `creds`
/// (ADR-034 verifier selection + ADR-084 provider), dials `addr` /// (ADR-034 verifier selection + ADR-084 provider), dials `addr`
/// on `alpn`, returns a `Connection` via /// on `alpn`, returns a `Connection` via
/// `Connection::from_quinn_with_alpn`. The `server_name` is the /// `Connection::from_quinn_with_alpn`. The `server_name` is the
@@ -167,10 +167,10 @@ impl AlknetClient {
addr: SocketAddr, addr: SocketAddr,
server_name: &str, server_name: &str,
alpn: &[u8], alpn: &[u8],
credentials: &CallCredentials, creds: &ConnectionCredentials,
) -> Result<Connection, ClientDialError>; ) -> Result<Connection, ClientDialError>;
/// TCP+TLS dial. Builds a `TlsClientConfig` from `credentials`, /// TCP+TLS dial. Builds a `TlsClientConfig` from `creds`,
/// connects a `TcpStream` to `addr`, wraps with `TlsConnector` /// connects a `TcpStream` to `addr`, wraps with `TlsConnector`
/// using `host` as the SNI, returns a `Connection` via /// using `host` as the SNI, returns a `Connection` via
/// `Connection::from_bidi` (ADR-065). Feature-gated on `tcp`. /// `Connection::from_bidi` (ADR-065). Feature-gated on `tcp`.
@@ -180,21 +180,23 @@ impl AlknetClient {
host: &str, host: &str,
addr: SocketAddr, addr: SocketAddr,
alpn: &[u8], alpn: &[u8],
credentials: &CallCredentials, creds: &ConnectionCredentials,
) -> Result<Connection, ClientDialError>; ) -> Result<Connection, ClientDialError>;
/// Iroh dial. Dials `node_id` on `alpn` via the iroh endpoint. /// Iroh dial. Dials on `alpn` via the iroh endpoint. The iroh path
/// The iroh path does NOT use `TlsClientConfig` — iroh has its /// does NOT use `TlsClientConfig` — iroh has its own TLS (shares the
/// own TLS (shares the `Ed25519SecretKey`, not the rustls config — /// `Ed25519SecretKey`, not the rustls config — ADR-087 §3, ADR-089
/// ADR-087 §3, ADR-089 §3). The verifier is iroh's `NodeId` match /// §3). The local key is extracted from `creds.local_identity`; the
/// (fingerprint pin by another name — ADR-034 §3). An unknown /// remote `NodeId` is derived from `creds.remote_identity.fingerprint`
/// iroh remote fails closed (no CA). Feature-gated on `iroh`. /// (`ed25519:<hex>` → `NodeId::from_bytes`). The verifier is iroh's
/// `NodeId` match (fingerprint pin by another name — ADR-034 §3).
/// An unknown iroh remote fails closed (no CA). Feature-gated on
/// `iroh`.
#[cfg(feature = "iroh")] #[cfg(feature = "iroh")]
pub async fn dial_iroh( pub async fn dial_iroh(
&self, &self,
node_id: iroh::NodeId,
alpn: &[u8], alpn: &[u8],
local_key: &alknet_core::config::Ed25519SecretKey, creds: &ConnectionCredentials,
) -> Result<Connection, ClientDialError>; ) -> Result<Connection, ClientDialError>;
} }
``` ```
@@ -204,9 +206,12 @@ The two rustls dials (`dial_quic`, `dial_tcp_tls`) share
pin for a known peer, CA-verify for an unknown X.509 remote, fail-closed pin for a known peer, CA-verify for an unknown X.509 remote, fail-closed
for an unknown raw-key remote) and the ADR-084 crypto provider for an unknown raw-key remote) and the ADR-084 crypto provider
(`aws_lc_rs`). The iroh dial is the exception: iroh has its own TLS and (`aws_lc_rs`). The iroh dial is the exception: iroh has its own TLS and
takes the `Ed25519SecretKey` directly, not a `rustls::ClientConfig`. takes the `Ed25519SecretKey` directly (extracted from
The consistency is in the rule (ADR-034), not in the type — the same `creds.local_identity`), not a `rustls::ClientConfig`. The consistency
exception as the server side (ADR-082, ADR-087 §3). is in the rule (ADR-034), not in the type — the same exception as the
server side (ADR-082, ADR-087 §3). All three dials take
`&ConnectionCredentials` — the unified transport-level credential
bundle (ADR-091).
### What the dial does NOT do ### What the dial does NOT do
@@ -266,7 +271,7 @@ invariant. The config's crate location (it can live in `alknet-client`,
detail; the shape is decided (ADR-090 §1). detail; the shape is decided (ADR-090 §1).
**The proxy is invisible above the dial.** `Connection`, dispatch, **The proxy is invisible above the dial.** `Connection`, dispatch,
`CallCredentials`, `TlsClientConfig`, the hub's `supervise_worker` `ConnectionCredentials`, `TlsClientConfig`, the hub's `supervise_worker`
closure — none know a proxy is in the path. The proxy is purely an closure — none know a proxy is in the path. The proxy is purely an
establishment concern, localized to `alknet-client`. A proxied QUIC establishment concern, localized to `alknet-client`. A proxied QUIC
connection still yields a `Connection::from_quinn_with_alpn`; a connection still yields a `Connection::from_quinn_with_alpn`; a
@@ -328,25 +333,25 @@ the alknet type level — see ADR-090 §"Two distinct SOCKS5 uses".
### Credentials ### Credentials
`AlknetClient`'s dials take a `CallCredentials` bundle — the shared `AlknetClient`'s dials take a `ConnectionCredentials` bundle — the
type from `alknet-core` (moved out of `alknet-call` so the dial does transport-level credential type from `alknet-core` (ADR-091). It
not depend on the call protocol; see ADR-089 §5). It carries the local carries the two dimensions every dial consumes: the local `TlsIdentity`
`TlsIdentity`, the optional call-protocol-level `auth_token`, and the (presented to the transport's identity layer) and the `RemoteIdentity`
`RemoteIdentity` for verifier selection. The credentials come from for verifier selection. The credentials come from `Capabilities`
`Capabilities` (ADR-014), never from environment variables — the (ADR-014), never from environment variables — the no-env-vars
no-env-vars invariant. The assembly layer derives them from the vault invariant. The assembly layer derives them from the vault at startup
at startup and passes them to each dial. and passes them to each dial.
**The `auth_token` is stripped at the TLS boundary.** `TlsClientConfig` `ConnectionCredentials` is **not** the call-protocol credential bundle.
and `ClientVerifierContext` are TLS-level types and do not carry the The call-protocol `auth_token` (a hub-correlated bearer for browsers
call-protocol auth token. The dial extracts the TLS-relevant fields and `alknet/register` — ADR-017 §7) is a per-request field on
from `CallCredentials` — `tls_identity` → the client-cert `call.requested` payloads, not a transport credential. It stays in the
`local_identity`, `remote_identity` → the fingerprint-pin input to call-protocol layer (`CallCredentials` in `alknet-call`); the dial does
`ClientVerifierContext` — and builds a `TlsClientConfig` from those not carry it. `Dispatcher::resolve_identity` resolves it via
alone. The `auth_token` travels with the `Connection` into the `IdentityProvider::resolve_from_token` at dispatch time; the `from_call`
protocol take-over (`spawn_dispatch` / `from_connection`), where it is forwarding handler sets it via `build_forwarded_payload`. This keeps
sent as the first call-protocol frame when the protocol requires it. `alknet-client` free of call-protocol coupling. See
This keeps `alknet-tls` free of any call-protocol coupling. [ADR-091](../../decisions/091-connectioncredentials-decouple-dial-from-call.md).
### The dialable ALPNs ### The dialable ALPNs
@@ -425,11 +430,13 @@ public key) is verified against the expected `NodeId`, which is
fingerprint-pinning by another name. An unknown iroh remote fails fingerprint-pinning by another name. An unknown iroh remote fails
closed (no CA to fall back to — ADR-034 §3, Assumption 1). closed (no CA to fall back to — ADR-034 §3, Assumption 1).
The `dial_iroh` method takes the `Ed25519SecretKey` as a parameter The `dial_iroh` method extracts the key from
rather than pulling it from `CallCredentials` because iroh does not use `creds.local_identity` (`ConnectionCredentials`) rather than taking a
the `TlsClientConfig` path. The assembly layer reads the key from separate `Ed25519SecretKey` parameter because the dial signature is
`StaticConfig` (in core) and passes it directly, same as the server unified — all three dials take `&ConnectionCredentials` (ADR-091). The
side's iroh endpoint construction. assembly layer reads the key from `StaticConfig` (in core) and passes
it via `ConnectionCredentials`, same as the server side's iroh endpoint
construction.
### Non-Rust native clients (out of scope) ### Non-Rust native clients (out of scope)
@@ -557,7 +564,7 @@ don't use a proxy don't pay the dep.
``` ```
alknet-client alknet-client
├── alknet-core (Connection, CallCredentials, RemoteIdentity, ├── alknet-core (Connection, ConnectionCredentials, RemoteIdentity,
│ Ed25519SecretKey, types) │ Ed25519SecretKey, types)
├── alknet-tls (TlsClientConfig — for quinn + tcp dials) ├── alknet-tls (TlsClientConfig — for quinn + tcp dials)
├── quinn (optional — dial_quic) ├── quinn (optional — dial_quic)
@@ -569,14 +576,15 @@ alknet-client
``` ```
`alknet-client` depends on `alknet-tls` (for `TlsClientConfig`) and `alknet-client` depends on `alknet-tls` (for `TlsClientConfig`) and
`alknet-core` (for `Connection`, `CallCredentials`, `RemoteIdentity`, `alknet-core` (for `Connection`, `ConnectionCredentials`,
and types). It does **not** depend on `alknet-call` or `RemoteIdentity`, and types). It does **not** depend on `alknet-call` or
`alknet-channels-call` — the dial is below the protocol. `alknet-channels-call` — the dial is below the protocol.
`CallCredentials` and `RemoteIdentity` live in `alknet-core` (moved `ConnectionCredentials` and `RemoteIdentity` live in `alknet-core`
from `alknet-call` per ADR-089 §5 — both the call and channels clients (transport-level credential types, moved from `alknet-call` per
need them, and the dial must not depend on the call protocol; see ADR-091 — the dial and the server-side transport construction both
ADR-089 §5). `FingerprintPinVerifier` lives in `alknet-tls` (moved from consume them, and the dial must not depend on the call protocol for the
`alknet-call` per ADR-087 §5 — it is a TLS concern, and credential type). `FingerprintPinVerifier` lives in `alknet-tls` (moved
from `alknet-call` per ADR-087 §5 — it is a TLS concern, and
`TlsClientConfig::new` constructs it; moving it lets `alknet-call` shed `TlsClientConfig::new` constructs it; moving it lets `alknet-call` shed
its direct `rustls` dep entirely). its direct `rustls` dep entirely).
@@ -606,7 +614,7 @@ alknet-worker (uses AlknetClient to dial a hub)
`AlknetClient` is one producer, but a test can hand them a `AlknetClient` is one producer, but a test can hand them a
`Connection::from_stream` directly. The dependency direction is: `Connection::from_stream` directly. The dependency direction is:
`alknet-client → alknet-tls → alknet-core`; the protocol crates are `alknet-client → alknet-tls → alknet-core`; the protocol crates are
parallel, not downstream of the dial. `CallCredentials` and parallel, not downstream of the dial. `ConnectionCredentials` and
`RemoteIdentity` live in `alknet-core` (not `alknet-call`), so the dial `RemoteIdentity` live in `alknet-core` (not `alknet-call`), so the dial
does not depend on the call protocol for the credential type. does not depend on the call protocol for the credential type.
@@ -632,8 +640,8 @@ let client = AlknetClient::new()
// .with_iroh(iroh_endpoint) — if iroh is needed // .with_iroh(iroh_endpoint) — if iroh is needed
// 3. Derive credentials from the vault (ADR-014 — no env vars). // 3. Derive credentials from the vault (ADR-014 — no env vars).
let creds = CallCredentials::new() let creds = ConnectionCredentials::new()
.with_tls_identity(TlsIdentity::RawKey(local_key)) .with_local_identity(TlsIdentity::RawKey(local_key))
.with_remote_identity(RemoteIdentity { .with_remote_identity(RemoteIdentity {
fingerprint: hub_fingerprint, // known peer → fingerprint pin fingerprint: hub_fingerprint, // known peer → fingerprint pin
}); });
@@ -665,8 +673,9 @@ All design decisions are documented as ADRs in
| ADR | Decision | Summary | | ADR | Decision | Summary |
|-----|----------|---------| |-----|----------|---------|
| [089](../../decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — native client dial seam | New crate `alknet-client`; client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); resolves OQ-55; `alknet/register` named, wire protocol deferred | | [089](../../decisions/089-alknetclient-native-dial-seam.md) | AlknetClient — native client dial seam | New crate `alknet-client`; client-side analogue of `AlknetEndpoint`; three dials (QUIC + TCP+TLS via `TlsClientConfig`, iroh via key); resolves OQ-55; `alknet/register` named, wire protocol deferred (§3/§5 amended by ADR-091 — dial takes `ConnectionCredentials`, not `CallCredentials`) |
| [090](../../decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | `AlknetClient` gains `with_socks5_proxy`; `dial_quic` routes via UDP ASSOCIATE, `dial_tcp_tls` via CONNECT, `dial_iroh` forces relay-only via an HTTP-to-SOCKS5 bridge; OQ-67 resolved; grounded in the quinn-proxy + iroh-proxy PoCs | | [090](../../decisions/090-client-dial-socks5-proxy-seam.md) | Client-Dial SOCKS5 Proxy Seam | `AlknetClient` gains `with_socks5_proxy`; `dial_quic` routes via UDP ASSOCIATE, `dial_tcp_tls` via CONNECT, `dial_iroh` forces relay-only via an HTTP-to-SOCKS5 bridge; OQ-67 resolved; grounded in the quinn-proxy + iroh-proxy PoCs |
| [091](../../decisions/091-connectioncredentials-decouple-dial-from-call.md) | `ConnectionCredentials` — decouple dial from call protocol | The dial credential bundle is `ConnectionCredentials` (transport-level: `local_identity` + `remote_identity`), not `CallCredentials` (call-protocol-level); all three dial signatures unify on `&ConnectionCredentials`; `dial_iroh`'s `node_id` derived from `remote_identity`; `auth_token` stays in the call-protocol layer; `CallCredentials` stays in `alknet-call` |
## Open Questions ## Open Questions
@@ -700,7 +709,10 @@ See [open-questions.md](../../open-questions.md) for full details.
## References ## References
- [ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) — - [ADR-089](../../decisions/089-alknetclient-native-dial-seam.md) —
the decision this spec implements the decision this spec implements (§3/§5 amended by ADR-091)
- [ADR-091](../../decisions/091-connectioncredentials-decouple-dial-from-call.md)
— `ConnectionCredentials` (the dial credential bundle; decouples the
dial from the call protocol)
- [ADR-090](../../decisions/090-client-dial-socks5-proxy-seam.md) — - [ADR-090](../../decisions/090-client-dial-socks5-proxy-seam.md) —
the SOCKS5 proxy seam (client-dial privacy) the SOCKS5 proxy seam (client-dial privacy)
- [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) — - [ADR-083](../../decisions/083-endpoint-as-accept-loop-runner.md) —
+6 -4
View File
@@ -15,10 +15,12 @@ vestigial); core no longer carries the accept-loop runner or its
transport deps (quinn, iroh, rcgen, rustls-acme). transport deps (quinn, iroh, rcgen, rustls-acme).
`Connection::from_quinn` / `from_iroh` stay in core's `types.rs` as `Connection::from_quinn` / `from_iroh` stay in core's `types.rs` as
shared constructors (gated on core's `quinn` / `iroh` features). shared constructors (gated on core's `quinn` / `iroh` features).
`CallCredentials` and `RemoteIdentity` move to `alknet-core` (from `ConnectionCredentials` and `RemoteIdentity` move to `alknet-core`
`alknet-call`, per ADR-089 §5) — both the call and channels clients (from `alknet-call`, per ADR-091) — the transport-level credential
need them, and the dial (`alknet-client`) must not depend on the call bundle consumed by the dial (`alknet-client`) and by server-side
protocol for the credential type. transport construction. `CallCredentials` (the call-protocol credential
bundle, including `auth_token`) stays in `alknet-call` — the dial does
not carry call-protocol dimensions.
## Documents ## Documents
+14 -11
View File
@@ -503,7 +503,8 @@ pub struct TlsClientConfig {
impl TlsClientConfig { impl TlsClientConfig {
/// Build a client TLS config. Takes two inputs, both derived from /// Build a client TLS config. Takes two inputs, both derived from
/// `Capabilities` (ADR-014) / `CallCredentials`-shaped values: /// `Capabilities` (ADR-014) / `ConnectionCredentials`-shaped values
/// (ADR-091):
/// ///
/// 1. `local_identity` — the local node's `TlsIdentity` (RFC 7250 /// 1. `local_identity` — the local node's `TlsIdentity` (RFC 7250
/// raw key or X.509), presented as the client cert. `None` → /// raw key or X.509), presented as the client cert. `None` →
@@ -555,14 +556,15 @@ The `ClientVerifierContext` carries the inputs to ADR-034's verifier
selection (whether a `PeerEntry` exists for the remote, the expected selection (whether a `PeerEntry` exists for the remote, the expected
fingerprint). The exact struct shape is an implementation detail; the fingerprint). The exact struct shape is an implementation detail; the
decisions are in ADR-034. `ClientVerifierContext` is derived from decisions are in ADR-034. `ClientVerifierContext` is derived from
`CallCredentials` (in `alknet-core`) at the dial site — `AlknetClient` `ConnectionCredentials` (in `alknet-core`, per ADR-091) at the dial
extracts the TLS-relevant fields (`tls_identity` → `local_identity`, site — `AlknetClient` extracts the TLS-relevant fields
`remote_identity` → fingerprint-pin input) and builds a (`local_identity` → client cert, `remote_identity` → fingerprint-pin
`ClientVerifierContext` from those. The call-protocol `auth_token` is input) and builds a `ClientVerifierContext` from the latter. The
**stripped at the TLS boundary** — it never reaches `TlsClientConfig` or call-protocol `auth_token` is not in `ConnectionCredentials` — it is a
`ClientVerifierContext`; it travels with the `Connection` into the per-request field on `call.requested` payloads (a call-protocol / hub
protocol take-over. The `TlsError` variant granularity (covering both concept), not a transport credential; it never reaches `TlsClientConfig`
server and client errors) is decided — see or `ClientVerifierContext`. The `TlsError` variant granularity (covering
both server and client errors) is decided — see
[ADR-088](../../decisions/088-tlserror-shape.md) and the [ADR-088](../../decisions/088-tlserror-shape.md) and the
[`TlsError`](#tlserror) section below. [`TlsError`](#tlserror) section below.
@@ -693,8 +695,9 @@ alknet-core (loses TLS setup code + endpoint)
├── (rustls — only for fingerprint.rs types, if kept) ├── (rustls — only for fingerprint.rs types, if kept)
alknet-call (pure protocol crate — no TLS/transport deps per ADR-089 §5) alknet-call (pure protocol crate — no TLS/transport deps per ADR-089 §5)
└── alknet-core (ProtocolHandler, Connection, types; CallCredentials/RemoteIdentity └── alknet-core (ProtocolHandler, Connection, types; ConnectionCredentials/
moved to core per ADR-089 §5) RemoteIdentity moved to core per ADR-091; CallCredentials stays in
alknet-call)
alknet-hub (multi-transport endpoint) alknet-hub (multi-transport endpoint)
├── alknet-tls (TlsServerConfig — shared across quinn + TCP) ├── alknet-tls (TlsServerConfig — shared across quinn + TCP)
@@ -2,7 +2,10 @@
## Status ## Status
Accepted (resolves OQ-64) Accepted (resolves OQ-64; input framing amended 2026-07-16 by ADR-091 —
`ClientVerifierContext` is derived from `ConnectionCredentials.remote_identity`,
not `CallCredentials.remote_identity`; the `auth_token` is not in the
dial's credential bundle)
## Context ## Context
@@ -122,6 +125,16 @@ in ADR-034; the struct is a bag of already-decided inputs). It is
sketched lightly here; the full variant-granularity of `TlsError` is sketched lightly here; the full variant-granularity of `TlsError` is
OQ-63 (the next session). OQ-63 (the next session).
> **Amendment 2026-07-16 (ADR-091):** `ClientVerifierContext` is derived
> from `ConnectionCredentials.remote_identity` (the transport-level
> credential bundle), not `CallCredentials.remote_identity`. The dial
> (`AlknetClient::dial_quic` / `dial_tcp_tls`) extracts
> `creds.remote_identity` from `ConnectionCredentials` and builds
> `ClientVerifierContext` from it. `CallCredentials` is no longer in the
> dial's path — its `auth_token` dimension is a call-protocol concept,
> not a transport credential. See
> [ADR-091](091-connectioncredentials-decouple-dial-from-call.md).
This is **not** the dial. `TlsClientConfig` produces a This is **not** the dial. `TlsClientConfig` produces a
`rustls::ClientConfig`; the caller (the transport-specific dial helper `rustls::ClientConfig`; the caller (the transport-specific dial helper
— now `AlknetClient::dial_quic` / `dial_tcp_tls` per ADR-089; the — now `AlknetClient::dial_quic` / `dial_tcp_tls` per ADR-089; the
@@ -2,7 +2,14 @@
## Status ## Status
Accepted (resolves OQ-55) Accepted (resolves OQ-55; §3 and §5 amended 2026-07-16 by ADR-091 —
the dial credential bundle is `ConnectionCredentials` (transport-level),
not `CallCredentials` (call-protocol-level); all three dial signatures
unify on `&ConnectionCredentials`; `dial_iroh`'s `node_id` parameter is
derived from `remote_identity`; the `auth_token` stays in the
call-protocol layer, not the dial; `CallCredentials` stays in
`alknet-call`, only `ConnectionCredentials`/`RemoteIdentity` move to
`alknet-core`)
## Context ## Context
@@ -195,6 +202,9 @@ impl AlknetClient {
local_key: &alknet_core::config::Ed25519SecretKey, local_key: &alknet_core::config::Ed25519SecretKey,
) -> Result<Connection, ClientDialError>; ) -> Result<Connection, ClientDialError>;
} }
// NOTE: The signatures above are the ORIGINAL (pre-ADR-091) shapes.
// ADR-091 amends §3 — see the amendment note below.
``` ```
The three dials share `CallCredentials` (the local identity + remote The three dials share `CallCredentials` (the local identity + remote
@@ -205,6 +215,33 @@ server side's "iroh shares the key, not the config" (ADR-082, ADR-087
§3) — the consistency is in the rule (ADR-034 verifier selection), not §3) — the consistency is in the rule (ADR-034 verifier selection), not
in the type. in the type.
> **Amendment 2026-07-16 (ADR-091):** The dial credential bundle is
> `ConnectionCredentials`, not `CallCredentials`. `CallCredentials`
> couples the dial to the call protocol (its `auth_token` field is a
> call-protocol / hub-layer concept — bearer-token identity correlation
> for browsers and `alknet/register`, not a transport credential). The
> dial uses only the transport-identity dimensions (`local_identity` +
> `remote_identity`); those move to `ConnectionCredentials` in
> `alknet-core`. All three dial signatures unify on
> `&ConnectionCredentials`:
>
> ```rust
> dial_quic(addr, server_name, alpn, creds: &ConnectionCredentials) -> Connection
> dial_tcp_tls(host, addr, alpn, creds: &ConnectionCredentials) -> Connection
> dial_iroh(alpn, creds: &ConnectionCredentials) -> Connection
> ```
>
> The `node_id: iroh::NodeId` parameter on `dial_iroh` is removed — it
> is derived from `creds.remote_identity.fingerprint`
> (`ed25519:<hex>` → `NodeId::from_bytes`), the same extraction pattern
> the rustls dials use for the verifier. The consistency is now in both
> the rule (ADR-034) and the type. `CallCredentials` stays in
> `alknet-call` as the call-protocol credential bundle; the
> `auth_token` is a per-request field on `call.requested` payloads
> (set by the caller or the `from_call` forwarding handler, resolved by
> `Dispatcher::resolve_identity`), not a dial-level credential. See
> [ADR-091](091-connectioncredentials-decouple-dial-from-call.md).
### 4. The dial is transport-polymorphic across the native endpoint type ### 4. The dial is transport-polymorphic across the native endpoint type
The native endpoint type (ADR-086) has QUIC + TCP+TLS (both The native endpoint type (ADR-086) has QUIC + TCP+TLS (both
@@ -259,6 +296,18 @@ options the original ADR-089 draft called a "two-way-door
implementation detail"; it is not implementation detail — it determines implementation detail"; it is not implementation detail — it determines
the dep graph, and the dep graph requires it. the dep graph, and the dep graph requires it.
> **Amendment 2026-07-16 (ADR-091):** This consequence is superseded.
> `CallCredentials` does **not** move to `alknet-core` — it stays in
> `alknet-call` (it is the call-protocol credential bundle; its
> `auth_token` field is a call-protocol / hub-layer concept, not a
> transport credential). What moves to `alknet-core` is
> `ConnectionCredentials` (a new type carrying only the
> transport-identity dimensions: `local_identity` + `remote_identity`)
> and `RemoteIdentity`. The dial consumes `ConnectionCredentials`, not
> `CallCredentials`. All three dial signatures unify on
> `&ConnectionCredentials`. See
> [ADR-091](091-connectioncredentials-decouple-dial-from-call.md).
**Consequence: `FingerprintPinVerifier` moves to `alknet-tls`.** With **Consequence: `FingerprintPinVerifier` moves to `alknet-tls`.** With
`connect` removed and the verifier-selection logic centralized in `connect` removed and the verifier-selection logic centralized in
`TlsClientConfig::new` (ADR-087), `FingerprintPinVerifier` has no `TlsClientConfig::new` (ADR-087), `FingerprintPinVerifier` has no
@@ -0,0 +1,313 @@
# ADR-091: `ConnectionCredentials` — Decouple the Dial Credentials from the Call Protocol
## Status
Accepted (amends ADR-089 §3 and §5; amends ADR-087's `TlsClientConfig::new`
input framing)
## Context
ADR-089 extracted the dial into `AlknetClient` and moved `CallCredentials`
from `alknet-call` to `alknet-core` so the dial would not depend on the
call protocol. The three dial signatures were:
```rust
dial_quic(addr, server_name, alpn, credentials: &CallCredentials) -> Connection
dial_tcp_tls(host, addr, alpn, credentials: &CallCredentials) -> Connection
dial_iroh(node_id: iroh::NodeId, alpn, local_key: &Ed25519SecretKey) -> Connection
```
Two problems surfaced on review:
### Problem 1: the iroh dial signature is asymmetric
`dial_quic` and `dial_tcp_tls` take `&CallCredentials`; `dial_iroh` takes
a bare `&Ed25519SecretKey` + a separate `node_id: iroh::NodeId`. The
asymmetry exists because iroh has its own TLS (it shares the key, not
the rustls config — ADR-087 §3), so the iroh dial bypasses
`TlsClientConfig` and reads the key directly. But the asymmetry forces
the caller to know which dimension of the credential bundle each
transport consumes, and it leaves no path for the iroh dial to receive
the same inputs as the rustls dials — even though all three consume the
same two things: a local identity (key/cert) and an expected remote
identity (fingerprint).
### Problem 2: `CallCredentials` couples the dial to the call protocol
`CallCredentials` carries three dimensions (ADR-017 §7):
1. `tls_identity: Option<TlsIdentity>` — the local node's key/cert
2. `auth_token: Option<AuthToken>` — a call-protocol-level bearer token
3. `remote_identity: Option<RemoteIdentity>` — the expected remote fingerprint
The dial uses only dimensions 1 and 3 (the transport-identity layer).
Dimension 2 (`auth_token`) is a **call-protocol** concept: it correlates
a token to an identity via `IdentityProvider::resolve_from_token` — a
mechanism that exists for two hub-dependent cases where TLS-fingerprint
identity is unavailable:
- **Browsers** — no raw-key support, no client cert the hub can
fingerprint; the browser authenticates via a bearer token over
HTTP/WebSocket, and the hub's `IdentityProvider` resolves it.
- **`alknet/register`** — a native worker that hasn't been enrolled dials
in with no prior peer relationship; a registration token (or open
registration) establishes identity, not a TLS fingerprint.
Both depend on a **hub** running `IdentityProvider` with token-to-identity
mapping. A pure P2P connection (two nodes with raw-key identities) never
needs `auth_token` — the TLS fingerprint IS the identity.
`auth_token` is not a transport credential. It is a per-request field on
`call.requested` payloads (`Dispatcher::resolve_identity` reads
`payload.get("auth_token")`; the `from_call` forwarding handler sets it
via `build_forwarded_payload`). The dial never delivers it to the
protocol take-over — `spawn_dispatch(&self, connection: Connection)`
takes no credentials, and `Connection` (a `Box<dyn BidiStreamSource>`)
carries no `auth_token` field. The `auth_token` in `CallCredentials` is
unused by the dial and dropped after `connect()` in the current code.
By moving `CallCredentials` (with `auth_token` in it) to `alknet-core`
for the dial's benefit, ADR-089 §5 would drag a call-protocol concept
into the shared-types crate *for the dial's benefit* — when the dial
doesn't use it. The dial should consume a transport-level credential
bundle, not a call-protocol one.
### The two identity models
Underneath the three transports, there are two identity-consumption
models, both consuming the same two dimensions:
| Model | Transports | Consumes | What the transport does |
|-------|-----------|----------|------------------------|
| **rustls config** | QUIC (quinn), TCP+TLS (tokio-rustls) | `local_identity` → `TlsClientConfig` (client cert); `remote_identity` → verifier (`FingerprintPinVerifier` / `WebPkiServerVerifier`) | Builds `rustls::ClientConfig`, hands to transport connector |
| **key-native** | iroh, SSH (future — `docs/research/references/ssh/russh/06-usage-patterns.md`) | `local_identity` → `Ed25519SecretKey` → transport's key type (`iroh::SecretKey`, russh key); `remote_identity` → fingerprint → transport's verifier (`NodeId` match, known_hosts) | Reads the key directly; transport handles identity internally |
The difference is *how* each model consumes the dimensions, not *what*
they are. A unified credential bundle carrying just those two dimensions
lets every dial extract what its transport's identity layer needs,
without call-protocol coupling.
## Decision
### `ConnectionCredentials` — the dial's credential bundle
A new type in `alknet-core`, carrying the two transport-identity
dimensions every dial consumes:
```rust
/// Transport-level credentials for an outbound dial. Consumed by
/// `AlknetClient`'s dial methods and (for the server side) by the
/// assembly layer when building transports. Carries only the dimensions
/// the transport's identity layer needs — the local identity (key/cert
/// presented to the transport) and the expected remote identity
/// (fingerprint, driving verifier selection per ADR-034).
///
/// This is NOT the call-protocol credential bundle. The call-protocol
/// `auth_token` (hub-correlated bearer for browsers / `alknet/register`)
/// is a per-request field on `call.requested` payloads, not a
/// transport credential. It stays in the call-protocol layer.
pub struct ConnectionCredentials {
/// The local node's identity (RFC 7250 raw key or X.509), presented
/// to the transport's identity layer. rustls dials → `TlsClientConfig`
/// (client cert via `RawKeyClientCertResolver`); iroh/SSH dials →
/// key directly (`iroh::SecretKey::from_bytes`, russh key).
pub local_identity: Option<TlsIdentity>,
/// Expected identity of the remote node. `Some(fingerprint)` → pin
/// (known peer); `None` → CA verification for X.509 remotes or
/// fail-closed for Ed25519 raw-key remotes (ADR-034 §2/§3). `None`
/// is the public-X.509-endpoint state, not a missing field.
pub remote_identity: Option<RemoteIdentity>,
}
```
`RemoteIdentity` moves with `ConnectionCredentials` to `alknet-core`
(both are transport-level types; the dial and the server-side transport
construction both consume them).
### Unified dial signatures
All three dials take `&ConnectionCredentials`:
```rust
impl AlknetClient {
#[cfg(feature = "quinn")]
pub async fn dial_quic(
&self,
addr: SocketAddr,
server_name: &str,
alpn: &[u8],
creds: &ConnectionCredentials,
) -> Result<Connection, ClientDialError>;
#[cfg(feature = "tcp")]
pub async fn dial_tcp_tls(
&self,
host: &str,
addr: SocketAddr,
alpn: &[u8],
creds: &ConnectionCredentials,
) -> Result<Connection, ClientDialError>;
#[cfg(feature = "iroh")]
pub async fn dial_iroh(
&self,
alpn: &[u8],
creds: &ConnectionCredentials,
) -> Result<Connection, ClientDialError>;
}
```
The `node_id: iroh::NodeId` parameter on `dial_iroh` is removed — it is
derived from `creds.remote_identity.fingerprint` (`ed25519:<hex>` →
`NodeId::from_bytes`), the same way the rustls dials derive their
verifier from `remote_identity`. The consistency is now in both the rule
(ADR-034) and the type.
Each dial extracts what its transport's identity layer needs:
- **rustls dials** (`dial_quic`, `dial_tcp_tls`): `creds.local_identity`
→ `TlsClientConfig::new` (client cert); `creds.remote_identity` →
`ClientVerifierContext` (verifier selection).
- **iroh dial** (`dial_iroh`): `creds.local_identity` →
`Ed25519SecretKey` → `iroh::SecretKey::from_bytes`;
`creds.remote_identity.fingerprint` → `NodeId` (verifier).
### `CallCredentials` stays in `alknet-call`
`CallCredentials` remains the **call-protocol** credential bundle. Its
`auth_token` field stays — the call protocol uses it (the `from_call`
forwarding handler populates `auth_token` on outgoing `call.requested`
payloads; the hub's `Dispatcher::resolve_identity` resolves it via
`IdentityProvider::resolve_from_token`). The call protocol layer
assembles `CallCredentials` from `ConnectionCredentials` (the
transport-level dimensions) + the call-protocol `auth_token`, or the
caller provides the `auth_token` per-request via `call_with_payload`.
`CallCredentials` does **not** move to `alknet-core`. ADR-089 §5's move
of `CallCredentials` to core is superseded by this ADR: only
`ConnectionCredentials` and `RemoteIdentity` move to core. The call
protocol's own credential type stays in the call crate.
### `TlsClientConfig::new` input framing
`TlsClientConfig::new` (ADR-087) takes a `ClientVerifierContext` derived
from the credential bundle's `remote_identity`. The rustls dials extract
`creds.local_identity` and `creds.remote_identity` from
`ConnectionCredentials` and build `ClientVerifierContext` from the latter
— the same extraction ADR-087 described, just from
`ConnectionCredentials` instead of `CallCredentials`. The `auth_token`
dimension is simply not present in `ConnectionCredentials`, so the
"stripped at the TLS boundary" framing (ADR-089's claim that the token
"travels with the Connection") is no longer needed — the token was never
in the dial's credential bundle to strip.
### Future `dial_ssh` validates the shape
An SSH dial (`docs/research/references/ssh/russh/06-usage-patterns.md`)
consumes the same two dimensions:
- `check_server_key(&mut self, key: &ssh_key::PublicKey)` — the verifier
(fingerprint pin against known_hosts = `remote_identity`)
- `authenticate_publickey("user", PrivateKeyWithHashAlg::new(...))` —
local identity (the Ed25519 key = `local_identity`)
- `channel_open_session()` → `Connection::from_bidi` (ADR-065)
`dial_ssh(addr, alpn, creds: &ConnectionCredentials)` fits the same
signature. The SSH host-key verification is fingerprint-pinning
(known_hosts), which is what `remote_identity` carries. The local SSH
key is the same Ed25519 key iroh and raw-key quinn use. The pattern is
general — `ConnectionCredentials` covers it without call-protocol
coupling. SSH itself is unspecced (not yet specced — comes after
channels, tunnels, TTY rework), but the russh usage patterns confirm
the credential dimensions.
## Consequences
**Positive:**
- **The dial is fully decoupled from the call protocol.**
`ConnectionCredentials` carries only transport-identity dimensions;
`alknet-client` has no call-protocol coupling in its credential type.
The `auth_token` (a call-protocol / hub-layer concept) stays in
`alknet-call` where it belongs.
- **All three dial signatures are unified.** A caller no longer needs to
know that iroh takes a bare key while quinn/tcp take a credential
bundle — all take `&ConnectionCredentials`. The `node_id` parameter on
`dial_iroh` is derived from `remote_identity`, the same extraction
pattern the rustls dials use for the verifier.
- **The `auth_token` spec inaccuracy is fixed.** ADR-089 claimed the
`auth_token` "travels with the `Connection` into the protocol
take-over, where it is sent as the first call-protocol frame." This
was aspirational — `Connection` carries no `auth_token`, and
`spawn_dispatch` takes no credentials. With `auth_token` out of the
dial's credential bundle entirely, the claim is no longer needed. The
token is a per-request field on `call.requested` payloads, set by the
caller or the `from_call` forwarding handler.
- **`dial_ssh` fits the same shape when it arrives.** The credential
dimensions SSH needs (local key + expected host key) are exactly what
`ConnectionCredentials` carries. No future ADR needed for the SSH dial
signature.
- **`CallCredentials` stays in `alknet-call` — its home.** The call
protocol's own credential type is not dragged into core for the dial's
benefit. Core gets `ConnectionCredentials` (transport-level); the call
crate keeps `CallCredentials` (protocol-level).
**Negative:**
- **`CallCredentials` loses two fields.** `tls_identity` and
`remote_identity` move to `ConnectionCredentials` in core;
`CallCredentials` becomes `{auth_token: Option<AuthToken>}` (or is
restructured — the call protocol may assemble it from
`ConnectionCredentials` + `auth_token` at the take-over site, or the
caller provides `auth_token` per-request). The exact restructure is a
two-way-door implementation detail; the one-way decision is that the
transport dimensions leave `CallCredentials`.
- **The assembly layer assembles two credential bundles, not one.** Where
ADR-089 had the assembly layer build one `CallCredentials`, it now
builds `ConnectionCredentials` (for the dial) and, separately,
provides the `auth_token` to the call-protocol layer (for the
per-request payload). This is the correct layering — the dial and the
protocol consume different dimensions — but it is one more type at the
assembly site.
- **ADR-089 §5's "CallCredentials moves to core" is superseded.** The
move target changes from `CallCredentials` to `ConnectionCredentials`
+ `RemoteIdentity`. `CallCredentials` stays in `alknet-call`. This
affects the extraction plan's Phase 0 (the additive credentials move).
## Door type
**One-way.** The dial signatures (`dial_quic` / `dial_tcp_tls` /
`dial_iroh` all taking `&ConnectionCredentials`) are the public API
surface of `alknet-client`. The credential-type decoupling
(`ConnectionCredentials` in core, `CallCredentials` in call) determines
the dep graph (`alknet-client` depends on `alknet-core` for
`ConnectionCredentials`, not on `alknet-call` for `CallCredentials`).
Reversing would mean re-coupling the dial to the call protocol's
credential type and re-asymmetrizing the iroh dial. The crate is
greenfield (Phase 3 of the extraction plan), so the door is still open
now — this ADR records the decision before implementation.
## References
- ADR-089 — `AlknetClient` native dial seam (§3 dial signatures amended
— all take `&ConnectionCredentials`; §5 move amended —
`ConnectionCredentials`/`RemoteIdentity` move to core, not
`CallCredentials`)
- ADR-087 — `TlsClientConfig` not blocked on dial (input framing
amended — `ClientVerifierContext` derived from
`ConnectionCredentials.remote_identity`, not `CallCredentials`)
- ADR-034 — client-side verifier selection (the rule
`ConnectionCredentials.remote_identity` drives — unchanged)
- ADR-017 §7 — the three credential dimensions (the source of
`CallCredentials`'s three fields; this ADR splits the transport
dimensions from the protocol dimension)
- `crates/alknet-call/src/protocol/dispatch.rs` —
`Dispatcher::resolve_identity` reads `payload.get("auth_token")`
(per-request, not connection-level)
- `crates/alknet-call/src/client/from_call.rs` —
`build_forwarded_payload` sets `auth_token` on outgoing payloads
(per-request, the hub's own token)
- `docs/research/references/ssh/russh/06-usage-patterns.md` — the SSH
client usage patterns (check_server_key + authenticate_publickey)
validating the `ConnectionCredentials` shape for a future `dial_ssh`
+100 -72
View File
@@ -16,8 +16,8 @@ with what intermediate states.
The migration is **additive-then-subtractive**: build all three new The migration is **additive-then-subtractive**: build all three new
crates first (no breakage), then prune the old code from core and call crates first (no breakage), then prune the old code from core and call
(breakage confined to a single phase), then fix the http residual. Six (breakage confined to a single phase), then fix the http residual. Seven
phases, four compilable intermediate states. The heaviest single file phases (0-6), each leaving the workspace compilable. The heaviest single file
(`endpoint.rs`, 1606 lines) is not a monolith — it's three concerns (`endpoint.rs`, 1606 lines) is not a monolith — it's three concerns
welded together, each going to a different destination. welded together, each going to a different destination.
@@ -87,27 +87,35 @@ already removed per ADR-065; tests use `from_stream` with
## The seven phases ## The seven phases
### Phase 0: Move `CallCredentials`/`RemoteIdentity` to `alknet-core` (additive, no breakage) ### Phase 0: Move `ConnectionCredentials`/`RemoteIdentity` to `alknet-core` (additive, no breakage)
**What:** Add `CallCredentials` + `RemoteIdentity` to a new **What:** Add `ConnectionCredentials` + `RemoteIdentity` to a new
`crates/alknet-core/src/credentials.rs` (~50 lines). Update `crates/alknet-core/src/credentials.rs` (~40 lines).
`alknet-core/src/lib.rs` to `pub mod credentials` + re-export. Update `ConnectionCredentials` is the transport-level credential bundle
`alknet-call/src/client/mod.rs` to re-export from core instead of (ADR-091) — it carries `local_identity` + `remote_identity` (the two
defining locally. No other changes. dimensions the dial consumes). `CallCredentials` stays in
`alknet-call` (it is the call-protocol bundle; its `auth_token` field
is a per-request call-protocol concept, not a transport credential).
Update `alknet-core/src/lib.rs` to `pub mod credentials` + re-export.
Update `alknet-call` to import `ConnectionCredentials` + `RemoteIdentity`
from core and re-export them; `CallCredentials` retains `auth_token` and
references the core types for the transport dimensions. No other
changes.
**Why first:** It's independent of the three new crates, purely **Why first:** It's independent of the three new crates, purely
additive (core gains types, nothing breaks), and means `alknet-client` additive (core gains types, nothing breaks), and means `alknet-client`
(Phase 3) never has a temporary dep on `alknet-call`. The dep graph is (Phase 3) never has a temporary dep on `alknet-call`. The dep graph is
clean from the start. clean from the start. `ConnectionCredentials` (not `CallCredentials`)
is what moves — the dial consumes transport-level dimensions, not
call-protocol dimensions (ADR-091).
**Compilable state:** `cargo test` passes across the workspace. **Compilable state:** `cargo test` passes across the workspace.
`alknet-call` re-exports the types from core; its own code + tests `alknet-call` imports the types from core; its own code + tests
import via `alknet_call::client::CallCredentials` (unchanged — the continue to work via the re-export.
re-export preserves the path).
**Done when:** `cargo test` passes, `CallCredentials` + **Done when:** `cargo test` passes, `ConnectionCredentials` +
`RemoteIdentity` are defined in `alknet-core`, `alknet-call` re-exports `RemoteIdentity` are defined in `alknet-core`, `alknet-call` imports
them. them from core, `CallCredentials` stays in `alknet-call`.
### Phase 1: Create `alknet-tls` (greenfield, additive) ### Phase 1: Create `alknet-tls` (greenfield, additive)
@@ -126,6 +134,12 @@ helpers from `alknet-call/client/call_client.rs` (lines 189-320).
**Deps:** `alknet-core` (TlsIdentity, Ed25519SecretKey, fingerprint), **Deps:** `alknet-core` (TlsIdentity, Ed25519SecretKey, fingerprint),
`rustls`, `rustls-pemfile`, `rustls-native-certs`, `webpki-roots`, `rustls`, `rustls-pemfile`, `rustls-native-certs`, `webpki-roots`,
`rcgen`, `tokio`, optional `quinn`/`tokio-rustls`/`rustls-acme`. `rcgen`, `tokio`, optional `quinn`/`tokio-rustls`/`rustls-acme`.
`rustls-native-certs` and `webpki-roots` are **always-present (not
feature-gated)** — the unknown-X.509-remote CA-verification path in
`TlsClientConfig::new` is transport-agnostic; the `webpki-roots`
fallback merges built-in roots when the platform store is empty so
`NoRootAnchors` is unreachable in practice (ADR-088 §5). Do not gate
them under `quinn`/`tcp`.
**Compilable state:** `alknet-tls` builds and tests standalone. Core and **Compilable state:** `alknet-tls` builds and tests standalone. Core and
call are unchanged — the old code still exists (duplicated). No call are unchanged — the old code still exists (duplicated). No
@@ -209,15 +223,16 @@ self-contained, no other crate changed.
**What:** New crate `crates/alknet-client/`. The `AlknetClient` dial **What:** New crate `crates/alknet-client/`. The `AlknetClient` dial
seam — three dial methods (`dial_quic`/`dial_tcp_tls`/`dial_iroh`), seam — three dial methods (`dial_quic`/`dial_tcp_tls`/`dial_iroh`),
consuming `TlsClientConfig` from `alknet-tls` + `CallCredentials` from all unified on `&ConnectionCredentials` (ADR-091), consuming
`TlsClientConfig` from `alknet-tls` + `ConnectionCredentials` from
`alknet-core`. The SOCKS5 proxy path (ADR-090) is feature-gated. `alknet-core`. The SOCKS5 proxy path (ADR-090) is feature-gated.
**Types:** `AlknetClient`, `ClientDialError`, `Socks5ProxyConfig`, **Types:** `AlknetClient`, `ClientDialError`, `Socks5ProxyConfig`,
`Socks5Credentials` (behind `socks5` feature). `Socks5Credentials` (behind `socks5` feature).
**Deps:** `alknet-core` (Connection, CallCredentials, RemoteIdentity, **Deps:** `alknet-core` (Connection, ConnectionCredentials,
Ed25519SecretKey), `alknet-tls` (TlsClientConfig), optional RemoteIdentity, Ed25519SecretKey), `alknet-tls` (TlsClientConfig),
`quinn`/`tokio-rustls`/`iroh`/`fast-socks5`. optional `quinn`/`tokio-rustls`/`iroh`/`fast-socks5`.
**Feature gates:** `quinn = ["dep:quinn", "alknet-tls/quinn", **Feature gates:** `quinn = ["dep:quinn", "alknet-tls/quinn",
"alknet-core/quinn"]`, `tcp = ["dep:tokio-rustls", "alknet-tls/tcp"]`, "alknet-core/quinn"]`, `tcp = ["dep:tokio-rustls", "alknet-tls/tcp"]`,
@@ -238,8 +253,9 @@ self-contained, no other crate changed.
from `lib.rs`. Remove the heavy deps (`quinn`, `iroh`, `rcgen`, from `lib.rs`. Remove the heavy deps (`quinn`, `iroh`, `rcgen`,
`rustls-pemfile`, `rustls-acme`) from `Cargo.toml` — but keep the `rustls-pemfile`, `rustls-acme`) from `Cargo.toml` — but keep the
`quinn`/`iroh` *features* (they gate `Connection::from_quinn`/ `quinn`/`iroh` *features* (they gate `Connection::from_quinn`/
`from_iroh` in `types.rs`). Add `CallCredentials` + `RemoteIdentity` `from_iroh` in `types.rs`). Add `ConnectionCredentials` +
to core (from `alknet-call`). `RemoteIdentity` to core (from `alknet-call` — done in Phase 0;
`CallCredentials` stays in `alknet-call` per ADR-091).
**The `lib.rs` change:** **The `lib.rs` change:**
```rust ```rust
@@ -252,7 +268,7 @@ pub mod config;
// After: // After:
pub mod auth; pub mod auth;
pub mod config; pub mod config;
pub mod credentials; // ← new (CallCredentials, RemoteIdentity) pub mod credentials; // ← new (ConnectionCredentials, RemoteIdentity)
// ... rest unchanged // ... rest unchanged
``` ```
@@ -284,19 +300,24 @@ lightweight (~3200 LOC, no heavy transport deps).
### Phase 5: Prune `alknet-call` (subtractive, breakage confined) ### Phase 5: Prune `alknet-call` (subtractive, breakage confined)
**What:** Delete `connect()` + all TLS helpers + `ClientError` from **What:** Delete `connect()` + all TLS helpers + `ClientError` from
`call_client.rs`. Move `CallCredentials`/`RemoteIdentity` to core `call_client.rs`. The transport dimensions (`ConnectionCredentials`/
(already done in Phase 4 — here we just remove the old definitions + `RemoteIdentity`) already moved to core in Phase 0; here we remove the
update imports). Update `Cargo.toml` to drop `quinn`/`rustls`/ old definitions from `call_client.rs` and update imports.
`rustls-native-certs`/`rustls-pemfile`. Rewrite the tests that used `CallCredentials` stays in `alknet-call` (retaining `auth_token`,
`connect` to use `spawn_dispatch` directly (with `Connection::from_stream` referencing the core types — ADR-091). Update `Cargo.toml` to drop
mocks) or `AlknetClient::dial_quic` + `spawn_dispatch`. `quinn`/`rustls`/`rustls-native-certs`/`rustls-pemfile`. Rewrite the
tests that used `connect` to use `spawn_dispatch` directly (with
`Connection::from_stream` mocks) or `AlknetClient::dial_quic` +
`spawn_dispatch`.
**The `call_client.rs` after prune:** **The `call_client.rs` after prune:**
- `CallClient` struct + `new` + `registry` + `identity_provider` + - `CallClient` struct + `new` + `registry` + `identity_provider` +
`spawn_dispatch` (~85 lines — unchanged) `spawn_dispatch` (~85 lines — unchanged)
- `CallConnection` + `Dispatcher` wiring (stays — protocol) - `CallConnection` + `Dispatcher` wiring (stays — protocol)
- `RemoteIdentity`/`CallCredentials` — removed (now in `alknet-core`, - `RemoteIdentity`/`ConnectionCredentials` — removed from
re-imported from there) `call_client.rs` (now in `alknet-core`, re-imported from there).
`CallCredentials` stays in `alknet-call` (retaining `auth_token` —
ADR-091).
- `ClientError` — removed - `ClientError` — removed
- `connect` + all `build_*`/`select_*`/`load_*`/`Ed25519SigningKey`/ - `connect` + all `build_*`/`select_*`/`load_*`/`Ed25519SigningKey`/
`RawKeyClientCertResolver`/`NoClientCertResolver`/ `RawKeyClientCertResolver`/`NoClientCertResolver`/
@@ -304,8 +325,11 @@ mocks) or `AlknetClient::dial_quic` + `spawn_dispatch`.
**The `Cargo.toml` after prune:** `quinn`, `rustls`, **The `Cargo.toml` after prune:** `quinn`, `rustls`,
`rustls-native-certs`, `rustls-pemfile` all leave. The `quinn` feature `rustls-native-certs`, `rustls-pemfile` all leave. The `quinn` feature
either disappears or becomes a no-op (it only gated `connect` + the is **removed** (it only gated `connect` + the TLS helpers, both
TLS helpers, both removed). `alknet-call` becomes a pure protocol crate. removed — keeping it as a no-op would mislead a user enabling
`quinn` on `alknet-call` expecting QUIC support; removing it surfaces
any stray `#[cfg(feature = "quinn")]` the prune missed). `alknet-call`
becomes a pure protocol crate.
**Test impact (per the test audit below):** the lib tests in **Test impact (per the test audit below):** the lib tests in
`call_client.rs` (16 tests) split into 6 that stay unchanged `call_client.rs` (16 tests) split into 6 that stay unchanged
@@ -367,11 +391,11 @@ wrapper is gone.
| After phase | State | | After phase | State |
|-------------|-------| |-------------|-------|
| 0 (credentials) | `CallCredentials`/`RemoteIdentity` in core; call re-exports; no breakage | | 0 (credentials) | `ConnectionCredentials`/`RemoteIdentity` in core; call imports from core; `CallCredentials` stays in call; no breakage |
| 1 (tls) | `alknet-tls` builds standalone; core/call/http unchanged (old code duplicated) | | 1 (tls) | `alknet-tls` builds standalone; core/call/http unchanged (old code duplicated) |
| 2 (endpoint) | `alknet-endpoint` builds standalone; core still has old `endpoint.rs` (duplicate) | | 2 (endpoint) | `alknet-endpoint` builds standalone; core still has old `endpoint.rs` (duplicate) |
| 3 (client) | `alknet-client` builds standalone; call still has old `connect` (duplicate) | | 3 (client) | `alknet-client` builds standalone; call still has old `connect` (duplicate) |
| 4 (core prune) | core is lightweight; `endpoint.rs` gone; `CallCredentials` in core | | 4 (core prune) | core is lightweight; `endpoint.rs` gone; `ConnectionCredentials` in core |
| 5 (call prune) | call is pure protocol; `connect` + TLS helpers gone; Category B tests already moved | | 5 (call prune) | call is pure protocol; `connect` + TLS helpers gone; Category B tests already moved |
| 6 (http fix) | http has no `QuicStream` wrapper; clean `accept_bi` path | | 6 (http fix) | http has no `QuicStream` wrapper; clean `accept_bi` path |
@@ -386,9 +410,9 @@ tests — the TLS tests moved in Phase 1, the protocol tests use
The ordering is **deps before dependents, additive before subtractive**: The ordering is **deps before dependents, additive before subtractive**:
- **Phase 0** (`CallCredentials` to core) first because it's - **Phase 0** (`ConnectionCredentials` to core) first because it's
independent, additive, and makes `alknet-client` (Phase 3) never independent, additive, and makes `alknet-client` (Phase 3) never
depend on `alknet-call`. ~50 lines moved, zero breakage. depend on `alknet-call`. ~40 lines moved, zero breakage.
- `alknet-tls` (Phase 1) because both `alknet-endpoint` (indirectly — - `alknet-tls` (Phase 1) because both `alknet-endpoint` (indirectly —
the assembly layer builds `TlsServerConfig`) and `alknet-client` the assembly layer builds `TlsServerConfig`) and `alknet-client`
(directly — `TlsClientConfig`) depend on it. It has no dep on the (directly — `TlsClientConfig`) depend on it. It has no dep on the
@@ -399,8 +423,8 @@ The ordering is **deps before dependents, additive before subtractive**:
tls first means the assembly layer's transport-building code has a tls first means the assembly layer's transport-building code has a
home from the start. home from the start.
- `alknet-client` (Phase 3) depends on `alknet-tls` - `alknet-client` (Phase 3) depends on `alknet-tls`
(`TlsClientConfig`) + `alknet-core` (`CallCredentials` — moved in (`TlsClientConfig`) + `alknet-core` (`ConnectionCredentials` — moved
Phase 0, so the dep is clean from the start). in Phase 0, so the dep is clean from the start).
- Phases 4-5 (the prunes) go last because they're subtractive. The - Phases 4-5 (the prunes) go last because they're subtractive. The
new crates (0-3) must exist first so the pruned code's new crates (0-3) must exist first so the pruned code's
functionality has a home. functionality has a home.
@@ -409,34 +433,37 @@ The ordering is **deps before dependents, additive before subtractive**:
after ADR-065 landed (which it did). Putting it last keeps the after ADR-065 landed (which it did). Putting it last keeps the
extraction phases clean. extraction phases clean.
- Phases 4-5 (the prunes) go last because they're subtractive. The
new crates (1-3) must exist first so the pruned code's
functionality has a home.
- Phase 6 (http fix) goes last because it's independent of the
extraction — it's a residual fix that could happen at any point
after ADR-065 landed (which it did). Putting it last keeps the
extraction phases clean.
## Resolved decisions ## Resolved decisions
### `CallCredentials`/`RemoteIdentity` move — Phase 0 (before Phase 1) ### `ConnectionCredentials`/`RemoteIdentity` move — Phase 0 (before Phase 1)
**Decision:** Move `CallCredentials`/`RemoteIdentity` to `alknet-core` **Decision:** Move `ConnectionCredentials`/`RemoteIdentity` to
as a standalone additive step *before any new crate is created*. It's `alknet-core` as a standalone additive step *before any new crate is
independent of everything else, purely additive (core gains two small created* (ADR-091). It's independent of everything else, purely
types, nothing breaks), and `alknet-call` re-exports them from core additive (core gains two small types, nothing breaks), and
so its own code + tests don't change yet. This means `alknet-client` `alknet-call` imports them from core so its own code + tests don't
(Phase 3) never depends on `alknet-call` — the dep graph is clean from change yet. This means `alknet-client` (Phase 3) never depends on
the start, no temporary dep to clean up later. `alknet-call` — the dep graph is clean from the start, no temporary dep
to clean up later.
The move is ~50 lines (struct definitions + builder impls) into a new `ConnectionCredentials` (not `CallCredentials`) is what moves — it is
the transport-level credential bundle (`local_identity` +
`remote_identity`), carrying only the dimensions the dial consumes.
`CallCredentials` stays in `alknet-call` because its `auth_token` field
is a call-protocol / hub-layer concept (bearer-token identity
correlation for browsers and `alknet/register`), not a transport
credential. See ADR-091 for the full rationale.
The move is ~40 lines (struct definitions + builder impls) into a new
`crates/alknet-core/src/credentials.rs` (or `auth.rs` — `auth.rs` `crates/alknet-core/src/credentials.rs` (or `auth.rs` — `auth.rs`
already holds `AuthToken`, so `credentials.rs` is cleaner to keep the already holds `AuthToken`, so `credentials.rs` is cleaner to keep the
auth module from growing). `alknet-call`'s `client/mod.rs` changes auth module from growing). `alknet-call`'s `client/mod.rs` imports
`pub use call_client::{CallCredentials, RemoteIdentity}` to `ConnectionCredentials` + `RemoteIdentity` from core and re-exports
`pub use alknet_core::{CallCredentials, RemoteIdentity}` (re-export). them; `CallCredentials` stays defined in `alknet-call` (retaining
No test changes — the tests import `CallCredentials` from `auth_token`, referencing the core types for the transport dimensions).
`alknet_call::client`, which still works via the re-export. Test changes are minimal — the Category A tests that reference
`CallCredentials` directly may need import updates depending on how
`CallCredentials` is restructured.
### Phase 5 test audit — `call_client.rs` (16 tests) ### Phase 5 test audit — `call_client.rs` (16 tests)
@@ -460,13 +487,17 @@ transport-agnostic. These survive the prune unchanged.
| `call_client_is_send_sync` | 705 | trait bounds | | `call_client_is_send_sync` | 705 | trait bounds |
| `remote_identity_none_is_load_bearing_not_defaulted` | 921 | `CallCredentials::new()` | | `remote_identity_none_is_load_bearing_not_defaulted` | 921 | `CallCredentials::new()` |
**Category B — TLS/verifier tests, move to `alknet-tls` (8 tests):** **Category B — TLS/verifier tests, move to `alknet-tls` (10 tests):**
These test `FingerprintPinVerifier`, `build_client_auth`, These test `FingerprintPinVerifier`, `build_client_auth`,
`select_server_verifier`, and `build_quinn_client_config` directly. `select_server_verifier`, and `build_quinn_client_config` directly.
They're `#[cfg(feature = "quinn")]`-gated and test the TLS helpers, They're `#[cfg(feature = "quinn")]`-gated and test the TLS helpers,
not the call protocol. They move to `alknet-tls` in Phase 1 (adapted not the call protocol. They move to `alknet-tls` in Phase 1 (adapted
to test `TlsClientConfig::new` instead of the free functions). to test `TlsClientConfig::new` instead of the free functions). The
two `build_quinn_client_config` tests test the full config build
(verifier + client-auth + provider wired together) and are adapted
to test `TlsClientConfig::new` + `for_quinn()` instead of the free
function.
| Test | Line | What it tests | Move target | | Test | Line | What it tests | Move target |
|------|------|---------------|-------------| |------|------|---------------|-------------|
@@ -481,11 +512,6 @@ to test `TlsClientConfig::new` instead of the free functions).
| `build_quinn_client_config_with_raw_key_identity_builds_without_error` | 893 | full config build | `alknet-tls` | | `build_quinn_client_config_with_raw_key_identity_builds_without_error` | 893 | full config build | `alknet-tls` |
| `build_quinn_client_config_with_no_remote_identity_builds_without_error` | 909 | CA-verify config | `alknet-tls` | | `build_quinn_client_config_with_no_remote_identity_builds_without_error` | 909 | CA-verify config | `alknet-tls` |
Wait — that's 10, not 8. The two `build_quinn_client_config` tests
test the full config build (verifier + client-auth + provider wired
together). They move to `alknet-tls` and are adapted to test
`TlsClientConfig::new` + `for_quinn()` instead of the free function.
**Category C — `connect` integration test, remove (0 tests):** **Category C — `connect` integration test, remove (0 tests):**
No test in `call_client.rs` actually calls `connect()`. The tests that No test in `call_client.rs` actually calls `connect()`. The tests that
@@ -507,11 +533,13 @@ The one reference to `connect()` is in a doc comment (line 76:
**Net Phase 5 test impact:** 6 tests stay unchanged (Category A), 10 **Net Phase 5 test impact:** 6 tests stay unchanged (Category A), 10
tests move to `alknet-tls` in Phase 1 (Category B), 0 tests need tests move to `alknet-tls` in Phase 1 (Category B), 0 tests need
rewriting. The `from_call.rs` tests (27) stay unchanged. The prune rewriting. The `from_call.rs` tests (27) stay unchanged. The prune
of `call_client.rs` is mechanical: delete lines 90-640 (the error of `call_client.rs` is mechanical: delete `ClientError`, `connect`,
enum + `connect` + TLS helpers), keep lines 102-187 (`CallClient` + and all the TLS helpers (`build_*`, `select_*`, `load_*`,
`spawn_dispatch`), update imports. The tests (lines 640-930) keep the `Ed25519SigningKey`, `RawKeyClientCertResolver`, `NoClientCertResolver`,
Category A tests, remove the Category B tests (moved in Phase 1), and `FingerprintPinVerifier`); keep `CallClient` + `new` + `spawn_dispatch`
update the one doc comment. unchanged; update imports. The test suite keeps the Category A tests,
removes the Category B tests (moved in Phase 1), and updates the one
doc comment.
This is much simpler than the initial estimate of "~290 lines of test This is much simpler than the initial estimate of "~290 lines of test
restructuring." The actual test work is: move 10 tests to `alknet-tls` restructuring." The actual test work is: move 10 tests to `alknet-tls`