@claxedo/workspace-relay 0.6.0 → 0.8.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 +63 -9
- package/dist/auth.d.ts +54 -3
- package/dist/bun.d.ts +105 -19
- package/dist/bun.mjs +2492 -0
- package/dist/cloudflare.d.ts +111 -0
- package/dist/directory.d.ts +17 -6
- package/dist/http.d.ts +2 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +957 -1517
- package/dist/main.d.ts +72 -43
- package/dist/main.mjs +20454 -39451
- package/dist/server.d.ts +139 -17
- package/dist/upstream-websocket.d.ts +34 -0
- package/dist/user-hosted-forwarding.d.ts +40 -0
- package/dist/worker.d.ts +42 -2
- package/dist/workerd-fixture/binary-frame-worker.d.ts +33 -0
- package/dist/workerd-fixture/boot.d.ts +18 -0
- package/package.json +24 -9
package/README.md
CHANGED
|
@@ -64,7 +64,15 @@ const relay = Bun.serve({
|
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
const token = await mintRuntimeAccessToken(
|
|
67
|
-
{
|
|
67
|
+
{
|
|
68
|
+
principalKind: "user",
|
|
69
|
+
actorId: "user_1",
|
|
70
|
+
actorKind: "human",
|
|
71
|
+
orgId: "org_1",
|
|
72
|
+
workspaceId: "ws_1",
|
|
73
|
+
hostId: "host_1",
|
|
74
|
+
role: "editor",
|
|
75
|
+
},
|
|
68
76
|
runtimeAccessKey.privateKey,
|
|
69
77
|
"EdDSA",
|
|
70
78
|
)
|
|
@@ -116,6 +124,49 @@ SSE, PTY, and WebSocket streams may live until normal close, reconnect, relay
|
|
|
116
124
|
drain, host disconnect, or process restart. If immediate stream revocation is a
|
|
117
125
|
requirement, the control plane must also close the session/runtime channel.
|
|
118
126
|
|
|
127
|
+
### Host Tunnel Serving-Generation Fence
|
|
128
|
+
|
|
129
|
+
A Host Tunnel Token minted by a connect-enrolled host carries `enrollment_id`
|
|
130
|
+
and `generation`. A relay built by `src/main.ts` or `src/worker.ts` — that is,
|
|
131
|
+
any relay with a resolver — asks
|
|
132
|
+
`GET <CLAXEDO_RELAY_RESOLVER_URL>/host-generation?enrollmentId=` on every
|
|
133
|
+
admission and re-checks established tunnels every 30 s (both adapters). The
|
|
134
|
+
verdicts, in order:
|
|
135
|
+
|
|
136
|
+
| Control plane answer | Admission | Established tunnel |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| Same generation, not revoked | admitted | kept |
|
|
139
|
+
| Higher generation | `403 host_generation_superseded` | closed `1008` |
|
|
140
|
+
| Lower generation | `403 host_generation_unknown` | closed `1008` |
|
|
141
|
+
| Enrollment revoked or paused | `403 host_enrollment_revoked` | closed `1008` |
|
|
142
|
+
| `404 relay_resolver_enrollment_not_found` | `403 host_enrollment_unknown` | closed `1008` |
|
|
143
|
+
| Any other status, a bare 404 (route missing), malformed body, or the 5 s deadline | `503 host_generation_lookup_unavailable` (retry) | survives two consecutive failures, closed `1012` on the third |
|
|
144
|
+
|
|
145
|
+
A token without a generation is admitted without asking the control plane. It
|
|
146
|
+
never displaces a socket that carries a generation: for one host+workspace
|
|
147
|
+
identity (Bun) or one room (Cloudflare), an incumbent with a generation yields
|
|
148
|
+
only to an equal or higher generation, and an incumbent without one yields to
|
|
149
|
+
any later socket. Every refusal is audited as `host_tunnel.denied` with the
|
|
150
|
+
code as `reason` (Bun; the Cloudflare room has no audit hook).
|
|
151
|
+
|
|
152
|
+
A `host.registration.update` frame is re-admitted the same way, with the
|
|
153
|
+
socket's own claims as the first incumbent: an update whose token carries no
|
|
154
|
+
generation, or a lower one than the socket holds, is refused (closed `1008
|
|
155
|
+
Host tunnel registration update superseded`); one that passes is then checked
|
|
156
|
+
against the control plane exactly as a connect is and closed with the table's
|
|
157
|
+
established-tunnel code on refusal (`1012` for an unavailable lookup, without
|
|
158
|
+
the outage grace). An accepted update replaces the socket's claims and
|
|
159
|
+
identities, and a socket that became fenced starts the 30 s re-check (or arms
|
|
160
|
+
the hibernation alarm).
|
|
161
|
+
|
|
162
|
+
A relay composed directly from `createWorkspaceRelayBun` /
|
|
163
|
+
`createWorkspaceRelayDurableObjectRoom` without `resolveHostGeneration` — the
|
|
164
|
+
desktop and self-hosted composition — admits tokens without a generation
|
|
165
|
+
newest-wins and refuses any token that carries one with
|
|
166
|
+
`403 host_generation_unverifiable` (an update: closed `1008`). A generation is
|
|
167
|
+
a fence the relay cannot verify without the resolver, and the refusal is not
|
|
168
|
+
retryable because the resolver is a property of the composition.
|
|
169
|
+
|
|
119
170
|
### Forwarding Boundary
|
|
120
171
|
|
|
121
172
|
The relay strips client-supplied `x-forwarded-for`, `x-forwarded-host`,
|
|
@@ -190,8 +241,11 @@ production fail-closed gate at `src/main.ts`.
|
|
|
190
241
|
| --- | --- |
|
|
191
242
|
| `CLAXEDO_RELAY_BIND_HOST`, `CLAXEDO_RELAY_BIND_PORT` | Listening socket. Loopback by default. |
|
|
192
243
|
| `CLAXEDO_RELAY_PUBLIC_URL` | Externally-resolvable URL the relay advertises in tokens. |
|
|
193
|
-
| `CLAXEDO_RELAY_RESOLVER_URL` |
|
|
244
|
+
| `CLAXEDO_RELAY_RESOLVER_URL` | Control-plane resolver base (`https://<control-plane>/internal/relay`). The relay derives `/target`, `/revocation`, and `/host-generation` from it. Required by the Bun process; the Worker also accepts `CLAXEDO_CENTRAL_URL` and appends `/internal/relay`. |
|
|
194
245
|
| `CLAXEDO_RELAY_RESOLVER_TOKEN` | Bearer token the relay sends to the resolver. **Required in production.** |
|
|
246
|
+
| `CLAXEDO_RELAY_HOST_GENERATION_URL` | Optional absolute URL of the host-generation lookup. Unset (the normal case) derives `<CLAXEDO_RELAY_RESOLVER_URL>/host-generation`; set it only when the lookup lives at a different origin than the rest of the resolver. There is no way to turn the fence off on a resolver-backed relay. |
|
|
247
|
+
| `CLAXEDO_RELAY_HOST_GENERATION_CACHE_TTL_MS` | Cache TTL for host-generation answers. Defaults to 10000. A superseded tunnel closes within the re-check interval (30 s) plus this TTL. |
|
|
248
|
+
| `CLAXEDO_RELAY_TARGET_CACHE_TTL_MS`, `CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS` | Cache TTLs for `/target` (default 30000 on Bun, 5000 on the Worker) and `/revocation` (default 10000) answers. |
|
|
195
249
|
| `CLAXEDO_RELAY_JWKS_URL` | Optional remote JWKS the relay uses to verify runtime-access tokens. |
|
|
196
250
|
| `CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM` | Inline public key (alternative to JWKS). |
|
|
197
251
|
| `CLAXEDO_RELAY_HOST_VERIFY_PEM` | Public PEM the relay uses to verify host-tunnel tokens. |
|
|
@@ -235,7 +289,7 @@ The `TokenVerifier` interface intentionally contains only `verify(token)`.
|
|
|
235
289
|
JWKS fetching, audience binding, and replay caches belong to the
|
|
236
290
|
implementation. Two reference implementations ship in the protocol
|
|
237
291
|
package: `createHttpTokenVerifier` for remote verifiers
|
|
238
|
-
(
|
|
292
|
+
(token introspection, custom OIDC), and `createStaticTokenVerifier`
|
|
239
293
|
for tests and self-hosted single-tenant setups.
|
|
240
294
|
|
|
241
295
|
`createHttpTokenVerifier` is a reference implementation. Its HTTPS endpoint is
|
|
@@ -301,8 +355,8 @@ gateway pattern lives in `claxedo-server`:
|
|
|
301
355
|
|
|
302
356
|
- The user's browser calls `/api/claxedo/...`.
|
|
303
357
|
- `claxedo-server` mounts `workspaceRuntimeProxy` middleware
|
|
304
|
-
(`packages/claxedo-server/src/
|
|
305
|
-
app at `packages/claxedo-server/src/server.ts:93`.
|
|
358
|
+
(`packages/claxedo-server/src/workspace/runtime-dispatch/internals.ts:205`) on its top-level Hono
|
|
359
|
+
app at `packages/claxedo-server/src/deployments/local/server.ts:93`.
|
|
306
360
|
- The proxy resolves the active workspace target via
|
|
307
361
|
`internal-relay.ts` (control-plane auth — see "Seam: internal-relay
|
|
308
362
|
vs workspace-relay" below), strips its own prefix, and forwards
|
|
@@ -315,16 +369,16 @@ checks that gate it.
|
|
|
315
369
|
|
|
316
370
|
## Seam: `internal-relay` vs `workspace-relay`
|
|
317
371
|
|
|
318
|
-
| | `claxedo-server/src/routes/internal-relay.ts` | `@claxedo/workspace-relay` |
|
|
372
|
+
| | `claxedo-server/src/deployments/shared-routes/internal-relay.ts` | `@claxedo/workspace-relay` |
|
|
319
373
|
| --- | --- | --- |
|
|
320
374
|
| Layer | Control-plane auth | Tunnel transport |
|
|
321
375
|
| Trust | Authenticates the relay process itself (loopback or bearer) | Authenticates the user's runtime-access token |
|
|
322
376
|
| Routes | `GET /internal/relay/target`, `GET /internal/relay/revocation` | WS upgrade + tunnel framing |
|
|
323
|
-
| Owners | `claxedo-server` (depends on
|
|
377
|
+
| Owners | `claxedo-server` (depends on the workspace authority, the identity provider, audit log) | `workspace-relay` (no authority or identity-provider dependency) |
|
|
324
378
|
| Lives in | `packages/claxedo-server/` (server-side only) | `packages/workspace-relay/` (own process) |
|
|
325
379
|
|
|
326
|
-
This split is deliberate: the relay never reads
|
|
327
|
-
directly. It calls back to `claxedo-server` over HTTP for resolver
|
|
380
|
+
This split is deliberate: the relay never reads the workspace authority or the
|
|
381
|
+
identity provider directly. It calls back to `claxedo-server` over HTTP for resolver
|
|
328
382
|
decisions. The decision data crosses the seam as
|
|
329
383
|
`RuntimeAccessTokenActiveResult` (defined in `server.ts`).
|
|
330
384
|
|
package/dist/auth.d.ts
CHANGED
|
@@ -16,10 +16,16 @@ export type RelayAccess = RelayClaimPair["access"];
|
|
|
16
16
|
export type RelayBacking = RelayClaimPair["backing"];
|
|
17
17
|
export type RelayJwtAlgorithm = (typeof algorithms)[number];
|
|
18
18
|
export type RelayRole = "viewer" | "editor" | "admin" | "owner";
|
|
19
|
+
export type ActorKind = "human" | "agent";
|
|
19
20
|
export type RuntimeAccessTokenClaims = {
|
|
20
21
|
iss: typeof runtimeAccessTokenIssuer;
|
|
21
22
|
aud: typeof runtimeAccessTokenAudience;
|
|
22
|
-
|
|
23
|
+
principal_kind: "user" | "service";
|
|
24
|
+
actor_id: string;
|
|
25
|
+
actor_kind: "human" | "agent";
|
|
26
|
+
actor_public_id?: string;
|
|
27
|
+
actor_name?: string;
|
|
28
|
+
actor_avatar_url?: string;
|
|
23
29
|
org_id: string;
|
|
24
30
|
workspace_id: string;
|
|
25
31
|
host_id: string;
|
|
@@ -31,7 +37,12 @@ export type RuntimeAccessTokenClaims = {
|
|
|
31
37
|
export type RelayHostTokenClaims = {
|
|
32
38
|
iss: typeof relayHostTokenIssuer;
|
|
33
39
|
aud: typeof relayHostTokenAudience;
|
|
34
|
-
|
|
40
|
+
principal_kind: "user" | "service";
|
|
41
|
+
actor_id: string;
|
|
42
|
+
actor_kind: "human" | "agent";
|
|
43
|
+
actor_public_id?: string;
|
|
44
|
+
actor_name?: string;
|
|
45
|
+
actor_avatar_url?: string;
|
|
35
46
|
org_id: string;
|
|
36
47
|
workspace_id: string;
|
|
37
48
|
host_id: string;
|
|
@@ -39,6 +50,8 @@ export type RelayHostTokenClaims = {
|
|
|
39
50
|
exp: number;
|
|
40
51
|
iat: number;
|
|
41
52
|
jti: string;
|
|
53
|
+
/** Durable parent Runtime Access Token id used for revocation checks. */
|
|
54
|
+
parent_jti: string;
|
|
42
55
|
} & RelayClaimPair;
|
|
43
56
|
export type HostTunnelTokenClaims = {
|
|
44
57
|
iss: typeof runtimeAccessTokenIssuer;
|
|
@@ -46,6 +59,15 @@ export type HostTunnelTokenClaims = {
|
|
|
46
59
|
sub: string;
|
|
47
60
|
host_id: string;
|
|
48
61
|
workspace_ids: string[];
|
|
62
|
+
/**
|
|
63
|
+
* Serving-generation fence. `generation` is only valid together with
|
|
64
|
+
* `enrollment_id`; a relay with a host-generation resolver refuses a token
|
|
65
|
+
* whose generation is below the enrollment's current one. A token without a
|
|
66
|
+
* generation is admitted exactly as before the fence existed, which is what
|
|
67
|
+
* desktop and self-hosted mints still produce.
|
|
68
|
+
*/
|
|
69
|
+
enrollment_id?: string;
|
|
70
|
+
generation?: number;
|
|
49
71
|
exp: number;
|
|
50
72
|
iat: number;
|
|
51
73
|
jti: string;
|
|
@@ -55,7 +77,12 @@ export declare class WorkspaceRelayAuthError extends Error {
|
|
|
55
77
|
constructor(code: "invalid_relay_token" | "relay_token_workspace_mismatch" | "relay_token_host_mismatch" | "relay_token_claims_invalid", message: string);
|
|
56
78
|
}
|
|
57
79
|
type RuntimeInput = {
|
|
58
|
-
|
|
80
|
+
principalKind: "user" | "service";
|
|
81
|
+
actorId: string;
|
|
82
|
+
actorKind: ActorKind;
|
|
83
|
+
actorPublicId?: string;
|
|
84
|
+
actorName?: string;
|
|
85
|
+
actorAvatarUrl?: string;
|
|
59
86
|
orgId: string;
|
|
60
87
|
workspaceId: string;
|
|
61
88
|
hostId: string;
|
|
@@ -65,6 +92,8 @@ type RuntimeInput = {
|
|
|
65
92
|
now?: number;
|
|
66
93
|
};
|
|
67
94
|
type RelayHostInput = RuntimeInput & RelayClaimPair & {
|
|
95
|
+
/** The Runtime Access Token jti from which this one-request RHT is derived. */
|
|
96
|
+
parentJti: string;
|
|
68
97
|
/**
|
|
69
98
|
* Optional `kid` to embed in the JWT protected header. Verifiers using a
|
|
70
99
|
* JWKS resolver dispatch on this to pick the matching key, so a freshly
|
|
@@ -78,6 +107,8 @@ type HostTunnelInput = {
|
|
|
78
107
|
subject: string;
|
|
79
108
|
hostId: string;
|
|
80
109
|
workspaceIds: string[];
|
|
110
|
+
enrollmentId?: string;
|
|
111
|
+
generation?: number;
|
|
81
112
|
ttlSeconds?: number;
|
|
82
113
|
jti?: string;
|
|
83
114
|
now?: number;
|
|
@@ -90,6 +121,7 @@ type ExpectedHostTunnel = {
|
|
|
90
121
|
hostId: string;
|
|
91
122
|
workspaceIds: string[];
|
|
92
123
|
};
|
|
124
|
+
export declare function isHostGeneration(value: unknown): value is number;
|
|
93
125
|
export declare function isRelayClaimPair(input: {
|
|
94
126
|
access?: unknown;
|
|
95
127
|
backing?: unknown;
|
|
@@ -101,6 +133,25 @@ export declare function isRelayClaimPair(input: {
|
|
|
101
133
|
*/
|
|
102
134
|
export type RelayKeyResolver = JWTVerifyGetKey;
|
|
103
135
|
export type RelayKey = CryptoKey | Uint8Array | RelayKeyResolver;
|
|
136
|
+
/**
|
|
137
|
+
* Re-imports the PUBLIC half of an EdDSA relay-host signing key.
|
|
138
|
+
*
|
|
139
|
+
* `importJWK` is declared `Promise<CryptoKey | Uint8Array>` because a symmetric
|
|
140
|
+
* (`oct`) JWK imports as raw bytes. An EdDSA public JWK never does, so the byte
|
|
141
|
+
* branch is a configuration error worth failing loudly on — the two callers
|
|
142
|
+
* (`main.ts` for Bun, `worker.ts` for Cloudflare) previously asserted it away
|
|
143
|
+
* with their own copies of this round-trip.
|
|
144
|
+
*/
|
|
145
|
+
export declare function deriveRelayHostPublicKey(privateKey: CryptoKey): Promise<CryptoKey>;
|
|
146
|
+
/**
|
|
147
|
+
* The relay-host key id, derived from the key's public component.
|
|
148
|
+
*
|
|
149
|
+
* ONE implementation on purpose: a Bun relay and a Cloudflare relay signing
|
|
150
|
+
* with the same key must publish the same `kid`, or a token minted by one fails
|
|
151
|
+
* key lookup at the other. It is written against WebCrypto rather than
|
|
152
|
+
* `node:crypto` so the workerd bundle can use it too.
|
|
153
|
+
*/
|
|
154
|
+
export declare function deriveRelayHostKid(publicKey: CryptoKey): Promise<string>;
|
|
104
155
|
/**
|
|
105
156
|
* Mint variants accept only a signing key — never a resolver. Resolvers are
|
|
106
157
|
* verification-only. This narrowed alias keeps the mint signatures unchanged.
|
package/dist/bun.d.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { type WorkspaceRelayOptions } from "./server";
|
|
2
|
+
import { type RuntimeAccessTokenClaims } from "./auth";
|
|
3
|
+
import { type UpstreamWebSocketConstructor } from "./upstream-websocket";
|
|
4
|
+
/**
|
|
5
|
+
* Bun expresses HTTP idle timeout in seconds. Runtime SSE heartbeats arrive
|
|
6
|
+
* every 30 seconds, so this exceeds that interval while still bounding idle
|
|
7
|
+
* unauthenticated and incomplete HTTP connections.
|
|
8
|
+
*/
|
|
9
|
+
export declare const WORKSPACE_RELAY_IDLE_TIMEOUT_SECONDS = 45;
|
|
2
10
|
type RelayClientWebSocketData = {
|
|
3
11
|
kind: "client";
|
|
12
|
+
claims: RuntimeAccessTokenClaims;
|
|
4
13
|
upstreamUrl: string;
|
|
5
14
|
headers: Record<string, string>;
|
|
6
15
|
queue: Array<{
|
|
7
16
|
payload: string | Buffer<ArrayBuffer>;
|
|
8
17
|
queuedAt: number;
|
|
9
18
|
}>;
|
|
19
|
+
/** Running byte size of `queue`; reset when the queue is flushed upstream. */
|
|
20
|
+
queuedBytes?: number;
|
|
10
21
|
upstream?: WebSocket;
|
|
11
22
|
upstreamOpenTimer?: ReturnType<typeof setTimeout>;
|
|
12
23
|
trace?: RelayClientWebSocketTrace;
|
|
24
|
+
accessCheckTimer?: ReturnType<typeof setInterval>;
|
|
25
|
+
expiryTimer?: ReturnType<typeof setTimeout>;
|
|
13
26
|
};
|
|
14
27
|
type RelayClientWebSocketTrace = {
|
|
15
28
|
acceptedAt: number;
|
|
@@ -23,24 +36,33 @@ type RelayHostTunnelWebSocketData = {
|
|
|
23
36
|
kind: "host-tunnel";
|
|
24
37
|
hostId: string;
|
|
25
38
|
workspaceIds: string[];
|
|
39
|
+
/** Fence pair from the last verified Host Tunnel Token (connect or registration update); absent for tokens without one. */
|
|
40
|
+
enrollmentId?: string;
|
|
41
|
+
generation?: number;
|
|
26
42
|
pending: Map<string, PendingTunnelHttpResponse>;
|
|
27
|
-
channels: Map<string,
|
|
43
|
+
channels: Map<string, RelayUserHostedClientWebSocket>;
|
|
28
44
|
heartbeat?: ReturnType<typeof setInterval>;
|
|
29
45
|
missedPongs: number;
|
|
46
|
+
generationCheckTimer?: ReturnType<typeof setInterval>;
|
|
47
|
+
generationCheckFailures: number;
|
|
30
48
|
messageBuffer: string;
|
|
31
49
|
};
|
|
32
50
|
type RelayUserHostedClientWebSocketData = {
|
|
33
51
|
kind: "user-hosted-client";
|
|
52
|
+
claims: RuntimeAccessTokenClaims;
|
|
34
53
|
hostId: string;
|
|
35
54
|
workspaceId: string;
|
|
36
55
|
channelId: string;
|
|
37
56
|
path: string;
|
|
38
57
|
relayHostToken: string;
|
|
58
|
+
accessCheckTimer?: ReturnType<typeof setInterval>;
|
|
59
|
+
expiryTimer?: ReturnType<typeof setTimeout>;
|
|
39
60
|
};
|
|
40
61
|
type RelayWebSocketData = RelayClientWebSocketData | RelayHostTunnelWebSocketData | RelayUserHostedClientWebSocketData;
|
|
41
|
-
type
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
/** The socket type Bun hands every handler: one type carrying the data union. */
|
|
63
|
+
type RelayWebSocket = Bun.ServerWebSocket<RelayWebSocketData>;
|
|
64
|
+
type RelayHostTunnelWebSocket = Bun.ServerWebSocket<RelayHostTunnelWebSocketData>;
|
|
65
|
+
type RelayUserHostedClientWebSocket = Bun.ServerWebSocket<RelayUserHostedClientWebSocketData>;
|
|
44
66
|
type PendingTunnelHttpResponse = {
|
|
45
67
|
controller: ReadableStreamDefaultController<Uint8Array>;
|
|
46
68
|
stream: ReadableStream<Uint8Array>;
|
|
@@ -51,14 +73,40 @@ type PendingTunnelHttpResponse = {
|
|
|
51
73
|
bytesQueued: number;
|
|
52
74
|
slowConsumerTimeout?: ReturnType<typeof setTimeout>;
|
|
53
75
|
responseStarted: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* The relay's own CORS headers for this request, applied to whatever the
|
|
78
|
+
* tunnelled host answers with. The browser talks to the RELAY, so the
|
|
79
|
+
* relay's allowlist is the one that decides what it may read — the host
|
|
80
|
+
* behind the tunnel runs its own, unrelated policy, and passing its headers
|
|
81
|
+
* through made a relay's configured app origin readable only when the host
|
|
82
|
+
* happened to allow it too.
|
|
83
|
+
*/
|
|
84
|
+
corsHeaders: (upstream: Headers) => Headers;
|
|
54
85
|
};
|
|
55
86
|
export type WorkspaceRelayHostTunnelOptions = {
|
|
87
|
+
/**
|
|
88
|
+
* Test seam replacing Host Tunnel Token verification. A boolean carries no
|
|
89
|
+
* claims, so a tunnel admitted through it has no generation and is never
|
|
90
|
+
* fenced.
|
|
91
|
+
*/
|
|
56
92
|
authorizeHostTunnel?: (request: Request, input: {
|
|
57
93
|
hostId: string;
|
|
58
94
|
workspaceIds: string[];
|
|
59
95
|
}) => boolean | Promise<boolean>;
|
|
60
96
|
hostTunnelPingIntervalMs?: number;
|
|
61
97
|
hostTunnelMaxMissedPongs?: number;
|
|
98
|
+
/**
|
|
99
|
+
* How often an established host tunnel's generation is re-checked against
|
|
100
|
+
* `resolveHostGeneration`. Defaults to 30s; 0 disables the check. Because
|
|
101
|
+
* the lookup may answer from its cache, a superseded tunnel closes within
|
|
102
|
+
* this interval plus the lookup's cache TTL.
|
|
103
|
+
*/
|
|
104
|
+
hostGenerationCheckIntervalMs?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Consecutive unavailable lookups an established tunnel survives before it
|
|
107
|
+
* is closed 1012 for the host to reconnect. Defaults to 3.
|
|
108
|
+
*/
|
|
109
|
+
hostGenerationOutageGraceAttempts?: number;
|
|
62
110
|
hostTunnelStateDebounceMs?: number;
|
|
63
111
|
};
|
|
64
112
|
export type WorkspaceRelayBackpressureOptions = {
|
|
@@ -70,18 +118,31 @@ export type WorkspaceRelayBackpressureOptions = {
|
|
|
70
118
|
directHttpConcurrency?: number;
|
|
71
119
|
upstreamWebSocketOpenTimeoutMs?: number;
|
|
72
120
|
upstreamWebSocketPreOpenQueueMaxFrames?: number;
|
|
121
|
+
upstreamWebSocketPreOpenQueueMaxBytes?: number;
|
|
73
122
|
upstreamWebSocket?: UpstreamWebSocketConstructor;
|
|
74
123
|
webSocketBufferedAmountMaxBytes?: number;
|
|
75
124
|
};
|
|
76
|
-
export type WorkspaceRelayBunOptions = WorkspaceRelayHostTunnelOptions & WorkspaceRelayBackpressureOptions
|
|
125
|
+
export type WorkspaceRelayBunOptions = WorkspaceRelayHostTunnelOptions & WorkspaceRelayBackpressureOptions & {
|
|
126
|
+
/** How often established user WebSockets are re-checked for revocation. Defaults to 30s. */
|
|
127
|
+
runtimeAccessTokenActiveCheckIntervalMs?: number;
|
|
128
|
+
/** Clock injection used to calculate the local token-expiry deadline. */
|
|
129
|
+
now?: () => number;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Declared as function-valued properties, not methods, because every member is
|
|
133
|
+
* a closure over the adapter's counters and is routinely handed around
|
|
134
|
+
* detached (`metricsSources.fragmentation ?? telemetry.getFragmentationStats`).
|
|
135
|
+
* None of them reads `this`, and the same shape is used by
|
|
136
|
+
* `WorkspaceRelayMetricsSources` in `./server`.
|
|
137
|
+
*/
|
|
77
138
|
export type WorkspaceRelayBunTelemetry = {
|
|
78
|
-
getFragmentationStats()
|
|
79
|
-
resetFragmentationStats()
|
|
80
|
-
getSlowConsumerStats()
|
|
81
|
-
resetSlowConsumerStats()
|
|
139
|
+
getFragmentationStats: () => FragmentationStats;
|
|
140
|
+
resetFragmentationStats: () => void;
|
|
141
|
+
getSlowConsumerStats: () => SlowConsumerStats;
|
|
142
|
+
resetSlowConsumerStats: () => void;
|
|
82
143
|
};
|
|
83
144
|
/**
|
|
84
|
-
*
|
|
145
|
+
* Drain controller exposed by `createWorkspaceRelayBun`.
|
|
85
146
|
*
|
|
86
147
|
* On SIGTERM the operator (see `installShutdownDrainHandler` in main.ts)
|
|
87
148
|
* flips `setDraining(true)`; from that point onward the relay
|
|
@@ -93,16 +154,41 @@ export type WorkspaceRelayBunTelemetry = {
|
|
|
93
154
|
* `pendingCount()` reports the total number of in-flight tunnel HTTP
|
|
94
155
|
* responses across every connected host tunnel — this is what the operator
|
|
95
156
|
* polls via `waitForDrain(timeoutMs)` before hard-closing remaining sockets.
|
|
157
|
+
*
|
|
158
|
+
* Function-valued properties rather than methods, for the same reason as
|
|
159
|
+
* `WorkspaceRelayBunTelemetry`: `isDraining` and `pendingCount` are passed
|
|
160
|
+
* detached into `createWorkspaceRelay`, and none of them reads `this`.
|
|
96
161
|
*/
|
|
97
162
|
export type WorkspaceRelayBunDrainController = {
|
|
98
|
-
isDraining()
|
|
99
|
-
setDraining(value: boolean)
|
|
100
|
-
pendingCount()
|
|
101
|
-
waitForDrain(timeoutMs: number)
|
|
163
|
+
isDraining: () => boolean;
|
|
164
|
+
setDraining: (value: boolean) => void;
|
|
165
|
+
pendingCount: () => number;
|
|
166
|
+
waitForDrain: (timeoutMs: number) => Promise<{
|
|
102
167
|
drained: boolean;
|
|
103
168
|
remaining: number;
|
|
104
169
|
}>;
|
|
105
170
|
};
|
|
171
|
+
/**
|
|
172
|
+
* Bytes queued in a socket's send buffer, or `undefined` if it cannot report.
|
|
173
|
+
*
|
|
174
|
+
* Bun spells this `getBufferedAmount()` — a method, not the browser's
|
|
175
|
+
* `bufferedAmount` property. Reading the property on a Bun socket yields
|
|
176
|
+
* `undefined`, and `undefined > limit` is always false, so a guard built on it
|
|
177
|
+
* never fires.
|
|
178
|
+
*
|
|
179
|
+
* Fails open on a socket that cannot report, deliberately: an unguarded healthy
|
|
180
|
+
* connection beats a guard that kills healthy connections. This is also why the
|
|
181
|
+
* guard is not portable to `cloudflare.ts` — workerd's WebSocket exposes no
|
|
182
|
+
* buffer depth, so bounding that path needs a protocol-level credit/ack window.
|
|
183
|
+
*/
|
|
184
|
+
export declare const relayBufferedBytes: (socket: unknown) => number | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* True only when the socket reports a depth over the limit. Unknown ⇒ false.
|
|
187
|
+
*
|
|
188
|
+
* Exported for `bun.test.ts`: a loopback peer drains faster than a test can
|
|
189
|
+
* fill it, so the decision is asserted directly.
|
|
190
|
+
*/
|
|
191
|
+
export declare const relayOverBackpressureLimit: (socket: unknown, limitBytes: number) => boolean;
|
|
106
192
|
export type FragmentationStats = {
|
|
107
193
|
fragmentsBuffered: number;
|
|
108
194
|
oversizedClosed: number;
|
|
@@ -114,7 +200,7 @@ export type SlowConsumerStats = {
|
|
|
114
200
|
};
|
|
115
201
|
declare function createSlowConsumerStats(): SlowConsumerStats;
|
|
116
202
|
declare function enqueueChunkWithBackpressure(input: {
|
|
117
|
-
ws:
|
|
203
|
+
ws: RelayHostTunnelWebSocket;
|
|
118
204
|
requestId: string;
|
|
119
205
|
entry: PendingTunnelHttpResponse;
|
|
120
206
|
chunk: Uint8Array;
|
|
@@ -126,12 +212,12 @@ export declare const __slowConsumerInternalsForTest: {
|
|
|
126
212
|
createSlowConsumerStats: typeof createSlowConsumerStats;
|
|
127
213
|
};
|
|
128
214
|
export declare function createWorkspaceRelayBun(options: WorkspaceRelayOptions, bunOptions?: WorkspaceRelayBunOptions): {
|
|
129
|
-
fetch(request: Request, server: Bun.Server<RelayWebSocketData>)
|
|
215
|
+
fetch: (request: Request, server: Bun.Server<RelayWebSocketData>) => Promise<Response | undefined>;
|
|
130
216
|
websocket: {
|
|
131
217
|
maxPayloadLength: number;
|
|
132
|
-
message(ws:
|
|
133
|
-
open(ws:
|
|
134
|
-
close(ws:
|
|
218
|
+
message(ws: RelayWebSocket, message: string | Buffer<ArrayBuffer>): void;
|
|
219
|
+
open(ws: RelayWebSocket): void;
|
|
220
|
+
close(ws: RelayWebSocket, code: number, reason: string): void;
|
|
135
221
|
};
|
|
136
222
|
drain: WorkspaceRelayBunDrainController;
|
|
137
223
|
telemetry: WorkspaceRelayBunTelemetry;
|