Files
web/test/echo.test.ts
zeekay a8b7ddf2ea fix: browser-safe root entry (v0.1.1)
FIX 2 — the @zap-proto/web root no longer leaks server-only Node code.

`serve` and the `MintCap` auth slot reach `node:module` (createRequire) /
`node:http` / `node:net`; importing them from the package root pulled those
into browser bundles. src/index.ts now re-exports only the browser-safe
client surface (connect, browserWsTransport, Conn, WebRpcError,
ZapParseError). `serve` / `nodeWsTransport` / `MintCap` move to the existing
`@zap-proto/web/server` sub-path (server.ts re-exports nodeWsTransport +
MintCap so /server is the complete Node API). The `.` / `./server` /
`./client` exports are unchanged in shape — only the root surface shrank.

Bumps @zap-proto/zap dep to ^1.2.0 (the browser-safe root + zapgen bin).

Adds a happy-dom test asserting the root import resolves under a DOM
environment, that serve/nodeWsTransport are not reachable from root, and
that the built dist/index.js imports no Node built-in (comment-stripped
scan, so JSDoc prose does not trip the guard). README documents the
browser-safe root vs the Node `/server` sub-path.
2026-06-16 11:11:25 -07:00

118 lines
3.6 KiB
TypeScript

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
import { createServer, type Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { WebSocket } from "ws";
import { Status, type Call, type Response } from "@zap-proto/zap";
import { connect, WebRpcError, type Conn } from "../src/index.js";
import { serve } from "../src/server.js";
import {
EchoArgs,
EchoResult,
newEchoArgs,
newEchoResult,
} from "./gen/echo_zap.js";
// EchoCap interface, one method: echo(text) -> (text). The .zap interface
// method ordinal — here method 0 is `echo`.
const ECHO = 0;
// A server-side EchoCap impl: decode EchoArgs from the call payload, echo the
// text straight back as an EchoResult body. This is exactly what a zapgen
// server skeleton would dispatch into.
function echoHandler(call: Call): Response {
if (call.method !== ECHO) {
return {
status: Status.NotFound,
promiseID: call.promiseID,
body: new Uint8Array(0),
};
}
const args = EchoArgs.wrap(call.payload);
const body = newEchoResult({ msg: args.msg });
return { status: Status.OK, promiseID: call.promiseID, body };
}
let server: Server;
let url: string;
function listen(s: Server): Promise<string> {
return new Promise((resolve) => {
s.listen(0, "127.0.0.1", () => {
const addr = s.address();
if (addr && typeof addr === "object") {
resolve(`ws://127.0.0.1:${addr.port}/zap`);
} else {
throw new Error("no address");
}
});
});
}
afterEach(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
describe("@zap-proto/web echo roundtrip", () => {
beforeEach(async () => {
server = createServer((_req, res) => {
res.writeHead(404);
res.end();
});
serve(server, {
// mintCap slot: accept anyone carrying a bearer; reject the rest.
mintCap: async (req) => {
const auth = req.headers.authorization;
return auth ? { bearer: auth } : null;
},
// rootCap(ctx) yields the dispatch handler for this connection.
rootCap: () => echoHandler,
});
url = await listen(server);
});
it("roundtrips a ZAP message over a real WebSocket", async () => {
const { bootstrap, close } = await connect<Conn>(url, {
bearer: "test-token",
WebSocketImpl: WebSocket as unknown as typeof globalThis.WebSocket,
});
const resp = await bootstrap.call(ECHO, {
payload: argsFor("hello"),
});
expect(resp.status).toBe(Status.OK);
expect(EchoResult.wrap(resp.body).msg).toBe("hello");
close();
});
it("rejects the upgrade with 401 when mintCap returns null", async () => {
await expect(
connect<Conn>(url, {
// no bearer → mintCap returns null → HTTP 401, upgrade rejected.
WebSocketImpl: WebSocket as unknown as typeof globalThis.WebSocket,
}),
).rejects.toBeInstanceOf(WebRpcError);
});
it("shares one conn across two sequential calls", async () => {
const { bootstrap, close } = await connect<Conn>(url, {
bearer: "test-token",
WebSocketImpl: WebSocket as unknown as typeof globalThis.WebSocket,
});
const first = await bootstrap.call(ECHO, { payload: argsFor("one") });
const second = await bootstrap.call(ECHO, { payload: argsFor("two") });
expect(EchoResult.wrap(first.body).msg).toBe("one");
expect(EchoResult.wrap(second.body).msg).toBe("two");
close();
});
});
// Build an EchoArgs payload via the generated builder.
function argsFor(text: string): Uint8Array {
return newEchoArgs({ msg: text });
}