@signicode/verser2-guest-node
v0.9.3
Published
Node Guest, Broker, Agent, Dispatcher, and fetch helpers for Verser2.
Maintainers
Readme
@signicode/verser2-guest-node
Run a Node HTTP handler behind NAT or a firewall without opening an inbound
port. This package connects outbound to a verser2 Host over persistent TLS
HTTP/2 (with optional mTLS), dispatches routed requests to your local
http.Server handler in-process, and gives Brokers several ways to send
requests — direct request(), an http.Agent, an Undici Dispatcher, a
pre-wired fetch, and VWS/1 framed WebSockets.
Audience: Node.js services acting as verser2 Guests, and Node applications
that call those Guests as Brokers. Requires a Host (@signicode/verser2-host).
Public API
createVerserNodeGuest(options)— create a Node GuestcreateVerserBroker(options)— create a Node BrokerMinimalIncomingMessage,MinimalServerResponse— minimal HTTP/1 request/response shims for local handler dispatch- Types: Guest/Broker options, request/response, lifecycle, dispatch, route lifecycle events
guest.revokeRoutes(domains)— revoke advertised route domains via the dedicated/verser/guest/revokerequest path; resolves with{ status: 'ack'|'partial'|'error' }broker.onRouteChange(listener)— observe route lifecycle events (added,removed,changed,degraded) with payload{ type, targetId, domain, reason?, generation? }- Constant:
VERSER2_GUEST_NODE_PACKAGE_NAME VerserWebSocket— VWS/1 WebSocket object;webSocket()andattachWebSocket()provide the direct Broker/Guest APIs.NativeVerserWebSocket— EventTarget-style adapter frombroker.nativeWebSocket()/guest.attachNativeWebSocket().
Basic usage
import http from 'node:http';
import { createVerserNodeGuest, createVerserBroker } from '@signicode/verser2-guest-node';
const broker = createVerserBroker({
hostUrl: 'https://localhost:8443',
brokerId: 'broker-a',
tls: { caFile: '/etc/verser/ca.crt' },
});
const guest = createVerserNodeGuest({
hostUrl: 'https://localhost:8443',
guestId: 'client-a',
tls: { caFile: '/etc/verser/ca.crt' },
});
const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' });
res.end('ok');
});
guest.attach(server, 'client-a.local.test');
await broker.connect();
await guest.connect();
await broker.waitForRoute('client-a.local.test');
const response = await broker.request({
targetId: 'client-a',
method: 'GET',
path: '/',
});VWS/1 WebSockets
Node Broker and Guest WebSockets use explicit VWS/1 framed messages over the existing TLS HTTP/2 connection:
guest.attachWebSocket((_open, ws) => {
ws.on('message', async (data, options) => await ws.send(data, options));
ws.on('close', (code, reason) => console.log(code, reason));
}, 'chat.local.test');
const ws = await broker.webSocket({ targetId: 'client-a', domain: 'chat.local.test' });
await ws.send('hello', { type: 'text' });send() observes backpressure. This is not generic HTTP/1 upgrade forwarding;
Agent/Dispatcher upgrades, CONNECT/RFC8441, L4 forwarding, and federated
WebSocket routes use the authenticated versioned federation-VWS stream and are
supported when every Host hop provides that endpoint. Route advertisements stay
neutral: the Broker does not preflight WebSocket capability.
Broker routing
The Broker provides multiple ways to route requests:
broker.request()— direct request APIbroker.createAgent()—http:Agent that routes via the Broker without DNSbroker.createDispatcher()— Undici Dispatcher forfetch(url, { dispatcher })broker.createFetch()— pre-wired fetch helper
Brokers may advertise an optional brokerDomain in their Host registration
payload. The value is normalized (trimmed, lowercased, trailing dot removed)
before it is sent. When the Host enables remote mTLS, the Host requires the
normalized value to exactly match a DNS Subject Alternative Name on the
Broker's client certificate (no wildcard or CN fallback) and rejects the
registration otherwise. When omitted, no domain is sent and behavior is
unchanged. The legacy brokerHopDomain option is rejected at construction,
not aliased. On Hosts with a routeAuthorizer configured, a Broker-selected
federation request from a Broker without a registered domain fails with
authorization-denied before forwarding.
Broker request paths follow internal 307 and 308 redirects by default when
the response Location hostname exactly matches an advertised verser2 route.
The redirected request is resolved through the Broker route table, preserves the
original method, headers, path/query, and replayable body, and is bounded by
maxInternalRedirects (default 3) and internalRedirectReplayBufferBytes
(default 16 KiB). If the body is too large to replay or the target hostname is
not advertised, the original redirect response is returned unchanged. Exceeding
the redirect count fails with a protocol-error.
broker.createFetch() defaults Undici's redirect option to manual so fallback
redirect responses remain visible to callers instead of being followed through
DNS. Pass an explicit redirect option to override that fetch-level behavior.
Broker route lifecycle observation
Brokers can observe route changes reactively without polling:
const unsubscribe = broker.onRouteChange((event) => {
console.log(event.type, event.domain, event.reason);
// e.g. 'added', 'removed', 'changed', 'degraded'
});
// Later, to stop observing:
unsubscribe();The internal route snapshot (getRoutes()) is updated before listeners fire.
See the Lifecycle and errors docs for
event types, reasons, and degraded-route behavior.
Guest route revocation
A connected Guest can selectively revoke its advertised routes without closing the connection:
const result = await guest.revokeRoutes(['app.example.com', 'api.example.com']);
// result.status === 'ack' | 'partial' | 'error'The Host responds with ack (all revoked), partial (some failed), or error
(entire request rejected). The revocation uses the dedicated
/verser/guest/revoke request path.
Caveats
- Node Guest/Broker use outbound TLS HTTP/2.
attach()accepts anhttp.Serverwith a request listener or a listener function; it does not calllisten().- When no domain is supplied to
attach(), the Guest ID is used as the route domain. - Minimal HTTP objects do not implement the full Node
IncomingMessage/ServerResponse/ socket surface. - Generic WebSocket upgrade, CONNECT/RFC8441, trailers, and informational responses are not forwarded. VWS/1 is the only supported WebSocket surface; Agent and Dispatcher upgrade requests remain unsupported.
- Agent keep-alive pooling, HTTPS Agent behavior, and advanced socket features are not implemented.
