# alknet-endpoint-refactor — Endpoint as Pure ALPN Dispatcher **Status:** Findings complete. The endpoint refactor and the TLS extraction are interlocked; this document captures the full picture so neither gets done in isolation and drops half the context. **Date:** 2026-07-13 **Scope:** `AlknetEndpoint` becomes a pure accept-loop runner (no transport construction); `alknet-tls` extraction (`TlsServerConfig`) provides the shared cert config the assembly layer needs to build transports. Both changes are enabled by the pre-implementation window — nobody is using this code yet, and the issues surfaced from trying to write the first real consumers (hub, workers, HTTP). --- ## TL;DR 1. **`AlknetEndpoint` conflates two concerns:** transport construction (building `quinn::Endpoint`, `iroh::Endpoint`, wiring TLS) and dispatch (take a connection, extract ALPN, look up handler, build `AuthContext`, spawn). The TLS extraction forces a separation because the TLS config has to be built once and shared across transports — the endpoint can't build it internally anymore. 2. **The endpoint becomes a pure accept-loop runner with a public dispatch method.** It owns the transport endpoints it runs accept loops for (quinn, iroh), and it exposes `dispatch()` for transport accept loops it doesn't own (TCP+TLS, future SSH, future WebTransport). Every transport calls the same dispatch path — no duplicated `build_auth_context` or handler-lookup logic at the assembly layer. The `StaticConfig` parameter, `tls_identity` reading, and transport construction all move to the assembly layer. 3. **TCP+TLS is a first-class transport, not a sibling afterthought.** ADR-010 Amendment 1 made TCP+TLS a "sibling" because the endpoint's dispatch logic was crate-private and couldn't be shared. With a public `dispatch()` method, the TCP+TLS accept loop calls into the endpoint — same path as quinn and iroh. The hub needs TCP+TLS as a day-1 requirement (HTTPS for browsers, raw-key fallback for native clients when UDP is blocked); it is the most common transport after raw-key QUIC, not an edge case. 4. **A hub holds one or two `TlsServerConfig`s, not one.** A hub serving both browsers and native workers holds a raw-key config (QUIC + TCP+TLS fallback for native clients) and an X.509/ACME config (TCP+TLS for HTTPS, QUIC for WebTransport when it revives). The cert-reuse value is *within* each identity: one config per identity, shared across that identity's transports. Iroh and SSH share the *key* with the raw-key TLS path, not the `TlsServerConfig`. 5. **The TCP fallback for native clients doesn't need X.509.** The raw-key `TlsServerConfig` serves both QUIC and TCP+TLS. X.509 is only for the browser/HTTPS path. Raw-key and X.509 clients can connect to either server type — the server cert type and client cert type are independent in TLS. `AcceptAnyCertVerifier` already accepts both. 6. **This is a one-way door (endpoint struct shape) done during the pre-implementation window.** The cost of doing it now is the same refactor that has to happen anyway. The cost of not doing it is a hybrid model (quinn TLS injected, iroh key read internally, TCP+TLS outside with duplicated dispatch) that will confuse every future reader and block the hub crate (the first multi-transport consumer). --- ## 1. The transport landscape ### 1.1 The full transport matrix | Transport | TLS identity | Serves | Who connects | Status | |-----------|-------------|--------|---------------|--------| | QUIC (raw key) | RFC 7250 Ed25519 | `alknet/channels`, `alknet/call` | Native clients (workers, spokes) | Day 1 | | TCP+TLS (raw key fallback) | RFC 7250 Ed25519 | Same as above | Native clients (UDP blocked) | Day 1 | | TCP+TLS (X.509 / ACME) | X.509 | `h2`/`http/1.1` (HTTPS) | Browsers, HTTP clients | Day 1 | | QUIC (X.509 / ACME) | X.509 | WebTransport | Browsers | Deferred (ADR-044; revives with channels) | | iroh | Ed25519 (own TLS) | `alknet/channels`, `alknet/call` | P2P native clients | Day 1 | | TCP (SSH) | Ed25519 key (SSH) | `alknet/ssh` channels | Legacy SSH clients | Future | ### 1.2 Two cert identities, shared within each A hub serving both browsers and native workers holds **two** `TlsServerConfig`s: - **Raw key config** — built from `TlsIdentity::RawKey(Ed25519SecretKey)`. Shared across: - QUIC listener (`for_quinn()`) — native clients with UDP - TCP+TLS listener (`for_tcp_tls()`) — native clients when UDP blocked - The same `Ed25519SecretKey` also feeds iroh and (future) SSH - **X.509 config** — built from `TlsIdentity::X509` or `TlsIdentity::Acme`. Shared across: - TCP+TLS listener (`for_tcp_tls()`) — HTTPS for browsers - QUIC listener (`for_quinn()`) — WebTransport when it revives The cert-reuse value is *within* each identity, not across them. A hub doesn't share one cert for everything — it shares one cert per identity across that identity's transports. This is why `AlknetEndpoint::new` taking a single `Arc` (as ADR-082 currently specifies) is wrong for the hub case: the hub has two configs. The endpoint shouldn't take any TLS config — the assembly layer builds the transports and hands the endpoint pre-built quinn/iroh endpoints. ### 1.3 Iroh and SSH share the key, not the TLS config Iroh has its own TLS built into the `Endpoint` (RFC 7250 raw keys). It does not consume a `rustls::ServerConfig`. SSH uses the Ed25519 key for its own handshake. Both share the `Ed25519SecretKey` with the raw-key `TlsServerConfig`, but neither receives a `TlsServerConfig`. The assembly layer reads `Ed25519SecretKey` from `StaticConfig` and hands it to: - `TlsServerConfig::new(TlsIdentity::RawKey(key), ...)` — for quinn/TCP+TLS - `iroh::SecretKey::from_bytes(key.as_bytes())` — for iroh - (future) the SSH handler's key configuration ### 1.4 Raw-key / X.509 mixing in the TLS handshake A raw-key client can connect to an X.509 server and vice versa. In TLS, the server cert type and client cert type are independent. A server presenting an X.509 cert can accept a client presenting a raw key (RFC 7250), as long as both sides have RFC 7250 support in their rustls config. `AcceptAnyCertVerifier` already accepts both, and its `supported_verify_schemes()` includes ED25519. The mixing cases: - **Raw-key client → X.509 server**: client verifies server via CA (ADR-034 public X.509 case) or fingerprint pin (known hub); server extracts client's `ed25519:` fingerprint. - **X.509 client → raw-key server**: client verifies server via fingerprint pin (raw-key remotes are always known peers); server extracts client's `SHA256:` fingerprint. This means the TCP fallback for native clients doesn't need X.509 — the raw-key config serves both QUIC and TCP+TLS. X.509 is only for the browser/HTTPS path. ### 1.5 What's required vs. what's built now All transports in the matrix above are **requirements** — not "nice to have." But we do not have to build all of them right now. The abstractions (the endpoint refactor, the `TlsServerConfig` extraction, `Connection::from_stream`/`from_bidi`) are what make the full matrix tractable without blocking any of the future transports. The point is to get the abstractions right so that each transport can be added without changing the core. By volume, the most common use case is raw-key QUIC (native P2P, workers). But we have to support the HTTP crate and X.509 from day one. The TCP fallback for native clients (raw key) is important too since UDP is blocked on some networks. A worker has to register with the hub via HTTP POST in many cases, then connect via QUIC if available and fall back to TCP if not. --- ## 2. The endpoint refactor ### 2.1 Current state: the endpoint conflates construction and orchestration `AlknetEndpoint::new` currently does three things: 1. **Builds the quinn endpoint** — reads `static_config.tls_identity`, constructs TLS internally (`TlsSetup::new`, `build_rustls_server_config`), wraps it into `quinn::ServerConfig`, binds `quinn::Endpoint::server()`. 2. **Builds the iroh endpoint** — reads `static_config.tls_identity` again (for the Ed25519 key), calls `build_iroh_endpoint()`. 3. **Runs accept loops** for whichever of those it built, dispatching by ALPN through the `HandlerRegistry`. TCP+TLS is not in this picture at all. ADR-010 Amendment 1 made it a "sibling accept loop" that runs outside the endpoint — the assembly layer builds it and dispatches through the same `HandlerRegistry`. But the dispatch logic (`build_auth_context`, fingerprint extraction, the `acme-tls/1` guard, handler lookup + spawn) is crate-private (`dispatch_quinn` / `dispatch_iroh` in `endpoint.rs`), so the assembly layer can't call it. A TCP+TLS accept loop has to duplicate it or call `HandlerRegistry::get` and `build_auth_context` directly (the latter is also crate-private). The "sibling" framing was a workaround for the endpoint being welded to quinn — not a deliberate design. The result is a three-tier asymmetry: quinn is built and dispatched internally, iroh is built internally and dispatched internally, TCP+TLS is built outside and dispatched outside (with duplicated logic). The TLS extraction surfaces this because it pulls quinn's TLS construction out, leaving iroh's key reading and TCP+TLS's second-class status as visible inconsistencies. ### 2.2 The dispatch logic is the endpoint's real value Stripped of transport construction, what the endpoint provides is **dispatch**: take a connection, extract its ALPN, look up the handler, build the `AuthContext`, spawn the handler. This is the same for every transport. The transport-specific parts are: - **ALPN extraction** — quinn: `handshake_data().protocol`; iroh: `connecting.alpn().await`; TCP+TLS: `tls_stream.alpn()` (from the `tokio-rustls` handshake result). - **Fingerprint extraction** — quinn: `peer_identity()` → `CertificateDer`; iroh: `remote_id()` → `ed25519:`; TCP+TLS: `tls_stream.peer_certificates()` → `CertificateDer`. - **Connection construction** — quinn: `from_quinn_with_alpn`; iroh: `from_iroh`; TCP+TLS: `from_bidi(tls_stream, alpn, remote_addr)`. - **No-handler close** — quinn: `connection.close()`; iroh: `connection.close()`; TCP+TLS: drop the stream. Everything between those transport-specific bookends — the ACME ALPN guard, the handler lookup, the `build_auth_context` call, the `tokio::spawn` — is identical. If the endpoint exposes a public `dispatch` method that takes the already-extracted ALPN, fingerprint, and `Connection`, every transport's accept loop calls the same dispatch path. No duplication. ### 2.3 The new endpoint shape The endpoint is a pure accept-loop runner + a public dispatch method. It owns the transport endpoints it runs accept loops for (quinn, iroh), and it exposes `dispatch` for transport accept loops that it doesn't own (TCP+TLS, future SSH, future WebTransport). ```rust pub struct AlknetEndpoint { quinn: Option, iroh: Option, handlers: Arc, dynamic: Arc>, identity_provider: Arc, shutdown_tx: watch::Sender, shutdown_rx: watch::Receiver, drain_timeout: Duration, } impl AlknetEndpoint { pub fn new( handlers: HandlerRegistry, dynamic: Arc>, identity_provider: Arc, drain_timeout: Duration, ) -> Self; pub fn with_quinn(mut self, endpoint: quinn::Endpoint) -> Self; pub fn with_iroh(mut self, endpoint: iroh::Endpoint) -> Self; /// Dispatch a pre-established connection by ALPN. Called by the /// endpoint's own accept loops (quinn, iroh) after transport- /// specific extraction, and by external accept loops (TCP+TLS, /// future SSH, future WebTransport) after their own extraction. /// The caller supplies the ALPN, the fingerprint (if available), /// the remote address (if available), and the `Connection`. pub fn dispatch( &self, connection: Connection, alpn: Vec, fingerprint: Option, remote_addr: Option, ); /// Run the endpoint's accept loops. Returns when shutdown is /// signaled. The caller drives shutdown via `shutdown_sender()`. pub async fn run(self: Arc); } ``` The `StaticConfig` parameter disappears from `new()`. The assembly layer reads `StaticConfig`, builds the transports, and hands the quinn/iroh endpoints to `AlknetEndpoint` via builder methods. `build_iroh_endpoint` and `build_quinn_server_config_from_rustls` move out of core — the assembly layer (or `alknet-tls` as a convenience) builds the transport endpoints. TCP+TLS accept loops call `endpoint.dispatch(connection, alpn, fingerprint, remote_addr)` — the same dispatch path quinn and iroh use. No duplicated `build_auth_context` or handler-lookup logic at the assembly layer. The hub's TCP+TLS accept loop is still a sibling (it owns its `TcpListener` and `TlsAcceptor`), but its dispatch is first-class — it calls into the endpoint, not around it. This resolves the ADR-010 Amendment 1 workaround. Amendment 1 made TCP+TLS a sibling because the endpoint couldn't share its dispatch logic. Now it can — `dispatch` is public. The sibling framing (the accept loop lives outside the endpoint struct) survives; the second-class dispatch (duplicated logic) doesn't. ### 2.4 What the assembly layer does For a hub serving native clients (raw key) and browsers (X.509): 1. Read `StaticConfig`. 2. Build the raw-key `TlsServerConfig` from `TlsIdentity::RawKey(key)`. 3. Build the X.509 `TlsServerConfig` from `TlsIdentity::Acme { ... }` (or `X509`). 4. Build the quinn endpoint for native clients: `raw_key_tls.for_quinn()` → `quinn::Endpoint::server()`. 5. Build the iroh endpoint: `iroh::Endpoint::builder().secret_key(...)`. 6. Construct `AlknetEndpoint::new(registry, ...)` with `.with_quinn()` and `.with_iroh()`. 7. Build the TCP+TLS accept loop for native fallback: `raw_key_tls.for_tcp_tls()` → `TcpListener` + `TlsAcceptor` → `endpoint.dispatch(from_bidi(tls_stream, alpn, addr), alpn, fp, addr)`. 8. Build the TCP+TLS accept loop for HTTPS: `x509_tls.for_tcp_tls()` → `TcpListener` + `TlsAcceptor` → `endpoint.dispatch(...)`. 9. (Future) Build the quinn endpoint for WebTransport: `x509_tls.for_quinn()` → `quinn::Endpoint::server()`. 10. Spawn the TCP+TLS accept loops (they call `endpoint.dispatch`). 11. Spawn `endpoint.run()` — runs the quinn/iroh accept loops. For a bare-bones P2P node (no public IP, no ACME, no browsers): just step 1, step 5, step 6 (with only `.with_iroh()`), step 11. No `TlsServerConfig` needed at all. ### 2.5 What this resolves - **C5 (iroh path ownership):** the endpoint doesn't build iroh; the assembly layer does. No ambiguity. - **TCP+TLS second-class dispatch:** `dispatch` is public; TCP+TLS accept loops call the same path as quinn/iroh. No duplicated `build_auth_context` or handler-lookup logic. ADR-010 Amendment 1's workaround is retired. - **The single-`Arc` problem:** ADR-082 currently specifies `AlknetEndpoint::new` taking `Arc`, but a hub has two configs (raw key + X.509). With the endpoint taking no TLS config, this disappears. - **The `static_config.tls_identity` reading:** the endpoint no longer reads it. The assembly layer reads it and builds the transports. - **The `acme_state_handle` on the endpoint:** the ACME task handle lives on `TlsServerConfig` (the assembly layer owns it), not on the endpoint. --- ## 3. Interaction with `alknet-tls` extraction (ADR-082) ### 3.1 The TLS extraction is the trigger, not the whole refactor The TLS extraction (`alknet-tls` crate with `TlsServerConfig`) is what surfaced the endpoint's conflation. But the endpoint refactor is a separate, larger change that ADR-082 should reference but not contain. ### 3.2 ADR-082 changes With the endpoint refactor, ADR-082's `AlknetEndpoint::new` signature changes. Instead of: ```rust pub async fn new( static_config: &StaticConfig, tls_config: Arc, // ← was the new param handlers: HandlerRegistry, dynamic: Arc>, identity_provider: Arc, ) -> Result; ``` The endpoint takes no TLS config at all (see §2.3). ADR-082 should reference the endpoint refactor ADR (to be written) for the new signature, and focus on what `alknet-tls` provides: `TlsServerConfig` and its accessors. ### 3.3 What moves out of `alknet-core/endpoint.rs` | Code | Destination | |------|-------------| | `build_rustls_server_config()` | `alknet-tls` | | `TlsSetup` / ACME state machine | `alknet-tls` (`TlsServerConfig::new` ACME path) | | `RawKeyCertResolver` | `alknet-tls` | | `Ed25519SigningKey` | `alknet-tls` | | `AcceptAnyCertVerifier` | `alknet-tls` | | `SelfSignedCert` / `generate_self_signed_cert()` | `alknet-tls` | | `load_cert_chain()` / `load_private_key()` | `alknet-tls` | | `build_quinn_server_config_from_rustls()` | `alknet-tls` (`for_quinn()`) | | `build_iroh_endpoint()` | Assembly layer (or a helper; construction decision is assembly-layer) | What stays in `alknet-core/endpoint.rs`: - `AlknetEndpoint` struct (accept-loop runner + public `dispatch`) - `HandlerRegistry` - `dispatch` (public — takes ALPN, fingerprint, `Connection`; calls `build_auth_context`, looks up handler, spawns) - `dispatch_quinn` / `dispatch_iroh` (private — do transport-specific extraction, then call `dispatch`) - `run_quinn_accept_loop` / `run_iroh_accept_loop` - `extract_quinn_alpn` / `extract_quinn_client_fingerprint` / `extract_iroh_client_fingerprint` - The `acme-tls/1` early-return guard (inside `dispatch_quinn`; the guard is quinn-specific — ACME challenges arrive over QUIC. TCP+TLS has no ACME ALPN guard because `acme-tls/1` is only negotiated over QUIC for the TLS-ALPN-01 challenge. A TCP+TLS listener serving HTTPS does not advertise `acme-tls/1`.) `build_auth_context` becomes a private helper called by `dispatch`, not a standalone function — it's the shared core of the dispatch path and should not be called directly by the assembly layer. The assembly layer calls `dispatch`, which calls `build_auth_context` internally. What stays in `alknet-core/config.rs`: - `TlsIdentity` enum (config type) - `Ed25519SecretKey` (config type) - `AcmeDirectory` (config type) - `StaticConfig` (config type) What stays in `alknet-core/fingerprint.rs` (pending OQ-59): - `fingerprint_from_cert_der`, `extract_ed25519_raw_key_from_spki` ### 3.4 Ordering question: tls first or core refactor first? This is deferred to the task-decomposition phase. Both are interlocked: the TLS extraction removes code from `endpoint.rs`, and the endpoint refactor changes what stays. The natural ordering might be: 1. **Endpoint refactor first** — change `AlknetEndpoint` to a pure accept-loop runner, move `build_iroh_endpoint` and `build_quinn_server_config_from_rustls` out of core. At this point, the TLS setup code still exists in core but is called by the assembly layer instead of the endpoint. 2. **TLS extraction second** — move the TLS setup code into `alknet-tls`, build `TlsServerConfig`, replace the assembly layer's direct calls with `TlsServerConfig::new` / `for_quinn()` / `for_tcp_tls()`. But this is a task-decomposition decision, not an architecture decision. The architecture is: both changes happen, the endpoint becomes a pure accept-loop runner, and `alknet-tls` provides the shareable cert config. --- ## 4. The remaining review items (from the tls spec review) These are unaffected by the endpoint refactor. They're about the TLS crate's API surface and the OQ/ADR status. ### 4.1 C2: Define `TlsError` type The `TlsError` type is used in every public signature but never defined. Needs a variant list. The current code uses `EndpointError::TlsConfig(io::Error)`. The new type should cover: - IO errors (file read failures for cert/key loading) - rustls config errors (protocol version, cipher suite setup) - Quinn config errors (`QuicServerConfig::try_from` failure) - ACME feature-not-enabled (when `TlsIdentity::Acme` is configured without the `acme` feature) ### 4.2 C3 + C6: ACME task lifecycle and cert renewal The ACME state machine task is spawned by `TlsServerConfig::new` (ACME path) and its `JoinHandle` is held on `TlsServerConfig`. The spec currently says "dropping this handle does NOT stop the ACME state machine" but never says what *does* stop it. Cert renewal behavior (confirmed from the reverse-proxy reference): | Identity | Renewal behavior | Drain needed? | |----------|-----------------|---------------| | ACME | Live — `ResolvesServerCertAcme` swaps cert in place; all listeners sharing the `Arc` pick up the renewed cert on the next TLS handshake | No | | X.509 (manual) | Loaded once at startup via `with_single_cert`; no live renewal | Yes (restart to load new cert) | | RawKey | No expiry — Ed25519 keys don't expire | N/A | | SelfSigned | Dev only — restart to regenerate | N/A | The ACME renewal mechanism: `TlsServerConfig::new` (ACME path) calls `state.resolver()` to get an `Arc`, wires it into the `rustls::ServerConfig` via `with_cert_resolver(resolver)`, and spawns the `AcmeState` event loop. When the event loop processes a renewed cert (`EventOk::DeployedNewCert`), it updates the resolver's internal cert in place. Since `for_quinn()` and `for_tcp_tls()` clone the `rustls::ServerConfig` (which shares the `Arc`), both the quinn endpoint and the TCP+TLS acceptor see the renewed cert on the next handshake. No drain, no restart, no listener replacement. The shutdown path: the ACME task's lifetime is the renewal lifetime. Shutting it down stops renewal (but existing listeners keep serving the last-known cert until it expires). Restarting the process re-spawns the state machine, which deploys the cached cert (`DeployedCachedCert`) and resumes renewal. The `TlsServerConfig` needs a `shutdown()` method (or a `Drop` impl that aborts the handle) so the assembly layer can cleanly stop the ACME task on process shutdown. ### 4.3 C4: OQ-59 (fingerprint.rs location) OQ-59 is open but the spec treats `fingerprint.rs` staying in core as decided. The "Likely resolution" in OQ-59 is Option A (stay in core), with solid rationale: `alknet-call`'s client-side `FingerprintPinVerifier` uses fingerprint functions and shouldn't have to depend on `alknet-tls` (which would pull `rustls` + `tokio` + optionally `quinn`/`tokio-rustls`/`rustls-acme` into every client-only deployment). Recommendation: resolve OQ-59 to Option A (stay in core) — it's a two-way door (moving a module later is a refactor, not a wire-format change), and the rationale is clear. ### 4.4 W3: Feature-gate edge cases - `TlsIdentity::Acme` configured without the `acme` feature — should return a runtime error (matching the current code's behavior). - `default = []` with no features — `new()` and `rustls_config()` work but `for_quinn`/`for_tcp_tls` don't exist. This is a supported config for iroh-only deployments (no `TlsServerConfig` needed at all — the assembly layer just uses the `Ed25519SecretKey` directly). - `acme` without `quinn` or `tcp` — ACME task spawns, `rustls_config()` available, but no transport accessor. Valid for a pure cert-provider use case (not currently expected, but not blocked). ### 4.5 W4: aws_lc_rs crypto provider ADR The `aws_lc_rs::default_provider()` choice is a load-bearing decision (FIPS status, platform support) with no ADR. Either add a clause to ADR-082 (or ADR-027) recording the choice, or write a small standalone ADR. The invariant "do not switch to `ring` or the process-default provider without an ADR" is already in the spec; the decision record should exist. ### 4.6 W5: ADR-082 status ADR-082 is "Proposed." If implementation is about to begin (and the endpoint refactor is part of the same work), the ADR should move to "Accepted" — with OQ-59 noted as a non-blocking open question that can be resolved alongside implementation. "Proposed" for an actively-implemented extraction is a stale status. --- ## 5. The reverse-proxy as reference The reverse-proxy (`/workspace/@alkdev/reverse-proxy/`) is a useful reference for the TLS setup, with the caveat that it is X.509-only (manual + ACME). The relevant code: - `src/tls/acme.rs` — `AcmeTlsConfig::setup()` constructs the `AcmeState` and gets the `resolver()`. `spawn_acme_state()` runs the event loop with a `tokio::select!` against a shutdown signal (so the ACME task can be cleanly stopped, not just aborted). - `src/tls/acceptor.rs` — `TlsMode` enum (`Manual` vs `Acme`) holds the `Arc` and the `Arc`. `setup_tls()` builds the `TlsAcceptor` from the server config. - `src/tls/config.rs` — `crypto_provider()` constructs the `aws_lc_rs` provider with restricted cipher suites. Manual cert loading via `rustls-pemfile`. What the reverse-proxy does that alknet-tls should learn from: - The ACME task is spawned with a shutdown signal (`tokio::select!` against `shutdown_rx.changed()`), not just a bare `JoinHandle`. This is cleaner than abort — the state machine gets to finish its current operation and exit gracefully. (Relevant to C3.) - The `TlsMode` enum separates the static config from the runtime objects — `Manual(Arc)` vs `Acme { default_config, resolver }`. This mirrors the `TlsServerConfig` / `TlsIdentity` split. - The `ResolvesServerCertAcme` is the live renewal mechanism — the `TlsAcceptor` built from `default_config` (which holds an `Arc` to the resolver) picks up renewed certs on the next handshake. No drain, no restart. (Relevant to C6.) What the reverse-proxy doesn't cover (because it's X.509-only): - RFC 7250 raw keys (the `RawKeyCertResolver` / `Ed25519SigningKey` path) - Multiple transport sharing (it's TCP-only; no quinn, no iroh) - The endpoint-as-accept-loop-runner pattern (it has its own `serve_https_listener` that's simpler than `AlknetEndpoint`) --- ## 6. Open questions for the next session 1. **C2 (`TlsError` variants):** what error types should `TlsError` wrap? The current `EndpointError::TlsConfig(io::Error)` is too narrow — it doesn't cover `rustls::Error` or ACME-specific errors. Need a variant list. 2. **C3 (ACME shutdown):** should `TlsServerConfig` have a `shutdown()` method (clean, explicit) or a `Drop` impl (automatic, but surprising)? The reverse-proxy uses a shutdown signal (`tokio::select!` against `shutdown_rx.changed()`) rather than `abort()`. Should alknet-tls do the same? 3. **C4 (OQ-59):** resolve to Option A (stay in core) or keep open? Recommendation: resolve to Option A. 4. **Endpoint refactor ADR:** draft ADR-083 (amending ADR-010) for the endpoint-as-accept-loop-runner shape, or fold it into ADR-082? Recommendation: standalone ADR — it changes ADR-010's endpoint design, which is a broader change than the TLS extraction. 5. **Ordering:** does the endpoint refactor land before the TLS extraction, or after? Deferred to task decomposition.