kcode-k1-http 0.2.0

Axum orchestration for authenticated K1 HTTP requests
Documentation
# K1 HTTP orchestration

This library provides one listener-free Axum spine. Its complete public API is:

```rust
pub use kcode_k1_http_request::CanonicalUsername;

pub struct Config {
    pub server_id: String,
    pub public_origin: String,
    pub max_body_bytes: usize,
}

#[derive(Clone, Copy, Eq, PartialEq)]
pub struct Identity;
impl Identity {
    pub fn new(user_id: [u8; 12], public_key: [u8; 32]) -> Self;
    pub fn user_id(&self) -> &[u8; 12];
    pub fn public_key(&self) -> &[u8; 32];
}

#[derive(Clone)]
pub struct Principal;
impl Principal {
    pub fn user_id(&self) -> &[u8; 12];
    pub fn username(&self) -> &CanonicalUsername;
}

#[derive(Clone)]
pub struct RegistrationPrincipal;
impl RegistrationPrincipal {
    pub fn username(&self) -> &CanonicalUsername;
    pub fn public_key(&self) -> &[u8; 32];
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IdentityError { Unavailable }

#[async_trait]
pub trait IdentityProvider: Send + Sync + 'static {
    async fn lookup(&self, username: &CanonicalUsername)
        -> Result<Option<Identity>, IdentityError>;
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConfigError {
    EmptyServerId,
    InvalidPublicOrigin,
    InvalidBodyLimit,
}

pub struct K1Http;
impl K1Http {
    pub fn new(
        config: Config,
        replay: kcode_k1_http_replay::ReplayWindow,
        identities: Arc<dyn IdentityProvider>,
    ) -> Result<Self, ConfigError>;

    pub fn router(
        &self,
        registration: axum::routing::MethodRouter,
        terms: axum::routing::MethodRouter,
        authenticated: axum::Router,
    ) -> axum::Router;
}
```

`IdentityError` and `ConfigError` implement `Display` and `Error`. `server_id` must be nonempty, `max_body_bytes` nonzero, and `public_origin` an absolute HTTP(S) origin with authority, no userinfo, query, fragment, or non-root path. Its exact text is signed. Its exact authority is the sole accepted `Host`; forwarding headers are ignored.

`router` mounts the supplied registration router at exact public `/api/register`, the supplied pathless terms router at exact public `/api/terms`, and nests supplied authenticated routes below `/api`. Registration is POST only. Terms is GET and HEAD only; other methods fail with 405 and `Allow: GET, HEAD` without reaching a supplied handler. Terms GET and HEAD are not authenticated. Every other supplied API route, method mismatch, and fallback is authenticated. The terms bytes, signature semantics, and acceptance submission are caller-owned; account acceptance storage remains outside this library.

Every protected request has exactly one `K1-Username`, `K1-Epoch`, `K1-Nonce`, `K1-Body-SHA256`, and `K1-Signature`; registration also has one `K1-Public-Key`. Binary fields use canonical unpadded Base64URL and fixed 16-, 32-, 64-, and 32-byte decoded lengths. Epochs are decimal `u64`. Content-Type is absent or unique and its exact ASCII value is signed. Method and the original encoded origin-form path and query are signed without query or percent normalization.

Authenticated processing parses the envelope, validates its epoch, performs one identity lookup without an HTTP-owned lock, verifies the signature and bounded body, admits the replay tuple, attaches `Principal`, and runs the endpoint. The private replay identity is the 12-byte user ID zero-extended to 32 bytes. Invalid signatures consume no nonce; successful authentication consumes it before the endpoint. Unknown identity and failed verification are indistinguishable. Registration first enforces POST, then parses the envelope, selects the candidate key, validates the epoch, verifies the signature and bounded body, attaches `RegistrationPrincipal`, and runs the endpoint without identity lookup or replay admission. Missing or duplicate candidate-key headers are malformed envelopes; one present but invalid candidate key is an authentication failure. Durable registration replay and idempotency belong to the supplied handler.

Authority validation precedes routing and CORS preflights. Preflights remain public and are intercepted outside the registration and terms method guards. CORS allows any origin without credentials, GET, POST, PUT, PATCH, DELETE, OPTIONS, and HEAD, and Content-Type plus every K1 request header; it exposes `K1-Epoch`. API responses, including terms, preflights, method failures, and fallbacks, carry `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, and the current `K1-Epoch` when available.

Errors are concise JSON: 421 `invalid_request_authority`; 400 `malformed_authentication_envelope`; 401 `stale_epoch`; 401 `authentication_failed`; 400 `body_digest_mismatch`; 413 `body_too_large`; 409 `replay`; 429 `nonce_capacity`; 503 `identity_provider_unavailable`; and 503 `epoch_unavailable`. No request, identity, key, nonce, path, body, provider, operating-system, invite, or account detail is emitted or logged.

Package work is O(header + body): one bounded body allocation and SHA-256 hash, one leaf Ed25519 verification, one identity lookup, and expected-O(1) replay calls. Request memory is bounded by the configured body limit plus headers and verification state. No HTTP-owned lock spans lookup or body collection, and a blocked lookup does not block unrelated requests. Replay synchronization and durable transitions remain replay-owned. There are no package-owned listeners, network/provider calls, retries, background tasks, sessions, cookies, account storage, or business authorization. The provider-free conformance suite exercises bodies through its configured 64 KiB boundary within the testkit's broad 30-second verifier envelope. Larger caller-configured valid bodies have the same linear work but no measured wall-clock promise.