docs(http): add ADR-043 WebTransport bidirectional ALPN substrate; fix spec drift from mid-spec pivot
A consistency review of the alknet-http specs found two classes of
issues: internal contradictions from the mid-spec pivot (the to_openapi
gateway pattern landed in prose but not in cross-references), and a
systematic client→server assumption that only holds for the OpenAPI/MCP
case leaking into the WebTransport architecture.
Class 1 (internal contradictions):
- C1: to_openapi was half-refactored — body described the ADR-042
gateway pattern but the decisions table and ADR-036 still said
'paths mirror /{service}/{op}'. ADR-036's to_openapi clause is now
amended as superseded by ADR-042; the stale decisions row and README
Principle 2 are fixed.
- C2: the axum Router route list didn't include the 5 gateway endpoints
(/search, /schema, /call, /batch, /subscribe). Added them; clarified
/openapi.json as the gateway description doc; added gateway paths to
the decoy exclusion list.
- C3: ADR-034 §5 still talked about the 'h3/WebTransport deferral
bucket' that ADR-038 eliminated. Amended §5/Consequences/References
to drop the deferral framing (the auth-model decision stands; only
the 'when' wording was stale).
Class 2 (one-way direction assumption):
- C4/C5/C6: the WebTransport specs framed the session as browser→hub
one-way, when the call protocol is bidirectional and WebTransport is
a general ALPN transport substrate. New ADR-043 reframes WebTransport
as a bidirectional ALPN transport substrate (call protocol is the
first/canonical target; needs no WASM parser), names the call
protocol's bidirectionality over WebTransport sessions, and states
the inbound no-PeerId connection-local overlay as the mirror of
ADR-034 §2. webtransport.md is updated to reflect this framing;
ADR-040 is repositioned (not superseded) as the substrate's non-call-
ALPN mechanism.
- C7: the HTTP/1.1+HTTP/2 surface's one-directionality is now named as
a lossy consequence of HTTP request/response; WebTransport is named
as the surface that restores the bidirectional call model.
- C8: overview.md acknowledges the from/to direction model is
OpenAPI/MCP-specific, not a call-protocol property.
A review subagent pass on ADR-043 + webtransport.md found no critical
issues; warnings W1-W3 (residual browser-as-subject framing, ADR-009
rationale in spec, opening abstract tone) and suggestions S2/S4/S5
were addressed.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-28
|
last_updated: 2026-06-29
|
||||||
---
|
---
|
||||||
|
|
||||||
# Alknet Architecture
|
# Alknet Architecture
|
||||||
@@ -18,7 +18,7 @@ The storage and auth strategy research (`docs/research/alknet-storage-strategy/f
|
|||||||
|
|
||||||
The alknet-call crate is **implemented and reviewed** — both the server-side core and the client/adapter surface (207 lib + 2 integration tests passing). The alknet-core and alknet-call crate specs are in draft; the alknet-vault crate specs are stable.
|
The alknet-call crate is **implemented and reviewed** — both the server-side core and the client/adapter surface (207 lib + 2 integration tests passing). The alknet-core and alknet-call crate specs are in draft; the alknet-vault crate specs are stable.
|
||||||
|
|
||||||
**alknet-http specs drafted.** The alknet-http crate (HTTP interface — `h2`/`http/1.1`/`h3` server + `from_openapi`/`to_openapi`/`from_mcp`/`to_mcp` adapters) now has architecture specs: [crates/http/](crates/http/) (overview, http-server, http-adapters, http-mcp, webtransport) and seven new ADRs — [ADR-036](decisions/036-http-to-call-operation-mapping.md) (HTTP-to-call mapping), [ADR-037](decisions/037-mcp-stdio-transport-exclusion.md) (MCP stdio exclusion), [ADR-038](decisions/038-http3-and-webtransport-as-first-class.md) (HTTP/3 + WebTransport as first-class, correcting the Phase 0 deferral framing), [ADR-039](decisions/039-http-server-and-client-host-colocated.md) (HTTP server + client host colocated in one crate), [ADR-040](decisions/040-webtransport-alpn-stream-proxy.md) (WebTransport ALPN-stream-proxy — browser → WebTransport stream → any ALPN handler via WASM parser; the "VPN-like without being a VPN" use case), [ADR-041](decisions/041-mcp-tool-gateway-pattern.md) (`to_mcp` tool-gateway pattern — 4 fixed gateway tools instead of one tool per operation, addressing LLM context tool-bloat), [ADR-042](decisions/042-openapi-gateway-pattern.md) (`to_openapi` gateway pattern — 5 fixed gateway endpoints instead of one path per operation; per-caller AccessControl-filtered API surface). ADR-003 Amendment 1 clarifies that `alknet-call` is a protocol-foundation crate (the `alknet-http` → `alknet-call` dependency edge). The specs are in draft; implementation has not started. Three open questions carried: OQ-38 (WebTransport standalone relay service scope — distinct from the in-process ALPN-stream-proxy resolved by ADR-040), OQ-39 (`to_openapi` published-spec versioning), OQ-40 (reqwest client config).
|
**alknet-http specs drafted and consistency-reviewed.** The alknet-http crate (HTTP interface — `h2`/`http/1.1`/`h3` server + `from_openapi`/`to_openapi`/`from_mcp`/`to_mcp` adapters) now has architecture specs: [crates/http/](crates/http/) (overview, http-server, http-adapters, http-mcp, webtransport) and eight ADRs — [ADR-036](decisions/036-http-to-call-operation-mapping.md) (HTTP-to-call mapping; direct-call surface), [ADR-037](decisions/037-mcp-stdio-transport-exclusion.md) (MCP stdio exclusion), [ADR-038](decisions/038-http3-and-webtransport-as-first-class.md) (HTTP/3 + WebTransport as first-class, correcting the Phase 0 deferral framing), [ADR-039](decisions/039-http-server-and-client-host-colocated.md) (HTTP server + client host colocated in one crate), [ADR-040](decisions/040-webtransport-alpn-stream-proxy.md) (WebTransport ALPN-stream-proxy — the substrate's mechanism for non-call ALPNs like SSH/git/SFTP via WASM parser; the "VPN-like without being a VPN" use case), [ADR-041](decisions/041-mcp-tool-gateway-pattern.md) (`to_mcp` tool-gateway pattern — 4 fixed gateway tools instead of one tool per operation, addressing LLM context tool-bloat), [ADR-042](decisions/042-openapi-gateway-pattern.md) (`to_openapi` gateway pattern — 5 fixed gateway endpoints instead of one path per operation; per-caller AccessControl-filtered API surface; supersedes ADR-036's original `to_openapi` clause), [ADR-043](decisions/043-webtransport-bidirectional-alpn-substrate.md) (WebTransport as a bidirectional ALPN transport substrate — reframes WebTransport as carrying ALPNs as bidirectional streams with the call protocol as the first/canonical target; names the call protocol's bidirectionality over WebTransport; states the inbound no-`PeerId` connection-local overlay as the mirror of ADR-034 §2). ADR-003 Amendment 1 clarifies that `alknet-call` is a protocol-foundation crate (the `alknet-http` → `alknet-call` dependency edge). A consistency review pass corrected drift from the mid-spec pivot (the `to_openapi` gateway pattern landed in the prose but not in cross-references; the WebTransport specs inherited the OpenAPI/MCP direction assumption that doesn't hold for the call protocol) — ADR-036's `to_openapi` clause is now amended as superseded by ADR-042, ADR-034 §5's "deferral bucket" wording is corrected (the decision stands), and the http specs now name the one-directional HTTP projection vs. the bidirectional WebTransport substrate. The specs are in draft; implementation has not started. Three open questions carried: OQ-38 (WebTransport standalone relay service scope — distinct from the in-process ALPN-stream-proxy resolved by ADR-040), OQ-39 (`to_openapi` published-spec versioning), OQ-40 (reqwest client config).
|
||||||
|
|
||||||
**Next step**: The storage/repo-pattern ADRs (030–033) are accepted and amend the core and call specs. The next implementation phase is the ADR-029 migration (peer-keyed overlays, `PeerRef` routing, retire `remote_safe`/`trusted_peer`) with the ADR-030 `PeerEntry` change and the ADR-032 `forwarded_for` field folded in — the `OperationContext`, `from_call` handler, and `AuthPolicy` are all under edit, making this the cheapest window. After that: alknet-http implementation (specs drafted, ADRs 036–038 proposed), which consumes the `CredentialStore` trait and the `OperationAdapter` contract. The alknet-ssh crate (the other post-core crate, specced in parallel) proceeds independently — it depends on `alknet-core`, not `alknet-call`.
|
**Next step**: The storage/repo-pattern ADRs (030–033) are accepted and amend the core and call specs. The next implementation phase is the ADR-029 migration (peer-keyed overlays, `PeerRef` routing, retire `remote_safe`/`trusted_peer`) with the ADR-030 `PeerEntry` change and the ADR-032 `forwarded_for` field folded in — the `OperationContext`, `from_call` handler, and `AuthPolicy` are all under edit, making this the cheapest window. After that: alknet-http implementation (specs drafted, ADRs 036–038 proposed), which consumes the `CredentialStore` trait and the `OperationAdapter` contract. The alknet-ssh crate (the other post-core crate, specced in parallel) proceeds independently — it depends on `alknet-core`, not `alknet-call`.
|
||||||
|
|
||||||
@@ -95,6 +95,7 @@ The alknet-call crate is **implemented and reviewed** — both the server-side c
|
|||||||
| [040](decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | Proposed |
|
| [040](decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | Proposed |
|
||||||
| [041](decisions/041-mcp-tool-gateway-pattern.md) | MCP Tool-Gateway Pattern for to_mcp | Proposed |
|
| [041](decisions/041-mcp-tool-gateway-pattern.md) | MCP Tool-Gateway Pattern for to_mcp | Proposed |
|
||||||
| [042](decisions/042-openapi-gateway-pattern.md) | OpenAPI Gateway Pattern for to_openapi | Proposed |
|
| [042](decisions/042-openapi-gateway-pattern.md) | OpenAPI Gateway Pattern for to_openapi | Proposed |
|
||||||
|
| [043](decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport as a Bidirectional ALPN Transport Substrate | Proposed |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters
|
|||||||
| [037](../../decisions/037-mcp-stdio-transport-exclusion.md) | MCP Stdio Transport Exclusion | Streamable HTTP only; stdio not built |
|
| [037](../../decisions/037-mcp-stdio-transport-exclusion.md) | MCP Stdio Transport Exclusion | Streamable HTTP only; stdio not built |
|
||||||
| [038](../../decisions/038-http3-and-webtransport-as-first-class.md) | HTTP/3 and WebTransport as First-Class HTTP Transports | `h3` in scope, not deferred |
|
| [038](../../decisions/038-http3-and-webtransport-as-first-class.md) | HTTP/3 and WebTransport as First-Class HTTP Transports | `h3` in scope, not deferred |
|
||||||
| [039](../../decisions/039-http-server-and-client-host-colocated.md) | HTTP Server and Client Host Colocated in alknet-http | One crate for server + client host (shared HTTP deps, shared mapping) |
|
| [039](../../decisions/039-http-server-and-client-host-colocated.md) | HTTP Server and Client Host Colocated in alknet-http | One crate for server + client host (shared HTTP deps, shared mapping) |
|
||||||
| [040](../../decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | Browser → WebTransport stream → any ALPN handler (SSH, git, SFTP) via WASM parser |
|
| [040](../../decisions/040-webtransport-alpn-stream-proxy.md) | WebTransport ALPN-Stream-Proxy | The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser |
|
||||||
| [041](../../decisions/041-mcp-tool-gateway-pattern.md) | MCP Tool-Gateway Pattern for to_mcp | 4 fixed gateway tools (search/schema/call/batch), not one tool per operation; Subscription excluded |
|
| [041](../../decisions/041-mcp-tool-gateway-pattern.md) | MCP Tool-Gateway Pattern for to_mcp | 4 fixed gateway tools (search/schema/call/batch), not one tool per operation; Subscription excluded |
|
||||||
| [042](../../decisions/042-openapi-gateway-pattern.md) | OpenAPI Gateway Pattern for to_openapi | 5 fixed gateway endpoints (search/schema/call/batch/subscribe), not one path per operation; per-caller AccessControl-filtered |
|
| [042](../../decisions/042-openapi-gateway-pattern.md) | OpenAPI Gateway Pattern for to_openapi | 5 fixed gateway endpoints (search/schema/call/batch/subscribe), not one path per operation; per-caller AccessControl-filtered |
|
||||||
|
| [043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport as a Bidirectional ALPN Transport Substrate | WebTransport carries ALPNs as bidirectional streams; call protocol is the first/canonical target (needs no WASM parser); both sides can initiate calls; no-`PeerId` non-peer clients use a connection-local overlay |
|
||||||
|
|
||||||
## Relevant Open Questions
|
## Relevant Open Questions
|
||||||
|
|
||||||
@@ -69,9 +70,14 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters
|
|||||||
[overview.md](overview.md).
|
[overview.md](overview.md).
|
||||||
2. **The HTTP surface is a projection of the call protocol.** An HTTP
|
2. **The HTTP surface is a projection of the call protocol.** An HTTP
|
||||||
request at `POST /fs/readFile` becomes a `call.requested` for
|
request at `POST /fs/readFile` becomes a `call.requested` for
|
||||||
`/fs/readFile`. The HTTP path IS the operation path; `to_openapi`
|
`/fs/readFile`. The HTTP path IS the operation path on the
|
||||||
describes this surface, it does not define a second one. See
|
**direct-call surface**. `to_openapi` *describes* a different surface
|
||||||
[ADR-036](../../decisions/036-http-to-call-operation-mapping.md).
|
— the 5-endpoint gateway (`/search`, `/schema`, `/call`, `/batch`,
|
||||||
|
`/subscribe`) that gates discovery and invocation behind a fixed
|
||||||
|
entry set. See [ADR-036](../../decisions/036-http-to-call-operation-mapping.md)
|
||||||
|
(direct-call surface) and [ADR-042](../../decisions/042-openapi-gateway-pattern.md)
|
||||||
|
(`to_openapi` gateway, superseding ADR-036's original `to_openapi`
|
||||||
|
clause).
|
||||||
3. **Standard ALPNs, not alknet ALPNs.** `h2`, `http/1.1`, `h3` are
|
3. **Standard ALPNs, not alknet ALPNs.** `h2`, `http/1.1`, `h3` are
|
||||||
IANA-registered ALPN strings. Any HTTP client (browser, curl, axios)
|
IANA-registered ALPN strings. Any HTTP client (browser, curl, axios)
|
||||||
connects without knowing about alknet — the TLS handshake negotiates
|
connects without knowing about alknet — the TLS handshake negotiates
|
||||||
@@ -89,13 +95,27 @@ on standard ALPNs, and hosts the HTTP-backed call-protocol adapters
|
|||||||
The browser streaming path uses QUIC streams directly. See
|
The browser streaming path uses QUIC streams directly. See
|
||||||
[ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md).
|
[ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md).
|
||||||
7. **The `h3` handler is an ALPN-stream-proxy for browsers.** A browser
|
7. **The `h3` handler is an ALPN-stream-proxy for browsers.** A browser
|
||||||
with a WASM parser can reach any ALPN handler (SSH, git, SFTP) via
|
with a WASM parser can reach any non-call ALPN handler (SSH, git,
|
||||||
WebTransport — no install, no native client, no VPN. SSH-over-
|
SFTP) via WebTransport — no install, no native client, no VPN. The
|
||||||
WebTransport is HTTPS-shaped at the network layer (anti-censorship).
|
call protocol needs no proxy (it speaks EventEnvelope directly);
|
||||||
See [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md).
|
the ALPN-stream-proxy is the substrate's mechanism for the protocols
|
||||||
|
that need a client-side parser. SSH-over-WebTransport is
|
||||||
|
HTTPS-shaped at the network layer (anti-censorship). See
|
||||||
|
[ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md)
|
||||||
|
and [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md).
|
||||||
8. **`h3` requires X.509.** Browsers don't support RFC 7250 raw keys
|
8. **`h3` requires X.509.** Browsers don't support RFC 7250 raw keys
|
||||||
(ADR-027). A node serving WebTransport must have an X.509 identity.
|
(ADR-027). A node serving WebTransport must have an X.509 identity.
|
||||||
This is a browser limitation, not an alknet decision.
|
This is a browser limitation, not an alknet decision.
|
||||||
|
9. **WebTransport is a bidirectional ALPN transport substrate.**
|
||||||
|
WebTransport carries ALPN protocols as bidirectional streams; the
|
||||||
|
call protocol is the first/canonical target (JSON-RPC over QUIC
|
||||||
|
streams, needs no WASM parser, runs in Deno/Node/browsers/native
|
||||||
|
Rust). Both sides of a WebTransport call-protocol session can
|
||||||
|
initiate calls — the call protocol's bidirectionality applies
|
||||||
|
unchanged. The HTTP/1.1 + HTTP/2 surface is the one-directional
|
||||||
|
projection (HTTP is request/response); WebTransport restores the
|
||||||
|
bidirectional call model. See
|
||||||
|
[ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -352,8 +352,8 @@ once published, the 5-endpoint gateway shape is one-way.
|
|||||||
| `from_openapi` provenance is a leaf | [ADR-022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | `composition_authority: None`, `scoped_env: None` |
|
| `from_openapi` provenance is a leaf | [ADR-022](../../decisions/022-handler-registration-provenance-and-composition-authority.md) | `composition_authority: None`, `scoped_env: None` |
|
||||||
| Error fidelity (`HTTP_<status>` codes) | [ADR-023](../../decisions/023-operation-error-schemas.md) | No collision with protocol codes; `to_openapi` projects back |
|
| Error fidelity (`HTTP_<status>` codes) | [ADR-023](../../decisions/023-operation-error-schemas.md) | No collision with protocol codes; `to_openapi` projects back |
|
||||||
| No-env-vars credential injection | [ADR-014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Handler reads `context.capabilities`, not env vars |
|
| No-env-vars credential injection | [ADR-014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Handler reads `context.capabilities`, not env vars |
|
||||||
| HTTP path = operation path | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | `to_openapi` paths mirror `/{service}/{op}` |
|
| HTTP path = operation path (direct-call surface) | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | `POST /{service}/{op}` → `call.requested` (the direct-call surface; not what `to_openapi` describes) |
|
||||||
| `to_openapi` gateway pattern | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | 5 fixed gateway endpoints (search/schema/call/batch/subscribe), not one path per operation; per-caller AccessControl-filtered |
|
| `to_openapi` gateway pattern | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | 5 fixed gateway endpoints (search/schema/call/batch/subscribe), not one path per operation; per-caller AccessControl-filtered. Supersedes ADR-036's original `to_openapi` "paths mirror `/{service}/{op}`" clause |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,11 @@ pub struct HttpAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The stealth decoy surface for paths that are not registered
|
/// The stealth decoy surface for paths that are not registered
|
||||||
/// operations (and not `/healthz`, `/openapi.json`, or the MCP route).
|
/// operations (and not `/healthz`, `/openapi.json`, the `to_openapi`
|
||||||
/// Set by the assembly layer at `HttpAdapter` construction. The
|
/// gateway endpoints `/search`/`/schema`/`/call`/`/batch`/`/subscribe`,
|
||||||
/// existence of the decoy path is fixed by ADR-010; the variant is a
|
/// or the MCP route). Set by the assembly layer at `HttpAdapter`
|
||||||
/// two-way-door config default.
|
/// construction. The existence of the decoy path is fixed by ADR-010;
|
||||||
|
/// the variant is a two-way-door config default.
|
||||||
pub enum DecoyConfig {
|
pub enum DecoyConfig {
|
||||||
/// Serve a fake `404 Not Found` (the default — matches the reference
|
/// Serve a fake `404 Not Found` (the default — matches the reference
|
||||||
/// implementation's "fake nginx 404").
|
/// implementation's "fake nginx 404").
|
||||||
@@ -99,10 +100,25 @@ identity provider through the router's state.
|
|||||||
The axum `Router` is the single routing surface for HTTP requests. It
|
The axum `Router` is the single routing surface for HTTP requests. It
|
||||||
contains:
|
contains:
|
||||||
|
|
||||||
- The call-protocol projection routes (`POST /{service}/{op}` →
|
- **The direct-call surface** (`POST /{service}/{op}` → `call.requested`
|
||||||
`call.requested` dispatch — ADR-036).
|
dispatch — ADR-036). This is the HTTP projection of the call protocol's
|
||||||
|
`/{service}/{op}` operation path; an HTTP client that knows the
|
||||||
|
operation name calls it directly.
|
||||||
|
- **The `to_openapi` gateway endpoints** (`/search`, `/schema`, `/call`,
|
||||||
|
`/batch`, `/subscribe` — ADR-042). These are the fixed 5-endpoint
|
||||||
|
gateway that an OpenAPI consumer uses to discover and invoke
|
||||||
|
operations without knowing operation names up front. `/call` and
|
||||||
|
`/subscribe` dispatch through the same `OperationRegistry::invoke()`
|
||||||
|
as the direct-call surface; `/search` and `/schema` dispatch the
|
||||||
|
`services/list` / `services/schema` discovery ops. The gateway and
|
||||||
|
the direct-call surface coexist on the same router — they are two
|
||||||
|
projections of the same operation registry, not two registries.
|
||||||
- `GET /healthz` (raw route, no auth, no call protocol).
|
- `GET /healthz` (raw route, no auth, no call protocol).
|
||||||
- `GET /openapi.json` (serves the `to_openapi` projection).
|
- `GET /openapi.json` (serves the `to_openapi` projection — the OpenAPI
|
||||||
|
document that *describes* the 5 gateway endpoints. Post-ADR-042 this
|
||||||
|
is the gateway's description doc, not a per-operation REST spec; the
|
||||||
|
doc describes the 5 fixed endpoints, and the per-caller operation
|
||||||
|
surface is discovered via `/search`, not preloaded into `paths`).
|
||||||
- The stealth decoy fallback (unknown paths).
|
- The stealth decoy fallback (unknown paths).
|
||||||
- (Feature-gated) `POST /mcp` (the `to_mcp` streamable HTTP service —
|
- (Feature-gated) `POST /mcp` (the `to_mcp` streamable HTTP service —
|
||||||
[http-mcp.md](http-mcp.md)).
|
[http-mcp.md](http-mcp.md)).
|
||||||
@@ -155,6 +171,32 @@ This is the HTTP/1.1 + HTTP/2 streaming projection. Over WebTransport
|
|||||||
(`h3`), the subscription projects directly onto a WebTransport
|
(`h3`), the subscription projects directly onto a WebTransport
|
||||||
bidirectional stream — no SSE framing (see [webtransport.md](webtransport.md)).
|
bidirectional stream — no SSE framing (see [webtransport.md](webtransport.md)).
|
||||||
|
|
||||||
|
### One-directional projection (HTTP request/response)
|
||||||
|
|
||||||
|
The HTTP/1.1 + HTTP/2 surface is a **lossy, one-directional projection**
|
||||||
|
of the call protocol. HTTP is request/response: the client initiates,
|
||||||
|
the server responds. The call protocol is bidirectional — both sides can
|
||||||
|
initiate calls (see
|
||||||
|
[../call/call-protocol.md](../call/call-protocol.md) §"Bidirectional
|
||||||
|
Calls": the server can call operations on the client just as the client
|
||||||
|
calls operations on the server). The HTTP projection carries only the
|
||||||
|
client→server call direction; the server→client call direction has no
|
||||||
|
HTTP expression (there is no HTTP mechanism for the server to initiate a
|
||||||
|
request to the client). `Subscription` streaming is the one partial
|
||||||
|
exception — the server streams `call.responded` frames back over the
|
||||||
|
SSE response — but even there, the *call* is client-initiated; only the
|
||||||
|
*results* flow server→client.
|
||||||
|
|
||||||
|
This is a structural property of HTTP, not a design choice in this
|
||||||
|
crate. WebTransport (`h3`) restores the bidirectional call model: a
|
||||||
|
WebTransport session is a long-lived connection over which either side
|
||||||
|
can open bidirectional streams and send `call.requested` events in
|
||||||
|
either direction — the call protocol's native bidirectionality applies
|
||||||
|
unchanged. See [webtransport.md](webtransport.md) and ADR-043. The
|
||||||
|
HTTP/1.1 + HTTP/2 surface is the projection for clients that only speak
|
||||||
|
HTTP; WebTransport is the surface for clients that can speak the call
|
||||||
|
protocol in both directions.
|
||||||
|
|
||||||
### Auth
|
### Auth
|
||||||
|
|
||||||
Inbound HTTP auth is `Authorization: Bearer <token>`, resolved via
|
Inbound HTTP auth is `Authorization: Bearer <token>`, resolved via
|
||||||
@@ -219,8 +261,9 @@ routes. `healthz` is the one exception. See ADR-036.
|
|||||||
### Stealth decoy
|
### Stealth decoy
|
||||||
|
|
||||||
For paths that are not registered operations (and not `/healthz`,
|
For paths that are not registered operations (and not `/healthz`,
|
||||||
`/openapi.json`, or the MCP route), the HTTP handler serves a decoy. The
|
`/openapi.json`, the `to_openapi` gateway endpoints `/search`/`/schema`/
|
||||||
decoy is configurable (`DecoyConfig`):
|
`/call`/`/batch`/`/subscribe`, or the MCP route), the HTTP handler serves
|
||||||
|
a decoy. The decoy is configurable (`DecoyConfig`):
|
||||||
|
|
||||||
- A fake `404 Not Found` (the default — matches the reference
|
- A fake `404 Not Found` (the default — matches the reference
|
||||||
implementation's "fake nginx 404").
|
implementation's "fake nginx 404").
|
||||||
@@ -235,9 +278,13 @@ config is a two-way-door default (an operator picks what to serve); the
|
|||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
|
|
||||||
- **The HTTP path IS the operation path.** `POST /fs/readFile` →
|
- **The HTTP path IS the operation path on the direct-call surface.**
|
||||||
`call.requested` for `fs/readFile`. No second routing table. See
|
`POST /fs/readFile` → `call.requested` for `fs/readFile`. No second
|
||||||
ADR-036.
|
routing table for the direct-call surface. See ADR-036. The
|
||||||
|
`to_openapi` gateway (`/search`, `/schema`, `/call`, `/batch`,
|
||||||
|
`/subscribe`) is a separate fixed-endpoint surface (ADR-042) that
|
||||||
|
coexists with the direct-call surface on the same axum `Router`; it
|
||||||
|
does not replace it.
|
||||||
- **`External` operations only.** `Internal` operations return `404`
|
- **`External` operations only.** `Internal` operations return `404`
|
||||||
on the HTTP handler.
|
on the HTTP handler.
|
||||||
- **Bearer-only auth.** `Authorization: Bearer` →
|
- **Bearer-only auth.** `Authorization: Bearer` →
|
||||||
@@ -256,7 +303,8 @@ config is a two-way-door default (an operator picks what to serve); the
|
|||||||
|
|
||||||
| Decision | ADR | Summary |
|
| Decision | ADR | Summary |
|
||||||
|----------|-----|---------|
|
|----------|-----|---------|
|
||||||
| Direct path mapping (HTTP path = operation path) | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | `POST /{service}/{op}` → `call.requested` |
|
| Direct path mapping (HTTP path = operation path) | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | `POST /{service}/{op}` → `call.requested` (direct-call surface) |
|
||||||
|
| `to_openapi` gateway endpoints on the router | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | `/search`/`/schema`/`/call`/`/batch`/`/subscribe` coexist with the direct-call surface |
|
||||||
| SSE projection for subscriptions over h2/http1.1 | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | `call.responded` stream → SSE frames |
|
| SSE projection for subscriptions over h2/http1.1 | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | `call.responded` stream → SSE frames |
|
||||||
| `/healthz` is a raw route | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | No auth, no call protocol |
|
| `/healthz` is a raw route | [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) | No auth, no call protocol |
|
||||||
| Stealth decoy | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | HTTP handler on standard ALPNs serves decoy |
|
| Stealth decoy | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | HTTP handler on standard ALPNs serves decoy |
|
||||||
|
|||||||
@@ -54,6 +54,24 @@ headers, streaming, SSE), so they belong in one crate. See
|
|||||||
[ADR-039](../../decisions/039-http-server-and-client-host-colocated.md)
|
[ADR-039](../../decisions/039-http-server-and-client-host-colocated.md)
|
||||||
for the colocation decision.
|
for the colocation decision.
|
||||||
|
|
||||||
|
A note on the "from/to" direction model: the `from_openapi`/`to_openapi`
|
||||||
|
and `from_mcp`/`to_mcp` adapters are *inherently directional* because
|
||||||
|
OpenAPI and MCP are client/server protocols — one side serves, the
|
||||||
|
other calls. That directionality is a property of those protocols, not
|
||||||
|
of the call protocol itself. The call protocol is bidirectional (see
|
||||||
|
[../call/call-protocol.md](../call/call-protocol.md) §"Bidirectional
|
||||||
|
Calls": both sides can initiate calls). The HTTP/1.1 + HTTP/2 surface
|
||||||
|
inherits HTTP's request/response constraint and projects the call
|
||||||
|
protocol one-directionally (client→server calls only — see
|
||||||
|
[http-server.md](http-server.md) §"One-directional projection").
|
||||||
|
WebTransport (`h3`) is the HTTP-family transport that restores the
|
||||||
|
call protocol's native bidirectionality — it is a transport substrate
|
||||||
|
for the call protocol (and, via the ALPN-stream-proxy, for any ALPN),
|
||||||
|
not a browser→hub one-way path. See [webtransport.md](webtransport.md)
|
||||||
|
and ADR-043. The "from/to" naming of the OpenAPI/MCP adapters should not
|
||||||
|
be read as a statement about the call protocol's directionality; it is
|
||||||
|
a statement about OpenAPI's and MCP's directionality.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -205,9 +223,10 @@ verified against this invariant. See ADR-014 and
|
|||||||
| MCP stdio transport exclusion | [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md) | Streamable HTTP only; stdio is not built (RCE vector) |
|
| MCP stdio transport exclusion | [ADR-037](../../decisions/037-mcp-stdio-transport-exclusion.md) | Streamable HTTP only; stdio is not built (RCE vector) |
|
||||||
| HTTP/3 + WebTransport first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | `h3` in scope, not deferred; browser streaming uses QUIC streams |
|
| HTTP/3 + WebTransport first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | `h3` in scope, not deferred; browser streaming uses QUIC streams |
|
||||||
| HTTP server + client host colocated | [ADR-039](../../decisions/039-http-server-and-client-host-colocated.md) | One crate for server + adapters (shared HTTP deps, shared mapping) |
|
| HTTP server + client host colocated | [ADR-039](../../decisions/039-http-server-and-client-host-colocated.md) | One crate for server + adapters (shared HTTP deps, shared mapping) |
|
||||||
| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | Browser → WebTransport stream → any ALPN handler (SSH, git, SFTP) via WASM parser |
|
| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser |
|
||||||
| `to_mcp` tool-gateway pattern | [ADR-041](../../decisions/041-mcp-tool-gateway-pattern.md) | 4 fixed gateway tools (search/schema/call/batch), not one tool per operation |
|
| `to_mcp` tool-gateway pattern | [ADR-041](../../decisions/041-mcp-tool-gateway-pattern.md) | 4 fixed gateway tools (search/schema/call/batch), not one tool per operation |
|
||||||
| `to_openapi` gateway pattern | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | 5 fixed gateway endpoints (search/schema/call/batch/subscribe); per-caller AccessControl-filtered |
|
| `to_openapi` gateway pattern | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | 5 fixed gateway endpoints (search/schema/call/batch/subscribe); per-caller AccessControl-filtered |
|
||||||
|
| WebTransport bidirectional ALPN substrate | [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | WebTransport carries ALPNs as bidirectional streams; call protocol is the first target; both sides can initiate calls; non-peer clients use a connection-local overlay |
|
||||||
| `alknet-call` is protocol-foundation | [ADR-003](../../decisions/003-crate-decomposition.md) Am. 1 | `alknet-http` depends on `alknet-call` (types, not peer handler) |
|
| `alknet-call` is protocol-foundation | [ADR-003](../../decisions/003-crate-decomposition.md) Am. 1 | `alknet-http` depends on `alknet-call` (types, not peer handler) |
|
||||||
| Bearer auth via `resolve_from_token` | [ADR-004](../../decisions/004-auth-as-shared-core.md) | HTTP handler credential source + resolution (settled) |
|
| Bearer auth via `resolve_from_token` | [ADR-004](../../decisions/004-auth-as-shared-core.md) | HTTP handler credential source + resolution (settled) |
|
||||||
| Stealth mode = HTTP handler on standard ALPNs | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Decoy for unknown paths (settled) |
|
| Stealth mode = HTTP handler on standard ALPNs | [ADR-010](../../decisions/010-alpn-router-and-endpoint.md) | Decoy for unknown paths (settled) |
|
||||||
|
|||||||
@@ -6,11 +6,17 @@ last_updated: 2026-06-29
|
|||||||
# WebTransport — the h3 ALPN handler
|
# WebTransport — the h3 ALPN handler
|
||||||
|
|
||||||
The `HttpAdapter` registration for the `h3` ALPN: HTTP/3 and
|
The `HttpAdapter` registration for the `h3` ALPN: HTTP/3 and
|
||||||
WebTransport. This document covers the WebTransport session/stream
|
WebTransport. WebTransport is a **bidirectional ALPN transport
|
||||||
handling, the browser streaming path, the ALPN-stream-proxy (browser
|
substrate** (ADR-043) — it carries ALPN protocols as bidirectional
|
||||||
access to any ALPN handler via WebTransport), and the relationship to
|
streams, with the call protocol as the first/canonical target (needs no
|
||||||
the `h2`/`http/1.1` server. The `h3` support is a first-class transport,
|
WASM parser) and the ALPN-stream-proxy (ADR-040) as the mechanism for
|
||||||
not a deferral (ADR-038).
|
non-call ALPNs (SSH, git, SFTP) that need a client-side parser. This
|
||||||
|
document covers the WebTransport session/stream handling, the
|
||||||
|
substrate's three stream destinations, the no-`PeerId` connection-local
|
||||||
|
overlay for non-peer clients, and the relationship to the `h2`/
|
||||||
|
`http/1.1` server (the one-directional projection WebTransport restores
|
||||||
|
bidirectionality for). The `h3` support is a first-class transport
|
||||||
|
(ADR-038).
|
||||||
|
|
||||||
## What
|
## What
|
||||||
|
|
||||||
@@ -20,18 +26,32 @@ enabled. It serves two things on a single `h3` connection:
|
|||||||
|
|
||||||
1. **HTTP/3 requests** — the standard HTTP/3 over QUIC framing. An
|
1. **HTTP/3 requests** — the standard HTTP/3 over QUIC framing. An
|
||||||
HTTP/3 request is dispatched through the same axum `Router` as `h2`/
|
HTTP/3 request is dispatched through the same axum `Router` as `h2`/
|
||||||
`http/1.1` requests (ADR-036 — the HTTP path IS the operation path).
|
`http/1.1` requests (ADR-036 — the HTTP path IS the operation path
|
||||||
From the axum router's perspective, an HTTP/3 request is just
|
on the direct-call surface; ADR-042 — the gateway endpoints). From
|
||||||
|
the axum router's perspective, an HTTP/3 request is just
|
||||||
another HTTP request; the framing difference is handled below the
|
another HTTP request; the framing difference is handled below the
|
||||||
router.
|
router. The HTTP/3 request path is the **one-directional projection**
|
||||||
2. **WebTransport sessions** — the browser streaming path. A WebTransport
|
(client→server calls only — HTTP is request/response; see
|
||||||
session is a long-lived connection over which the browser opens
|
[http-server.md](http-server.md) §"One-directional projection").
|
||||||
bidirectional and unidirectional streams. Streams within a session
|
2. **WebTransport sessions** — the **bidirectional** path. WebTransport
|
||||||
target one of three destinations (see [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md)):
|
is a transport substrate that carries ALPN protocols as
|
||||||
- The call protocol (`EventEnvelope` → the call `Dispatcher`),
|
bidirectional streams (ADR-043), not a browser→hub one-way path. A
|
||||||
|
WebTransport session is a long-lived connection over which either
|
||||||
|
side can open bidirectional and unidirectional streams. Streams
|
||||||
|
within a session target one of three destinations (see
|
||||||
|
[ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md)):
|
||||||
|
- The call protocol (`EventEnvelope` → the call `Dispatcher`) — the
|
||||||
|
canonical target; needs no WASM parser because the EventEnvelope
|
||||||
|
framing is platform/language/runtime agnostic (JSON-RPC over QUIC
|
||||||
|
streams). Both sides can initiate calls — the call protocol's
|
||||||
|
bidirectionality applies unchanged (ADR-043 §2,
|
||||||
|
[../call/call-protocol.md](../call/call-protocol.md) §
|
||||||
|
"Bidirectional Calls").
|
||||||
- An ALPN handler proxy (the stream is handed to another ALPN
|
- An ALPN handler proxy (the stream is handed to another ALPN
|
||||||
handler like `SshAdapter` — the browser runs a WASM parser for the
|
handler like `SshAdapter` — the client runs a WASM parser for the
|
||||||
target protocol), or
|
target protocol). This is the substrate's mechanism for non-call
|
||||||
|
ALPNs (SSH, git, SFTP) that need a parser on the client side
|
||||||
|
(ADR-043 §4).
|
||||||
- Another sub-protocol (declared at CONNECT time).
|
- Another sub-protocol (declared at CONNECT time).
|
||||||
|
|
||||||
The ALPN-stream-proxy is what makes the browser a universal alknet
|
The ALPN-stream-proxy is what makes the browser a universal alknet
|
||||||
@@ -39,23 +59,38 @@ client: with a WASM parser for SSH (or SFTP, git), a browser can reach
|
|||||||
any ALPN handler via WebTransport, no install, no native client, no
|
any ALPN handler via WebTransport, no install, no native client, no
|
||||||
VPN. This is the "VPN-like without being a VPN" use case the project
|
VPN. This is the "VPN-like without being a VPN" use case the project
|
||||||
was originally built for, now on a clean foundation. See
|
was originally built for, now on a clean foundation. See
|
||||||
[ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md).
|
[ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) and
|
||||||
|
the substrate framing in [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md).
|
||||||
|
|
||||||
### Why h3 is a first-class transport
|
### Why h3 is a first-class transport
|
||||||
|
|
||||||
WebTransport is the browser streaming transport. QUIC streams are cheap
|
WebTransport is the bidirectional streaming transport for the call
|
||||||
(multiplexed over one connection, no head-of-line blocking), and
|
protocol and a transport substrate for any ALPN. QUIC streams are
|
||||||
WebTransport is supported in major browsers. The call protocol's
|
cheap (multiplexed over one connection, no head-of-line blocking), and
|
||||||
subscription/streaming model maps onto WebTransport streams with no
|
WebTransport is supported in major browsers and beyond (Deno, Node,
|
||||||
translation loss — a `call.responded` stream over a WebTransport
|
native Rust). The call protocol's subscription/streaming model maps
|
||||||
bidirectional stream is the native representation, not an SSE
|
onto WebTransport streams with no translation loss — a `call.responded`
|
||||||
translation (which is the projection for `h2`/`http/1.1` clients per
|
stream over a WebTransport bidirectional stream is the native
|
||||||
ADR-036).
|
representation, not an SSE translation (which is the projection for
|
||||||
|
`h2`/`http/1.1` clients per ADR-036).
|
||||||
|
|
||||||
The Phase 0 research framing ("defer h3/WebTransport past v1") was a
|
More importantly, **WebTransport restores the call protocol's
|
||||||
residual of the "two-way door as deferral" anti-pattern (ADR-009 §"What
|
bidirectionality** that the HTTP/1.1 + HTTP/2 surface structurally
|
||||||
this framework is NOT"). WebTransport is in scope, in this crate, as a
|
cannot carry. HTTP is request/response — the client initiates, the
|
||||||
first-class transport. See ADR-038.
|
server responds; the server→client *call* direction has no HTTP
|
||||||
|
expression (see [http-server.md](http-server.md) §"One-directional
|
||||||
|
projection"). WebTransport is a long-lived connection over which either
|
||||||
|
side can open bidirectional streams and send `call.requested` in either
|
||||||
|
direction — the call protocol's native bidirectionality applies
|
||||||
|
unchanged (ADR-043 §2). WebTransport is also supported beyond browsers
|
||||||
|
(Deno, Node, native Rust via `wtransport`), and the call protocol —
|
||||||
|
JSON-RPC over QUIC streams — is platform/language/runtime agnostic, so
|
||||||
|
call-protocol-over-WebTransport is a general bidirectional RPC
|
||||||
|
substrate, not a browser-only path (ADR-043 §1).
|
||||||
|
|
||||||
|
WebTransport is in scope, in this crate, as a first-class transport
|
||||||
|
(ADR-038). See [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md)
|
||||||
|
for the substrate framing.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -90,7 +125,7 @@ HTTP/3 request and a WebTransport stream.
|
|||||||
### WebTransport session and stream handling
|
### WebTransport session and stream handling
|
||||||
|
|
||||||
Once a WebTransport session is established (via extended CONNECT), the
|
Once a WebTransport session is established (via extended CONNECT), the
|
||||||
browser creates bidirectional streams within it. The handler dispatches
|
client creates bidirectional streams within it. The handler dispatches
|
||||||
each stream to one of three destinations, determined by the session's
|
each stream to one of three destinations, determined by the session's
|
||||||
CONNECT path (the routing key, declared at CONNECT time — not by peeking
|
CONNECT path (the routing key, declared at CONNECT time — not by peeking
|
||||||
the first application frame):
|
the first application frame):
|
||||||
@@ -102,14 +137,28 @@ the first application frame):
|
|||||||
for `EventEnvelope` and [../call/client-and-adapters.md](../call/client-and-adapters.md)
|
for `EventEnvelope` and [../call/client-and-adapters.md](../call/client-and-adapters.md)
|
||||||
§"Shared Dispatcher" for the `Dispatcher` — the same dispatch loop
|
§"Shared Dispatcher" for the `Dispatcher` — the same dispatch loop
|
||||||
the `CallAdapter` uses for `alknet/call` connections, ADR-012,
|
the `CallAdapter` uses for `alknet/call` connections, ADR-012,
|
||||||
stream-agnostic correlation). The browser speaks the EventEnvelope
|
stream-agnostic correlation). The client speaks the EventEnvelope
|
||||||
wire format directly over the WebTransport stream.
|
wire format directly over the WebTransport stream.
|
||||||
|
|
||||||
|
**Bidirectionality (ADR-043 §2):** the call-protocol session inherits
|
||||||
|
the call protocol's native bidirectionality — both sides can initiate
|
||||||
|
calls. The client calls operations on the hub; the hub can call
|
||||||
|
operations registered on the client's side, over the same session,
|
||||||
|
using the same `PendingRequestMap` and `EventEnvelope` framing as
|
||||||
|
`alknet/call` (see [../call/call-protocol.md](../call/call-protocol.md)
|
||||||
|
§"Bidirectional Calls"). The browser case (ADR-034 §4) is the common
|
||||||
|
case where the client registers no operations of its own, so the
|
||||||
|
server→client call direction is unused — that is a use-case scoping,
|
||||||
|
not an architectural limitation. A non-browser WebTransport client
|
||||||
|
(Deno, Node, a peer preferring WebTransport) that registers
|
||||||
|
operations receives calls from the hub over the same session.
|
||||||
- **`/alknet/<name>` → ALPN-handler proxy session.** Each bidirectional
|
- **`/alknet/<name>` → ALPN-handler proxy session.** Each bidirectional
|
||||||
stream is handed to the target ALPN handler (e.g., `SshAdapter` for
|
stream is handed to the target ALPN handler (e.g., `SshAdapter` for
|
||||||
`/alknet/ssh`, `GitAdapter` for `/alknet/git`) as a `Connection`
|
`/alknet/ssh`, `GitAdapter` for `/alknet/git`) as a `Connection`
|
||||||
wrapping the WebTransport stream. The browser runs a WASM parser for
|
wrapping the WebTransport stream. The client runs a WASM parser for
|
||||||
the target protocol and speaks it directly over the stream. This is
|
the target protocol and speaks it directly over the stream. This is
|
||||||
the ALPN-stream-proxy — see
|
the substrate's mechanism for non-call ALPNs (ADR-043 §4) — the
|
||||||
|
ALPN-stream-proxy — see
|
||||||
[ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md).
|
[ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md).
|
||||||
The `h3` handler looks up the target ALPN handler in the
|
The `h3` handler looks up the target ALPN handler in the
|
||||||
`HandlerRegistry` (`HttpAdapter` holds `Arc<HandlerRegistry>` for
|
`HandlerRegistry` (`HttpAdapter` holds `Arc<HandlerRegistry>` for
|
||||||
@@ -124,14 +173,17 @@ the first application frame):
|
|||||||
confirmation for sessions that multiplex sub-protocols, not the
|
confirmation for sessions that multiplex sub-protocols, not the
|
||||||
routing mechanism.
|
routing mechanism.
|
||||||
|
|
||||||
The browser's `WebTransport` JS API is the client side of this:
|
The browser's `WebTransport` JS API is one client side of this:
|
||||||
`new WebTransport('https://hub.example.com/alknet/ssh')` →
|
`new WebTransport('https://hub.example.com/alknet/ssh')` →
|
||||||
`transport.createBidirectionalStream()` → the browser's WASM SSH client
|
`transport.createBidirectionalStream()` → the browser's WASM SSH client
|
||||||
reads/writes the stream as a `BiStream` (ADR-007). No SSE translation,
|
reads/writes the stream as a `BiStream` (ADR-007). No SSE translation,
|
||||||
no HTTP framing — the target protocol speaks directly over the
|
no HTTP framing — the target protocol speaks directly over the
|
||||||
WebTransport stream. For the call-protocol session, the browser writes
|
WebTransport stream. For the call-protocol session, the browser writes
|
||||||
`EventEnvelope` frames; for an SSH session, the browser runs the WASM
|
`EventEnvelope` frames; for an SSH session, the browser runs the WASM
|
||||||
SSH parser.
|
SSH parser. A non-browser client (Deno, Node, native Rust) speaks the
|
||||||
|
same wire formats over the same substrate without a WASM parser — the
|
||||||
|
call protocol needs no parser, and native ALPN clients (SSH, git) use
|
||||||
|
native parsers rather than WASM.
|
||||||
|
|
||||||
### Subscription projection (native, not SSE)
|
### Subscription projection (native, not SSE)
|
||||||
|
|
||||||
@@ -143,14 +195,23 @@ closes the stream with an error frame. This is the native streaming
|
|||||||
projection; SSE (ADR-036) is the projection for `h2`/`http/1.1` clients
|
projection; SSE (ADR-036) is the projection for `h2`/`http/1.1` clients
|
||||||
that don't speak WebTransport.
|
that don't speak WebTransport.
|
||||||
|
|
||||||
### ALPN-stream-proxy (ADR-040)
|
### ALPN-stream-proxy (ADR-040, repositioned by ADR-043 §4)
|
||||||
|
|
||||||
The ALPN-stream-proxy is the `h3` handler's third stream destination and
|
The ALPN-stream-proxy is the `h3` handler's third stream destination and
|
||||||
the browser's gateway to every ALPN handler. A browser opens a
|
the substrate's mechanism for non-call ALPNs — the protocols (SSH, git,
|
||||||
WebTransport session to `/alknet/ssh` (or `/alknet/git`, `/alknet/sftp`),
|
SFTP) that need a client-side parser, unlike the call protocol which
|
||||||
and the `h3` handler hands each bidirectional stream within that session
|
speaks EventEnvelope directly. ADR-040 framed it as "the browser's
|
||||||
to the target ALPN handler as a `Connection`. The browser runs a WASM
|
gateway to every ALPN handler"; ADR-043 §4 repositions it as the
|
||||||
parser for the target protocol and speaks it directly over the stream.
|
substrate's non-call-ALPN mechanism, of which the browser use case is
|
||||||
|
the primary (but not the only) instance. The decision in ADR-040 (the
|
||||||
|
`HandlerRegistry` reference, path-based routing) stands unchanged; the
|
||||||
|
framing is what ADR-043 refines.
|
||||||
|
|
||||||
|
The browser use case: a browser opens a WebTransport session to
|
||||||
|
`/alknet/ssh` (or `/alknet/git`, `/alknet/sftp`), and the `h3` handler
|
||||||
|
hands each bidirectional stream within that session to the target ALPN
|
||||||
|
handler as a `Connection`. The browser runs a WASM parser for the
|
||||||
|
target protocol and speaks it directly over the stream.
|
||||||
|
|
||||||
**Why this matters:** SSH-over-WebTransport is HTTPS-shaped at the
|
**Why this matters:** SSH-over-WebTransport is HTTPS-shaped at the
|
||||||
network layer (WebTransport is HTTP/3 over QUIC over UDP, the same as
|
network layer (WebTransport is HTTP/3 over QUIC over UDP, the same as
|
||||||
@@ -217,6 +278,43 @@ alknet peer (ADR-034 §4): it gets no `PeerId`, does not enter
|
|||||||
`PeerCompositeEnv`, and its "ops" are WebTransport streams served by
|
`PeerCompositeEnv`, and its "ops" are WebTransport streams served by
|
||||||
the `h3` handler, not entries in the call-protocol peer-keyed overlay.
|
the `h3` handler, not entries in the call-protocol peer-keyed overlay.
|
||||||
|
|
||||||
|
### The no-`PeerId` connection-local overlay (ADR-043 §3)
|
||||||
|
|
||||||
|
A non-peer WebTransport client (a browser, or any WebTransport client
|
||||||
|
that is not a `PeerEntry`-bearing alknet peer) has **no `PeerId` on the
|
||||||
|
hub's side**. The connection is served by the `h3` handler; the
|
||||||
|
bearer-token-resolved `Identity` authorizes calls via
|
||||||
|
`AccessControl::check`, but the connection does not enter
|
||||||
|
`PeerCompositeEnv` and has no peer-keyed overlay entry. This is the
|
||||||
|
**inbound mirror of ADR-034 §2** (the outgoing pure-client X.509 case:
|
||||||
|
ops discovered land in "that connection's Layer 2 overlay" —
|
||||||
|
connection-local, no `PeerId`). On the inbound WebTransport path, ops
|
||||||
|
the client registers (if any) land in a connection-local Layer 2
|
||||||
|
overlay on the hub side — same pattern, opposite direction.
|
||||||
|
|
||||||
|
The `CallAdapter`'s `compose_root_env` builds the root
|
||||||
|
`OperationContext.env` from:
|
||||||
|
|
||||||
|
- the curated base (Layer 0),
|
||||||
|
- **this connection's** local overlay (Layer 2 — connection-scoped, not
|
||||||
|
peer-keyed), and
|
||||||
|
- the active session overlay (if any, ADR-024).
|
||||||
|
|
||||||
|
There is no `PeerCompositeEnv` entry because there is no `PeerId` to key
|
||||||
|
it. An implementer building `compose_root_env` for a WebTransport
|
||||||
|
session applies the ADR-034 §2 connection-local-overlay pattern (mirror
|
||||||
|
direction) and does not hunt for a `PeerId` that isn't there.
|
||||||
|
|
||||||
|
The case where the WebTransport client *is* a `PeerEntry`-bearing
|
||||||
|
alknet peer (a hub or spoke node that prefers WebTransport as its
|
||||||
|
transport) is the symmetric case: the connection has a `PeerId`
|
||||||
|
(resolved from the bearer token via
|
||||||
|
`IdentityProvider::resolve_from_token` → `Identity.id` =
|
||||||
|
`PeerEntry.peer_id`, ADR-030), and ops the peer registers land in the
|
||||||
|
peer-keyed overlay, exactly as they would over `alknet/call`. The
|
||||||
|
no-`PeerId` pattern above is the *non-peer* case; the peer case is
|
||||||
|
unchanged from the `alknet/call` model. See ADR-043 §3.
|
||||||
|
|
||||||
### Stealth on h3
|
### Stealth on h3
|
||||||
|
|
||||||
The `h3` handler participates in the same stealth model as `h2`/
|
The `h3` handler participates in the same stealth model as `h2`/
|
||||||
@@ -261,9 +359,20 @@ as a first-class transport.
|
|||||||
authenticates by bearer token, gets no `PeerId` (ADR-034 §4).
|
authenticates by bearer token, gets no `PeerId` (ADR-034 §4).
|
||||||
- **WebTransport streams target one of three destinations** (the
|
- **WebTransport streams target one of three destinations** (the
|
||||||
session's CONNECT path is the routing key): the call protocol
|
session's CONNECT path is the routing key): the call protocol
|
||||||
(`EventEnvelope` → `Dispatcher`), an ALPN handler proxy (→
|
(`EventEnvelope` → `Dispatcher`, bidirectional — both sides can
|
||||||
`HandlerRegistry` lookup → target handler's `handle()`), or another
|
initiate calls), an ALPN handler proxy (→ `HandlerRegistry` lookup
|
||||||
sub-protocol. See ADR-040.
|
→ target handler's `handle()`, the substrate's non-call-ALPN
|
||||||
|
mechanism), or another sub-protocol. See ADR-040 and ADR-043.
|
||||||
|
- **The call-protocol WebTransport session is bidirectional.** Both
|
||||||
|
sides can initiate calls, inheriting the call protocol's native
|
||||||
|
bidirectionality (ADR-043 §2). The browser case where the client
|
||||||
|
registers no ops is a use-case scoping, not an architectural
|
||||||
|
limitation.
|
||||||
|
- **Non-peer WebTransport clients use a connection-local overlay.**
|
||||||
|
A WebTransport client with no `PeerId` (browser, or any non-peer
|
||||||
|
client) has its registered ops land in a connection-local Layer 2
|
||||||
|
overlay, not the peer-keyed `PeerCompositeEnv`. This is the inbound
|
||||||
|
mirror of ADR-034 §2. See ADR-043 §3.
|
||||||
- **The ALPN-stream-proxy requires `Arc<HandlerRegistry>` on
|
- **The ALPN-stream-proxy requires `Arc<HandlerRegistry>` on
|
||||||
`HttpAdapter`.** The `h3` handler looks up ALPN handlers in the
|
`HttpAdapter`.** The `h3` handler looks up ALPN handlers in the
|
||||||
registry; the `h2`/`http/1.1` path does not use it. The registry is
|
registry; the `h2`/`http/1.1` path does not use it. The registry is
|
||||||
@@ -280,7 +389,8 @@ as a first-class transport.
|
|||||||
| Decision | ADR | Summary |
|
| Decision | ADR | Summary |
|
||||||
|----------|-----|---------|
|
|----------|-----|---------|
|
||||||
| `h3`/WebTransport is first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | In scope, not deferred; browser streaming uses QUIC streams |
|
| `h3`/WebTransport is first-class | [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md) | In scope, not deferred; browser streaming uses QUIC streams |
|
||||||
| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | Browser → WebTransport stream → any ALPN handler (SSH, git, SFTP) via WASM parser |
|
| WebTransport is a bidirectional ALPN transport substrate | [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md) | Carries ALPNs as bidirectional streams; call protocol is the first/canonical target (needs no WASM parser); both sides can initiate calls |
|
||||||
|
| WebTransport ALPN-stream-proxy | [ADR-040](../../decisions/040-webtransport-alpn-stream-proxy.md) | The substrate's mechanism for non-call ALPNs (SSH, git, SFTP) — browser → WebTransport stream → target ALPN handler via WASM parser |
|
||||||
| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3` needs X.509 (browser limitation) |
|
| Browsers require X.509 | [ADR-027](../../decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | `h3` needs X.509 (browser limitation) |
|
||||||
| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Bearer token, no `PeerId` |
|
| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) | Bearer token, no `PeerId` |
|
||||||
| WebTransport streams → call protocol directly | [ADR-012](../../decisions/012-call-protocol-stream-model.md) | Stream-agnostic; WebTransport stream = QUIC bidirectional stream |
|
| WebTransport streams → call protocol directly | [ADR-012](../../decisions/012-call-protocol-stream-model.md) | Stream-agnostic; WebTransport stream = QUIC bidirectional stream |
|
||||||
@@ -302,10 +412,16 @@ See [open-questions.md](../../open-questions.md) for full details.
|
|||||||
|
|
||||||
- [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md)
|
- [ADR-038](../../decisions/038-http3-and-webtransport-as-first-class.md)
|
||||||
— the decision that `h3` is in scope
|
— the decision that `h3` is in scope
|
||||||
|
- [ADR-043](../../decisions/043-webtransport-bidirectional-alpn-substrate.md)
|
||||||
|
— the substrate framing: WebTransport carries ALPNs as bidirectional
|
||||||
|
streams; call protocol is the first target; bidirectionality; the
|
||||||
|
no-`PeerId` connection-local overlay
|
||||||
- [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) —
|
- [ADR-036](../../decisions/036-http-to-call-operation-mapping.md) —
|
||||||
the HTTP-to-call mapping (the HTTP/3 request path uses the same
|
the HTTP-to-call mapping (the HTTP/3 request path uses the same
|
||||||
axum `Router`)
|
axum `Router`)
|
||||||
- [overview.md](overview.md) — crate overview, feature gates
|
- [overview.md](overview.md) — crate overview, feature gates
|
||||||
- [http-server.md](http-server.md) — the `h2`/`http/1.1` companion
|
- [http-server.md](http-server.md) — the `h2`/`http/1.1` companion
|
||||||
|
(§"One-directional projection" — the lossy HTTP/1.1+HTTP/2 surface
|
||||||
|
WebTransport restores bidirectionality for)
|
||||||
- `/workspace/wtransport/` — pure-Rust WebTransport reference
|
- `/workspace/wtransport/` — pure-Rust WebTransport reference
|
||||||
implementation (the `h3` feature's candidate dependency)
|
implementation (the `h3` feature's candidate dependency)
|
||||||
@@ -214,7 +214,7 @@ This keeps the peer graph populated only by full alknet nodes (role 3
|
|||||||
hubs and role-3-style spoke nodes), never by browsers or pure HTTP
|
hubs and role-3-style spoke nodes), never by browsers or pure HTTP
|
||||||
clients.
|
clients.
|
||||||
|
|
||||||
### 5. WebTransport relay-as-proxy is deferred with h3 / WebTransport
|
### 5. WebTransport relay-as-proxy is a transport-only feature, scoped separately
|
||||||
|
|
||||||
A **WebTransport proxy** that terminates the browser's WebTransport
|
A **WebTransport proxy** that terminates the browser's WebTransport
|
||||||
connection and proxies encrypted traffic to a hub's P2P endpoint
|
connection and proxies encrypted traffic to a hub's P2P endpoint
|
||||||
@@ -231,13 +231,18 @@ is a real feature, especially for the browser-to-P2P-peer case. It is
|
|||||||
Ed25519 identity is the same `ed25519:<hex>` whether the client
|
Ed25519 identity is the same `ed25519:<hex>` whether the client
|
||||||
connected directly or through the proxy.
|
connected directly or through the proxy.
|
||||||
|
|
||||||
WebTransport support is already deferred past v1 in the alknet-http
|
> **Amendment (wording only — the decision stands):** An earlier draft
|
||||||
Phase 0 findings (decision point DH-2, "h3/WebTransport — in v1 or
|
> of this section framed the relay-as-proxy as belonging to an
|
||||||
deferred?"). The WebTransport-relay-as-proxy feature
|
> "h3/WebTransport deferral bucket" and "lands when `h3` /
|
||||||
belongs in that same deferral bucket — it lands when `h3` /
|
> WebTransport lands." That framing was a residual of the "two-way door
|
||||||
WebTransport lands, and it does not require any change to the auth
|
> as deferral" anti-pattern (ADR-009 §"What this framework is NOT")
|
||||||
model in this ADR. It is recorded here so it is not lost; it is not an
|
> that [ADR-038](038-http3-and-webtransport-as-first-class.md) was later
|
||||||
open question for the auth model.
|
> written to reject — `h3`/WebTransport is a first-class transport, in
|
||||||
|
> scope, not deferred. The *auth-model* decision in this §5 (the proxy
|
||||||
|
> is transport-only; it does not change identity resolution) is
|
||||||
|
> unchanged. The *scope* question (does the proxy belong in
|
||||||
|
> `alknet-http` or a separate relay crate?) is tracked as OQ-38 — a
|
||||||
|
> genuine scope question, not a deferral.
|
||||||
|
|
||||||
### 6. On-chain / smart-contract peer discovery fits the OQ-36 adapter pattern
|
### 6. On-chain / smart-contract peer discovery fits the OQ-36 adapter pattern
|
||||||
|
|
||||||
@@ -300,9 +305,10 @@ It is noted here only to confirm it does not reopen OQ-37.
|
|||||||
WebTransport/HTTPS) is confirmed to need no new types — ADR-030's
|
WebTransport/HTTPS) is confirmed to need no new types — ADR-030's
|
||||||
`fingerprints: Vec<String>` already covers it.
|
`fingerprints: Vec<String>` already covers it.
|
||||||
- The WebTransport-relay-as-proxy and on-chain-discovery use cases are
|
- The WebTransport-relay-as-proxy and on-chain-discovery use cases are
|
||||||
recorded with clear homes (h3/WebTransport deferral bucket; OQ-36
|
recorded with clear homes (the relay-as-proxy is a transport-only
|
||||||
adapter pattern) so they don't get lost and don't reopen the auth
|
feature whose scope is tracked as OQ-38; the on-chain discovery
|
||||||
model.
|
follows the OQ-36 adapter pattern) so they don't get lost and don't
|
||||||
|
reopen the auth model.
|
||||||
|
|
||||||
**Negative:**
|
**Negative:**
|
||||||
- The `alknet-http` and `alknet-call` client paths must branch on
|
- The `alknet-http` and `alknet-call` client paths must branch on
|
||||||
@@ -389,8 +395,13 @@ It is noted here only to confirm it does not reopen OQ-37.
|
|||||||
repo/adapter pattern (trait in core, adapter additive in a separate
|
repo/adapter pattern (trait in core, adapter additive in a separate
|
||||||
crate)
|
crate)
|
||||||
- `docs/research/alknet-http/phase-0-findings.md` — DH-2 (h3 /
|
- `docs/research/alknet-http/phase-0-findings.md` — DH-2 (h3 /
|
||||||
WebTransport deferred past v1); the WebTransport-relay-as-proxy
|
WebTransport; the original "deferred past v1" framing is rejected by
|
||||||
feature noted in this ADR's §5 belongs in that deferral bucket
|
ADR-038); the WebTransport-relay-as-proxy feature noted in this ADR's
|
||||||
|
§5 is a transport-only feature whose scope is tracked as OQ-38
|
||||||
|
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — `h3` /
|
||||||
|
WebTransport is a first-class transport, not deferred (amends the
|
||||||
|
"deferral bucket" wording in this ADR's §5; the auth-model decision
|
||||||
|
stands)
|
||||||
- `docs/research/references/iroh/iroh/04-sub-crates.md` — iroh's
|
- `docs/research/references/iroh/iroh/04-sub-crates.md` — iroh's
|
||||||
transport relay (`iroh-relay`), referenced to distinguish it from
|
transport relay (`iroh-relay`), referenced to distinguish it from
|
||||||
alknet's hub role
|
alknet's hub role
|
||||||
|
|||||||
@@ -57,6 +57,21 @@ spec's `paths` mirror the `/{service}/{op}` operation paths. An external
|
|||||||
client reading the OpenAPI doc learns the same routes the HTTP handler
|
client reading the OpenAPI doc learns the same routes the HTTP handler
|
||||||
serves; there is no second mapping.
|
serves; there is no second mapping.
|
||||||
|
|
||||||
|
> **Amendment (superseded by [ADR-042](042-openapi-gateway-pattern.md) on
|
||||||
|
> the `to_openapi` clause):** The paragraph above described the original
|
||||||
|
> "per-operation-paths projection" — `to_openapi` generating one OpenAPI
|
||||||
|
> path entry per `External` operation, mirroring `/{service}/{op}`. ADR-042
|
||||||
|
> replaces this with the **gateway pattern**: `to_openapi` generates 5
|
||||||
|
> fixed gateway endpoints (`/search`, `/schema`, `/call`, `/batch`,
|
||||||
|
> `/subscribe`) instead of one path per operation. The "no second routing
|
||||||
|
> table" property is preserved (the gateway endpoints are fixed; the
|
||||||
|
> per-caller operation surface is discovered via `/search`, not preloaded
|
||||||
|
> into a generated path set). The direct-call surface (`POST
|
||||||
|
> /{service}/{op}`) that this ADR defines is **unchanged** — ADR-042 only
|
||||||
|
> changes what `to_openapi` *describes*, not what the HTTP handler
|
||||||
|
> *serves*. A traditional per-operation-paths OpenAPI projection remains
|
||||||
|
> available as an additive alternative (ADR-042 §5).
|
||||||
|
|
||||||
### HTTP method semantics
|
### HTTP method semantics
|
||||||
|
|
||||||
The call protocol's `OperationType` (`Query`, `Mutation`, `Subscription`,
|
The call protocol's `OperationType` (`Query`, `Mutation`, `Subscription`,
|
||||||
@@ -191,6 +206,10 @@ without auth before identity is resolvable.
|
|||||||
`to_openapi` as a projection; published-spec compatibility contract
|
`to_openapi` as a projection; published-spec compatibility contract
|
||||||
- [ADR-023](023-operation-error-schemas.md) — error schema fidelity in
|
- [ADR-023](023-operation-error-schemas.md) — error schema fidelity in
|
||||||
`from_openapi`/`to_openapi`; HTTP status mapping
|
`from_openapi`/`to_openapi`; HTTP status mapping
|
||||||
|
- [ADR-042](042-openapi-gateway-pattern.md) — supersedes this ADR's
|
||||||
|
`to_openapi` clause (the per-operation-paths projection is replaced by
|
||||||
|
the 5-endpoint gateway pattern; the direct-call surface this ADR
|
||||||
|
defines is unchanged)
|
||||||
- OQ-13 (resolved) — operation path format `/{service}/{op}`
|
- OQ-13 (resolved) — operation path format `/{service}/{op}`
|
||||||
- `docs/research/alknet-http/phase-0-findings.md` DH-3 — the decision this
|
- `docs/research/alknet-http/phase-0-findings.md` DH-3 — the decision this
|
||||||
ADR resolves
|
ADR resolves
|
||||||
|
|||||||
@@ -40,8 +40,14 @@ WebTransport stream. So the browser:
|
|||||||
The hub's `h3` handler needs to hand that WebTransport stream to the
|
The hub's `h3` handler needs to hand that WebTransport stream to the
|
||||||
target ALPN handler (e.g., `SshAdapter`) as if it were a QUIC stream
|
target ALPN handler (e.g., `SshAdapter`) as if it were a QUIC stream
|
||||||
arriving on that ALPN. The `h3` handler becomes an **ALPN-stream-proxy**:
|
arriving on that ALPN. The `h3` handler becomes an **ALPN-stream-proxy**:
|
||||||
a browser-side gateway that gives browsers access to any ALPN handler
|
a WebTransport-client-side gateway (browser or otherwise) that gives
|
||||||
via WebTransport.
|
WebTransport clients access to any non-call ALPN handler via WebTransport.
|
||||||
|
> Repositioned by [ADR-043](043-webtransport-bidirectional-alpn-substrate.md)
|
||||||
|
> §4: the proxy is the substrate's mechanism for non-call ALPNs (SSH,
|
||||||
|
> git, SFTP) that need a client-side parser, distinct from the call
|
||||||
|
> protocol which speaks EventEnvelope directly and needs no proxy. The
|
||||||
|
> browser is the primary use case; the decision (the `HandlerRegistry`
|
||||||
|
> reference, path-based routing) is unchanged.
|
||||||
|
|
||||||
### Why this matters
|
### Why this matters
|
||||||
|
|
||||||
@@ -275,6 +281,10 @@ Two layers, same as a native `alknet/ssh` connection.
|
|||||||
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — `h3` is
|
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — `h3` is
|
||||||
first-class (this ADR adds the ALPN-stream-proxy as the third stream
|
first-class (this ADR adds the ALPN-stream-proxy as the third stream
|
||||||
destination)
|
destination)
|
||||||
|
- [ADR-043](043-webtransport-bidirectional-alpn-substrate.md) §4 —
|
||||||
|
repositions this ADR's framing: the proxy is the substrate's mechanism
|
||||||
|
for non-call ALPNs (not the browser's gateway to every ALPN). The
|
||||||
|
decision stands; the framing is refined.
|
||||||
- `crates/http/webtransport.md` — the spec that implements this proxy
|
- `crates/http/webtransport.md` — the spec that implements this proxy
|
||||||
- `crates/core/endpoint.md` — `HandlerRegistry` (the registry the
|
- `crates/core/endpoint.md` — `HandlerRegistry` (the registry the
|
||||||
`h3` handler gains a reference to)
|
`h3` handler gains a reference to)
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
# ADR-043: WebTransport as a Bidirectional ALPN Transport Substrate
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`alknet-http`'s `h3`/WebTransport specs
|
||||||
|
([webtransport.md](../crates/http/webtransport.md),
|
||||||
|
[ADR-040](040-webtransport-alpn-stream-proxy.md)) describe the
|
||||||
|
WebTransport session as a browser-reached path: a browser opens a
|
||||||
|
WebTransport session to a hub, the hub's `h3` handler serves it. The
|
||||||
|
two stream destinations described (call-protocol `EventEnvelope`, and
|
||||||
|
the ALPN-handler proxy) are both framed browser→server: the browser
|
||||||
|
initiates, the hub responds.
|
||||||
|
|
||||||
|
That framing is correct for the browser case (ADR-034 §4 — browsers are
|
||||||
|
not alknet peers; they connect to a hub and authenticate by bearer
|
||||||
|
token), but it is **not the general case**, and writing the spec as if
|
||||||
|
it were leaks an assumption that is only true for the OpenAPI/MCP
|
||||||
|
direction model into the WebTransport architecture. Three concrete
|
||||||
|
problems result:
|
||||||
|
|
||||||
|
### Problem 1 — the call protocol is bidirectional; the WebTransport spec is not
|
||||||
|
|
||||||
|
The call protocol is explicitly bidirectional
|
||||||
|
([call-protocol.md](../crates/call/call-protocol.md) §"Bidirectional
|
||||||
|
Calls"): *"Both sides of the connection can initiate calls. The server
|
||||||
|
can call operations on the client just as the client calls operations
|
||||||
|
on the server."* The `CallConnection`/`Dispatcher` dispatch loop is
|
||||||
|
stream-agnostic (ADR-012) — a WebTransport bidirectional stream is a
|
||||||
|
QUIC bidirectional stream, and the call protocol's bidirectionality
|
||||||
|
applies unchanged over it.
|
||||||
|
|
||||||
|
The current `webtransport.md` describes only the browser-initiates-a-
|
||||||
|
call direction. A reader would reasonably conclude WebTransport is a
|
||||||
|
one-directional session (browser calls hub, hub responds), when in
|
||||||
|
fact a WebTransport call-protocol session inherits the call protocol's
|
||||||
|
bidirectionality: the hub can call operations registered on the
|
||||||
|
browser/WebTransport-client side, exactly as it can over `alknet/call`.
|
||||||
|
The spec doesn't say this, doesn't scope it down, and doesn't say *why*
|
||||||
|
it's scoped down. It's just silent.
|
||||||
|
|
||||||
|
### Problem 2 — the ALPN-stream-proxy is framed as "browser reaches hub ALPNs via WASM," not as "WebTransport carries ALPNs as streams"
|
||||||
|
|
||||||
|
ADR-040 frames the ALPN-stream-proxy as the browser's gateway to every
|
||||||
|
ALPN handler: a browser with a WASM parser for SSH (or SFTP, git) can
|
||||||
|
reach any ALPN handler via WebTransport. That framing is correct and
|
||||||
|
important (the anti-censorship property — SSH-over-WebTransport is
|
||||||
|
HTTPS-shaped — is real). But it bakes the browser-initiated direction
|
||||||
|
into the architecture.
|
||||||
|
|
||||||
|
WebTransport is more general than that: a WebTransport stream is a
|
||||||
|
QUIC bidirectional stream (ADR-012), and the `BiStream` trait
|
||||||
|
(`AsyncRead + AsyncWrite + Send + Unpin`, ADR-007) is source-agnostic.
|
||||||
|
WebTransport can carry **any** ALPN protocol as streams, in either
|
||||||
|
direction, between any two endpoints that can terminate WebTransport —
|
||||||
|
not only browser→hub. The call protocol is the **first/canonical**
|
||||||
|
target because it is already JSON-RPC over QUIC streams and needs no
|
||||||
|
WASM parser (the EventEnvelope framing is platform/language/runtime
|
||||||
|
agnostic), but it is one target among possible many. SSH, git, SFTP
|
||||||
|
are additional targets that require a WASM parser on the client side.
|
||||||
|
|
||||||
|
The current framing — "browser runs a WASM parser that reaches the
|
||||||
|
hub's ALPN handler" — is a *use case* of the proxy, not the *nature* of
|
||||||
|
it. The nature is: **WebTransport is a transport substrate that carries
|
||||||
|
ALPN protocols as bidirectional streams; the call protocol is the
|
||||||
|
straightforward first target, and any other ALPN can be proxied the same
|
||||||
|
way.**
|
||||||
|
|
||||||
|
### Problem 3 — "browsers are not peers" reconciles awkwardly with the WebTransport call session, and the reconciliation isn't stated
|
||||||
|
|
||||||
|
ADR-034 §4 establishes that a browser over WebTransport authenticates by
|
||||||
|
bearer token, gets no `PeerId`, and doesn't enter `PeerCompositeEnv`
|
||||||
|
(the peer-keyed overlay). ADR-034 §2 establishes the analogous
|
||||||
|
**outgoing** case: a pure-client X.509 dial has no client-side `PeerId`,
|
||||||
|
and ops discovered via `from_call`/`from_openapi`/`from_mcp` land in
|
||||||
|
"that connection's Layer 2 overlay" — connection-local, not in the
|
||||||
|
peer-keyed overlay.
|
||||||
|
|
||||||
|
The **inbound** WebTransport case is the mirror of ADR-034 §2: a
|
||||||
|
browser (or any non-peer WebTransport client) connects to a hub, the
|
||||||
|
hub's `h3` handler hands its streams to the call protocol's
|
||||||
|
`Dispatcher`, and the connection has no `PeerId` on the hub's side
|
||||||
|
either. Ops the browser registers (if it registers any — e.g., a
|
||||||
|
browser-based agent exposing local ops) land in a connection-local
|
||||||
|
Layer 2 overlay, exactly like the outgoing pure-client X.509 case.
|
||||||
|
`compose_root_env` builds the root `OperationContext.env` from the
|
||||||
|
curated base + that connection's local overlay + (if active) the
|
||||||
|
session overlay — *without* a peer-keyed entry, because there is no
|
||||||
|
`PeerId` to key it.
|
||||||
|
|
||||||
|
The current `webtransport.md` doesn't say this. A reader would
|
||||||
|
reasonably ask: *if this is the same `Dispatcher` as `alknet/call`,
|
||||||
|
where's the `PeerId`? how does `compose_root_env` build the root env for
|
||||||
|
a no-`PeerId` WebTransport call session?* The answer exists — it's the
|
||||||
|
ADR-034 §2 connection-local-overlay pattern applied inbound — it's
|
||||||
|
just not written down in the http crate.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. WebTransport is a bidirectional ALPN transport substrate; the call protocol is the first target
|
||||||
|
|
||||||
|
The `h3`/WebTransport handler is reframed: WebTransport is a
|
||||||
|
**transport substrate** that carries ALPN protocols as bidirectional
|
||||||
|
streams, not a browser→hub one-way path. The call protocol is the
|
||||||
|
**first/canonical target** — it is already JSON-RPC over QUIC streams
|
||||||
|
(ADR-012), needs no WASM parser (the EventEnvelope framing is
|
||||||
|
platform/language/runtime agnostic), and is supported in runtimes that
|
||||||
|
speak WebTransport (Deno, Node, browsers, native Rust via `wtransport`).
|
||||||
|
Other ALPN protocols (SSH, git, SFTP) are additional targets that
|
||||||
|
require a WASM parser on the browser/client side; the ALPN-stream-proxy
|
||||||
|
(ADR-040) is the mechanism for those targets. The call-protocol-over-
|
||||||
|
WebTransport path needs no proxy — it speaks the EventEnvelope wire
|
||||||
|
format directly.
|
||||||
|
|
||||||
|
This is a **framing** change to ADR-040 and `webtransport.md`, not a
|
||||||
|
structural change. The three stream destinations (call protocol,
|
||||||
|
ALPN-handler proxy, other sub-protocols) are unchanged; what changes is
|
||||||
|
how they are described. The call-protocol destination is the substrate's
|
||||||
|
canonical use; the ALPN-handler proxy is the substrate carrying other
|
||||||
|
ALPNs. The browser→hub direction is one use case of the substrate, not
|
||||||
|
its definition.
|
||||||
|
|
||||||
|
### 2. The WebTransport call-protocol session inherits the call protocol's bidirectionality
|
||||||
|
|
||||||
|
A WebTransport session opened to `/` or `/alknet/call` is a
|
||||||
|
call-protocol session. Within it, **both sides can initiate calls** —
|
||||||
|
the WebTransport client can call operations on the hub, and the hub can
|
||||||
|
call operations registered on the WebTransport client's side. This is
|
||||||
|
the call protocol's native bidirectionality (call-protocol.md §
|
||||||
|
"Bidirectional Calls"), applying unchanged over the WebTransport stream.
|
||||||
|
The `Dispatcher` is the same dispatch loop the `CallAdapter` uses for
|
||||||
|
`alknet/call` connections (ADR-012 — stream-agnostic correlation).
|
||||||
|
|
||||||
|
The browser case (ADR-034 §4) is the common case: a browser connects
|
||||||
|
to a hub, calls the hub's operations, and registers no operations of
|
||||||
|
its own — the server→client call direction is unused because the browser
|
||||||
|
has nothing to call. That is a use-case scoping, not an architectural
|
||||||
|
limitation. A non-browser WebTransport client (a Deno process, a Node
|
||||||
|
process, another alknet node that prefers WebTransport over raw
|
||||||
|
`alknet/call` QUIC) that registers operations on its side receives
|
||||||
|
calls from the hub over the same session. The spec must state this,
|
||||||
|
not leave it implicit.
|
||||||
|
|
||||||
|
### 3. The no-`PeerId` connection-local overlay (inbound mirror of ADR-034 §2)
|
||||||
|
|
||||||
|
A WebTransport call-protocol session from a non-peer client (a browser,
|
||||||
|
or any WebTransport client that is not a `PeerEntry`-bearing alknet
|
||||||
|
peer) has **no `PeerId` on the hub's side**. The connection is served by
|
||||||
|
the `h3` handler; the browser/client authenticates by bearer token
|
||||||
|
(ADR-034 §4); the resolved `Identity` authorizes calls via
|
||||||
|
`AccessControl::check`, but the connection does not enter
|
||||||
|
`PeerCompositeEnv` and has no peer-keyed overlay entry.
|
||||||
|
|
||||||
|
This is the **inbound mirror of ADR-034 §2** (the outgoing pure-client
|
||||||
|
X.509 case). Outbound: a `CallClient` dials a public X.509 endpoint,
|
||||||
|
ops discovered land in "that connection's Layer 2 overlay" —
|
||||||
|
connection-local, no `PeerId`. Inbound: a WebTransport client connects
|
||||||
|
to a hub, ops the client registers (if any) land in a connection-local
|
||||||
|
Layer 2 overlay on the hub side — same pattern, opposite direction. The
|
||||||
|
`CallAdapter`'s `compose_root_env` builds the root
|
||||||
|
`OperationContext.env` from:
|
||||||
|
|
||||||
|
- the curated base (Layer 0),
|
||||||
|
- **this connection's** local overlay (Layer 2 — connection-scoped, not
|
||||||
|
peer-keyed), and
|
||||||
|
- the active session overlay (if any, ADR-024).
|
||||||
|
|
||||||
|
There is no `PeerCompositeEnv` entry because there is no `PeerId` to key
|
||||||
|
it. This is the explicit closure of the "browser as peer" path
|
||||||
|
(ADR-034 §4) on the inbound side — the same closure ADR-034 §2 makes on
|
||||||
|
the outbound side. `webtransport.md` must state it so an implementer
|
||||||
|
building `compose_root_env` for a WebTransport session knows the
|
||||||
|
connection-local-overlay pattern applies and does not hunt for a
|
||||||
|
`PeerId` that isn't there.
|
||||||
|
|
||||||
|
The case where the WebTransport client *is* a `PeerEntry`-bearing
|
||||||
|
alknet peer (a hub or spoke node that prefers WebTransport as its
|
||||||
|
transport) is the symmetric case: the connection has a `PeerId`
|
||||||
|
(resolved from the bearer token via `IdentityProvider::resolve_from_token`
|
||||||
|
→ `Identity.id` = `PeerEntry.peer_id`, ADR-030), and ops the peer
|
||||||
|
registers land in the peer-keyed overlay, exactly as they would over
|
||||||
|
`alknet/call`. The no-`PeerId` pattern above is the *non-peer* case; the
|
||||||
|
peer case is unchanged from the `alknet/call` model.
|
||||||
|
|
||||||
|
### 4. ADR-040's ALPN-stream-proxy is the substrate's mechanism for non-call ALPNs
|
||||||
|
|
||||||
|
ADR-040 (the ALPN-stream-proxy) is not superseded by this ADR; it is
|
||||||
|
**repositioned**. The proxy is the substrate's mechanism for carrying
|
||||||
|
ALPN protocols *other than the call protocol* — SSH, git, SFTP — that
|
||||||
|
require a WASM parser on the client side. The call protocol needs no
|
||||||
|
proxy (it speaks EventEnvelope directly); the ALPN-stream-proxy is for
|
||||||
|
the protocols that do. The browser→hub direction is the primary use
|
||||||
|
case (a browser with a WASM SSH client reaching the hub's SSH handler),
|
||||||
|
but it is not the only one — any WebTransport-capable endpoint can
|
||||||
|
proxy any ALPN via the same mechanism.
|
||||||
|
|
||||||
|
This reframing does not change ADR-040's decision (the `h3` handler
|
||||||
|
gains `Arc<HandlerRegistry>`, streams route by CONNECT path); it
|
||||||
|
changes how the decision is described. The "three stream destinations"
|
||||||
|
in `webtransport.md` remain; what changes is the framing of the
|
||||||
|
ALPN-stream-proxy as the substrate's non-call-ALPN mechanism, not as
|
||||||
|
the browser's gateway.
|
||||||
|
|
||||||
|
### 5. HTTP/1.1 + HTTP/2 is the one-directional projection; WebTransport is the bidirectional one
|
||||||
|
|
||||||
|
The HTTP/1.1 + HTTP/2 surface projects the call protocol
|
||||||
|
one-directionally (client→server calls only — HTTP is request/response;
|
||||||
|
the server→client call direction has no HTTP expression). This is
|
||||||
|
named as a lossy consequence of HTTP in `http-server.md` §
|
||||||
|
"One-directional projection." WebTransport is the HTTP-family transport
|
||||||
|
that **restores** the call protocol's bidirectionality: a WebTransport
|
||||||
|
session is a long-lived connection over which either side can open
|
||||||
|
streams and send `call.requested` in either direction. The two surfaces
|
||||||
|
coexist on the `h3` ALPN (HTTP/3 requests use the axum `Router` — the
|
||||||
|
one-directional projection; WebTransport sessions use the call
|
||||||
|
protocol `Dispatcher` — the bidirectional one). An HTTP/3 request is
|
||||||
|
never a WebTransport stream, and vice versa (the HTTP/3 frame type
|
||||||
|
distinguishes them — see `webtransport.md`).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
- The WebTransport spec stops silently inheriting the OpenAPI/MCP
|
||||||
|
direction assumption. The call protocol's bidirectionality is named
|
||||||
|
as a property of WebTransport call sessions, not left implicit.
|
||||||
|
- The ALPN-stream-proxy is framed as the substrate's non-call-ALPN
|
||||||
|
mechanism, not as a browser-only gateway. The call protocol is named
|
||||||
|
as the first/canonical target — the easy case that needs no WASM
|
||||||
|
parser and runs in Deno, Node, and browsers.
|
||||||
|
- The inbound no-`PeerId` connection-local overlay is stated, so an
|
||||||
|
implementer building `compose_root_env` for a WebTransport session
|
||||||
|
applies the ADR-034 §2 pattern (mirror direction) and does not hunt
|
||||||
|
for a `PeerId`.
|
||||||
|
- The HTTP/1.1 + HTTP/2 one-directional projection is named as a lossy
|
||||||
|
consequence, and WebTransport is named as the surface that restores
|
||||||
|
bidirectionality. The two surfaces' relationship is clear.
|
||||||
|
- A non-browser WebTransport client (Deno, Node, a peer preferring
|
||||||
|
WebTransport) is a first-class case, not an accident of the spec's
|
||||||
|
browser framing.
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
- The WebTransport spec gains complexity: the browser-only framing was
|
||||||
|
simpler to describe. The bidirectional framing requires stating both
|
||||||
|
the browser case (no registered ops, server→client call direction
|
||||||
|
unused) and the non-browser case (registered ops, bidirectional
|
||||||
|
calls). This is honest complexity — the substrate is more general
|
||||||
|
than the browser-only framing suggested.
|
||||||
|
- The "browser is not a peer" property (ADR-034 §4) now has a
|
||||||
|
counterpart statement for the inbound overlay path. Readers must
|
||||||
|
understand two cases: peer WebTransport clients (in the peer-keyed
|
||||||
|
overlay) and non-peer WebTransport clients (in the connection-local
|
||||||
|
overlay). This mirrors the outbound ADR-034 §2/§3 split and is not
|
||||||
|
new structural complexity, but it is now stated in the http crate,
|
||||||
|
which it wasn't before.
|
||||||
|
- The ALPN-stream-proxy's reframing (substrate mechanism for non-call
|
||||||
|
ALPNs, not browser gateway) means ADR-040's prose reads slightly
|
||||||
|
differently from the spec's prose. ADR-040 is not superseded; its
|
||||||
|
*decision* (the `HandlerRegistry` reference, path-based routing)
|
||||||
|
stands. Its *framing* is repositioned by this ADR. A future amendment
|
||||||
|
to ADR-040 could inline the repositioning; for now this ADR records
|
||||||
|
it and `webtransport.md` reflects it.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
1. **The call protocol's bidirectionality applies unchanged over
|
||||||
|
WebTransport.** The `Dispatcher` is stream-agnostic (ADR-012); a
|
||||||
|
WebTransport bidirectional stream is a QUIC bidirectional stream.
|
||||||
|
No protocol change is needed to support server→client calls over
|
||||||
|
WebTransport — the same `call.requested`/`call.responded` framing
|
||||||
|
works in both directions, correlated by request ID, as it does over
|
||||||
|
`alknet/call`.
|
||||||
|
|
||||||
|
2. **The browser case is the common non-peer case; non-browser
|
||||||
|
WebTransport clients are the general case.** Most WebTransport
|
||||||
|
clients in v1 are browsers (the anti-censorship / universal-client
|
||||||
|
use case). Non-browser WebTransport clients (Deno, Node, native
|
||||||
|
Rust) are supported by the same code path; they may or may not be
|
||||||
|
peers depending on whether they present a `PeerEntry`-resolvable
|
||||||
|
bearer token. The spec describes both cases; the implementation is
|
||||||
|
one code path with a branch on "does this connection have a
|
||||||
|
`PeerId`?" at `compose_root_env` time.
|
||||||
|
|
||||||
|
3. **The ALPN-stream-proxy is not the only mechanism for non-call ALPNs
|
||||||
|
over WebTransport.** A future WebTransport session type could carry
|
||||||
|
non-call ALPNs without the proxy's `HandlerRegistry` lookup (e.g., a
|
||||||
|
session that negotiates a single ALPN at CONNECT time and speaks it
|
||||||
|
directly, without per-stream registry routing). The proxy is the
|
||||||
|
mechanism specified by ADR-040; this ADR does not foreclose others,
|
||||||
|
but does not spec them either (scope — not needed for the current
|
||||||
|
use cases).
|
||||||
|
|
||||||
|
4. **`PeerId` resolution for peer WebTransport clients follows the
|
||||||
|
same path as `alknet/call`.** A peer connecting over WebTransport
|
||||||
|
presents a bearer token; the hub resolves it via
|
||||||
|
`IdentityProvider::resolve_from_token`; the resulting `Identity.id`
|
||||||
|
is the `PeerId` (ADR-030). There is no WebTransport-specific peer
|
||||||
|
resolution path — the bearer-token path is the same regardless of
|
||||||
|
transport. This is an assumption, not a new decision: it follows from
|
||||||
|
ADR-004, ADR-030, and ADR-034 §4.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR-012](012-call-protocol-stream-model.md) — stream-agnostic
|
||||||
|
correlation (a WebTransport stream is a QUIC bidirectional stream;
|
||||||
|
the `Dispatcher` is the same dispatch loop)
|
||||||
|
- [ADR-007](007-bistream-type-definition.md) — `BiStream` trait
|
||||||
|
(source-agnostic; the contract a WebTransport stream satisfies)
|
||||||
|
- [ADR-027](027-tls-identity-redesign-acme-rawkey-decoupling.md) —
|
||||||
|
browsers require X.509 (the `h3` handler is domain-hosted)
|
||||||
|
- [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) §2 (outbound
|
||||||
|
no-`PeerId` connection-local overlay — this ADR's §3 is the inbound
|
||||||
|
mirror), §4 (browsers are not peers — the non-peer WebTransport case)
|
||||||
|
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — `h3` is
|
||||||
|
first-class (this ADR refines the framing, not the scope)
|
||||||
|
- [ADR-040](040-webtransport-alpn-stream-proxy.md) — the ALPN-stream-
|
||||||
|
proxy (this ADR repositions it as the substrate's non-call-ALPN
|
||||||
|
mechanism; the decision stands)
|
||||||
|
- [ADR-029](029-peer-graph-routing-model.md) — `PeerCompositeEnv` /
|
||||||
|
`PeerRef` (the peer-keyed overlay that non-peer WebTransport clients
|
||||||
|
do not enter)
|
||||||
|
- [ADR-030](030-peerentry-and-identity-id-decoupling.md) — `PeerId`
|
||||||
|
source (`Identity.id` from bearer-token resolution)
|
||||||
|
- `crates/http/webtransport.md` — the spec this ADR refines
|
||||||
|
- `crates/http/http-server.md` §"One-directional projection" — the
|
||||||
|
HTTP/1.1 + HTTP/2 lossy projection this ADR contrasts WebTransport
|
||||||
|
against
|
||||||
|
- `crates/call/call-protocol.md` §"Bidirectional Calls" — the
|
||||||
|
bidirectionality this ADR names as a WebTransport property
|
||||||
Reference in New Issue
Block a user