Skip to content

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.

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.

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 }):

client.ts
connectChannel(clientRuntime, appChannel, {
peer: serverCoord,
storage,
securityLevel: ESecurityLevel.encrypted,
transports: [
{ carrier: wsCarrier(() => ({ url: wsUrl })) }, // secure (default)
{ carrier: httpCarrier(() => ({ url: httpUrl })), secure: false }, // plain fallback
],
});

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:

server.ts
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] });

Next: Cloudflare Durable Objects →