@volter/twin-tunnel 0.1.37 → 1.0.1

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 (87) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +13 -121
  3. package/dist/generated/pack-facts.json +54 -0
  4. package/dist/journeys/coverage.json +109 -0
  5. package/dist/journeys/customer-life.json +588 -0
  6. package/dist/journeys/customer-life.outline.md +34 -0
  7. package/dist/journeys/customer-life.outline.verdict.json +42 -0
  8. package/dist/journeys/customer-life.verdict.json +51 -0
  9. package/dist/journeys/decisions.json +23 -0
  10. package/dist/journeys/demand.json +24 -0
  11. package/dist/journeys/score.json +134 -0
  12. package/dist/journeys/unreachable.json +12 -0
  13. package/dist/journeys/vendor-examples.json +7 -0
  14. package/dist/journeys/vendor-examples.verdict.json +18 -0
  15. package/dist/spec/SOURCE.md +30 -0
  16. package/dist/spec/openapi.json +50 -0
  17. package/dist/spec/sources.json +1 -0
  18. package/dist/src/cli.js +15 -53
  19. package/dist/src/fetch.d.ts +2 -0
  20. package/dist/src/fetch.js +19 -0
  21. package/dist/src/generated/surface.gen.json +1 -0
  22. package/dist/src/generated/ui.gen.json +1 -0
  23. package/dist/src/index.d.ts +5 -7
  24. package/dist/src/index.js +9 -42
  25. package/dist/src/manifest.d.ts +2 -0
  26. package/dist/src/manifest.js +49 -0
  27. package/dist/src/semantics/around.d.ts +2 -0
  28. package/dist/src/semantics/around.js +72 -0
  29. package/dist/src/semantics/doors.d.ts +3 -0
  30. package/dist/src/semantics/doors.js +18 -0
  31. package/dist/src/semantics/index.d.ts +2 -0
  32. package/dist/src/semantics/index.js +1 -0
  33. package/dist/src/semantics/shared.d.ts +22 -0
  34. package/dist/src/semantics/shared.js +73 -0
  35. package/dist/src/semantics/sockets.d.ts +2 -0
  36. package/dist/src/semantics/sockets.js +161 -0
  37. package/dist/src/semantics/states.d.ts +1 -0
  38. package/dist/src/semantics/states.js +3 -0
  39. package/dist/src/server.d.ts +7 -0
  40. package/dist/src/server.js +9 -0
  41. package/generated/pack-facts.json +54 -0
  42. package/journeys/coverage.json +109 -0
  43. package/journeys/customer-life.json +588 -0
  44. package/journeys/customer-life.outline.md +34 -0
  45. package/journeys/customer-life.outline.verdict.json +42 -0
  46. package/journeys/customer-life.verdict.json +51 -0
  47. package/journeys/decisions.json +23 -0
  48. package/journeys/demand.json +24 -0
  49. package/journeys/score.json +134 -0
  50. package/journeys/unreachable.json +12 -0
  51. package/journeys/vendor-examples.json +7 -0
  52. package/journeys/vendor-examples.verdict.json +18 -0
  53. package/package.json +17 -32
  54. package/spec/SOURCE.md +30 -0
  55. package/spec/openapi.json +50 -0
  56. package/spec/sources.json +1 -0
  57. package/src/cli.ts +15 -41
  58. package/src/fetch.ts +20 -0
  59. package/src/generated/surface.gen.json +1 -0
  60. package/src/generated/ui.gen.json +1 -0
  61. package/src/index.ts +10 -52
  62. package/src/manifest.ts +54 -0
  63. package/src/semantics/around.ts +68 -0
  64. package/src/semantics/doors.ts +21 -0
  65. package/src/semantics/index.ts +6 -0
  66. package/src/semantics/shared.ts +78 -0
  67. package/src/semantics/sockets.ts +127 -0
  68. package/src/semantics/states.ts +3 -0
  69. package/src/server.ts +10 -0
  70. package/dist/src/tunnel-budget.d.ts +0 -21
  71. package/dist/src/tunnel-budget.js +0 -26
  72. package/dist/src/tunnel-capabilities.d.ts +0 -4
  73. package/dist/src/tunnel-capabilities.js +0 -2091
  74. package/dist/src/tunnel-conformance.d.ts +0 -10
  75. package/dist/src/tunnel-conformance.js +0 -63
  76. package/dist/src/tunnel-connector.d.ts +0 -21
  77. package/dist/src/tunnel-connector.js +0 -68
  78. package/dist/src/tunnel-server.d.ts +0 -109
  79. package/dist/src/tunnel-server.js +0 -1197
  80. package/dist/src/tunnel-twin.d.ts +0 -175
  81. package/dist/src/tunnel-twin.js +0 -236
  82. package/src/tunnel-budget.ts +0 -34
  83. package/src/tunnel-capabilities.ts +0 -1770
  84. package/src/tunnel-conformance.ts +0 -62
  85. package/src/tunnel-connector.ts +0 -64
  86. package/src/tunnel-server.ts +0 -1194
  87. package/src/tunnel-twin.ts +0 -339
@@ -0,0 +1,78 @@
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
+ // source: spec:/x-reference/domain "voltertest.xyz"
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
+
19
+
20
+ /** The registered control session holding a tunnel, if a client holds it now. */
21
+ export const holder = (ctx: Pick<HandlerContext, 'sockets'>, id: string): SocketSession | undefined =>
22
+ ctx.sockets('control').find((s) => s.state.registered === true && s.state.tunnelId === id);
23
+
24
+ export const b64encode = (bytes: Uint8Array): string => btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join(''));
25
+ export const b64decode = (text: string): Uint8Array<ArrayBuffer> => Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
26
+
27
+ // source: spec:/x-reference/auth "The relay answers preflight itself"
28
+ export function cors(request: Request): Record<string, string> {
29
+ const origin = request.headers.get('origin');
30
+ return origin ? {
31
+ 'access-control-allow-origin': origin,
32
+ 'access-control-allow-methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
33
+ 'access-control-allow-headers': request.headers.get('access-control-request-headers') || 'Content-Type, Authorization, X-Sandbox-Id',
34
+ 'access-control-allow-credentials': 'true',
35
+ } : {};
36
+ }
37
+
38
+ /** Request metadata lasts only until the local client starts its reply, on that live connection. */
39
+ export function requests(s: SocketSession): Map<string, Request> {
40
+ return (s.state.pendingRequests ??= new Map()) as Map<string, Request>;
41
+ }
42
+
43
+ // source: spec:/x-reference/auth "Build the downstream response headers: drop hop-by-hop + downstream CORS"
44
+ export function responseHeaders(downstream: Headers, request: Request): Headers {
45
+ const headers = new Headers(downstream);
46
+ for (const key of ['transfer-encoding', 'connection', 'access-control-allow-origin', 'access-control-allow-methods', 'access-control-allow-headers', 'access-control-allow-credentials', 'x-frame-options']) headers.delete(key);
47
+ // source: spec:/x-reference/auth "strip CSP frame-ancestors / X-Frame-Options"
48
+ for (const key of ['content-security-policy', 'content-security-policy-report-only']) {
49
+ const value = headers.get(key);
50
+ if (value) {
51
+ const kept = value.split(';').filter((d) => !d.trim().startsWith('frame-ancestors')).join(';').trim();
52
+ if (kept) headers.set(key, kept); else headers.delete(key);
53
+ }
54
+ }
55
+ // source: spec:/x-reference/auth "only a downstream response that clearly says"
56
+ const cache = (headers.get('cache-control') ?? '').toLowerCase();
57
+ const publicCache = /(?:^|,)\s*public(?:\s|,|$)/.test(cache) && !/(?:^|,)\s*(?:private|no-store|no-cache)(?:\s|,|=|$)/.test(cache);
58
+ const path = new URL(request.url).pathname;
59
+ if (!publicCache || path === '/api' || path.startsWith('/api/')) {
60
+ headers.set('cache-control', 'private, no-store, max-age=0');
61
+ headers.set('cloudflare-cdn-cache-control', 'no-store');
62
+ headers.set('cdn-cache-control', 'no-store');
63
+ const vary = new Set((headers.get('vary') ?? '').split(',').map((v) => v.trim()).filter(Boolean));
64
+ vary.add('Cookie'); vary.add('Authorization'); headers.set('vary', [...vary].join(', '));
65
+ }
66
+ for (const [key, value] of Object.entries(cors(request))) headers.set(key, value);
67
+ return headers;
68
+ }
69
+
70
+ // source: spec:/x-reference/relay "Malformed tunnel response"
71
+ export function bufferedBody(text: unknown): Uint8Array<ArrayBuffer> | null | undefined {
72
+ try { return text ? b64decode(String(text)) : null; } catch { return undefined; }
73
+ }
74
+
75
+ /** A provisional connection claim prevents two initial registrants passing the live-session decision. */
76
+ export function claimant(ctx: Pick<HandlerContext, 'sockets'>, id: string): SocketSession | undefined {
77
+ return ctx.sockets('control').find((s) => s.state.superseded !== true && (s.state.registered === true || s.state.registering === true) && s.state.tunnelId === id);
78
+ }
@@ -0,0 +1,127 @@
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, bufferedBody, claimant, requests, responseHeaders, type Row, sha256, TUNNEL, tunnelUrl } 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
+ // source: spec:/x-reference/relay "Invalid tunnel secret"
23
+ if (!account) return reject(s, 'Invalid tunnel secret', 4003, 'Invalid tunnel secret');
24
+ // the control URL's ?id= names the tunnel (the routing relay's authority), else the register frame's, else one minted
25
+ // This id names a stored reservation: mint it, then record the reservation with its owner below. ctx.issue is for
26
+ // issued ids whose subjects the vendor does not store; its empty row would look like another account's reservation.
27
+ // source: spec:/x-reference/relay "Prefer the ?id= captured at accept (authoritative"
28
+ const id = String(s.state.tunnelId || msg.tunnelId || ctx.mint(TUNNEL));
29
+ const held = ctx.row(TUNNEL, id);
30
+ // source: spec:/x-reference/relay "Tunnel ID reserved"
31
+ if (held && held._owner !== account.id) return reject(s, `Tunnel ID '${id}' is reserved by another account.`, 4002, 'Tunnel ID reserved');
32
+ const live = claimant(ctx, id);
33
+ if (live && live !== s) {
34
+ // source: spec:/x-reference/relay "Pass { replace: true } to take over."
35
+ if (msg.replace !== true) return reject(s, `Tunnel ID '${id}' is already in use by another client. Pass { replace: true } to take over.`, 4002, 'Tunnel ID already in use');
36
+ // source: spec:/x-reference/relay "Tunnel replaced"
37
+ live.state.superseded = true;
38
+ live.state.disconnectReason = 'Tunnel replaced';
39
+ live.state.registered = false;
40
+ // source: spec:/x-reference/relay "Replaced by new client"
41
+ live.close(4001, 'Replaced by new client');
42
+ }
43
+ Object.assign(s.state, { registering: true, tunnelId: id, account: account.id });
44
+ try { if (!held) await ctx.record(TUNNEL, { _owner: account.id, reservedAt: ctx.occurredAt }, id); }
45
+ finally { s.state.registering = false; }
46
+ if (s.state.superseded === true || !ctx.sockets('control').includes(s)) return;
47
+ Object.assign(s.state, { registered: true, tunnelId: id, account: account.id, authRequired: msg.authRequired !== false });
48
+ // source: spec:/x-reference/protocol "Acknowledges a successful registration; carries the public URL + usage."
49
+ s.send({ type: 'registered', tunnelId: id, url: tunnelUrl(id) });
50
+ }
51
+
52
+ export const control: SocketEngine<HandlerContext> = {
53
+ open(s, ctx) {
54
+ s.state.tunnelId = new URL(ctx.call.request.url).searchParams.get('id') ?? '';
55
+ },
56
+ async message(s, data, ctx) {
57
+ let msg: Row;
58
+ try { msg = obj(JSON.parse(data)); } catch { return; }
59
+ if (msg.type === 'register') return register(s, msg, ctx);
60
+ if (s.state.registered !== true) return;
61
+ const reqId = String(msg.reqId ?? '');
62
+ const request = requests(s).get(reqId);
63
+ // source: spec:/x-reference/protocol "A complete (buffered) response"
64
+ if (msg.type === 'response') {
65
+ if (!request) return;
66
+ const headers = responseHeaders(headersOf(msg.headers), request);
67
+ const body = bufferedBody(msg.body);
68
+ // source: spec:/x-reference/relay "Malformed tunnel response"
69
+ if (body === undefined) s.fulfil(reqId, new Response('Malformed tunnel response', { status: 502, headers }));
70
+ else s.fulfil(reqId, new Response(request.method === 'HEAD' ? null : body, { status: Number(msg.status || 200), headers }));
71
+ // source: spec:/x-reference/protocol "Start of a streamed response"
72
+ } else if (msg.type === 'response-start') {
73
+ if (!request) return;
74
+ const status = Number(msg.status || 200);
75
+ const headers = responseHeaders(headersOf(msg.headers), request);
76
+ // source: spec:/x-reference/relay "HEAD is bodyless by contract too."
77
+ if (request.method === 'HEAD' || status === 204 || status === 205 || status === 304) {
78
+ s.fulfil(reqId, new Response(null, { status, headers }));
79
+ } else {
80
+ const body = new ReadableStream<Uint8Array>({ start: (c) => { streams(s).set(reqId, c); }, cancel: () => cancelStream(s, reqId) });
81
+ armStreamIdle(s, reqId, ctx);
82
+ s.fulfil(reqId, new Response(body, { status, headers }));
83
+ }
84
+ // source: spec:/x-reference/protocol "One streamed response body chunk."
85
+ } else if (msg.type === 'response-chunk') {
86
+ const stream = streams(s).get(reqId);
87
+ if (stream) {
88
+ try { stream.enqueue(b64decode(String(msg.data ?? ''))); s.fulfil(`idle:${reqId}`, null); armStreamIdle(s, reqId, ctx); }
89
+ catch { streams(s).delete(reqId); s.fulfil(`idle:${reqId}`, null); }
90
+ }
91
+ // source: spec:/x-reference/protocol "End of a streamed response."
92
+ } else if (msg.type === 'response-end') {
93
+ const stream = streams(s).get(reqId);
94
+ streams(s).delete(reqId);
95
+ s.fulfil(`idle:${reqId}`, null);
96
+ if (stream) { try { stream.close(); } catch { /* already closed */ } }
97
+ }
98
+ },
99
+ close(s) {
100
+ s.state.registered = false;
101
+ s.state.closed = true;
102
+ for (const c of streams(s).values()) { try { c.error(new Error('Tunnel closed')); } catch { /* already ended */ } }
103
+ streams(s).clear();
104
+ },
105
+ };
106
+
107
+ // source: spec:/x-reference/relay "Idle watchdog for a streaming response"
108
+ function armStreamIdle(s: SocketSession, reqId: string, ctx: HandlerContext): void {
109
+ // source: spec:/x-reference/relay "120000"
110
+ void s.expect(`idle:${reqId}`, 120_000).catch(() => { if (ctx.sockets('control').includes(s)) expireStream(s, reqId); });
111
+ }
112
+
113
+ // source: spec:/x-reference/relay "stream idle timeout"
114
+ function expireStream(s: SocketSession, reqId: string): void {
115
+ const stream = streams(s).get(reqId);
116
+ if (!stream) return;
117
+ streams(s).delete(reqId);
118
+ try { stream.error(new Error('stream idle timeout')); } catch { /* already closed */ }
119
+ s.send({ type: 'request-abort', reqId });
120
+ }
121
+
122
+ // source: spec:/x-reference/protocol "The relay asks the client to abort an in-flight request."
123
+ function cancelStream(s: SocketSession, reqId: string): void {
124
+ streams(s).delete(reqId);
125
+ s.fulfil(`idle:${reqId}`, null);
126
+ s.send({ type: 'request-abort', reqId });
127
+ }
@@ -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>;