@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 CHANGED
@@ -64,7 +64,15 @@ const relay = Bun.serve({
64
64
  })
65
65
 
66
66
  const token = await mintRuntimeAccessToken(
67
- { subject: "user_1", orgId: "org_1", workspaceId: "ws_1", hostId: "host_1", role: "editor" },
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` | Where to fetch `RuntimeAccessTokenActiveResult`s (typically `claxedo-server/internal/relay/target`). |
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
- (Clerk-style introspection, custom OIDC), and `createStaticTokenVerifier`
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/proxy.ts:205`) on its top-level Hono
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 Convex authority, Clerk, audit log) | `workspace-relay` (no Convex/Clerk dependency) |
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 Convex or Clerk
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
- sub: string;
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
- sub: string;
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
- subject: string;
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, Bun.ServerWebSocket<RelayUserHostedClientWebSocketData>>;
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 UpstreamWebSocketConstructor = {
42
- new (url: string, options: Bun.WebSocketOptions): WebSocket;
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(): FragmentationStats;
79
- resetFragmentationStats(): void;
80
- getSlowConsumerStats(): SlowConsumerStats;
81
- resetSlowConsumerStats(): void;
139
+ getFragmentationStats: () => FragmentationStats;
140
+ resetFragmentationStats: () => void;
141
+ getSlowConsumerStats: () => SlowConsumerStats;
142
+ resetSlowConsumerStats: () => void;
82
143
  };
83
144
  /**
84
- * T9: drain controller exposed by `createWorkspaceRelayBun`.
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(): boolean;
99
- setDraining(value: boolean): void;
100
- pendingCount(): number;
101
- waitForDrain(timeoutMs: number): Promise<{
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: Bun.ServerWebSocket<RelayHostTunnelWebSocketData>;
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>): Promise<Response | undefined>;
215
+ fetch: (request: Request, server: Bun.Server<RelayWebSocketData>) => Promise<Response | undefined>;
130
216
  websocket: {
131
217
  maxPayloadLength: number;
132
- message(ws: Bun.ServerWebSocket<RelayWebSocketData>, message: string | Buffer<ArrayBuffer>): void;
133
- open(ws: Bun.ServerWebSocket<RelayWebSocketData>): void;
134
- close(ws: Bun.ServerWebSocket<RelayWebSocketData>, code: number, reason: string): void;
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;