Security Levels
From self-asserted identity to fully encrypted frames — picked per connection.
ESecurityLevel is used by both connectChannel and serveChannel:
none— identity self-asserted, no handshake. Fastest; fine for dev or trusted networks.authenticated— the handshake verifies identity (sign/verify + trust-on-first-use key pin); frames are unencrypted.encrypted— authenticated plus every frame AES-GCM encrypted with the handshake-derived key.
How it negotiates
Section titled “How it negotiates”The connector picks its level. An acceptor by default negotiates any of the three per connection, so one endpoint serves all three. The client identifies itself with its runtime coordinate plus a persisted crypto identity; the server pins client keys trust-on-first-use.
Persisting the server’s binding (via a hibernatable carrier) lets an authenticated / encrypted connection resume after eviction without re-handshaking.
One channel, every level
Section titled “One channel, every level”The same secure { carrier: wsCarrier(...) } transport works at any level, and httpCarrier runs the same secure session over HTTP (handshake → token → encrypted frames), with request/reply correlation provided for free by the HTTP transaction.
Pair a secure WebSocket with a plain HTTP fallback by giving the acceptor an httpAcceptorCarrier({ secure: false }):
connectChannel(clientRuntime, appChannel, { peer: serverCoord, storage, securityLevel: ESecurityLevel.encrypted, transports: [ { carrier: wsCarrier(() => ({ url: wsUrl })) }, // secure (default) { carrier: httpCarrier(() => ({ url: httpUrl })), secure: false }, // plain fallback ],});Stateless secure HTTP exchange
Section titled “Stateless secure HTTP exchange”The secure HTTP exchange is stateless — its handshake and session ride sealed tokens (sealed under the server’s own crypto identity), so any isolate can serve any request. You don’t need a Durable Object just to keep a handshake’s two POSTs together:
const server = serveChannel(runtime, appChannel, { clientEnv, storage, // only backs the server's crypto identity; sessions never touch it carriers: [httpAcceptorCarrier()], handlers: [appHandler],});// app.post("/action", (req) => server.fetch(req))On an eventually-consistent store (e.g. Cloudflare KV), provision the identity once up front so a transient read-miss can’t mint a second identity:
import { ClientCryptoKeyLink } from "@nice-code/util";
const link = new ClientCryptoKeyLink({ storageAdapter: storage, identityMode: "required" });await link.provisionIdentity(); // once, out-of-band (a deploy step / first boot)
serveChannel(runtime, appChannel, { clientEnv, storage, link, carriers: [httpAcceptorCarrier()], handlers: [appHandler] });