# alkhttp HTTP interface for the alk stack: serves HTTP/1.1 + HTTP/2 on standard ALPNs (with WebSocket upgrade carrying the channels protocol for browser bidirectional access) and hosts the HTTP-backed call-protocol adapters. alkhttp wraps [`alkcall`](https://crates.io/crates/alkcall) — the pure call + channels protocol crate — and turns its operation registry into an HTTP surface. It is the HTTP server host (a `ProtocolHandler` for the IANA `h2`/`http/1.1` ALPNs) and the HTTP client host (the import adapters that call out over `reqwest`) in one crate. ## What's inside **Server side (`server` feature)** — the producer-facing HTTP surface: - `HttpAdapter` — an axum `Router` driven by hyper's HTTP/1.1 and HTTP/2 connection handling over a `BiStream`, served on standard ALPNs so any HTTP client connects without knowing about the alk stack. - Six fixed gateway endpoints — the sole HTTP invoke path (no per-operation REST tree): `GET /search`, `GET /schema?name=…`, `POST /call`, `POST /batch`, `POST /subscribe` (SSE), `POST /publish` (NDJSON). - `/healthz`, `/openapi.json` (a `to_openapi` projection of the local registry), and `/mcp` (the MCP tool gateway, feature `mcp`). - WebSocket upgrade on `/alk/channels` carrying the channels protocol: 8-byte chunk multiplexing with channel 0 pre-negotiated as `alk/call`, so a browser runs the native bidirectional call-protocol session. - Bearer auth, a stealth decoy surface for unknown paths, custom-route mounting, and assembly hooks for WS session caps and timeouts. **Client side (`client` feature)** — the consumer-facing import adapters, each producing ordinary `HandlerRegistration` bundles for an `alkcall` registry: - `from_openapi` — import an external HTTP API described by an OpenAPI document (JSON or YAML), forwarding calls over the shared reqwest client. - `from_jsonschema` — import one non-OpenAPI HTTP endpoint behind a caller-built `OperationSpec`. - `from_mcp` — import a remote MCP server's tools as operations over streamable HTTP (feature `mcp`). - `from_wss` — import a remote alk node's operations over a WSS channels connection (feature `wss`). - `SharedHttpClient` — the hot-reloadable reqwest stack all `from_*` forwarding rides: same-host-only redirects, idempotent-only retries with a wall-clock budget, `Retry-After` support, TLS config, and streaming byte caps. **Both sides (`openapi` feature, implied)** — the shared OpenAPI document model plus the reverse projections `to_openapi` (generate the gateway's OpenAPI document from the registry) and `to_mcp` (expose local operations as MCP tools). ## Quick start ### Serve the gateway over HTTP `HttpAdapter` implements alkcall's `ProtocolHandler`; the endpoint / accept loop is the consumer's concern (dial and TLS live downstream). Wire it for the ALPN you serve and drive it with your connection source: ```rust use std::sync::Arc; use alkcall::core::auth::IdentityProvider; use alkcall::core::types::Connection; use alkcall::registry::registration::OperationRegistry; use alkhttp::server::{DecoyConfig, HttpAdapter}; let registry = Arc::new(OperationRegistry::new()); let provider: Arc = /* your identity provider */; let adapter = HttpAdapter::h2(provider, registry) .with_decoy(DecoyConfig::NotFound); // adapter.alpn() == b"h2" — register it in your handler registry for // that ALPN; each accepted Connection is handled by // ProtocolHandler::handle(&adapter, connection, &auth). ``` Once wired, HTTP callers get the fixed gateway surface. Discovery is per-caller, filtered by `AccessControl`: ```text GET /search → { "request_id": …, "result": "ok", "output": { "operations": [ … ] } } GET /schema?name=fs/readFile → the operation's input/output schema POST /call ← { "operation": "fs/readFile", "input": { … } } POST /batch ← [ { "operation": …, "input": … }, … ] POST /subscribe → SSE stream of call-protocol envelopes POST /publish ← NDJSON body of published chunks ``` `/openapi.json` serves the same surface as an OpenAPI document, so standard tooling can drive the gateway without knowing the alk stack. ### Import an OpenAPI-described API ```rust use std::collections::HashMap; use std::sync::Arc; use alkhttp::adapters::{FromOpenAPI, HttpServiceConfig, OpenAPISpec}; use alkhttp::client::{HttpClientConfig, SharedHttpClient}; let doc = r#"{"openapi":"3.0.0","info":{"title":"Widgets","version":"1"}, "paths":{"/widgets":{"get":{"operationId":"listWidgets", "responses":{"200":{"content":{"application/json":{"schema":{}}}}}}}}}"#; let spec = OpenAPISpec::from_json(doc)?; let config = HttpServiceConfig { namespace: "widgets".to_string(), base_url: "https://widgets.example.com".to_string(), auth: None, // or Some(HttpAuthScheme::Bearer) — the credential is // injected per-call from Capabilities, never from config default_headers: HashMap::new(), }; let http_client = Arc::new(SharedHttpClient::new(HttpClientConfig::default())?); let adapter = FromOpenAPI::new(spec, config, http_client); let registrations = alkcall::client::OperationAdapter::import(&adapter).await?; let mut registry = alkcall::registry::registration::OperationRegistry::new(); for reg in registrations { registry.register(reg)?; } // "widgets/listWidgets" is now callable — and composable — through the // call protocol and the gateway. ``` Imported operations register `Visibility::Internal` (they are composition material); an External facade op composed over them via `ctx.env.invoke` is what the wire sees. ### WebSocket for browsers A browser upgrades `GET /alk/channels` (Bearer token in the `Authorization` header) to a WebSocket and speaks the channels protocol in binary messages: an 8-byte chunk header multiplexes N channels; channel 0 is pre-negotiated as `alk/call` and runs the native call-protocol session. Both sides can initiate calls; the browser can also register operations and open channels — it is a full consumer and producer on its own connection-local overlay. ## Feature flags The crate is feature-sided — build one side, or both: | Feature | Default | Gates | |---------|---------|-------| | `server` | yes | the axum host, gateway routes, WS upgrade, `/healthz`, `/openapi.json`, `to_openapi` | | `client` | yes | the shared outbound client host, `from_openapi`, `from_jsonschema` | | `openapi` | (implied) | the shared OpenAPI document model (implied by both sides) | | `mcp` | no | `from_mcp` (needs `client`) and `to_mcp` (needs `server`) | | `wss` | no | the `from_wss` consumer adapter | | `h2`, `http1` | yes | the server's HTTP protocol features (imply `server`) | A lean single-side build takes `default-features = false` plus the side it needs: ```toml alkhttp = { version = "0.5.0", default-features = false, features = ["server"] } ``` ## Security posture - **No secret material on the wire.** Request/response payloads and headers carry no keys or tokens beyond the caller's own Bearer credential. Outbound credentials flow vault → assembly layer → `Capabilities` → handler; the `from_*` adapters are the injection point, and no handler reads `std::env::var`. - **Stealth mode.** The HTTP surface serves on standard ALPNs, and unregistered paths answer with a configurable decoy (fake nginx 404, static site, or redirect) instead of advertising the gateway. - **Tight forwarding defaults.** Same-host-only redirects, idempotent-only retries with a wall-clock budget and `Retry-After` ceiling, 30 s request / 10 s connect / 30 s read timeouts, a 2 MiB gateway body cap (8 MiB on `/mcp`), and a 1 GiB streamed-bytes cap per subscription. - **Internal-by-default imports.** Adapter-registered operations are invisible to direct wire calls (`NOT_FOUND`) until composed behind an External facade. ## Documentation - [Architecture docs](docs/architecture/README.md) — the authoritative spec: ADRs 001–071, component documents, and open questions. - [API docs](https://docs.rs/alkhttp) — full crate documentation on docs.rs. - [Changelog](CHANGELOG.md) ## License MIT OR Apache-2.0 — see [LICENSE-MIT](LICENSE-MIT) and [LICENSE-APACHE](LICENSE-APACHE).