@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.
@@ -11,6 +11,44 @@ export type WorkspaceRelayDurableObjectNamespace = {
11
11
  get: (id: WorkspaceRelayDurableObjectId, options?: WorkspaceRelayDurableObjectGetOptions) => WorkspaceRelayDurableObjectStub;
12
12
  };
13
13
  export declare const DEFAULT_RELAY_LOCATION_HINT = "apac";
14
+ /**
15
+ * The complete set of Cloudflare Durable Object location hints.
16
+ *
17
+ * Cloudflare rejects an unknown hint, and a rejected `get()` fails the request —
18
+ * so an unrecognised region must fall back rather than be passed through. This
19
+ * list is the validator for that.
20
+ */
21
+ export declare const RELAY_LOCATION_HINTS: readonly ["wnam", "enam", "sam", "weur", "eeur", "apac", "oc", "afr", "me"];
22
+ export type RelayLocationHint = typeof RELAY_LOCATION_HINTS[number];
23
+ /** A workspace region carried on the request, if the caller supplied one. */
24
+ export declare const RELAY_REGION_HEADER = "x-claxedo-workspace-region";
25
+ /**
26
+ * Chooses the location hint for a workspace's Durable Object.
27
+ *
28
+ * This decision is PERMANENT: a DO's location is fixed at first creation and
29
+ * never migrates, so a workspace created with the wrong hint pays the extra
30
+ * ocean crossing on every frame for its entire life. That is why an
31
+ * unrecognised value falls back to the configured default instead of being
32
+ * passed through or guessed at.
33
+ *
34
+ * Precedence, most authoritative first:
35
+ * 1. the workspace's own region (header or `?region=`), which is what the
36
+ * control plane knows and the only signal tied to the WORKSPACE rather than
37
+ * to whoever happens to be connecting first;
38
+ * 2. the requesting user's Cloudflare edge country, a decent proxy when the
39
+ * workspace has no region yet;
40
+ * 3. the deployment-wide configured hint (`CLAXEDO_RELAY_LOCATION_HINT`), then
41
+ * `DEFAULT_RELAY_LOCATION_HINT`.
42
+ */
43
+ export declare function relayLocationHint(input: {
44
+ configured?: string;
45
+ region?: string;
46
+ country?: string;
47
+ }): string;
48
+ /** The workspace region a request declares, if any. */
49
+ export declare function relayRequestRegion(request: Request): string | undefined;
50
+ /** The Cloudflare edge country for the request, when running on Cloudflare. */
51
+ export declare function relayRequestCountry(request: Request): string | undefined;
14
52
  export type WorkspaceRelayDurableObjectEnv = Record<string, unknown>;
15
53
  export type WorkspaceRelayDurableObjectGatewayOptions = {
16
54
  bindingName?: string;
@@ -43,6 +81,7 @@ export type WorkspaceRelayDurableObjectRoomOptions = WorkspaceRelayOptions & {
43
81
  connectWebSocket?: WorkspaceRelayDurableObjectConnectWebSocket;
44
82
  hibernation?: WorkspaceRelayDurableObjectHibernation;
45
83
  upstreamWebSocketPreOpenQueueMaxFrames?: number;
84
+ upstreamWebSocketPreOpenQueueMaxBytes?: number;
46
85
  tunnelPendingHttpCap?: number;
47
86
  tunnelStartedStreamCap?: number;
48
87
  tunnelChannelCap?: number;
@@ -52,6 +91,31 @@ export type WorkspaceRelayDurableObjectRoomOptions = WorkspaceRelayOptions & {
52
91
  slowConsumerTimeoutMs?: number;
53
92
  runtimeAccessTokenActiveCheckIntervalMs?: number;
54
93
  workspaceTargetActiveCheckIntervalMs?: number;
94
+ /**
95
+ * Consecutive resolver-unreachable answers tolerated on an established
96
+ * connection before closing it. Applies ONLY to "could not reach the
97
+ * resolver"; an explicit revoked/inactive answer still closes immediately.
98
+ */
99
+ resolverOutageGraceAttempts?: number;
100
+ /**
101
+ * Interval for the alarm-based revocation re-check that covers hibernated
102
+ * connections, where `setInterval` watchers cannot survive. 0 disables it.
103
+ * The hibernated host-generation check rides the same alarm.
104
+ */
105
+ hibernatedRevocationCheckIntervalMs?: number;
106
+ /**
107
+ * How often an established host tunnel's serving generation is re-checked
108
+ * against `resolveHostGeneration` on the non-hibernating path. Defaults to
109
+ * 30s; 0 disables it. The lookup may answer from its cache, so a superseded
110
+ * tunnel closes within this interval plus the lookup's cache TTL.
111
+ */
112
+ hostGenerationCheckIntervalMs?: number;
113
+ /**
114
+ * Durable Object alarm surface, supplied by the Worker (`state.storage`) so the
115
+ * room can schedule the hibernation-safe revocation re-check. Absent on the
116
+ * non-hibernating path and in tests that do not exercise it.
117
+ */
118
+ alarms?: WorkspaceRelayDurableObjectAlarms;
55
119
  traceSampleRate?: number;
56
120
  traceForceHeaderSecret?: string;
57
121
  traceLog?: (event: WorkspaceRelayTraceLogEvent) => void;
@@ -72,11 +136,29 @@ export type WorkspaceRelayDurableObjectHibernation = {
72
136
  acceptWebSocket: (socket: WorkspaceRelayDurableObjectSocket) => void;
73
137
  getWebSockets: () => WorkspaceRelayDurableObjectSocket[];
74
138
  };
139
+ /**
140
+ * The Durable Object alarm surface, narrowed to what the room needs.
141
+ *
142
+ * Alarms are the only periodic mechanism that survives hibernation — a
143
+ * `setInterval` dies when the DO is evicted, which is exactly why the token and
144
+ * target watchers are not installed on the hibernating path. Backed by
145
+ * `state.storage` in the Worker.
146
+ */
147
+ export type WorkspaceRelayDurableObjectAlarms = {
148
+ getAlarm: () => Promise<number | null> | number | null;
149
+ setAlarm: (scheduledTime: number) => Promise<void> | void;
150
+ deleteAlarm?: () => Promise<void> | void;
151
+ };
75
152
  export type WorkspaceRelaySocketAttachment = {
76
153
  kind: "host-tunnel";
77
154
  hostId: string;
78
155
  workspaceIds: string[];
79
156
  connectedAt: number;
157
+ /** Fence pair from the admitting Host Tunnel Token; absent on sockets attached before the fence existed. */
158
+ enrollmentId?: string;
159
+ generation?: number;
160
+ /** Consecutive unavailable generation lookups, persisted so hibernation cannot reset the outage grace. */
161
+ generationCheckFailures?: number;
80
162
  } | {
81
163
  kind: "user-hosted-client";
82
164
  channelId: string;
@@ -96,6 +178,30 @@ export type WorkspaceRelayDurableObjectDrainController = {
96
178
  remaining: number;
97
179
  }>;
98
180
  };
181
+ /**
182
+ * Exported for `relay-base64.test.ts`, which proves byte-equality against a
183
+ * per-byte reference across the chunk boundaries. This sits on the path every
184
+ * frame pays, so its equivalence is worth asserting directly rather than
185
+ * inferring from a round-trip.
186
+ */
187
+ export declare function bytesToBase64(bytes: Uint8Array): string;
188
+ /**
189
+ * Runs the two frame decoders over one runtime-delivered `event.data` and
190
+ * reports what they produced.
191
+ *
192
+ * Exists so `relay-workerd-binary.test.ts` can assert the decoders against what
193
+ * REAL workerd delivers. The rest of the suite drives hand-rolled socket
194
+ * doubles, which by construction hand us whatever we chose to hand ourselves —
195
+ * so they cannot catch the hazard that binary `event.data` becomes a `Blob` at
196
+ * `compatibility_date >= 2026-03-17`. Test-support surface, not a relay path.
197
+ */
198
+ export declare function relayFrameDiagnostics(data: unknown): Promise<{
199
+ frameDropped: boolean;
200
+ payloadDropped: boolean;
201
+ binary?: boolean | undefined;
202
+ dataBase64?: string | undefined;
203
+ payloadBase64: string | undefined;
204
+ }>;
99
205
  export declare function workspaceRelayDurableObjectRoomName(workspaceId: string): string;
100
206
  export declare function workspaceRelayDurableObjectWorkspaceId(request: Request): string | undefined;
101
207
  export declare function setWorkspaceRelayAppOrigins(raw: string | undefined): void;
@@ -106,6 +212,11 @@ export declare function createWorkspaceRelayDurableObjectGateway(options?: Works
106
212
  export declare function createWorkspaceRelayDurableObjectRoom(options: WorkspaceRelayDurableObjectRoomOptions): {
107
213
  fetch(request: Request): Promise<Response>;
108
214
  webSocketMessage(socket: WorkspaceRelayDurableObjectSocket, message: string | ArrayBuffer): Promise<void>;
215
+ /**
216
+ * Durable Object alarm entry point. Runs the hibernation-safe revocation
217
+ * sweep. The Worker must forward `alarm()` here.
218
+ */
219
+ alarm(): Promise<void>;
109
220
  webSocketClose(socket: WorkspaceRelayDurableObjectSocket, code: number, reason: string): void;
110
221
  webSocketError(socket: WorkspaceRelayDurableObjectSocket): void;
111
222
  state(): {
@@ -1,3 +1,8 @@
1
+ /**
2
+ * One tunnel's presence, recorded once per workspace it serves. `workspaceIds`
3
+ * is the full set that tunnel registered, so a reader that found the entry
4
+ * through one workspace still sees the others.
5
+ */
1
6
  export type HostTunnelPresence = {
2
7
  hostId: string;
3
8
  workspaceIds: string[];
@@ -5,13 +10,21 @@ export type HostTunnelPresence = {
5
10
  lastPongAt: number;
6
11
  expiresAt: number;
7
12
  };
13
+ /**
14
+ * Presence is keyed by (host, workspace): one host may hold a separate tunnel
15
+ * per workspace on a Bun relay, and a later registration for a workspace takes
16
+ * that workspace over without touching the host's other entries. `recordPong`
17
+ * and `disconnectHost` take the optional workspace set for the same reason —
18
+ * without it they act on every entry of the host, which is what a
19
+ * single-workspace Cloudflare room wants.
20
+ */
8
21
  export type WorkspaceRelayDirectory = {
9
22
  registerHostTunnel(input: {
10
23
  hostId: string;
11
24
  workspaceIds: string[];
12
25
  }): HostTunnelPresence;
13
- recordPong(hostId: string): HostTunnelPresence | undefined;
14
- disconnectHost(hostId: string): void;
26
+ recordPong(hostId: string, workspaceIds?: string[]): HostTunnelPresence | undefined;
27
+ disconnectHost(hostId: string, workspaceIds?: string[]): void;
15
28
  activeHost(input: {
16
29
  hostId: string;
17
30
  workspaceId: string;
@@ -29,10 +42,8 @@ export type WorkspaceRelayDirectory = {
29
42
  */
30
43
  dispose(): void;
31
44
  /**
32
- * T31: count of currently-active (non-expired) host presence entries.
33
- * Equivalent to running `sweep()` then returning the residual map size, but
34
- * implemented as a single pass without mutating eviction state when the
35
- * caller only wants a snapshot. Surfaced via the `/metrics` endpoint as
45
+ * Count of hosts with at least one non-expired presence entry, taken as a
46
+ * snapshot without evicting. Surfaced via `/metrics` as
36
47
  * `directory.activeHostCount`.
37
48
  */
38
49
  size(): number;
package/dist/http.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export declare function bearerToken(header: string | null | undefined): string | undefined;
1
+ import { bearerToken } from "@claxedo/helpers/string";
2
+ export { bearerToken };
2
3
  export declare function errorBody(code: string, message: string): {
3
4
  error: {
4
5
  code: string;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from "./auth";
2
- export * from "./bun";
3
2
  export * from "./cloudflare";
4
3
  export * from "./cors-origins";
5
4
  export * from "./directory";