Files
alknet/docs/research/alknet-crate-extraction/findings.md
T
glm-5.2 ec46fc6d59 docs(research): fix ConnectionCredentials/CallCredentials wording in Phase 5 prune + source map
The Phase 5 'call_client.rs after prune' list inaccurately described
ConnectionCredentials as 'removed from call_client.rs' — it is a new
type in alknet-core, not a pre-existing type in call. Reworded to:
RemoteIdentity is removed (moved to core in Phase 0); CallCredentials
is restructured (transport dimensions leave for ConnectionCredentials
in core, auth_token stays). The source map table row for lines 40-88
is also corrected: the destination is a split, not a uniform move to
alknet-core.
2026-07-17 06:25:04 +00:00

571 lines
29 KiB
Markdown

# alknet-crate-extraction — Migration from the welded core to the extracted crates
**Status:** Findings in progress — mapping the existing code to the
target shape, phase by phase, so the migration can be ordered to keep
the tree compilable at each step.
**Date:** 2026-07-16
**Scope:** The three new crates (`alknet-tls`, `alknet-endpoint`,
`alknet-client`) + the prune of `alknet-core` and `alknet-call` + the
`alknet-http` residual fix. The specs are confirmed tight (reviewed +
amended); this doc is the *how* — what code moves where, in what order,
with what intermediate states.
---
## TL;DR
The migration is **additive-then-subtractive**: build all three new
crates first (no breakage), then prune the old code from core and call
(breakage confined to a single phase), then fix the http residual. Seven
phases (0-6), each leaving the workspace compilable. The heaviest single file
(`endpoint.rs`, 1606 lines) is not a monolith — it's three concerns
welded together, each going to a different destination.
---
## What exists now (the source map)
### `crates/alknet-core/src/endpoint.rs` — 1606 lines, three concerns
The file is not 1600 lines of one thing. It's three concerns that the
extraction separates:
| Lines | Concern | Destination | LOC |
|-------|---------|-------------|-----|
| 1-492 | `AlknetEndpoint` struct, `HandlerRegistry`, `dispatch_quinn`/`dispatch_iroh`, accept loops, `build_auth_context`, `has_iroh_identity` | `alknet-endpoint` | ~492 |
| 493-934 | `TlsSetup`, `build_rustls_server_config`, `build_quinn_server_config_from_rustls`, `RawKeyCertResolver`, `Ed25519SigningKey`, `AcceptAnyCertVerifier`, `SelfSignedCert`, `generate_self_signed_cert`, `load_cert_chain`, `load_private_key`, `build_iroh_endpoint` | `alknet-tls` (TLS setup) / assembly layer (iroh builder) | ~442 |
| 935-1606 | 42 test functions | split by subject | ~671 |
The endpoint struct + dispatch + accept loops (the part that goes to
`alknet-endpoint`) is ~492 lines of implementation — not 1600. The TLS
setup code (~442 lines) goes to `alknet-tls`. The tests (~671 lines)
split by what they test. The file *feels* monolithic because the
`#[cfg(feature = "quinn")]` gates weave the three concerns together,
but the extraction unwinds that.
### `crates/alknet-call/src/client/call_client.rs` — 930 lines
| Lines | Concern | Destination | LOC |
|-------|---------|-------------|-----|
| 40-88 | `RemoteIdentity`, `CallCredentials` (struct + builder) | split: `RemoteIdentity` + new `ConnectionCredentials` → `alknet-core` (`credentials.rs`); `CallCredentials` restructured (transport dimensions leave, `auth_token` stays) → stays in `alknet-call` (ADR-091) | ~48 |
| 90-100 | `ClientError` enum | **removed** (only produced by `connect`) | ~10 |
| 102-187 | `CallClient` struct + `new` + `spawn_dispatch` | **stays** (the pure protocol take-over) | ~85 |
| 189-320 | `build_quinn_client_config`, `build_client_auth`, `select_server_verifier`, `load_platform_root_cert_store`, `load_cert_chain`, `load_private_key`, `Ed25519SigningKey`, `RawKeyClientCertResolver`, `NoClientCertResolver`, `FingerprintPinVerifier` | `alknet-tls` | ~130 |
| 321-640 | `CallConnection`, `Dispatcher` wiring, wire-protocol helpers | **stays** (protocol) | ~320 |
| 640-930 | Tests (use `connect`, `CallCredentials`, TLS helpers) | rewrite to use `spawn_dispatch` directly or `AlknetClient` | ~290 |
The call crate's prune is ~140 lines of implementation + ~290 lines of
tests that need rewriting. What remains is the pure protocol: `CallClient`
+ `CallConnection` + `Dispatcher` + the wire protocol.
### `crates/alknet-http/src/server/adapter.rs` — the residual
The `HttpAdapter::handle` method does `connection.accept_bi()` → wraps
the send/recv pair in a hand-rolled `QuicStream` (lines 271-300, an
`AsyncRead+AsyncWrite` adapter) → feeds it to `serve_io()`. But
`serve_io` already accepts *any* `AsyncRead+AsyncWrite` (line 244). The
`QuicStream` wrapper is a hand-rolled version of what
`Connection::from_bidi`/`from_stream` (ADR-065, already landed) provides
natively.
The residual: `handle` assumes a quinn-style multi-stream connection
(`accept_bi` returns a fresh bidi stream). For a `from_bidi` connection
(TCP+TLS), `accept_bi` yields the single bidi stream once (ADR-070's
yield-once contract). The `QuicStream` wrapper works but is unnecessary
— the streams from `accept_bi` are already `AsyncRead+AsyncWrite`. The
fix is to drop `QuicStream` and use the streams directly (or via
`TokioIo::new`). This is a small change (~30 lines removed) but needs
verification against ADR-070's `accept_bi` semantics for `from_bidi`.
The `QuicStreamDuplex` test helper (lines 456-471) is the test-side
equivalent — it can be replaced with `Connection::from_stream` test
helpers (the `MockConnection`/`ConnectionKind::Mock` variants were
already removed per ADR-065; tests use `from_stream` with
`tokio::io::sink`/`empty`).
---
## The seven phases
### Phase 0: Move `ConnectionCredentials`/`RemoteIdentity` to `alknet-core` (additive, no breakage)
**What:** Add `ConnectionCredentials` + `RemoteIdentity` to a new
`crates/alknet-core/src/credentials.rs` (~40 lines).
`ConnectionCredentials` is the transport-level credential bundle
(ADR-091) — it carries `local_identity` + `remote_identity` (the two
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
additive (core gains types, nothing breaks), and means `alknet-client`
(Phase 3) never has a temporary dep on `alknet-call`. The dep graph is
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.
`alknet-call` imports the types from core; its own code + tests
continue to work via the re-export.
**Done when:** `cargo test` passes, `ConnectionCredentials` +
`RemoteIdentity` are defined in `alknet-core`, `alknet-call` imports
them from core, `CallCredentials` stays in `alknet-call`.
### Phase 1: Create `alknet-tls` (greenfield, additive)
**What:** New crate `crates/alknet-tls/`. Extract the TLS setup code
from `alknet-core/endpoint.rs` (lines 493-934) + the client-side TLS
helpers from `alknet-call/client/call_client.rs` (lines 189-320).
**Types:** `TlsServerConfig`, `TlsClientConfig`, `TlsError`,
`FingerprintPinVerifier`, `RawKeyCertResolver`,
`RawKeyClientCertResolver`, `NoClientCertResolver`, `Ed25519SigningKey`
(consolidated — one copy, used by both server + client),
`AcceptAnyCertVerifier`, `SelfSignedCert`, `generate_self_signed_cert`,
`load_cert_chain`, `load_private_key`, `load_platform_root_cert_store`
(+ the `webpki-roots` fallback — new, not extracted).
**Deps:** `alknet-core` (TlsIdentity, Ed25519SecretKey, fingerprint),
`rustls`, `rustls-pemfile`, `rustls-native-certs`, `webpki-roots`,
`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
call are unchanged — the old code still exists (duplicated). No
breakage. The new crate's tests are the moved tests from
`endpoint.rs` (the TLS-setup tests) + the moved tests from
`call_client.rs` (the verifier/client-config tests).
**Tests that move here (from `endpoint.rs`):**
- `raw_key_cert_resolver_only_raw_public_keys`
- `self_signed_cert_generation_produces_cert_and_key`
- `acme_directory_production_url` / `staging_url` / `custom_url`
- `tls_setup_x509_returns_no_acme_state`
- `build_rustls_server_config_raw_key_succeeds`
- `build_rustls_server_config_self_signed_succeeds`
- `build_quinn_server_config_from_rustls_succeeds`
- `load_private_key_returns_error_when_no_key_present` / `_file_missing`
- `load_cert_chain_returns_error_when_file_missing`
- `accept_any_cert_verifier_*` (4 tests)
- `ed25519_signing_key_*` (6 tests)
- `raw_key_cert_resolver_debug_is_implemented`
**Tests that move here (from `call_client.rs`):** the
`FingerprintPinVerifier` + `build_quinn_client_config` tests (need
adaptation — they currently test via `connect`, should test via
`TlsClientConfig::new` directly).
**Done when:** `cargo test -p alknet-tls` passes, the crate is
self-contained, no other crate changed.
### Phase 2: Create `alknet-endpoint` (greenfield, additive)
**What:** New crate `crates/alknet-endpoint/`. Extract the endpoint
struct + dispatch + accept loops from `alknet-core/endpoint.rs` (lines
1-492), built fresh against the ADR-083 shape (`new(handlers, dynamic,
identity_provider, drain_timeout)` + `with_quinn`/`with_iroh`/`with_tcp_tls`
+ public `dispatch` + `run`/`shutdown`). No `EndpointError` (removed per
the spec review). `shutdown()` is infallible.
**Types:** `AlknetEndpoint`, `HandlerRegistry`, `TcpTlsListener`,
private `dispatch_quinn`/`dispatch_iroh`/`dispatch_tcp_tls`,
`build_auth_context`, the `extract_*` helpers.
**Deps:** `alknet-core` (Connection, ProtocolHandler, AuthContext,
IdentityProvider, DynamicConfig), optional `quinn`/`iroh`/`tokio-rustls`,
`tokio`, `arc-swap`, `tracing`.
**Module breakdown (the 492 lines split):**
- `registry.rs` — `HandlerRegistry` (~40 lines + tests)
- `endpoint.rs` — `AlknetEndpoint` struct, `new`, builder methods,
`run`, `shutdown` (~120 lines)
- `dispatch.rs` — `dispatch` (public), `build_auth_context`, the
`acme-tls/1` guard (~80 lines)
- `accept/quinn.rs` — `dispatch_quinn`, `run_quinn_accept_loop`,
`extract_quinn_alpn`, `extract_quinn_client_fingerprint` (~80 lines)
- `accept/iroh.rs` — `dispatch_iroh`, `run_iroh_accept_loop`,
`extract_iroh_client_fingerprint` (~50 lines)
- `accept/tcp_tls.rs` — `dispatch_tcp_tls`, `run_tcp_tls_accept_loop`,
`extract_tcp_tls_alpn`, `extract_tcp_tls_client_fingerprint` (new,
~60 lines — not in the current code; written fresh per ADR-083)
This is a fresh build, not a move — the old `endpoint.rs` stays in core
until Phase 4. The new crate is written against the target shape, not
the old shape.
**Compilable state:** `alknet-endpoint` builds and tests standalone.
Core's `endpoint.rs` still exists (duplicate). No breakage.
**Tests that move here (from `endpoint.rs`):**
- `handler_registry_*` (5 tests)
- `build_auth_context_*` (3 tests)
- `dispatch_decision_logic_lookup_and_auth`
- `has_iroh_identity_*` (3 tests)
- `endpoint_constructs_with_iroh_raw_key_identity`
- `iroh_endpoint_runs_accept_loop_and_shutdown`
- `debug_for_alknet_endpoint_is_implemented_without_panicking`
**Done when:** `cargo test -p alknet-endpoint` passes, the crate is
self-contained, no other crate changed.
### Phase 3: Create `alknet-client` (greenfield, additive)
**What:** New crate `crates/alknet-client/`. The `AlknetClient` dial
seam — three dial methods (`dial_quic`/`dial_tcp_tls`/`dial_iroh`),
all unified on `&ConnectionCredentials` (ADR-091), consuming
`TlsClientConfig` from `alknet-tls` + `ConnectionCredentials` from
`alknet-core`. The SOCKS5 proxy path (ADR-090) is feature-gated.
**Types:** `AlknetClient`, `ClientDialError`, `Socks5ProxyConfig`,
`Socks5Credentials` (behind `socks5` feature).
**Deps:** `alknet-core` (Connection, ConnectionCredentials,
RemoteIdentity, Ed25519SecretKey), `alknet-tls` (TlsClientConfig),
optional `quinn`/`tokio-rustls`/`iroh`/`fast-socks5`.
**Feature gates:** `quinn = ["dep:quinn", "alknet-tls/quinn",
"alknet-core/quinn"]`, `tcp = ["dep:tokio-rustls", "alknet-tls/tcp"]`,
`iroh = ["dep:iroh", "alknet-core/iroh"]`, `socks5 = ["dep:fast-socks5"]`.
**Compilable state:** `alknet-client` builds and tests standalone. No
breakage — the old `CallClient::connect` still exists in
`alknet-call` (duplicate dial). The new crate's tests use
`AlknetClient::dial_*` + `spawn_dispatch`/`from_connection` or test
the dial in isolation with mock transports.
**Done when:** `cargo test -p alknet-client` passes, the crate is
self-contained, no other crate changed.
### Phase 4: Prune `alknet-core` (subtractive, breakage confined)
**What:** Delete `endpoint.rs` from core. Remove `pub mod endpoint`
from `lib.rs`. Remove the heavy deps (`quinn`, `iroh`, `rcgen`,
`rustls-pemfile`, `rustls-acme`) from `Cargo.toml` — but keep the
`quinn`/`iroh` *features* (they gate `Connection::from_quinn`/
`from_iroh` in `types.rs`). Add `ConnectionCredentials` +
`RemoteIdentity` to core (from `alknet-call` — done in Phase 0;
`CallCredentials` stays in `alknet-call` per ADR-091).
**The `lib.rs` change:**
```rust
// Before:
pub mod endpoint; // ← removed
pub mod auth;
pub mod config;
// ... rest unchanged
// After:
pub mod auth;
pub mod config;
pub mod credentials; // ← new (ConnectionCredentials, RemoteIdentity)
// ... rest unchanged
```
**The `Cargo.toml` change:** `quinn`/`iroh`/`rcgen`/`rustls-pemfile`/
`rustls-acme` leave `[dependencies]`. The `quinn`/`iroh` features stay
(gating `types.rs` constructors). The `acme` feature is removed
(vestigial — the ACME state machine is on `TlsServerConfig` in
`alknet-tls` now).
**The `fingerprint.rs` doc comment** (line 13) references
`alknet_core::endpoint` — update to reference `alknet-endpoint` /
`alknet-tls`.
**Breakage:** anything that imported `alknet_core::endpoint` breaks.
But per the source map, *nothing does* — no handler crate or call
imports the endpoint module. The only consumer is the future assembly
layer (hub/worker), which doesn't exist yet. So the prune is clean:
delete the file, update `lib.rs`, update `Cargo.toml`, fix the
`fingerprint.rs` comment. Core's own tests for `endpoint.rs` are gone
(moved to `alknet-tls` in Phase 1 and `alknet-endpoint` in Phase 2).
**Compilable state:** `cargo test -p alknet-core` passes (minus the
endpoint tests, which moved). The `quinn`/`iroh` features still work
(`Connection::from_quinn`/`from_iroh` in `types.rs`).
**Done when:** `cargo test -p alknet-core` passes, the crate is
lightweight (~3200 LOC, no heavy transport deps).
### Phase 5: Prune `alknet-call` (subtractive, breakage confined)
**What:** Delete `connect()` + all TLS helpers + `ClientError` from
`call_client.rs`. The transport dimensions (`ConnectionCredentials`/
`RemoteIdentity`) already moved to core in Phase 0; here we remove the
old definitions from `call_client.rs` and update imports.
`CallCredentials` stays in `alknet-call` (retaining `auth_token`,
referencing the core types — ADR-091). Update `Cargo.toml` to drop
`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:**
- `CallClient` struct + `new` + `registry` + `identity_provider` +
`spawn_dispatch` (~85 lines — unchanged)
- `CallConnection` + `Dispatcher` wiring (stays — protocol)
- `RemoteIdentity` — removed from `call_client.rs` (moved to
`alknet-core` in Phase 0; re-imported from there)
- `CallCredentials` — restructured, stays in `alknet-call`: the
transport dimensions (`tls_identity`, `remote_identity`) leave for
`ConnectionCredentials` in `alknet-core` (Phase 0); the `auth_token`
field stays (it is a call-protocol concept, not a transport credential
— ADR-091). `CallCredentials` references the core types for the
transport dimensions or assembles from `ConnectionCredentials` +
`auth_token` at the take-over site.
- `ClientError` — removed
- `connect` + all `build_*`/`select_*`/`load_*`/`Ed25519SigningKey`/
`RawKeyClientCertResolver`/`NoClientCertResolver`/
`FingerprintPinVerifier` — removed (now in `alknet-tls`)
**The `Cargo.toml` after prune:** `quinn`, `rustls`,
`rustls-native-certs`, `rustls-pemfile` all leave. The `quinn` feature
is **removed** (it only gated `connect` + the TLS helpers, both
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
`call_client.rs` (16 tests) split into 6 that stay unchanged
(protocol-level, use `spawn_dispatch(stub_connection())`) and 10 that
move to `alknet-tls` in Phase 1 (TLS/verifier tests). **Zero lib tests
need rewriting** — no test in `call_client.rs` calls `connect()`. The
`from_call.rs` tests (27 tests) stay unchanged — they use
`CallConnection` directly. The one integration test file
(`tests/two_node_call.rs`, 2 tests) calls `connect()` twice — it moves
to `alknet-client/tests/` (Phase 3) or is updated to use
`AlknetClient::dial_quic` + `spawn_dispatch` (Phase 5).
The implementation prune is mechanical: delete the error enum, delete
`connect`, delete the TLS helpers (~140 lines). The test work is: the
10 TLS tests already moved in Phase 1, the 6 protocol tests stay, the
integration test is handled separately.
**Compilable state:** `cargo test -p alknet-call` passes with the
rewritten tests. The crate has no TLS/transport deps.
**Done when:** `cargo test -p alknet-call` passes, the crate is a pure
protocol crate.
### Phase 6: Fix `alknet-http` (small, additive)
**What:** Remove the `QuicStream` wrapper from `server/adapter.rs`.
The `HttpAdapter::handle` method does `connection.accept_bi()` →
wraps in `QuicStream` → feeds to `serve_io`. After the fix, it does
`connection.accept_bi()` → uses the streams directly (they're already
`AsyncRead+AsyncWrite`) → feeds to `serve_io` via `TokioIo::new`.
**The `QuicStream` struct** (lines 271-300) is deleted. The
`QuicStreamDuplex` test helper (lines 456-471) is replaced with
`Connection::from_stream` test helpers.
**Verification needed:** confirmed — `StreamBidiStreamSource::accept_bi`
(`types.rs:482`) yields the underlying `(SendStream, RecvStream)` pair
once (then `ConnectionClosed`). The `SendStream`/`RecvStream` are
`from_stream`-wrapped adapters (lines 267/289) — already
`AsyncRead`/`AsyncWrite`. So `accept_bi()` on a `from_bidi` connection
returns streams directly usable as `AsyncRead+AsyncWrite` — no
`QuicStream` wrapper needed. The `BidiStreamSource` trait has a unified
intent (`accept_bi`/`open_bi`/`remote_addr`/`close`); all three impls
(quinn, iroh, stream) collapse into adapters with the same shape. The
`QuicStream` wrapper is redundant — delete it, use the streams from
`accept_bi()` directly via `TokioIo::new`. The `QuicStreamDuplex` test
helper is replaced with `Connection::from_stream` test helpers (the
`stub_connection()` pattern already used in `call_client.rs` tests).
**Compilable state:** `cargo test -p alknet-http` passes. The crate no
longer has the hand-rolled `QuicStream` wrapper.
**Done when:** `cargo test -p alknet-http` passes, the `QuicStream`
wrapper is gone.
---
## Intermediate states (compilable after each phase)
| After phase | State |
|-------------|-------|
| 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) |
| 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) |
| 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 |
| 6 (http fix) | http has no `QuicStream` wrapper; clean `accept_bi` path |
Phases 0-3 are purely additive — no existing code breaks, no tests
break. Phases 4-5 are subtractive — the pruned code's callers don't
exist yet (no assembly layer), so the breakage is confined to the
crate's own tests (and per the test audit, the call prune breaks zero
tests — the TLS tests moved in Phase 1, the protocol tests use
`spawn_dispatch` directly). Phase 6 is a small fix.
## Ordering rationale
The ordering is **deps before dependents, additive before subtractive**:
- **Phase 0** (`ConnectionCredentials` to core) first because it's
independent, additive, and makes `alknet-client` (Phase 3) never
depend on `alknet-call`. ~40 lines moved, zero breakage.
- `alknet-tls` (Phase 1) because both `alknet-endpoint` (indirectly —
the assembly layer builds `TlsServerConfig`) and `alknet-client`
(directly — `TlsClientConfig`) depend on it. It has no dep on the
other new crates.
- `alknet-endpoint` (Phase 2) depends only on `alknet-core` (already
exists) — it doesn't need `alknet-tls` (the endpoint takes
pre-built transports). It could go before `alknet-tls`, but putting
tls first means the assembly layer's transport-building code has a
home from the start.
- `alknet-client` (Phase 3) depends on `alknet-tls`
(`TlsClientConfig`) + `alknet-core` (`ConnectionCredentials` — moved
in Phase 0, so the dep is clean from the start).
- Phases 4-5 (the prunes) go last because they're subtractive. The
new crates (0-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
### `ConnectionCredentials`/`RemoteIdentity` move — Phase 0 (before Phase 1)
**Decision:** Move `ConnectionCredentials`/`RemoteIdentity` to
`alknet-core` as a standalone additive step *before any new crate is
created* (ADR-091). It's independent of everything else, purely
additive (core gains two small types, nothing breaks), and
`alknet-call` imports them from core so its own code + tests don't
change yet. This means `alknet-client` (Phase 3) never depends on
`alknet-call` — the dep graph is clean from the start, no temporary dep
to clean up later.
`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`
already holds `AuthToken`, so `credentials.rs` is cleaner to keep the
auth module from growing). `alknet-call`'s `client/mod.rs` imports
`ConnectionCredentials` + `RemoteIdentity` from core and re-exports
them; `CallCredentials` stays defined in `alknet-call` (retaining
`auth_token`, referencing the core types for the transport dimensions).
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)
The 16 tests in `call_client.rs` split into three categories:
**Category A — protocol-level, stay in `alknet-call`, no rewrite
needed (6 tests):**
These tests use `spawn_dispatch(stub_connection())` or
`CallCredentials` directly. They don't touch `connect` or any TLS
helper. `stub_connection()` (line 582) uses
`Connection::from_stream(tokio::io::channel(...))` — already
transport-agnostic. These survive the prune unchanged.
| Test | Line | What it tests |
|------|------|---------------|
| `call_credentials_builder_methods` | 652 | `CallCredentials` builder |
| `external_op_dispatches_and_populates_capabilities` | 665 | dispatch + capabilities |
| `unknown_op_returns_not_found` | 679 | dispatch error path |
| `spawn_dispatch_returns_live_call_connection` | 691 | `spawn_dispatch` + ALPN |
| `call_client_is_send_sync` | 705 | trait bounds |
| `remote_identity_none_is_load_bearing_not_defaulted` | 921 | `CallCredentials::new()` |
**Category B — TLS/verifier tests, move to `alknet-tls` (10 tests):**
These test `FingerprintPinVerifier`, `build_client_auth`,
`select_server_verifier`, and `build_quinn_client_config` directly.
They're `#[cfg(feature = "quinn")]`-gated and test the TLS helpers,
not the call protocol. They move to `alknet-tls` in Phase 1 (adapted
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 |
|------|------|---------------|-------------|
| `fingerprint_pin_verifier_matches_correct_ed25519_fingerprint` | 750 | verifier accept | `alknet-tls` |
| `fingerprint_pin_verifier_rejects_wrong_ed25519_fingerprint` | 769 | verifier reject | `alknet-tls` |
| `fingerprint_pin_verifier_matches_correct_sha256_fingerprint` | 789 | verifier X.509 accept | `alknet-tls` |
| `fingerprint_pin_verifier_rejects_wrong_sha256_fingerprint` | 806 | verifier X.509 reject | `alknet-tls` |
| `select_server_verifier_returns_ca_verifier_for_none` | 822 | CA path | `alknet-tls` |
| `select_server_verifier_returns_fingerprint_pin_for_some` | 839 | pin path | `alknet-tls` |
| `build_client_auth_presents_ed25519_raw_key_without_error` | 857 | client cert resolver | `alknet-tls` |
| `build_client_auth_none_resolves_to_no_client_cert` | 879 | no-cert resolver | `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` |
**Category C — `connect` integration test, remove (0 tests):**
No test in `call_client.rs` actually calls `connect()`. The tests that
exercise the full QUIC dial path are in `from_call.rs` (which has 27
tests) and in the integration tests. `call_client.rs`'s tests are all
either protocol-level (Category A) or TLS-helper-level (Category B).
This means the `connect` removal doesn't break any test in
`call_client.rs` itself — the tests that need a real connection already
use `spawn_dispatch(stub_connection())`.
**The `from_call.rs` tests (27 tests):** these use `CallConnection`
directly (constructed from `stub_connection()` or a mock), not
`connect`. They're protocol-level and stay in `alknet-call` unchanged.
The one reference to `connect()` is in a doc comment (line 76:
"the assembly layer calls `from_call` immediately after `connect()`")
— update the comment to say "after `AlknetClient::dial_*` +
`spawn_dispatch`".
**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
rewriting. The `from_call.rs` tests (27) stay unchanged. The prune
of `call_client.rs` is mechanical: delete `ClientError`, `connect`,
and all the TLS helpers (`build_*`, `select_*`, `load_*`,
`Ed25519SigningKey`, `RawKeyClientCertResolver`, `NoClientCertResolver`,
`FingerprintPinVerifier`); keep `CallClient` + `new` + `spawn_dispatch`
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
restructuring." The actual test work is: move 10 tests to `alknet-tls`
in Phase 1 (adapted to the new API), keep 6 tests unchanged, update one
doc comment. The `connect` removal breaks zero tests because no test
calls `connect`.
## Resolved questions
- **Phase 6 `accept_bi` semantics:** **Resolved.**
`StreamBidiStreamSource::accept_bi` yields the `(SendStream,
RecvStream)` pair once; the streams are already
`AsyncRead`/`AsyncWrite` adapters. The `QuicStream` wrapper is
redundant — delete it. The `BidiStreamSource` trait has a unified
intent; all three impls (quinn, iroh, stream) collapse into adapters
with the same shape. See Phase 6 above.
- **Integration test home:** **Resolved.** The dial + take-over
composition test moves to `alknet-client/tests/` with a minimal echo
`ProtocolHandler` on a test ALPN — no `alknet-call` dependency, no
circular path. The call-protocol-specific tests stay in
`alknet-call` (rewritten to use `spawn_dispatch` + loopback
`Connection`). See Phase 5 / open questions above.
- **Workspace `Cargo.toml`:** the three new crates need to be added to
the workspace member list. Trivial but worth noting.