Skip to content

Hosting Several Protocols

A server hosts one primary protocol and any number of additional application protocols, fixed when it is constructed (WIRE_PROTOCOL.md §3.1). Every request names its protocol in vgi_rpc.protocol, so method names may repeat across protocols without colliding.

import { Protocol, serveTcp, str, VgiRpcServer } from "@query-farm/vgi-rpc";
const app = new Protocol("acme.App.v1", { protocolVersion: "1.0.0" });
const reports = new Protocol("acme.Reports.v1").unary("render", {
params: { id: str },
result: { result: str },
handler: ({ id }) => ({ result: `report ${id}` }),
});
const server = new VgiRpcServer(app, { protocols: [reports] });
// The same host object serves every transport:
await server.run(); // stdio
// await serveTcp(server, { port: 0 }); // raw TCP
// await serveUnix(server, { unixPath }); // AF_UNIX
// await serveStream(server, { readable }); // any byte-stream pair
// createHttpHandler(server, { ... }); // HTTP / Cloudflare Workers

Rules:

  • Fixed for the server’s life. Register everything in the constructor (protocols, identity). Once a transport starts serving, addProtocol throws, so reflection output and every protocol_hash stay stable.
  • The same set on every transport. Hand the one VgiRpcServer to each launcher; a launcher given a built server refuses server-level options (serverId, dispatchHook, …) — they belong on the server.
  • Order is registration order, primary first. list_protocols reports application protocols in that order; framework protocols (vgi_rpc.Reflection.v1, vgi_rpc.Identity.v1) follow.
  • No reserved names. A name starting vgi_rpc. is refused, whether it is the binding name or the protocol’s own name. Names must be unique.
  • The protocol is the unit of optionality. There is no way to host a subset of a protocol’s methods; make an optional capability its own protocol.
  • Versions are per binding. Each protocol’s protocolVersion gates only its own methods.

vgi_rpc.Identity.v1 is framework-owned and opted into with the identity option:

import { IdentityImpl, VgiRpcServer } from "@query-farm/vgi-rpc";
const server = new VgiRpcServer(app, {
identity: new IdentityImpl({
resolveToken: async (token) => lookup(token), // null = unknown credential
introspectPrincipals: ["edge-proxy"], // required with resolveToken
}),
});

For “I could not find out” (a store outage), throw AuthUnavailableError from the hook: it is translated to identity_unavailable with its retry hint (vgi_rpc.RetryInfo). Host identity only where the transport authenticates callers (HTTP).

issue_grant mints a credential meant to be presented later as an ordinary bearer, and resolveToken resolves opaque bearers — so both feed back into HTTP authentication (WIRE_PROTOCOL.md §16):

  • Sealed grants, opt-in. Set VGI_RPC_GRANT_KEYS (comma-separated standard base64, exactly 32 bytes each; the first mints, all verify), or pass new VgiRpcServer(app, { grantKeys: GrantKeys.parse([...]) }). The framework then hosts issue_grant (unless your IdentityImpl supplies mintGrant) and accepts its own vgig1. tokens as bearers. Optional VGI_RPC_GRANT_AUDIENCE and VGI_RPC_GRANT_MAX_TTL_SECONDS (default 7 days). No key, no change; a malformed key fails construction.
  • resolveToken as a bearer authenticator. A hosted identity with resolveToken has bearers your own authenticate did not accept resolved through it: an identity authenticates (domain token), null is 401, and AuthUnavailableError / IdentityUnavailableError is 503 with Retry-After.
  • Order. Your authenticate → sealed grants → resolveToken. A bad vgig1. token is 401 and never reaches resolveToken.
  • A grant-authenticated caller has no auth_time, so it cannot mint another grant (stale_auth).
  • Sealed grants are not individually revocable: keep the maximum lifetime short; removing a key revokes all its grants.
  • If your authentication depends on proxy-injected evidence (proxyProofRequired, proxyAuthHeaders), the handler refuses to OR these in: compose grantAuthenticate / resolveTokenAuthenticate yourself and pass identityBearer: false.

@query-farm/vgi-rpc/conformance exports buildSecondaryProtocol() (the cross-port conformance.Secondary.v1) and the identity fixture policy, for conformance workers and SDK fixture workers. Not for production servers.