@zap-proto/web
v1.0.0
Published
Browser frontend RPC over ZAP — a drop-in tRPC replacement for Next.js / Remix / SvelteKit. Native ZAP envelopes over WebSocket binary frames, plus an optional JSON-over-HTTP face (httpServe) for the OpenAPI 3.1 surface emitted by zapgen. Layered on @zap-
Downloads
776
Readme
@zap-proto/web
Browser-frontend RPC over ZAP — a drop-in tRPC replacement for Next.js /
Remix / SvelteKit. Native ZAP envelopes over WebSocket binary frames, layered
cleanly on @zap-proto/zap (the ZAP wire
runtime). v0.1.0 — pre-1.0 by policy; no version tags until the full chain
works end-to-end.
Note on naming. The foundation runtime is
@zap-proto/zap(the canonical TypeScript ZAP runtime, repozap-proto/ts). This package — the WebSocket frontend layer — is@zap-proto/web, repozap-proto/web. It is not Cap'n Proto; ZAP is a zero-copy, zero-dependency binary format byte-compatible with the Go runtimegithub.com/zap-proto/goandgithub.com/luxfi/zap.
What @zap-proto/web IS
- A drop-in replacement for tRPC across Hanzo's TS apps (esign, console, platform), using native ZAP RPC over WebSocket instead of JSON-over-HTTP.
- An isomorphic transport — the same
WsTransportworks in Node (post HTTP-upgrade, over thewspackage) and in the browser (nativeWebSocket). - A Node
serve(httpServer, opts)harness that attaches to an existinghttp.Server, so Next.js (app.getRequestHandler()) or a Remix Node server share one port — integration is one line. - A browser
connect(url, opts)that opens the WS, waits foropen, and returns the bootstrap call surface typed by your generated bindings. - A
mintCapboundary hook at the WS upgrade so apps plug their own bearer→context minting logic.
What @zap-proto/web is NOT
- ❌ Not a Zod/procedure DSL like tRPC. Service shape comes from
.zapschemas (structs + method ordinals), codegen'd viazapgen --target=tsfromzap-proto/ts. - ❌ Not a JSON wrapper. Wire format is ZAP envelopes over WS binary
messages. Text frames are a protocol violation and close the socket with WS
code
1003. - ❌ Not Cap'n Proto. ZAP is its own zero-copy format. The bootstrap is the
connection's call surface (
bootstrap.call(method, { payload })), not a Cap'n Proto capability object. - ❌ Not a bearer-JWT framework.
mintCapis a slot — apps provide a fn(upgradeReq) => Promise<Ctx | null>.v0ships the slot; the canonical ML-DSA-65 signedCapabilitymint (perzap-spec/capabilities.zap— the 3408-byte signature footer, BLAKE3 identity, attenuable/revocable authority) lands in a later version. Until then, whatever non-null valuemintCapreturns becomes the per-connectionctx.
Install
pnpm i @zap-proto/web @zap-proto/zap
@zap-proto/zapis not yet published to npm. Thedependenciesentry already ships plain semver (^0.1.0); until it lands on npm, local tests resolve the specifier to the sibling source tree (../ts, thezap-proto/tsrepo) via thevitest.config.tsalias and thetsconfig.jsonpath. No change needed once@zap-proto/zapships — just drop the alias.
Quick start — Next.js (≈30 lines)
// server.ts
import http from "node:http";
import next from "next";
import { serve } from "@zap-proto/web/server";
import { newGreeting, HelloArgs } from "./gen/hello_zap.js"; // zapgen --target=ts
const app = next({ dev: process.env.NODE_ENV !== "production" });
const handle = app.getRequestHandler();
await app.prepare();
const server = http.createServer((req, res) => handle(req, res));
serve(server, {
path: "/zap",
// mintCap SLOT: turn a bearer into your app's ctx. Return null → HTTP 401.
mintCap: async (req) => {
const auth = req.headers.authorization;
return auth ? { org: await orgFromBearer(auth) } : null;
},
// rootCap(ctx): dispatch every decoded ZAP Call for this connection.
rootCap: (ctx) => (call) => {
const args = HelloArgs.wrap(call.payload);
return {
status: 200,
promiseID: call.promiseID,
body: newGreeting({ text: `hi ${args.name} @ ${ctx.org}` }),
};
},
});
server.listen(3000);// client.ts (browser)
import { connect } from "@zap-proto/web/client";
import { newHelloArgs, Greeting } from "./gen/hello_zap.js";
const { bootstrap, close } = await connect("wss://app.hanzo.ai/zap", {
bearer: sessionToken, // forwarded as Authorization on the upgrade (Node ws)
});
const resp = await bootstrap.call(/* method */ 0, {
payload: newHelloArgs({ name: "ada" }),
});
console.log(Greeting.wrap(resp.body).text); // "hi ada @ acme"
close();Browser bearer caveat. The browser's native
WebSocketcannot set anAuthorizationheader. In the browser,connect({ bearer })falls back to a?authorization=Bearer%20…query param the server can read; prefer a cookie sent automatically on the upgrade for production. The header path applies to a header-capableWebSocketImpl(thewspackage, used in Node/SSR/tests).
Schema → code
Service shape is .zap schemas compiled with the ZAP compiler from
zap-proto/ts:
zapgen --target=ts -single hello.zap # → hello_zap.ts (views + builders)Method ordinals (the interface method @n) are the integers you pass to
bootstrap.call(method, …) and match in your rootCap dispatch.
Entry points
The root entry (@zap-proto/web) is browser-safe — it imports no Node
built-ins and exposes only the client surface (connect, browserWsTransport,
Conn, errors). The Node server (serve, nodeWsTransport, the MintCap
auth slot) reaches node:module (createRequire) / node:http / node:net
and lives behind @zap-proto/web/server, so it never enters a browser bundle.
// Browser — the client connection.
import { connect } from "@zap-proto/web/client";
// Node — attach the RPC endpoint to an http.Server.
import { serve } from "@zap-proto/web/server";API
| Export | Entry | Purpose |
| --- | --- | --- |
| connect(url, opts?) | @zap-proto/web/client (or root) | Open a ZAP RPC WebSocket; returns { bootstrap, close }. |
| browserWsTransport(ws) | @zap-proto/web/transport (or root) | Wrap a native WebSocket as a WsTransport. |
| Conn, WebRpcError | @zap-proto/web (root, browser-safe) | Duplex RPC connection; transport-level RPC failure. |
| serve(httpServer, opts) | @zap-proto/web/server | Node only. Attach a ZAP RPC endpoint to a Node http.Server. |
| nodeWsTransport(ws) | @zap-proto/web/server (or /transport) | Node only. Wrap a ws WebSocket as a WsTransport. |
| MintCap<Ctx> | @zap-proto/web/server (or /auth) | Node only. The bearer→ctx slot. |
License
MIT © Lux Industries Inc.
