mirror of
https://github.com/zap-proto/web.git
synced 2026-10-30 15:03:09 +00:00
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.
118 lines
3.6 KiB
TypeScript
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 });
|
|
}
|