@volter/twin-tunnel 0.1.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.
- package/README.md +123 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +59 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +42 -0
- package/dist/src/tunnel-budget.d.ts +21 -0
- package/dist/src/tunnel-budget.js +26 -0
- package/dist/src/tunnel-capabilities.d.ts +4 -0
- package/dist/src/tunnel-capabilities.js +2091 -0
- package/dist/src/tunnel-conformance.d.ts +10 -0
- package/dist/src/tunnel-conformance.js +63 -0
- package/dist/src/tunnel-connector.d.ts +21 -0
- package/dist/src/tunnel-connector.js +68 -0
- package/dist/src/tunnel-server.d.ts +109 -0
- package/dist/src/tunnel-server.js +1197 -0
- package/dist/src/tunnel-twin.d.ts +175 -0
- package/dist/src/tunnel-twin.js +236 -0
- package/package.json +51 -0
- package/src/cli.ts +48 -0
- package/src/index.ts +53 -0
- package/src/tunnel-budget.ts +34 -0
- package/src/tunnel-capabilities.ts +1770 -0
- package/src/tunnel-conformance.ts +62 -0
- package/src/tunnel-connector.ts +64 -0
- package/src/tunnel-server.ts +1194 -0
- package/src/tunnel-twin.ts +339 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @volter/twin-tunnel
|
|
2
|
+
|
|
3
|
+
> **Legacy connector helpers:** this package still has callable helpers using the retired v1
|
|
4
|
+
> `syncPull` API. Those paths require migration before use on the current kernel;
|
|
5
|
+
> older helper descriptions below do not establish current compatibility. Check the
|
|
6
|
+
> [generated index](../../../generated/INDEX.md) for protocol standing and use the
|
|
7
|
+
> [shared model](../../../docs/concepts/the-model.md) for current state semantics.
|
|
8
|
+
|
|
9
|
+
A local twin of two tunnel surfaces used by Volter worlds:
|
|
10
|
+
|
|
11
|
+
- Cloudflare Quick Tunnel allocation (`POST /tunnel`) using the first-party `cloudflared`
|
|
12
|
+
quick-service field envelope. Generated field values are independent twin-local facts; the twin
|
|
13
|
+
does not assert undocumented relationships among `id`, `name`, `hostname`, or `account_tag`.
|
|
14
|
+
- The owned `@volter/tunnel` WebSocket control protocol and HTTP relay path.
|
|
15
|
+
|
|
16
|
+
The boundary is deliberate. The control plane is twinned; payload transport is real loopback
|
|
17
|
+
HTTP. `world-tunnel quick --url ...` prints the machine-readable
|
|
18
|
+
`VOLTER_SHARE_URL=http://127.0.0.1:...` handshake and genuinely forwards through that loopback
|
|
19
|
+
ingress. It does not claim Cloudflare edge QUIC, public DNS, TLS, reachability, or an SLA. Those
|
|
20
|
+
remain real deployment concerns. `quick --url` accepts a credential-free loopback HTTP(S) origin
|
|
21
|
+
only; path/query/fragment and backslash-normalized inputs are rejected rather than silently
|
|
22
|
+
discarded while forwarding.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Local share rehearsal used by world cookbooks
|
|
26
|
+
world-tunnel quick --url http://127.0.0.1:3000
|
|
27
|
+
|
|
28
|
+
# Standalone quick API + Volter relay
|
|
29
|
+
world-tunnel serve --port 8787 --root .volter/world/tunnel
|
|
30
|
+
world-tunnel conformance
|
|
31
|
+
# Cloudflare control URL: http://127.0.0.1:8787/tunnel
|
|
32
|
+
# @volter/tunnel host: http://127.0.0.1:8787
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
An unmodified `@volter/tunnel` client registers against the `serve` URL and forwards requests to
|
|
36
|
+
its configured local port. Its normal default (`authRequired: true`) is preserved: anonymous
|
|
37
|
+
requests receive 401, and the server's `mintToken(tunnelId)` helper creates a fake-local,
|
|
38
|
+
tunnel-bound HS256 token for Bearer/query/cookie rehearsal. Matching the owned relay default, a
|
|
39
|
+
valid signed token without `tid` is shared SSO while a present `tid` must match; start `serve` with
|
|
40
|
+
`--require-tid` to select the owned `REQUIRE_TID=true` policy. Basic-auth values remain only in the
|
|
41
|
+
live socket session; the append-only kernel stores a SHA-256 digest and configured bit, never the
|
|
42
|
+
plaintext. As upstream does, Basic auth is configured only when both user and password are
|
|
43
|
+
non-empty; an SDK-emitted empty value leaves that gate disabled. The twin accepts any non-empty
|
|
44
|
+
fake relay credential and rejects a missing one.
|
|
45
|
+
|
|
46
|
+
**Browser WebSockets relay for real.** A `ws://<relay>/__twin/tunnels/<id>/<path>` upgrade passes
|
|
47
|
+
the same Basic/JWT gates the HTTP path does, then bridges: the relay sends `ws-upgrade` with a
|
|
48
|
+
fresh `connId`, the tunnelled path and the browser’s own handshake headers; the client answers
|
|
49
|
+
`ws-ready` (or `ws-error`, which closes the browser socket with 1011 and the client’s own reason);
|
|
50
|
+
`ws-message` carries base64 payloads with the `binary` flag in both directions; `ws-close` bridges
|
|
51
|
+
either way with the vendor’s own code clamping. Frames a browser sends before `ws-ready` are
|
|
52
|
+
QUEUED and delivered in order rather than dropped, a control socket may only drive the `connId`s
|
|
53
|
+
it owns, and a control socket closing takes its bridges with it. `request-abort` is now sent from
|
|
54
|
+
ONE place for every way a request can end badly — timeout, visitor hang-up, malformed frame, owner
|
|
55
|
+
disconnect — so the client always stops producing for a request the relay has forgotten.
|
|
56
|
+
|
|
57
|
+
Still explicit capability todos: reliable end-to-end incremental delivery and its
|
|
58
|
+
backpressure/cancellation (see "the streaming trade" below), account/reservation management,
|
|
59
|
+
usage metering, inspector/replay, and Cloudflare managed tunnels.
|
|
60
|
+
|
|
61
|
+
There is no UI mirror. Both products' relevant surface is a CLI/control protocol; their hosted
|
|
62
|
+
dashboards are incidental account and operations tooling, not an agent navigation target.
|
|
63
|
+
|
|
64
|
+
## Coverage
|
|
65
|
+
|
|
66
|
+
The capability manifest in `src/tunnel-capabilities.ts` is the honest denominator for both the
|
|
67
|
+
Cloudflare Quick Tunnel allocation surface and the owned Volter relay. Run
|
|
68
|
+
`bun scripts/twin-capabilities.ts tunnel` for the current counts. Every capability is classified
|
|
69
|
+
as done or planned; nothing disappears behind a broad feature label.
|
|
70
|
+
|
|
71
|
+
### Done (proven, verifiable)
|
|
72
|
+
|
|
73
|
+
- Cloudflare Quick Tunnel allocation envelope, unique ephemeral allocations, persistence,
|
|
74
|
+
read-only refusal, and bounded reserved-route/loopback-origin handling.
|
|
75
|
+
- Real loopback forwarding through both the quick adapter and the owned relay, including method,
|
|
76
|
+
path, body, status, the owned cache/privacy response-header rules, CORS preflight,
|
|
77
|
+
malformed-frame containment, and single durable/live ownership under overlapping claims.
|
|
78
|
+
- Relay registration, replacement, disconnect, authentication, JWT bootstrap/cookie behavior,
|
|
79
|
+
session-only Basic-auth plaintext, persistence ordering, and live status.
|
|
80
|
+
- An authoritative 15-discriminator census imported from `@volter/tunnel-core`, with one explicit
|
|
81
|
+
capability row per frame; implemented frames have transport proofs and unimplemented frames
|
|
82
|
+
remain planned. Fourteen of the fifteen now carry transport proofs — everything but `quota` —
|
|
83
|
+
driven over a genuine loopback WebSocket against a real local server, with the pretend client
|
|
84
|
+
a harness shaped after the owned client's `handleWsUpgrade` — it dials with the offered
|
|
85
|
+
subprotocols, strips the hop-by-hop handshake headers, and reports a real local failure as
|
|
86
|
+
`ws-error`; it does not reproduce the client's `x-forwarded-*` re-stamping, its 15s dial
|
|
87
|
+
timeout or `safeClose`'s teardown, none of which any assertion depends on.
|
|
88
|
+
- Visitor-disconnect abort: a browser hang-up before `response-start` sends `request-abort`, and
|
|
89
|
+
the request is genuinely FORGOTTEN — a late response frame naming it is refused.
|
|
90
|
+
- Read-only connector pull, mapping, idempotence, refusal-state survival, conformance, and the
|
|
91
|
+
fail-closed persistent live-call budget.
|
|
92
|
+
|
|
93
|
+
### The streaming trade, measured
|
|
94
|
+
|
|
95
|
+
The relay BUFFERS a streamed response to `response-end` before answering the visitor, and that is
|
|
96
|
+
a measured choice rather than an unfinished one. On this runtime a response body that has begun
|
|
97
|
+
cannot be failed THROUGH `Bun.serve`'s response surface: erroring the `ReadableStream` behind a
|
|
98
|
+
`Response` ends the chunked body cleanly,
|
|
99
|
+
so a visitor reads a truncated payload as a successful 200 and cannot tell it from a complete one
|
|
100
|
+
— and a `content-length` that would have made the truncation detectable is dropped the moment the
|
|
101
|
+
body is a stream. `node:http` on the SAME runtime CAN signal it (`src/tunnel-stream-boundary.test.ts`
|
|
102
|
+
measures that too) — so this is a trade with a named alternative, not an impossibility: re-hosting
|
|
103
|
+
the HTTP plane would mean giving up `server.upgrade`, which the entire WebSocket bridge is built
|
|
104
|
+
on. The first two measurements go RED the day `Bun.serve` gains the ability; that is the signal to build
|
|
105
|
+
`tunnel.volter.http.streaming`, `…stream_backpressure` and `…stream_cancel_abort` and revisit the
|
|
106
|
+
trade. Committing the head today would buy latency and sell the ability to fail a response
|
|
107
|
+
honestly — which is what keeps the malformed-sequence 502 reachable at all.
|
|
108
|
+
`stream_backpressure` has a second, independent blocker: the owned protocol's fifteen
|
|
109
|
+
discriminators contain no pause frame, so a relay cannot ask a client to slow down.
|
|
110
|
+
|
|
111
|
+
### Planned (todo — covered by default, not yet built)
|
|
112
|
+
|
|
113
|
+
- Reliable incremental streaming, backpressure and cancellation (blocked as above).
|
|
114
|
+
- Reconnect/backoff, quota frames, forwarded/response-header rules, rate limiting, and
|
|
115
|
+
constant-time shared-secret comparison.
|
|
116
|
+
- Managed tunnel/account/reservation/usage/signup/inspector surfaces and confirmed public push.
|
|
117
|
+
- Cloudflare Quick Tunnel concurrency and SSE limitations, plus Cloudflare managed tunnels.
|
|
118
|
+
|
|
119
|
+
### No UI mirror
|
|
120
|
+
|
|
121
|
+
The modeled products expose a CLI, HTTP API, and WebSocket control protocol. Their hosted account
|
|
122
|
+
dashboards are operations tooling rather than an agent workflow, so this package deliberately has
|
|
123
|
+
no React mirror and no UI capabilities. Coverage is API + connector.
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { keepProcessAlive } from '@volter/world-core/lifecycle';
|
|
3
|
+
import { hasFlag, optionValue } from '@volter/world-core/args';
|
|
4
|
+
import { createTunnelTwinServer, parseTunnelLoopbackOrigin } from "./tunnel-server.js";
|
|
5
|
+
const [command, ...args] = process.argv.slice(2);
|
|
6
|
+
const rawPort = optionValue(args, '--port');
|
|
7
|
+
const port = rawPort === undefined ? undefined : Number(rawPort);
|
|
8
|
+
if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535)) {
|
|
9
|
+
process.stderr.write(`world-tunnel: --port must be an integer 0-65535 (got ${JSON.stringify(rawPort)})\n`);
|
|
10
|
+
process.exit(2);
|
|
11
|
+
}
|
|
12
|
+
const root = optionValue(args, '--root') || undefined;
|
|
13
|
+
if (command === 'serve') {
|
|
14
|
+
const server = createTunnelTwinServer({
|
|
15
|
+
...(port ? { port } : {}),
|
|
16
|
+
...(root ? { root } : {}),
|
|
17
|
+
readOnly: hasFlag(args, '--read-only'),
|
|
18
|
+
requireTid: hasFlag(args, '--require-tid'),
|
|
19
|
+
});
|
|
20
|
+
process.stdout.write(`tunnel twin listening on ${server.url}\n`);
|
|
21
|
+
process.stdout.write(`Volter SDK host=${server.url}; Cloudflare quick service=${server.url}\n`);
|
|
22
|
+
await keepProcessAlive();
|
|
23
|
+
}
|
|
24
|
+
else if (command === 'quick' || command === 'tunnel') {
|
|
25
|
+
const origin = optionValue(args, '--url');
|
|
26
|
+
if (!origin) {
|
|
27
|
+
process.stderr.write('world-tunnel quick requires --url http://127.0.0.1:<port>\n');
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
parseTunnelLoopbackOrigin(origin);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
process.stderr.write(`world-tunnel quick: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
const server = createTunnelTwinServer({ ...(port ? { port } : {}), ...(root ? { root } : {}) });
|
|
38
|
+
const allocation = await fetch(`${server.url}/tunnel`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
|
|
39
|
+
const body = await allocation.json();
|
|
40
|
+
if (!allocation.ok || !body.success || !body.result)
|
|
41
|
+
throw new Error('quick-tunnel allocation failed');
|
|
42
|
+
server.registerQuickOrigin(body.result.id, origin);
|
|
43
|
+
// The world runtime discovers the first URL in stdout. This is deliberately a loopback URL:
|
|
44
|
+
// the twin proves the control plane and genuine HTTP forwarding, not Cloudflare edge/DNS/TLS.
|
|
45
|
+
process.stdout.write(`VOLTER_SHARE_URL=${server.url}\n`);
|
|
46
|
+
await keepProcessAlive();
|
|
47
|
+
}
|
|
48
|
+
else if (command === 'conformance') {
|
|
49
|
+
// Dev-only and lazy: the runtime entrypoint does not pull conformance into normal serve/share.
|
|
50
|
+
const { checkTunnelConformance } = await import("./tunnel-conformance.js");
|
|
51
|
+
const report = await checkTunnelConformance({ ...(root ? { root } : {}) });
|
|
52
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
53
|
+
if (!report.ok)
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
process.stdout.write('Usage: world-tunnel serve|conformance [--port N] [--root DIR] [--read-only] [--require-tid]\n');
|
|
58
|
+
process.stdout.write(' world-tunnel quick --url http://127.0.0.1:<port> [--port N] [--root DIR]\n');
|
|
59
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createTunnelTwinFetch, createTunnelTwinHandler, createTunnelTwinServer } from './tunnel-server.js';
|
|
2
|
+
export { handleTunnelRelayMessage, handleTunnelTwinRequest } from './tunnel-twin.js';
|
|
3
|
+
export { liveTunnelExecute, mapTunnelRelay, pullTunnelRelay, syncTunnelFromReal } from './tunnel-connector.js';
|
|
4
|
+
export { TunnelBudget, TunnelBudgetError, TUNNEL_RATE_BUDGET, TUNNEL_BUDGET_CEILING, TUNNEL_BUDGET_WINDOW_MS, TUNNEL_CALL_WEIGHTS, } from './tunnel-budget.js';
|
|
5
|
+
export type { RelayControlMessage, RelaySession, TunnelRequest, TunnelResponse } from './tunnel-twin.js';
|
|
6
|
+
import type { TwinPack } from '@volter/world-core';
|
|
7
|
+
export declare const pack: TwinPack;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export { createTunnelTwinFetch, createTunnelTwinHandler, createTunnelTwinServer } from "./tunnel-server.js";
|
|
2
|
+
export { handleTunnelRelayMessage, handleTunnelTwinRequest } from "./tunnel-twin.js";
|
|
3
|
+
export { liveTunnelExecute, mapTunnelRelay, pullTunnelRelay, syncTunnelFromReal } from "./tunnel-connector.js";
|
|
4
|
+
export { TunnelBudget, TunnelBudgetError, TUNNEL_RATE_BUDGET, TUNNEL_BUDGET_CEILING, TUNNEL_BUDGET_WINDOW_MS, TUNNEL_CALL_WEIGHTS, } from "./tunnel-budget.js";
|
|
5
|
+
import { TUNNEL_RATE_BUDGET as RATE_BUDGET } from "./tunnel-budget.js";
|
|
6
|
+
export const pack = {
|
|
7
|
+
vendor: 'tunnel',
|
|
8
|
+
// The SAME object tunnel-budget.ts declares at module load — one source of truth, so
|
|
9
|
+
// registering the pack and importing the connector can never arm two different ceilings.
|
|
10
|
+
rateBudget: RATE_BUDGET,
|
|
11
|
+
// The CONTROL plane is HTTP/REST (POST /tunnel, GET /api/status). The relay's control link is a
|
|
12
|
+
// WebSocket and its data path is real loopback HTTP — neither is a raw line protocol, so this is
|
|
13
|
+
// not the `raw-tcp` class; the split is described in the README and spec-sources.json.
|
|
14
|
+
transport: 'rest',
|
|
15
|
+
archetype: 'proxy',
|
|
16
|
+
bin: 'world-tunnel',
|
|
17
|
+
// The subject types the twin stores: `quick_tunnel`/`relay_tunnel` written by tunnel-twin.ts,
|
|
18
|
+
// and `relay` folded by the connector's status pull (mapTunnelRelay).
|
|
19
|
+
resources: ['quick_tunnel', 'relay_tunnel', 'relay'],
|
|
20
|
+
specSource: 'No single first-party machine-readable spec spans this combined pack. The Cloudflare Quick Tunnel ' +
|
|
21
|
+
'half is pinned to first-party cloudflared commit 733bfb939963e150dcf5c4faddb1603f744fbc98 (POST ' +
|
|
22
|
+
'/tunnel and its {success,result,errors} envelope) plus Cloudflare\'s own Quick Tunnels docs; the ' +
|
|
23
|
+
'owned Volter relay half is pinned to volter-ai/volter-tunnel commit ' +
|
|
24
|
+
'a5abc6287384429d32cde200d8ecd042814e6619 (@volter/tunnel 2.0.5, @volter/tunnel-core\'s 15 control ' +
|
|
25
|
+
'frames). See spec-sources.json.',
|
|
26
|
+
description: 'Cloudflare Quick Tunnel control-plane and owned Volter relay twin — POST /tunnel allocation and the ' +
|
|
27
|
+
'@volter/tunnel WebSocket control protocol, with a genuine loopback HTTP data path (no Cloudflare ' +
|
|
28
|
+
'edge, DNS, TLS or SLA claimed).',
|
|
29
|
+
// INTERCEPTION RULING — hostsNone, the pack's own home for it: no injector entry by design:
|
|
30
|
+
// native cloudflared is an external binary, while @volter/tunnel opens a configurable
|
|
31
|
+
// WebSocket control channel and the CLI reads TUNNEL_SERVER_URL. Worlds wire that app-read
|
|
32
|
+
// endpoint directly; claiming api.trycloudflare.com in the Node injector would not redirect
|
|
33
|
+
// cloudflared and would falsely imply the Cloudflare edge data plane was twinned.
|
|
34
|
+
hostsNone: "no injector entry by design: native cloudflared is an external binary, while @volter/tunnel opens a configurable WebSocket control channel and the CLI reads TUNNEL_SERVER_URL. Worlds wire that app-read endpoint directly; claiming api.trycloudflare.com in the Node injector would not redirect cloudflared and would falsely imply the Cloudflare edge data plane was twinned",
|
|
35
|
+
// Adoption, moved off the central maps unchanged (descriptor-first back-migration, adding-a-twin.md §3,
|
|
36
|
+
// 2026-08-31).
|
|
37
|
+
adoption: {
|
|
38
|
+
// Internal Volter tunnel; the client is `@volter/tunnel` and there is no Python distribution.
|
|
39
|
+
pypi: [],
|
|
40
|
+
sdks: ['@volter/tunnel'], envStems: ['TUNNEL', 'VOLTERTUNNEL'],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
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';
|
|
@@ -0,0 +1,26 @@
|
|
|
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';
|
|
@@ -0,0 +1,4 @@
|
|
|
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>;
|