Three issues found in the http crate spec sanity check that would have
caused problems during task decomposition, now fixed:
C1 — /subscribe GET→POST: the gateway's /subscribe is an invoke endpoint
carrying { operation, input } in the body, but was listed as GET (which
has no body). Flipped to POST with Accept: text/event-stream negotiating
the SSE response, consistent with /call's flat-JSON-body invariant.
Browsers using EventSource can't POST but use WebSocket for the
bidirectional path; the HTTP gateway's /subscribe is for non-browser
HTTP clients (fetch + ReadableStream). Touches ADR-042, ADR-047,
ADR-048, http-adapters.md, http-server.md.
C2 — stale direct-call references: three spots contradicted ADR-047
(which removed the POST /{service}/{op} direct-call surface) and
ADR-046 §3 (which states /{service}/{op} is no longer reserved).
Cleaned up in http-server.md (custom-routes intro + collision list) and
ADR-046 §6 (default-surface list).
W2 — from_mcp output handling: the spec's fallback for tools without
outputSchema was Type.Unknown(), but the correct fallback is the MCP
ContentBlock union (text|image|audio|resource|resource_link) — a
well-defined MCP type, not Unknown. Fixed http-mcp.md with the full
structuredContent-preferred-over-content-blocks logic (matching the TS
adapter and rmcp SDK), enriched references with specific rmcp source
files. Also added shared-dispatch-spine notes to http-mcp.md and
http-adapters.md cross-referencing the new research findings.
Research (docs/research/alknet-http-gateway-factoring/findings.md):
to_mcp and to_openapi share a dispatch spine (resolve → invoke → map).
Recommendation: extract a thin shared struct now, not a GatewayDispatch
trait — the server-integration layers (axum routes vs rmcp
StreamableHttpService) and wire-framing stay per-gateway. A third
gateway is not on the horizon; if one appears its server-integration
needs its own shape anyway.
Minor: WS route precedence note (websocket.md), OpenAPISpec
shared-type-not-shape clarification (http-adapters.md), date bumps.
30 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-07-01 |
WebSocket — the Browser Bidirectional Path
WebSocket is the v1 browser bidirectional path to the call protocol.
h3/WebTransport is deferred per
ADR-044;
the deferred handler design is at
webtransport.md. This document specifies the WebSocket
upgrade handler on HttpAdapter, the framing, the dispatch path, the
identity model, the streaming projection, and the explicit relationship to
the HTTP gateway surface.
A WebSocket connection is a native EventEnvelope call-protocol session,
not the HTTP gateway shape — the gateway endpoints (/search, /schema,
/call, /batch, /subscribe, ADR-042/047) are the HTTP one-directional
projection and do not appear on the WebSocket path. See
ADR-048 for
the decision and its rationale.
What
The WebSocket path is an axum WS upgrade handler on the same HttpAdapter
that serves h2/http/1.1 (see http-server.md). A browser
(or any WS client — Node, a native app with a WS library) opens an HTTP/1.1
or HTTP/2 request to the upgrade path, authenticates by bearer token on the
upgrade request, and the resulting full-duplex WS connection carries
call-protocol EventEnvelope frames as binary WebSocket messages — one
envelope per message. The WS message stream is handed to the call protocol's
shared Dispatcher, which runs the same dispatch loop the CallAdapter uses
for alknet/call QUIC connections (ADR-012, stream-agnostic correlation).
Why
WebSocket is the HTTP-family transport that restores the call protocol's
native bidirectionality for browsers — HTTP/1.1 + HTTP/2 are
request/response (a one-directional projection of the call protocol),
while WS is a full-duplex, long-lived, framed-message channel over which
the call protocol's native EventEnvelope session runs unchanged. The
decision to use WebSocket for the browser bidirectional path (deferring
h3/WebTransport) is ADR-044;
the decision that the WS path carries the native session rather than the
HTTP gateway shape is ADR-048.
Both decisions' rationale is in those ADRs; this spec covers what the WS
path is and how an implementer builds it.
Prior art: @alkdev/pubsub
The browser/Node WS client is mostly already written. The
@alkdev/pubsub package (/workspace/@alkdev/pubsub/) has a working
WebSocket client (src/event-target-websocket-client.ts) and server
(src/event-target-websocket-server.ts) built on an
EventEnvelope { type, id, payload } shape — the envelope the alknet call
protocol's EventEnvelope was derived from (refined with typed event
names call.requested/call.responded/etc. and structured payloads).
The sibling @alkdev/operations package (/workspace/@alkdev/operations/)
shares the lineage, with one mechanical delta: path.do.op (dot-separated)
vs alknet's path/to/op (slash-separated). Syncing the pubsub/operations
WS client to the alknet envelope is a small adjustment (envelope shape,
event-name typing, path separator), not a from-scratch build. See
call-protocol.md §"Transport agnosticism" and
ADR-044 §"Concrete prior art".
Illustrative deployment: api.alk.dev hub-spokes-browser
A concrete topology this path serves (the early-stage deployment motivating
the spec promotion): api.alk.dev runs as a hub. The vast majority of
spokes are Rust processes using CallClient to connect to the hub over QUIC
on ALPN alknet/call — e.g., a git runner, container services — and are
peers (stable PeerId, fingerprint-pinned, in PeerCompositeEnv,
addressable via PeerRef::Specific per ADR-029). A browser UI for the
early stages (while a desktop app is being fleshed out) connects via
WebSocket and is a bidirectional call target during a live session, not
a peer-graph member (bearer token, no PeerId, connection-local Layer 2
overlay, dies when the tab closes). The browser UI calls services/list
to populate its view with only the ops its bearer-token identity is
authorized to call, calls services/schema for shapes, and invokes via
call.requested — per-privilege filtering comes free from the call
protocol's AccessControl::check(identity)-filtered services/list. The
browser operator UI sees only what its privs allow; the Rust spokes are
full-addressable peers. This is the canonical ADR-044 §5 scenario.
Architecture
The WS upgrade handler
The WS upgrade is an HTTP/1.1 or HTTP/2 request handled by an axum route on
HttpAdapter's router. The handler:
- Receives the HTTP upgrade request (axum's
WebSocketUpgradeextractor). - Resolves the caller's identity from the
Authorization: Bearerheader viaidentity_provider.resolve_from_token(&AuthToken { raw: token_bytes })(theAuthTokentype is from ../core/auth.md — a wrapper around the raw bearer token bytes) — the same auth path as any HTTP request (http-server.md §"Auth"). The upgrade is rejected (401) if no token is present; insufficient scopes for any op the browser later calls surface as403/FORBIDDENat call time, not at upgrade time (the upgrade doesn't know which ops the browser will call). - Upgrades to WebSocket (axum's
WebSocketUpgrade::on_upgrade), producing a full-duplexWebSocketstream. - Wraps the
WebSocketstream as aBiStream-satisfying transport — a WS binary message in either direction is oneEventEnvelopeframe (see §"Framing" below for the length-prefix decision). - Constructs a
Dispatcher(the shared dispatch loop, ../call/client-and-adapters.md §"Shared Dispatcher") with theArc<OperationRegistry>andArc<dyn IdentityProvider>theHttpAdapterholds, plus a connection-local Layer 2 overlay for any ops the browser registers (see §"Bidirectionality" below). - Spawns the dispatch task (
Dispatcher::run_loop) on a tokio task; the WS connection is live until either side closes it or the browser drops the handle (closes the tab).
The upgrade path is a single axum route. The default upgrade path is
/alknet/call (the deployment may override it via the extra_routes
mechanism of ADR-046, but a deployment that passes no custom routes gets
/alknet/call). The path must not collide with the reserved
gateway//healthz//openapi.json/MCP/custom-route paths per ADR-046's
collision rule; /alknet/call namespaces away from the reserved set
naturally. A deployment that builds a custom REST projection with
POST /{service}/{op} routes (ADR-047 §4) coexists with the WS upgrade
at /alknet/call — axum's Router::merge prioritizes specific routes
over wildcards, so the WS upgrade's exact /alknet/call path wins over
any /{service}/{op} wildcard a custom route projection might
register, and the two do not collide. The upgrade runs over HTTP/1.1
(the standard Upgrade: websocket header, RFC 6455) or HTTP/2 (the
extended CONNECT protocol, RFC 8441); axum/hyper supports both, and
the handler does not branch on which — the WS frame stream is the same
once the upgrade completes.
Framing: EventEnvelope over binary WS messages
Every message on the WS connection is a binary WebSocket message containing
one EventEnvelope:
pub struct EventEnvelope {
pub r#type: String, // "call.requested" | "call.responded" | "call.completed" | "call.aborted" | "call.error"
pub id: String, // Correlation key (request ID, subscription ID)
pub payload: Value, // serde_json::Value — schema depends on event type
}
This is the call protocol's wire format verbatim (see
../call/call-protocol.md §"Wire Format:
EventEnvelope"). The @alkdev/pubsub envelope ({ type, id, payload }) is
the shape the alknet EventEnvelope was derived from; the delta is typed
event names (call.requested etc.) and structured payloads, both of which
are already in the alknet envelope. The five event types
(call.requested, call.responded, call.completed, call.aborted,
call.error) carry request/response and subscription semantics exactly as
over QUIC — see ../call/call-protocol.md
§"Event Types" for the full table.
Length-prefix decision. The QUIC path frames EventEnvelope as a
4-byte big-endian length prefix + UTF-8 JSON body (see
../call/call-protocol.md §"Wire Format"),
because a QUIC bidirectional stream is an unbounded byte stream that needs
an explicit delimiter. A WebSocket binary message is already
length-delimited by the WS frame boundary — the receiver gets one complete
message per read, no partial reads across message boundaries (ADR-044
Assumption 1, verified by the @alkdev/pubsub prior art). The WS path
therefore carries no length prefix: one EventEnvelope JSON object = one
binary WS message, and the WS message boundary is the delimiter. The
implementation must not prepend the QUIC length prefix on outbound WS
messages or expect it on inbound ones — the two framings are deliberately
different, matching each transport's native boundary semantics. (The
FrameFramedReader/FrameFramedWriter types the QUIC dispatch loop uses
are replaced on the WS path by direct JSON serde over the WS message type;
the Dispatcher itself is transport-agnostic and consumes EventEnvelope
values, not raw bytes — see ../call/client-and-adapters.md
§"Shared Dispatcher".)
Binary payloads within EventEnvelope.payload follow the same base64-as-
JSON-string convention the QUIC path uses
(../call/call-protocol.md §"Wire Format") —
the envelope carries serde_json::Value and does not interpret binary
fields; that's a handler-level concern, transport-agnostic.
Text WS messages are not used; all call-protocol frames are binary. A client that sends a text message gets a protocol-level close (the WS handler validates message type).
Dispatch: the shared Dispatcher, unchanged
The WS message stream is handed to the Dispatcher — the same dispatch loop
the CallAdapter uses for alknet/call QUIC connections (ADR-017 §1; see
../call/client-and-adapters.md
§"Shared Dispatcher"). The dispatch half is one implementation; the
connection-establishment half differs (WS upgrade handler vs QUIC
accept/dial), but after establishment the Dispatcher runs identically:
- Reads
EventEnvelopeframes from the WS message stream. - For
call.requested: resolves the peer's identity (the bearer-token identity resolved at upgrade time, stored on the connection), runsAccessControl::check(identity)against the op'sAccessControl, dispatches viaOperationRegistry::invoke()if allowed, returnsFORBIDDEN(→call.error) before the handler runs if not. - For
call.responded/call.completed/call.aborted: correlates byidviaPendingRequestMap(keyed by request ID, not by transport — ADR-012). - Writes response
EventEnvelopeframes back as binary WS messages.
Peer authorization flows through the existing AccessControl::check against
the resolved identity — no RemoteFilter, no remote_safe gate (retired by
ADR-029 §3). An op with AccessControl::default() is callable by any
authenticated browser; an op with required_scopes is callable only by
browsers whose Identity.scopes satisfy them; an op with
Visibility::Internal is never callable from the wire (NOT_FOUND before
ACL). See ../call/client-and-adapters.md
§"CallClient" for the full mapping of the three remote_safe cases to
AccessControl/Visibility.
Bidirectionality (ADR-043 §2 transferred to WebSocket per ADR-044 §3)
The WS call-protocol session inherits the call protocol's native
bidirectionality: both sides can send call.requested frames. The browser
calls operations on the hub; the hub can call operations registered on the
browser's side, over the same session, using the same PendingRequestMap
and EventEnvelope framing as alknet/call.
The browser case where the client registers no operations of its own is the
common case — 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 browser that does expose ops (e.g., a UI that registers a
ui/dragged op the hub can call to push live updates) registers them in the
connection-local Layer 2 overlay (see §"Connection-local overlay" below),
and the hub reaches them through the live CallConnection handle — not
through PeerRef::Specific (the browser is not a peer; see §"Browsers are
not alknet peers").
Connection-local overlay (ADR-043 §3 transferred; ADR-024)
A browser over WebSocket has no PeerId on the hub's side. Any ops the
browser registers land in a connection-local Layer 2 overlay
(ADR-024) — a per-CallConnection overlay that dies when the connection
drops. This is the same mechanism ADR-034 §2 describes for the inbound
browser case: the browser is a bidirectional call target during a live
session, not a peer-graph member, and the connection-local overlay is what
gives it bidirectional-call capability without peer-graph membership.
When the WS connection closes (browser closes the tab, network drops), the
overlay and all its registered ops are dropped — no explicit deregistration
needed. A PeerRef::Specific("browser-X") from another node would route to
nothing, because there is no PeerEntry for the browser (see §"Browsers
are not alknet peers" below for why).
Streaming: native call.responded events, no SSE
A Subscription operation invoked over WS streams call.responded events
as binary WS messages directly — no SSE data: framing. SSE is the
h2/http/1.1 streaming projection (ADR-036 §Streaming, applied at the
gateway's /subscribe endpoint per ADR-042 §2); on WS it is unnecessary
because WS is already a framed full-duplex channel. The browser receives
call.responded events one per WS binary message, with the same id
correlating them to the original call.requested; call.completed closes
the subscription (no more events); call.aborted closes it with an error
frame. This mirrors how subscriptions work on the QUIC path — see
../call/call-protocol.md §"Streaming subscribe
example".
On WS client disconnect (the browser closes the tab mid-subscription),
the WS handler detects the stream close and sends call.aborted for the
in-flight subscription, which cascades to descendants per ADR-016.
Browsers are not alknet peers (ADR-034 §4, amended by ADR-044 §5)
A browser over WebSocket authenticates by bearer token, gets no PeerId,
does not enter PeerCompositeEnv, and its registered ops (if any) land in
the connection-local Layer 2 overlay (above). The rationale, stated in
ADR-044 §5 and amending ADR-034 §4 by reference, is a load-bearing
distinction:
"Peer" in alknet means an addressable node in the call-protocol peer
graph — a stable PeerId, reachable via PeerRef::Specific, whose ops
land in PeerCompositeEnv, whose identity is stable across reconnects. It
does not mean "any endpoint that exchanges calls during a live session."
A browser is the second thing but not the first, on three concrete grounds:
- No stable cryptographic identity of its own. A
PeerEntryis anchored to fingerprints (Ed25519, X.509) that the peer presents and the local node pins. A browser presents a bearer token the hub issued; the "identity" is the hub's bookkeeping for that token, not something the browser owns or that could be pinned by another node. There is nothing to put inPeerEntry.fingerprints. - Ephemeral. Close the tab → connection dies → the connection-local
Layer 2 overlay dies with it. A
PeerEntrykeyed to a browser would be a permanently-dead entry within seconds.PeerRef::Specific("browser-X")from another node would route to nothing. - Not addressable from other nodes.
PeerRef::Specificresolves throughPeerEntry→PeerId. Another alknet node has no way to reach "the browser currently connected to hub-A"; the hub holds that connection as a liveCallConnectionhandle, not as a peer-graph entry. The connection-local overlay is precisely the mechanism that gives the browser bidirectional-call capability without peer-graph membership.
This is the explicit closure of the "browser as peer" path, on both the inbound (this section) and outbound (ADR-034 §2) sides. The browser is a bidirectional call target during a live session, not a peer-graph member. The connection-local Layer 2 overlay (ADR-024, ADR-043 §3) is what makes the former possible without requiring the latter. This rationale applies transport-agnostically — to WebSocket, to WebTransport when it revives, and to any future browser transport.
Auth: bearer token on the upgrade request
Inbound WS auth is Authorization: Bearer <token> on the HTTP upgrade
request, resolved via IdentityProvider::resolve_from_token() — the same
path as any HTTP request (http-server.md §"Auth").
Bearer-only is the auth mechanism; other HTTP auth schemes are not
implemented for the WS upgrade (a deployment needing a different scheme
adds it as axum middleware on the upgrade route, two-way door). The
resolved identity is stored on the Connection for observability
(connection.set_identity(identity)), same as the HTTP handler.
The bearer token's Identity is what AccessControl::check runs against
when the browser calls an op via call.requested — the browser's
privileges are the token's privileges. This is the mechanism that gives the
browser per-privilege filtering for free: services/list is
AccessControl::check(identity)-filtered, so the browser's discovery
calls return only the ops its token is authorized to call. The
api.alk.dev operator UI sees only operator-authorized ops; a less-
privileged browser session sees a subset.
What WebSocket does not provide (deferred to WebTransport, ADR-044)
WebSocket carries the call protocol from a browser; it does not carry the non-call-ALPN substrate. The ALPN-stream-proxy (ADR-040) — a browser running a WASM parser for SSH/SFTP/git to reach a non-call ALPN — requires WebTransport's multi-stream model and is the speculative use case whose deferral is ADR-044's reversal trigger. A browser cannot reach SSH/SFTP/git ALPNs over WS in the v1 release; it can reach the call protocol, and through the call protocol any ops the hub exposes. When WebTransport revives, the two coexist: WSS stays as the simpler call-protocol path; WebTransport adds the ALPN-stream-proxy path. Neither replaces the other. See webtransport.md for the deferred design.
Constraints
- The WS path is the native
EventEnvelopesession, not the gateway shape (ADR-048). Theto_openapigateway endpoints (/search//schema//call//batch//subscribe) are the HTTP one-directional projection and do not appear on WS. Discovery is viaservices/listandservices/schemaas ordinary call-protocol ops, not WS-specific endpoints. Subscriptions project as nativecall.respondedevents, not SSE. - Bearer-only auth on the upgrade request.
Authorization: Bearer→resolve_from_token. The resolved identity drivesAccessControl::checkon everycall.requestedthe browser sends; per-privilege filtering is free viaservices/list's existingAccessControlfiltering. - Browsers are not alknet peers (ADR-034 §4, amended by ADR-044 §5).
Bearer token, no
PeerId, noPeerCompositeEnventry, connection-local Layer 2 overlay for any browser-registered ops. "Peer" means addressable peer-graph node, not "any endpoint that exchanges calls during a live session." EventEnvelopeframes are binary WS messages; one envelope per message, no length prefix (ADR-044 Assumption 1). The WS message boundary is the delimiter — the QUIC path's 4-byte length prefix is not carried on the WS path (a WebSocket binary message is already length-delimited by the WS frame boundary). Text messages are rejected. The property is verified by the@alkdev/pubsubprior art.- WebSocket is the v1 browser bidirectional path;
h3/WebTransport is deferred (ADR-044). The ALPN-stream-proxy (ADR-040) is not available in v1. Theh3ALPN and its feature gate are not implemented in the initial release; the WS path uses native axum WS support, no new dependency. - WS upgrade over HTTP/1.1 (
Upgrade: websocket, RFC 6455) or HTTP/2 (extended CONNECT, RFC 8441) is supported by the axum/hyper stack natively (ADR-044 Assumption 2).axum::extract::wsprovides the upgrade handler; the underlying connection is the same hyper HTTP connection theh2/http/1.1handler already drives. The handler does not branch on which HTTP version upgraded — the WS frame stream is the same once the upgrade completes. No new framing library is needed. - The shared
Dispatcherruns over the WS message stream unchanged (ADR-012). A WS message stream is anotherBiStream-satisfying transport; theDispatcherandPendingRequestMapare transport- agnostic. Only the connection-establishment half differs (WS upgrade vs QUIC accept/dial). - The default WS upgrade path is
/alknet/call; it must not collide with reserved paths (ADR-046). The gateway endpoints,/healthz,/openapi.json, the MCP route, and any custom routes take precedence;/alknet/callnamespaces away from the reserved set naturally. A deployment may override the path via theextra_routesmechanism (ADR-046).
Future: from_wss adapter (out of scope, named for discoverability)
A from_wss adapter — importing a remote alknet node's operations over a
WebSocket connection, mirroring from_call's pattern with WS as the
transport — is out of scope for the current alknet-http work and is
recorded here so it is not re-derived later. It is architecturally closer
to from_call than to from_openapi: from_openapi imports a
foreign-protocol surface (OpenAPI) and translates; from_call imports a
same-protocol endpoint over a different transport (QUIC) with no
translation. from_wss is the latter pattern with WS as the transport
instead of QUIC — open a WS connection, run services/list +
services/schema over it, register forwarding handlers that forward
call.requested over the WS connection. Because the Dispatcher is
stream-agnostic (ADR-012), it is from_call with a different
BiStream-satisfying transport.
Why it is out of scope now: it is a genuinely separate use case from the
browser-client case (it is "import a remote alknet node's ops over WSS,"
not "be a browser client"), and the consumers are not yet concrete enough
to commit the adapter's exact shape. The decision is made when a concrete
consumer arrives — a node that wants to import another node's ops but can
only reach it over WS (e.g., a browser-mediated relay, a restrictive-
network deployment). It is not needed for the v1 api.alk.dev topology
(Rust spokes use from_call over QUIC; the browser is a client, not an
importer). When revived, from_wss implements the OperationAdapter trait
(ADR-017 §5) and lives in alknet-http alongside from_openapi/from_mcp,
reusing the Dispatcher and the WS framing specified in this document.
This is a genuine scope decision (per ADR-009 §"What this framework is NOT" — a decision that "genuinely doesn't need to be made yet because the use case isn't concrete"), not a two-way-door deferral. The reversal trigger is a concrete deployment needing to import a remote alknet node's ops over WSS.
Design Decisions
| Decision | ADR | Summary |
|---|---|---|
Defer h3/WebTransport; browsers use WebSocket |
ADR-044 | WS is the v1 browser bidirectional path; h3/WebTransport deferred (scope); reversal trigger = concrete ALPN-stream-proxy use case |
WS carries the native EventEnvelope session, not the gateway shape |
ADR-048 | The gateway endpoints are HTTP-only; WS is the call protocol's native session; discovery via services/list/services/schema as call-protocol ops; subscriptions as native call.responded events, no SSE |
| Call protocol stream model (stream-agnostic correlation) | ADR-012 | A WS message stream is another BiStream-satisfying transport; the Dispatcher and PendingRequestMap run unchanged |
Call protocol client and adapter contract (Dispatcher shared) |
ADR-017 §1 | The WS handler constructs a Dispatcher and calls run_loop, same as CallAdapter/CallClient — the dispatch half is one implementation |
| Operation registry layering (Layer 2 connection-local overlay) | ADR-024 | Browser-registered ops (if any) land in a connection-local overlay that dies with the WS connection |
| Bidirectionality transferred from WebTransport to WebSocket | ADR-043 §2 (parked; §2/§3 transfer per ADR-044 §3) | Both sides can call.requested; the browser case where the client registers no ops is a use-case scoping, not an architectural limitation |
No-PeerId connection-local overlay transferred from WebTransport |
ADR-043 §3 (parked; transfers per ADR-044 §3) | A browser over WS has no PeerId; ops land in the connection-local overlay |
| Browsers are not alknet peers (rationale: addressability vs bidirectionality) | ADR-034 §4 (amended by ADR-044 §5) | Bearer token, no PeerId, no PeerCompositeEnv entry; "peer" means addressable peer-graph node, not "any endpoint that exchanges calls during a live session" |
Peer-graph routing model (peer authorization via AccessControl) |
ADR-029 §3 | AccessControl::check(identity) gates every call.requested from the browser; no remote_safe/trusted_peer (retired) |
| Abort cascade on WS disconnect | ADR-016 | WS close mid-subscription sends call.aborted, cascading to descendants |
Bearer auth via resolve_from_token |
ADR-004 | WS upgrade request credential source (same as HTTP) |
| Browsers require X.509 (TLS) | ADR-027 | The WS upgrade runs over the same h2/http/1.1 TLS as HTTP; browsers don't support RFC 7250 raw keys |
| Stealth: HTTP handler on standard ALPNs serves WS upgrade | ADR-010 | The WS upgrade route is on HttpAdapter's default surface; a port scanner sees the decoy for unknown paths |
| Custom routes collision rule | ADR-046 | The WS upgrade route must not collide with reserved default-surface paths; it namespaces away naturally |
Open Questions
See open-questions.md for full details.
- OQ-38 (open, scope): WebTransport standalone relay service scope —
the standalone relay (future
alknet-relay, fork of iroh-relay) is distinct from the in-process ALPN-stream-proxy (ADR-040, parked) and from the WebSocket browser path (this spec). The relay is a separate service for NAT traversal, not a mode of the WS handler; it does not affect the WS path.
No new open questions. The from_wss deferral (§"Future") is a scope
decision stated explicitly, not an open question — the reversal trigger is
concrete (a deployment needing to import a remote node's ops over WSS),
and there is no architectural question to resolve before then.
References
- ADR-044
— the ADR that committed WS as the v1 browser bidirectional path;
§"Concrete prior art" references the
@alkdev/pubsubWS client/server; §5 states the "browser is not a peer" rationale this spec carries. - ADR-048
— the ADR that commits the WS path as the native
EventEnvelopesession, not the HTTP gateway shape. - http-server.md — the
HttpAdapterthat hosts the WS upgrade handler; the one-directional HTTP projection (the gateway) the WS path is contrasted against. - webtransport.md — the deferred
h3/WebTransport handler; kept intact for revival. When WebTransport revives, the two coexist (WSS for the call protocol; WebTransport for the ALPN-stream- proxy). - ../call/call-protocol.md —
EventEnvelopewire format, theDispatcher, thePendingRequestMap, stream model, bidirectional calls, §"Transport agnosticism" (the pubsub-lineage note). - ../call/client-and-adapters.md — the
shared
Dispatcher(§"Shared Dispatcher"),services/listfiltering, theCallClient(the QUIC-side counterpart to the WS browser client). - ../call/operation-registry.md —
OperationRegistry::invoke(), Layer 2 connection-local overlay. /workspace/@alkdev/pubsub/src/event-target-websocket-client.ts,/workspace/@alkdev/pubsub/src/event-target-websocket-server.ts— TypeScript prior art: theEventEnvelope { type, id, payload }over WS binary messages. The alknetEventEnvelopeis a refined superset (typed event names, structured payloads); the delta is small and well-defined./workspace/@alkdev/operations/src/— TypeScript sibling package sharing the pubsub lineage; thepath.do.op(dot-separated) convention vs alknet'spath/to/op(slash-separated) is the minor mechanical delta a sync adjusts.