@volter/twin-tunnel 0.1.37 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +11 -121
  3. package/dist/generated/pack-facts.json +54 -0
  4. package/dist/spec/SOURCE.md +14 -0
  5. package/dist/spec/openapi.json +18 -0
  6. package/dist/src/cli.js +15 -53
  7. package/dist/src/fetch.d.ts +2 -0
  8. package/dist/src/fetch.js +19 -0
  9. package/dist/src/generated/surface.gen.json +1 -0
  10. package/dist/src/generated/ui.gen.json +1 -0
  11. package/dist/src/index.d.ts +5 -7
  12. package/dist/src/index.js +8 -42
  13. package/dist/src/manifest.d.ts +2 -0
  14. package/dist/src/manifest.js +42 -0
  15. package/dist/src/semantics/around.d.ts +2 -0
  16. package/dist/src/semantics/around.js +39 -0
  17. package/dist/src/semantics/doors.d.ts +3 -0
  18. package/dist/src/semantics/doors.js +18 -0
  19. package/dist/src/semantics/index.d.ts +2 -0
  20. package/dist/src/semantics/index.js +1 -0
  21. package/dist/src/semantics/shared.d.ts +17 -0
  22. package/dist/src/semantics/shared.js +15 -0
  23. package/dist/src/semantics/sockets.d.ts +2 -0
  24. package/dist/src/semantics/sockets.js +80 -0
  25. package/dist/src/semantics/states.d.ts +1 -0
  26. package/dist/src/semantics/states.js +3 -0
  27. package/dist/src/server.d.ts +7 -0
  28. package/dist/src/server.js +9 -0
  29. package/generated/pack-facts.json +54 -0
  30. package/package.json +16 -32
  31. package/spec/SOURCE.md +14 -0
  32. package/spec/openapi.json +18 -0
  33. package/src/cli.ts +15 -41
  34. package/src/fetch.ts +20 -0
  35. package/src/generated/surface.gen.json +1 -0
  36. package/src/generated/ui.gen.json +1 -0
  37. package/src/index.ts +9 -52
  38. package/src/manifest.ts +47 -0
  39. package/src/semantics/around.ts +41 -0
  40. package/src/semantics/doors.ts +21 -0
  41. package/src/semantics/index.ts +6 -0
  42. package/src/semantics/shared.ts +26 -0
  43. package/src/semantics/sockets.ts +69 -0
  44. package/src/semantics/states.ts +3 -0
  45. package/src/server.ts +10 -0
  46. package/dist/src/tunnel-budget.d.ts +0 -21
  47. package/dist/src/tunnel-budget.js +0 -26
  48. package/dist/src/tunnel-capabilities.d.ts +0 -4
  49. package/dist/src/tunnel-capabilities.js +0 -2091
  50. package/dist/src/tunnel-conformance.d.ts +0 -10
  51. package/dist/src/tunnel-conformance.js +0 -63
  52. package/dist/src/tunnel-connector.d.ts +0 -21
  53. package/dist/src/tunnel-connector.js +0 -68
  54. package/dist/src/tunnel-server.d.ts +0 -109
  55. package/dist/src/tunnel-server.js +0 -1197
  56. package/dist/src/tunnel-twin.d.ts +0 -175
  57. package/dist/src/tunnel-twin.js +0 -236
  58. package/src/tunnel-budget.ts +0 -34
  59. package/src/tunnel-capabilities.ts +0 -1770
  60. package/src/tunnel-conformance.ts +0 -62
  61. package/src/tunnel-connector.ts +0 -64
  62. package/src/tunnel-server.ts +0 -1194
  63. package/src/tunnel-twin.ts +0 -339
@@ -0,0 +1,47 @@
1
+ // The tunnel relay's manifest (volter-ai/volter-tunnel, spec/SOURCE.md): a protocol more than an API. A client holds
2
+ // the control socket (`control`, semantics/sockets.ts) and answers the public requests the relay sends down it
3
+ // (semantics/around.ts). Served is what Volter Harness's remote access uses (../journeys/demand.json).
4
+ import type { DerivedManifest } from '@volter/world-core';
5
+
6
+ export const manifest: DerivedManifest = {
7
+ vendor: 'tunnel',
8
+ service: 'tunnel',
9
+ body: { json: 'always' },
10
+ ids: { template: '{letters:8}' },
11
+ time: 'iso',
12
+ error: '{message}',
13
+ readOnly: { status: 403, message: 'This twin was started read-only; writes are refused.' },
14
+ notFound: { status: 404, message: 'Not Found' },
15
+ gap: { status: 404, message: 'Not Found' },
16
+ list: { style: 'envelope', envelope: { data: '{data}' }, limit: { param: 'limit', default: 100, max: 100 } },
17
+ deleted: {},
18
+ resources: {
19
+ _account: { idPrefix: '' },
20
+ // a tunnel id an account holds (one minted when a client names none)
21
+ _tunnel: { idPrefix: '' },
22
+ },
23
+ sockets: [
24
+ { id: 'control', path: '/ws', note: "a client's control channel: register, then answer each request the relay sends", source: 'https://github.com/volter-ai/volter-tunnel/blob/main/packages/core/src/protocol.ts' },
25
+ ],
26
+ doors: [
27
+ { id: 'accounts', method: 'POST', path: '/_twin/accounts', note: '{ slug }: an account on the relay and its token (the World application\'s TUNNEL_SECRET)' },
28
+ ],
29
+ discovery: {
30
+ twinOf: "Volter's tunnel relay (volter-tunnel) as Volter Harness's remote access uses it: a client's control socket, and every request to its tunnel answered through it",
31
+ stores: 'accounts and their tokens (by their SHA-256), and the tunnel ids they hold',
32
+ identity: "the account's token in the control channel's register frame",
33
+ },
34
+ descriptor: {
35
+ protocol: '3',
36
+ transport: 'rest',
37
+ archetype: 'proxy',
38
+ bin: 'world-tunnel',
39
+ resources: [],
40
+ specSource: 'written from volter-ai/volter-tunnel (spec/SOURCE.md)',
41
+ description: "Tunnel relay twin — a client's control socket (register, then each public request sent down it and answered by the frames it sends back), as Volter Harness's remote access shares a local server.",
42
+ adoption: { sdks: ['@volter/tunnel'], envStems: ['TUNNEL', 'VOLTERTUNNEL'] },
43
+ hosts: [{ host: 'voltertest.xyz' }, { suffix: '.voltertest.xyz' }, { host: 'www.voltertest.xyz', exclude: true }, { host: 'twins.voltertest.xyz', exclude: true }],
44
+ credentialDoor: { path: '/_twin/accounts', body: { slug: 'world' }, fill: { TUNNEL_SECRET: 'token', TUNNEL_SERVER_URL: 'server', SUPERCODE_TUNNEL_SERVER_URL: 'server' } },
45
+ endpointEnvNone: 'the relay URL is the credential door\'s (TUNNEL_SERVER_URL, SUPERCODE_TUNNEL_SERVER_URL), the World claiming voltertest.xyz',
46
+ },
47
+ };
@@ -0,0 +1,41 @@
1
+ // THE RELAY'S FRONT: a request to a tunnel (its host `<tunnelId>.voltertest.xyz`, or `?__tunnel=<id>` on a direct host,
2
+ // as the relay's worker routes one) is sent down the control socket of the client holding the tunnel and answered with
3
+ // what the client sends back (the kernel's request answered by a session); anything else is the relay's own.
4
+ import type { HandlerContext } from '@volter/world-core';
5
+ import { b64encode, DOMAIN, holder, RESERVED } from './shared.ts';
6
+
7
+ const text = (status: number, body: string): Response => new Response(body, { status, headers: { 'content-type': 'text/plain; charset=utf-8' } });
8
+ const HOP = new Set(['host', 'connection', 'upgrade', 'keep-alive', 'transfer-encoding', 'content-length']);
9
+
10
+ /** The tunnel a request is for: its host's label, or `?__tunnel=` on a direct host (the relay's local testing). */
11
+ function tunnelOf(request: Request): string | undefined {
12
+ const url = new URL(request.url);
13
+ const host = (request.headers.get('host') ?? url.host).toLowerCase().replace(/:\d+$/, '');
14
+ const label = host.endsWith(`.${DOMAIN}`) ? host.slice(0, -DOMAIN.length - 1) : undefined;
15
+ if (label && !label.includes('.') && !RESERVED.has(label)) return label;
16
+ const direct = host === 'localhost' || host.startsWith('127.') || host === '0.0.0.0' || !host.endsWith(DOMAIN);
17
+ return direct ? url.searchParams.get('__tunnel') ?? undefined : undefined;
18
+ }
19
+
20
+ export async function around(ctx: HandlerContext, next: (request?: Request) => Promise<Response>): Promise<Response> {
21
+ const request = ctx.call.request;
22
+ const id = tunnelOf(request);
23
+ if (!id) return next();
24
+ const client = holder(ctx, id);
25
+ if (!client) return text(502, 'Tunnel not connected');
26
+ const url = new URL(request.url);
27
+ url.searchParams.delete('__tunnel');
28
+ const body = request.method === 'GET' || request.method === 'HEAD' ? null : new Uint8Array(await request.arrayBuffer());
29
+ // a body goes in one frame: over the relay's limit it is refused at once
30
+ if (body && body.length > 750_000) return text(413, 'Request body too large to tunnel (limit ~750KB)');
31
+ const headers = Object.fromEntries([...request.headers].filter(([k]) => !HOP.has(k.toLowerCase()) && !k.toLowerCase().startsWith('x-volter')));
32
+ // the relay's correlation key for this request on this client's socket: the socket's own count, not World state (a
33
+ // request the relay forwards is no subject of the World)
34
+ const reqId = `${client.id}-${(client.state.requests = Number(client.state.requests ?? 0) + 1)}`;
35
+ client.send({ type: 'request', reqId, method: request.method, path: `${url.pathname}${url.search}`, headers, body: body?.length ? b64encode(body) : null });
36
+ try {
37
+ return (await client.expect(reqId, 30_000)) as Response;
38
+ } catch {
39
+ return text(504, 'Tunnel timeout');
40
+ }
41
+ }
@@ -0,0 +1,21 @@
1
+ // The World's doors (architecture, "Doors, screens and the gap"): an account on the relay and its token, as its signup
2
+ // makes one.
3
+ import type { HandlerContext } from '@volter/world-core';
4
+ import { ACCOUNT, sha256 } from './shared.ts';
5
+
6
+ /** An account by its slug (answered again for the same slug) and its token, drawn from the World's secret. */
7
+ export async function accounts(ctx: HandlerContext): Promise<Response> {
8
+ const b = (ctx.body && typeof ctx.body === 'object' ? ctx.body : {}) as Record<string, unknown>;
9
+ const slug = String(b.slug ?? '');
10
+ // the relay's account slug (server-cf/src/credits.ts, SLUG_RE)
11
+ if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(slug)) return Response.json({ message: 'slug must be lowercase letters, digits and dashes' }, { status: 400 });
12
+ let account = ctx.rowsRaw(ACCOUNT).find((a) => a.deleted !== true && a.slug === slug);
13
+ // Where the documentation stops: a token's form is the relay's own; the twin's is `vt_` and 40 letters and digits
14
+ const tokenOf = async (id: string): Promise<string> => `vt_${(await ctx.secret(`tunnel-token:${id}`)).replace(/[^A-Za-z0-9]/g, '').slice(0, 40)}`;
15
+ if (!account) {
16
+ const id = ctx.mint(ACCOUNT);
17
+ await ctx.record(ACCOUNT, { slug, token_sha256: sha256(ctx, await tokenOf(id)) }, id);
18
+ account = ctx.row(ACCOUNT, id)!;
19
+ }
20
+ return Response.json({ slug, token: await tokenOf(String(account.id)), server: `https://voltertest.xyz` }, { status: 201 });
21
+ }
@@ -0,0 +1,6 @@
1
+ // GENERATED by scripts/create-pack.ts --index: the handler map, by operationId, from the families' files (each
2
+ // handler named as its operation). Do not edit; add a handler to its family's file and generate again.
3
+ import type { Handler } from '@volter/world-core';
4
+
5
+ export const tunnelSemantics: Record<string, Handler> = {
6
+ };
@@ -0,0 +1,26 @@
1
+ // What the relay shares: accounts and their tokens (each by its SHA-256), the tunnel ids they hold, the relay's domain,
2
+ // and a tunnel's control session found among the World's open sockets.
3
+ import type { HandlerContext, SocketSession } from '@volter/world-core';
4
+
5
+ export type Row = Record<string, unknown>;
6
+ export type { HandlerContext };
7
+ export const ACCOUNT = '_account';
8
+ export const TUNNEL = '_tunnel';
9
+ // the relay's TUNNEL_DOMAIN (server-cf/wrangler.jsonc, spec/SOURCE.md)
10
+ export const DOMAIN = 'voltertest.xyz';
11
+ /** Labels of the domain that are not tunnels: the relay's own front (`www`, its RESERVED_HOSTS default) and the twins
12
+ * cloud Volter serves there. */
13
+ export const RESERVED = new Set(['www', 'twins']);
14
+
15
+ export const sha256 = (ctx: Pick<HandlerContext, 'crypto'>, v: string): string => ctx.crypto.digest('sha256', v);
16
+ export const tunnelUrl = (id: string): string => `https://${id}.${DOMAIN}`;
17
+
18
+ /** A tunnel id as the relay's registry takes one: lowercase letters, digits and inner dashes. */
19
+ export const validId = (id: string): boolean => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(id);
20
+
21
+ /** The registered control session holding a tunnel, if a client holds it now. */
22
+ export const holder = (ctx: Pick<HandlerContext, 'sockets'>, id: string): SocketSession | undefined =>
23
+ ctx.sockets('control').find((s) => s.state.registered === true && s.state.tunnelId === id);
24
+
25
+ export const b64encode = (bytes: Uint8Array): string => btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(''));
26
+ export const b64decode = (text: string): Uint8Array => Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
@@ -0,0 +1,69 @@
1
+ // The relay's control channel (the manifest's socket `control`, the kernel's sockets.ts): a client registers a tunnel id
2
+ // with its token and is told the tunnel's URL; then the relay sends it each public request (./../semantics/around.ts)
3
+ // and the client answers with a whole `response` or a stream (`response-start`, `response-chunk`s, `response-end`),
4
+ // which settle the request waiting on it by its `reqId`. Frames as volter-tunnel's protocol.ts defines them.
5
+ import type { HandlerContext, SocketEngine, SocketSession } from '@volter/world-core';
6
+ import { ACCOUNT, b64decode, holder, type Row, sha256, TUNNEL, tunnelUrl, validId } from './shared.ts';
7
+
8
+ type Streams = Map<string, ReadableStreamDefaultController<Uint8Array>>;
9
+ const obj = (v: unknown): Row => (v && typeof v === 'object' && !Array.isArray(v) ? v as Row : {});
10
+ const streams = (s: SocketSession): Streams => (s.state.streams ??= new Map()) as Streams;
11
+ const headersOf = (v: unknown): Headers => { const h = new Headers(); for (const [k, x] of Object.entries(obj(v))) h.set(k, String(x)); return h; };
12
+
13
+ /** A rejected registration: the error frame, then the close (protocol.ts `ErrorMsg`; tunnel-do's rejection). */
14
+ function reject(s: SocketSession, message: string, code: number, reason: string): void {
15
+ s.send({ type: 'error', message });
16
+ s.close(code, reason);
17
+ }
18
+
19
+ async function register(s: SocketSession, msg: Row, ctx: HandlerContext): Promise<void> {
20
+ const token = typeof msg.secret === 'string' ? msg.secret : '';
21
+ const account = token ? ctx.rowsRaw(ACCOUNT).find((a) => a.deleted !== true && a.token_sha256 === sha256(ctx, token)) : undefined;
22
+ if (!account) return reject(s, 'Tunnel rejected: credential is invalid or revoked. On an owner-authenticated device, run "volter-tunnel tokens" and restore this host token, or sign in again on the host.', 4003, 'badToken');
23
+ // the control URL's ?id= names the tunnel (the routing relay's authority), else the register frame's, else one minted
24
+ // This id names a stored reservation: mint it, then record the reservation with its owner below. ctx.issue is for
25
+ // issued ids whose subjects the vendor does not store; its empty row would look like another account's reservation.
26
+ const id = String(s.state.tunnelId || msg.tunnelId || ctx.mint(TUNNEL));
27
+ if (!validId(id)) return reject(s, `Tunnel rejected: invalid tunnel id ${id}`, 4029, 'invalidId');
28
+ const held = ctx.row(TUNNEL, id);
29
+ // Where the documentation stops: a tunnel id another account holds is refused, as the relay's reservation refuses it
30
+ if (held && held._owner !== account.id) return reject(s, `Tunnel rejected: ${id} is reserved by another account`, 4029, 'reserved');
31
+ const live = holder(ctx, id);
32
+ if (live && live !== s) {
33
+ // Where the documentation stops: a live registration of the id is refused unless this one replaces it
34
+ if (msg.replace !== true) return reject(s, `Tunnel rejected: ${id} is already connected`, 4029, 'inUse');
35
+ live.state.registered = false;
36
+ live.close(4001, 'Replaced by new client');
37
+ }
38
+ if (!held) await ctx.record(TUNNEL, { _owner: account.id, reservedAt: ctx.occurredAt }, id);
39
+ Object.assign(s.state, { registered: true, tunnelId: id, account: account.id, authRequired: msg.authRequired !== false });
40
+ s.send({ type: 'registered', tunnelId: id, url: tunnelUrl(id) });
41
+ }
42
+
43
+ export const control: SocketEngine<HandlerContext> = {
44
+ open(s, ctx) {
45
+ s.state.tunnelId = new URL(ctx.call.request.url).searchParams.get('id') ?? '';
46
+ },
47
+ async message(s, data, ctx) {
48
+ let msg: Row;
49
+ try { msg = obj(JSON.parse(data)); } catch { return; }
50
+ if (msg.type === 'register') return register(s, msg, ctx);
51
+ if (s.state.registered !== true) return;
52
+ const reqId = String(msg.reqId ?? '');
53
+ if (msg.type === 'response') {
54
+ s.fulfil(reqId, new Response(msg.body ? b64decode(String(msg.body)) as Uint8Array<ArrayBuffer> : null, { status: Number(msg.status ?? 502), headers: headersOf(msg.headers) }));
55
+ } else if (msg.type === 'response-start') {
56
+ const body = new ReadableStream<Uint8Array>({ start: (c) => { streams(s).set(reqId, c); } });
57
+ s.fulfil(reqId, new Response(body, { status: Number(msg.status ?? 502), headers: headersOf(msg.headers) }));
58
+ } else if (msg.type === 'response-chunk') {
59
+ streams(s).get(reqId)?.enqueue(b64decode(String(msg.data ?? '')));
60
+ } else if (msg.type === 'response-end') {
61
+ streams(s).get(reqId)?.close();
62
+ streams(s).delete(reqId);
63
+ }
64
+ },
65
+ close(s) {
66
+ for (const c of streams(s).values()) { try { c.error(new Error('Tunnel closed')); } catch { /* already ended */ } }
67
+ streams(s).clear();
68
+ },
69
+ };
@@ -0,0 +1,3 @@
1
+ // The relay's state rulings (architecture, "State"): a tunnel id is held by the account that registered it first; a
2
+ // session's registration is the connection's, never World state.
3
+ export const states = {};
package/src/server.ts ADDED
@@ -0,0 +1,10 @@
1
+ // FETCH-FIRST (runtime contract R12b): a fixed file, the kernel's serve seam around the pack's fetch (./fetch.ts).
2
+ import { serveHttp, type PackFetchOptions } from '@volter/world-core';
3
+ import { createTunnelFetch } from './fetch.ts';
4
+
5
+ export async function createTunnelTwinServer(options: PackFetchOptions & { port?: number } = {}): Promise<{ port: number; stop: () => void }> {
6
+ const fetch = createTunnelFetch(options);
7
+ // the vendor's sockets, when its manifest declares any (the kernel's sockets.ts)
8
+ const server = await serveHttp({ port: options.port ?? 0, idleTimeout: 60, fetch, ...(fetch.upgrade ? { upgrade: fetch.upgrade } : {}) });
9
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
10
+ }
@@ -1,21 +0,0 @@
1
- import { RateBudget, type RateBudgetDeclaration, type RateBudgetOptions } from '@volter/world-core';
2
- export declare const TUNNEL_BUDGET_WINDOW_MS = 60000;
3
- export declare const TUNNEL_BUDGET_CEILING = 60;
4
- export declare const TUNNEL_CALL_WEIGHTS: {
5
- readonly create: 3;
6
- readonly other: 2;
7
- };
8
- /** Cloudflare documents a 200 concurrent in-flight request cap for Quick Tunnels, but no scalar
9
- * create-control-API rate. Volter's relay publishes data-plane and signup rates, not a status-read
10
- * rate. The connector ledger is account-wide, so it uses the kernel's conservative fallback. */
11
- export declare const TUNNEL_RATE_BUDGET: RateBudgetDeclaration;
12
- export declare const tunnelCallWeight: (method: string, path: string) => number;
13
- export declare const tunnelBudgetPath: (opts?: {
14
- root?: string;
15
- token?: string;
16
- } | string) => string;
17
- export type TunnelBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
18
- export declare class TunnelBudget extends RateBudget {
19
- constructor(opts?: TunnelBudgetOptions);
20
- }
21
- export { RateBudgetError as TunnelBudgetError } from '@volter/world-core';
@@ -1,26 +0,0 @@
1
- import { declareRateBudget, rateBudgetPath, rateBudgetWeight, RateBudget, } from '@volter/world-core';
2
- const VENDOR = 'tunnel';
3
- export const TUNNEL_BUDGET_WINDOW_MS = 60_000;
4
- export const TUNNEL_BUDGET_CEILING = 60;
5
- export const TUNNEL_CALL_WEIGHTS = { create: 3, other: 2 };
6
- /** Cloudflare documents a 200 concurrent in-flight request cap for Quick Tunnels, but no scalar
7
- * create-control-API rate. Volter's relay publishes data-plane and signup rates, not a status-read
8
- * rate. The connector ledger is account-wide, so it uses the kernel's conservative fallback. */
9
- export const TUNNEL_RATE_BUDGET = {
10
- windowMs: TUNNEL_BUDGET_WINDOW_MS,
11
- ceiling: TUNNEL_BUDGET_CEILING,
12
- defaultWeight: TUNNEL_CALL_WEIGHTS.other,
13
- maxRetryAfterSeconds: 300,
14
- rules: [{ match: '^POST /tunnel$', weight: TUNNEL_CALL_WEIGHTS.create }],
15
- reason: 'Cloudflare Quick Tunnels documentation (developers.cloudflare.com, fetched 2026-08-20) publishes a 200 concurrent in-flight data-plane cap but no scalar quick-control creation rate; the first-party cloudflared source POSTs /tunnel once per quick session. The owned volter-tunnel relay (a5abc628, 2026-08-20) publishes data-plane and signup limits, not a status-read rate. This account-wide connector therefore uses the kernel fallback: 60 weighted units/60s, reads weight 2 (30/min) and tunnel allocation weight 3 (20/min), never more permissive than an undeclared vendor.',
16
- };
17
- declareRateBudget(VENDOR, TUNNEL_RATE_BUDGET);
18
- export const tunnelCallWeight = (method, path) => rateBudgetWeight(VENDOR, `${method.toUpperCase()} ${path}`);
19
- export const tunnelBudgetPath = (opts = {}) => {
20
- const value = typeof opts === 'string' ? { root: opts } : opts;
21
- return rateBudgetPath({ ...value, vendor: VENDOR });
22
- };
23
- export class TunnelBudget extends RateBudget {
24
- constructor(opts = {}) { super({ ...opts, vendor: VENDOR }); }
25
- }
26
- export { RateBudgetError as TunnelBudgetError } from '@volter/world-core';
@@ -1,4 +0,0 @@
1
- import { type CapabilityReport, type CapabilitySpec } from '@volter/world-tooling';
2
- export declare const TUNNEL_CAPABILITIES: CapabilitySpec[];
3
- export declare const TUNNEL_AREAS: readonly ["cloudflare_quick", "cloudflare_managed", "volter_control", "volter_http", "volter_websocket", "volter_auth", "volter_reservations", "volter_accounts", "volter_usage", "volter_signup", "volter_inspector", "volter_management", "connector"];
4
- export declare function tunnelCapabilities(): Promise<CapabilityReport>;