@claxedo/workspace-relay 0.7.0 → 0.10.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/dist/index.mjs CHANGED
@@ -1,11 +1,39 @@
1
1
  // src/auth.ts
2
- import { SignJWT, errors, jwtVerify } from "jose";
2
+ import { SignJWT, errors, exportJWK, importJWK, jwtVerify } from "jose";
3
+
4
+ // ../claxedo-helpers/dist/guards.mjs
5
+ function isRecord(value) {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+ function asRecord(value) {
9
+ return isRecord(value) ? value : void 0;
10
+ }
11
+ function isFiniteNumber(value) {
12
+ return typeof value === "number" && Number.isFinite(value);
13
+ }
14
+ function asFiniteNumber(value) {
15
+ return isFiniteNumber(value) ? value : void 0;
16
+ }
17
+ function numberClaim(payload, key) {
18
+ return asFiniteNumber(payload[key]);
19
+ }
20
+
21
+ // src/auth.ts
22
+ import { CURRENT_CHANNEL_IDENTITY_VERSION } from "@claxedo/workspace-relay-protocol";
3
23
  var algorithms = ["EdDSA", "ES256", "RS256"];
4
24
  var runtimeAccessTokenIssuer = "claxedo-control-plane";
5
25
  var runtimeAccessTokenAudience = "workspace-relay";
6
26
  var hostTunnelTokenAudience = "workspace-relay-host-tunnel";
7
27
  var relayHostTokenIssuer = "workspace-relay";
8
28
  var relayHostTokenAudience = "workspace-host-service";
29
+ var RUNTIME_ACCESS_TOKEN_ISSUED_AT_FLOOR_SECONDS = 1789948800;
30
+ function toChannelIdentityClaim(input) {
31
+ return {
32
+ channel: input.channel,
33
+ external_user_id: input.externalUserId,
34
+ identity_version: input.identityVersion
35
+ };
36
+ }
9
37
  var WorkspaceRelayAuthError = class extends Error {
10
38
  constructor(code, message) {
11
39
  super(message);
@@ -19,27 +47,84 @@ function seconds(input = Date.now()) {
19
47
  return Math.floor(input / 1e3);
20
48
  }
21
49
  function requireAlgorithm(input) {
22
- if (algorithms.includes(input)) return input;
50
+ const algorithm = algorithms.find((candidate) => candidate === input);
51
+ if (algorithm) return algorithm;
23
52
  throw new WorkspaceRelayAuthError("invalid_relay_token", "Unsupported relay token algorithm");
24
53
  }
25
54
  function stringClaim(payload, key) {
26
55
  const value = payload[key];
27
56
  return typeof value === "string" && value.trim() ? value : void 0;
28
57
  }
29
- function numberClaim(payload, key) {
30
- const value = payload[key];
31
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
32
- }
33
58
  function stringArrayClaim(payload, key) {
34
59
  const value = payload[key];
35
60
  return Array.isArray(value) && value.every((item) => typeof item === "string" && item.trim()) ? value : void 0;
36
61
  }
62
+ function isHostGeneration(value) {
63
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
64
+ }
65
+ function hostGenerationClaims(payload) {
66
+ const enrollment_id = stringClaim(payload, "enrollment_id");
67
+ const generation = payload.generation;
68
+ if (!isHostGeneration(generation) || !enrollment_id) return null;
69
+ return { enrollment_id, generation };
70
+ }
37
71
  function roleClaim(payload) {
38
72
  const value = stringClaim(payload, "role");
39
73
  return value === "viewer" || value === "editor" || value === "admin" || value === "owner" ? value : void 0;
40
74
  }
41
- function isRelayClaimPair(input) {
42
- return input.access === "cloud" && input.backing === "cloud-vm" || input.access === "user-hosted" && input.backing === "local-worktree";
75
+ function actorProfileClaims(payload) {
76
+ const actor_public_id = stringClaim(payload, "actor_public_id");
77
+ const actor_name = stringClaim(payload, "actor_name");
78
+ const actor_avatar_url = stringClaim(payload, "actor_avatar_url");
79
+ if (!actor_public_id && !actor_name && !actor_avatar_url) return {};
80
+ if (!actor_public_id || !actor_name) return void 0;
81
+ return { actor_public_id, actor_name, ...actor_avatar_url ? { actor_avatar_url } : {} };
82
+ }
83
+ function actorProfilePayload(input) {
84
+ if (!input.actorPublicId && !input.actorName && !input.actorAvatarUrl) return {};
85
+ if (!input.actorPublicId || !input.actorName) {
86
+ throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Actor display profile claims are incomplete");
87
+ }
88
+ return {
89
+ actor_public_id: input.actorPublicId,
90
+ actor_name: input.actorName,
91
+ ...input.actorAvatarUrl ? { actor_avatar_url: input.actorAvatarUrl } : {}
92
+ };
93
+ }
94
+ function channelIdentityPayload(input) {
95
+ return input.channelIdentity ? { channel_identity: toChannelIdentityClaim(input.channelIdentity) } : {};
96
+ }
97
+ function channelIdentityClaims(payload) {
98
+ const value = payload.channel_identity;
99
+ if (value === void 0) return void 0;
100
+ if (!isRecord(value)) {
101
+ throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Channel provenance claim is not a claims object");
102
+ }
103
+ const claim = value;
104
+ const channel = stringClaim(claim, "channel");
105
+ const external_user_id = stringClaim(claim, "external_user_id");
106
+ const identity_version = numberClaim(claim, "identity_version");
107
+ if (!channel || !external_user_id || identity_version === void 0 || !Number.isInteger(identity_version)) {
108
+ throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Channel provenance claim is incomplete");
109
+ }
110
+ if (identity_version < CURRENT_CHANNEL_IDENTITY_VERSION) {
111
+ throw new WorkspaceRelayAuthError(
112
+ "relay_token_claims_invalid",
113
+ "Channel provenance claim predates the current channel identity version"
114
+ );
115
+ }
116
+ return { channel, external_user_id, identity_version };
117
+ }
118
+ function checkIssuedAtFloor(iat) {
119
+ if (iat < RUNTIME_ACCESS_TOKEN_ISSUED_AT_FLOOR_SECONDS) {
120
+ throw new WorkspaceRelayAuthError(
121
+ "relay_token_claims_invalid",
122
+ "Runtime Access Token was issued before the channel provenance floor"
123
+ );
124
+ }
125
+ }
126
+ function isRelayBacking(input) {
127
+ return input === "cloud-vm" || input === "local-worktree" || input === "durable-object";
43
128
  }
44
129
  function checkHostTunnelTarget(payload, expected) {
45
130
  if (stringClaim(payload, "host_id") !== expected.hostId) {
@@ -58,6 +143,21 @@ function checkTarget(payload, expected) {
58
143
  throw new WorkspaceRelayAuthError("relay_token_host_mismatch", "Relay token host does not match request");
59
144
  }
60
145
  }
146
+ async function deriveRelayHostPublicKey(privateKey) {
147
+ const jwk = await exportJWK(privateKey);
148
+ const imported = await importJWK({ kty: jwk.kty, crv: jwk.crv, x: jwk.x }, "EdDSA", { extractable: true });
149
+ if (imported instanceof Uint8Array) {
150
+ throw new Error("Relay host public key imported as raw bytes; expected an EdDSA public key");
151
+ }
152
+ return imported;
153
+ }
154
+ async function deriveRelayHostKid(publicKey) {
155
+ const jwk = await exportJWK(publicKey);
156
+ const material = jwk.x ?? jwk.n ?? "";
157
+ if (!material) throw new Error("Cannot derive kid: public key has no public component");
158
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(material));
159
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 16);
160
+ }
61
161
  async function verifyJwt(token, key, input) {
62
162
  try {
63
163
  if (typeof key === "function") {
@@ -83,11 +183,20 @@ async function verifyJwt(token, key, input) {
83
183
  async function mintRuntimeAccessToken(input, key, alg) {
84
184
  const now = seconds(input.now);
85
185
  return await new SignJWT({
186
+ principal_kind: input.principalKind,
187
+ actor_id: input.actorId,
188
+ ...input.userId ? { user_id: input.userId } : {},
189
+ actor_kind: input.actorKind,
190
+ ...actorProfilePayload(input),
191
+ ...channelIdentityPayload(input),
86
192
  org_id: input.orgId,
87
193
  workspace_id: input.workspaceId,
88
194
  host_id: input.hostId,
89
- role: input.role
90
- }).setProtectedHeader({ alg: requireAlgorithm(alg) }).setIssuer(runtimeAccessTokenIssuer).setAudience(runtimeAccessTokenAudience).setSubject(input.subject).setIssuedAt(now).setExpirationTime(now + (input.ttlSeconds ?? 30 * 60)).setJti(input.jti ?? jti()).sign(key);
195
+ role: input.role,
196
+ ...tokenScopePayload(input.sessionId),
197
+ ...input.purpose ? { purpose: input.purpose } : {},
198
+ ...input.routingId !== void 0 ? { routing_id: input.routingId } : {}
199
+ }).setProtectedHeader({ alg: requireAlgorithm(alg) }).setIssuer(runtimeAccessTokenIssuer).setAudience(runtimeAccessTokenAudience).setIssuedAt(now).setExpirationTime(now + (input.ttlSeconds ?? 30 * 60)).setJti(input.jti ?? jti()).sign(key);
91
200
  }
92
201
  async function verifyRuntimeAccessToken(token, key, expected) {
93
202
  const result = await verifyJwt(token, key, {
@@ -99,8 +208,27 @@ async function verifyRuntimeAccessToken(token, key, expected) {
99
208
  if (!claims) {
100
209
  throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Runtime Access Token claims are incomplete");
101
210
  }
211
+ checkIssuedAtFloor(claims.iat);
102
212
  return claims;
103
213
  }
214
+ var TOKEN_CLAIMS_CLOCK_SKEW_SECONDS = 60;
215
+ var TOKEN_CLAIMS_MAX_LIFETIME_SECONDS = 24 * 60 * 60;
216
+ function checkTokenTimeClaims(payload, exp, tokenName) {
217
+ const now = seconds();
218
+ if (exp <= now - TOKEN_CLAIMS_CLOCK_SKEW_SECONDS) {
219
+ throw new WorkspaceRelayAuthError("relay_token_claims_invalid", `${tokenName} is expired`);
220
+ }
221
+ const nbf = numberClaim(payload, "nbf");
222
+ if (payload.nbf !== void 0 && nbf === void 0) {
223
+ throw new WorkspaceRelayAuthError("relay_token_claims_invalid", `${tokenName} nbf claim is not a finite number`);
224
+ }
225
+ if (nbf !== void 0 && nbf > now + TOKEN_CLAIMS_CLOCK_SKEW_SECONDS) {
226
+ throw new WorkspaceRelayAuthError("relay_token_claims_invalid", `${tokenName} is not yet valid`);
227
+ }
228
+ if (exp > now + TOKEN_CLAIMS_MAX_LIFETIME_SECONDS) {
229
+ throw new WorkspaceRelayAuthError("relay_token_claims_invalid", `${tokenName} lifetime exceeds the relay maximum`);
230
+ }
231
+ }
104
232
  function validateRuntimeAccessTokenClaims(input, expected) {
105
233
  const payload = input;
106
234
  if (stringClaim(payload, "iss") !== runtimeAccessTokenIssuer || stringClaim(payload, "aud") !== runtimeAccessTokenAudience) {
@@ -111,13 +239,20 @@ function validateRuntimeAccessTokenClaims(input, expected) {
111
239
  if (!claims) {
112
240
  throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Runtime Access Token claims are incomplete");
113
241
  }
242
+ checkIssuedAtFloor(claims.iat);
243
+ checkTokenTimeClaims(payload, claims.exp, "Runtime Access Token");
114
244
  return claims;
115
245
  }
116
246
  async function mintHostTunnelToken(input, key, alg) {
117
247
  const now = seconds(input.now);
248
+ if (!isHostGeneration(input.generation) || !input.enrollmentId?.trim()) {
249
+ throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Host Tunnel Token generation requires a non-negative integer and an enrollment id");
250
+ }
118
251
  return await new SignJWT({
119
252
  host_id: input.hostId,
120
- workspace_ids: input.workspaceIds
253
+ workspace_ids: input.workspaceIds,
254
+ enrollment_id: input.enrollmentId,
255
+ generation: input.generation
121
256
  }).setProtectedHeader({ alg: requireAlgorithm(alg) }).setIssuer(runtimeAccessTokenIssuer).setAudience(hostTunnelTokenAudience).setSubject(input.subject).setIssuedAt(now).setExpirationTime(now + (input.ttlSeconds ?? 5 * 60)).setJti(input.jti ?? jti()).sign(key);
122
257
  }
123
258
  async function verifyHostTunnelToken(token, key, expected) {
@@ -132,21 +267,54 @@ async function verifyHostTunnelToken(token, key, expected) {
132
267
  }
133
268
  return claims;
134
269
  }
270
+ function relayHostInputFromRuntimeClaims(claims) {
271
+ return {
272
+ principalKind: claims.principal_kind,
273
+ actorId: claims.actor_id,
274
+ ...claims.user_id ? { userId: claims.user_id } : {},
275
+ actorKind: claims.actor_kind,
276
+ parentJti: claims.jti,
277
+ ...claims.actor_public_id && claims.actor_name ? {
278
+ actorPublicId: claims.actor_public_id,
279
+ actorName: claims.actor_name,
280
+ ...claims.actor_avatar_url ? { actorAvatarUrl: claims.actor_avatar_url } : {}
281
+ } : {},
282
+ ...claims.channel_identity ? {
283
+ channelIdentity: {
284
+ channel: claims.channel_identity.channel,
285
+ externalUserId: claims.channel_identity.external_user_id,
286
+ identityVersion: claims.channel_identity.identity_version
287
+ }
288
+ } : {},
289
+ orgId: claims.org_id,
290
+ role: claims.role,
291
+ ...claims.session_id ? { sessionId: claims.session_id } : {},
292
+ ...claims.purpose ? { purpose: claims.purpose } : {}
293
+ };
294
+ }
135
295
  async function mintRelayHostToken(input, key, alg) {
136
- if (!isRelayClaimPair(input)) {
137
- throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Relay Host Token access/backing claims are inconsistent");
296
+ if (!isRelayBacking(input.backing)) {
297
+ throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Relay Host Token backing claim is not a placement");
138
298
  }
139
299
  const now = seconds(input.now);
140
300
  const protectedHeader = { alg: requireAlgorithm(alg) };
141
301
  if (input.kid) protectedHeader.kid = input.kid;
142
302
  return await new SignJWT({
303
+ principal_kind: input.principalKind,
304
+ actor_id: input.actorId,
305
+ ...input.userId ? { user_id: input.userId } : {},
306
+ actor_kind: input.actorKind,
307
+ ...actorProfilePayload(input),
308
+ ...channelIdentityPayload(input),
143
309
  org_id: input.orgId,
144
310
  workspace_id: input.workspaceId,
145
311
  host_id: input.hostId,
146
312
  role: input.role,
147
- access: input.access,
148
- backing: input.backing
149
- }).setProtectedHeader(protectedHeader).setIssuer(relayHostTokenIssuer).setAudience(relayHostTokenAudience).setSubject(input.subject).setIssuedAt(now).setExpirationTime(now + (input.ttlSeconds ?? 60)).setJti(input.jti ?? jti()).sign(key);
313
+ ...tokenScopePayload(input.sessionId),
314
+ ...input.purpose ? { purpose: input.purpose } : {},
315
+ backing: input.backing,
316
+ parent_jti: input.parentJti
317
+ }).setProtectedHeader(protectedHeader).setIssuer(relayHostTokenIssuer).setAudience(relayHostTokenAudience).setIssuedAt(now).setExpirationTime(now + (input.ttlSeconds ?? 60)).setJti(input.jti ?? jti()).sign(key);
150
318
  }
151
319
  async function verifyRelayHostToken(token, key, expected) {
152
320
  const result = await verifyJwt(token, key, {
@@ -163,41 +331,72 @@ async function verifyRelayHostToken(token, key, expected) {
163
331
  function runtimeClaims(payload) {
164
332
  const exp = numberClaim(payload, "exp");
165
333
  const iat = numberClaim(payload, "iat");
166
- const sub = stringClaim(payload, "sub");
334
+ const principal_kind = stringClaim(payload, "principal_kind");
335
+ const actor_id = stringClaim(payload, "actor_id");
336
+ const user_id = stringClaim(payload, "user_id");
337
+ const actor_kind = stringClaim(payload, "actor_kind");
167
338
  const jti2 = stringClaim(payload, "jti");
168
339
  const org_id = stringClaim(payload, "org_id");
169
340
  const workspace_id = stringClaim(payload, "workspace_id");
170
341
  const host_id = stringClaim(payload, "host_id");
171
342
  const role = roleClaim(payload);
172
- if (!exp || !iat || !sub || !jti2 || !org_id || !workspace_id || !host_id || !role) return;
343
+ const reach = tokenScopeClaims(payload);
344
+ const purpose = tokenPurposeClaims(payload);
345
+ if (!exp || !iat || !jti2 || !org_id || !workspace_id || !host_id || !role || !actor_id || !reach || !purpose || payload.user_id !== void 0 && !user_id || principal_kind !== "user" && principal_kind !== "service" || actor_kind !== "human" && actor_kind !== "agent" || principal_kind === "user" && actor_kind !== "human" || principal_kind === "service" && actor_kind !== "agent") return void 0;
346
+ const actorProfile = actorProfileClaims(payload);
347
+ if (!actorProfile) return void 0;
348
+ const channel_identity = channelIdentityClaims(payload);
349
+ const routing_id = stringClaim(payload, "routing_id");
173
350
  return {
174
351
  iss: runtimeAccessTokenIssuer,
175
352
  aud: runtimeAccessTokenAudience,
176
- sub,
353
+ principal_kind,
354
+ actor_id,
355
+ ...user_id ? { user_id } : {},
356
+ actor_kind,
357
+ ...actorProfile,
358
+ ...channel_identity ? { channel_identity } : {},
359
+ ...routing_id !== void 0 ? { routing_id } : {},
177
360
  org_id,
178
361
  workspace_id,
179
362
  host_id,
180
363
  role,
364
+ ...reach,
365
+ ...purpose,
181
366
  exp,
182
367
  iat,
183
368
  jti: jti2
184
369
  };
185
370
  }
371
+ function tokenScopePayload(sessionId) {
372
+ return sessionId === void 0 ? { scope: "workspace" } : { scope: "session", session_id: sessionId };
373
+ }
374
+ function tokenPurposeClaims(payload) {
375
+ if (payload.purpose === void 0) return {};
376
+ return payload.purpose === "turn-execution" ? { purpose: payload.purpose } : void 0;
377
+ }
378
+ function tokenScopeClaims(payload) {
379
+ const sessionId = stringClaim(payload, "session_id");
380
+ if (payload.scope === "workspace" && payload.session_id === void 0) return { scope: "workspace" };
381
+ if (payload.scope === "session" && sessionId) return { scope: "session", session_id: sessionId };
382
+ return void 0;
383
+ }
186
384
  function relayHostClaims(payload) {
187
385
  const base = runtimeClaims({
188
386
  ...payload,
189
387
  iss: runtimeAccessTokenIssuer,
190
388
  aud: runtimeAccessTokenAudience
191
389
  });
192
- const access = stringClaim(payload, "access");
390
+ if (payload.access !== void 0) return void 0;
193
391
  const backing = stringClaim(payload, "backing");
194
- const pair = { access, backing };
195
- if (!base || !isRelayClaimPair(pair)) return;
392
+ const parent_jti = stringClaim(payload, "parent_jti");
393
+ if (!base || !parent_jti || !isRelayBacking(backing)) return void 0;
196
394
  return {
197
395
  ...base,
198
396
  iss: relayHostTokenIssuer,
199
397
  aud: relayHostTokenAudience,
200
- ...pair
398
+ parent_jti,
399
+ backing
201
400
  };
202
401
  }
203
402
  function hostTunnelClaims(payload) {
@@ -207,151 +406,309 @@ function hostTunnelClaims(payload) {
207
406
  const jti2 = stringClaim(payload, "jti");
208
407
  const host_id = stringClaim(payload, "host_id");
209
408
  const workspace_ids = stringArrayClaim(payload, "workspace_ids");
210
- if (!exp || !iat || !sub || !jti2 || !host_id || !workspace_ids?.length) return;
409
+ const fence = hostGenerationClaims(payload);
410
+ if (!exp || !iat || !sub || !jti2 || !host_id || !workspace_ids?.length || fence === null) return void 0;
211
411
  return {
212
412
  iss: runtimeAccessTokenIssuer,
213
413
  aud: hostTunnelTokenAudience,
214
414
  sub,
215
415
  host_id,
216
416
  workspace_ids,
417
+ ...fence,
217
418
  exp,
218
419
  iat,
219
420
  jti: jti2
220
421
  };
221
422
  }
222
423
 
223
- // src/bun.ts
424
+ // ../claxedo-helpers/dist/api-error.mjs
425
+ function isRecord2(value) {
426
+ return value !== null && typeof value === "object" && !Array.isArray(value);
427
+ }
428
+ function asRecord2(value) {
429
+ return isRecord2(value) ? value : void 0;
430
+ }
431
+ var PUBLIC_API_ERRORS = {
432
+ repository_provider_unauthorized: { status: 502, message: "Repository provider refused authorization" },
433
+ repository_provider_unavailable: { status: 502, message: "Repository provider is unavailable" },
434
+ host_tunnel_timeout: { status: 503, retryable: true, message: "Host tunnel timed out" },
435
+ host_tunnel_response_body_too_large: { status: 413, message: "Host tunnel response body exceeds the relay limit" },
436
+ host_tunnel_unavailable: { status: 503, retryable: true, message: "Host tunnel is unavailable" },
437
+ workspace_checkpoint_unavailable: { status: 503, retryable: true, message: "Workspace checkpoint is unavailable" },
438
+ workspace_checkpoint_conflict: { status: 409, message: "Workspace checkpoint conflicted" },
439
+ invalid_input: { family: "access", status: 400, message: "The request names an invalid value" },
440
+ organization_policy_denied: { family: "access", status: 403, message: "Organization creation is disabled for this deployment" },
441
+ org_admin_required: { family: "access", status: 403, message: "Organization administrator authority is required" },
442
+ org_owner_required: { family: "access", status: 403, message: "Only an organization owner may grant, change or remove the owner role" },
443
+ org_owner_protected: { family: "access", status: 409, message: "The organization's founding owner cannot be removed or demoted" },
444
+ team_member_org_membership_required: { family: "access", status: 403, message: "The team member must belong to the team organization" },
445
+ project_member_org_membership_required: { family: "access", status: 403, message: "The project member must belong to the project organization" },
446
+ org_membership_required: { family: "access", status: 403, message: "Organization membership is required" },
447
+ project_admin_required: { family: "access", status: 403, message: "Project administrator authority is required" },
448
+ team_not_allowed_on_personal_org: { family: "access", status: 400, message: "Personal organizations cannot contain teams" },
449
+ team_member_target_required: { family: "access", status: 400, message: "Exactly one team member target is required" },
450
+ org_invitation_invalid: { family: "access", status: 409, message: "Invitation is invalid or unavailable" },
451
+ org_invitation_pending: { family: "access", status: 409, message: "An invitation to this address is already pending" },
452
+ org_invitation_rate_limited: { family: "access", status: 429, message: "Organization invitation limit exceeded" },
453
+ org_invitation_email_mismatch: { family: "access", status: 403, message: "Sign in with the invited verified email address" },
454
+ org_invitation_delivery_unavailable: { family: "access", status: 503, message: "Invitation delivery is unavailable" },
455
+ team_not_found: { family: "access", status: 404, message: "Team not found" },
456
+ team_member_not_found: { family: "access", status: 404, message: "Team member not found" },
457
+ org_member_not_found: { family: "access", status: 404, message: "Organization member not found" },
458
+ project_not_found: { family: "access", status: 404, message: "Project not found" },
459
+ project_member_not_found: { family: "access", status: 404, message: "Project member not found" },
460
+ project_member_owner_immutable: { family: "access", status: 409, message: "The project owner's access cannot be changed" },
461
+ resource_conflict: { family: "access", status: 409, message: "Organization or team authority changed concurrently" },
462
+ session_not_found: { family: "session_share", status: 404, message: "This session is not on the control plane, so it cannot be shared from People yet." },
463
+ session_share_admin_required: { family: "session_share", status: 403, message: "Only the person who started this session can manage its People." },
464
+ session_share_target_required: { family: "session_share", status: 400, message: "A session is shared with exactly one person" },
465
+ session_share_target_not_found: { family: "session_share", status: 404, message: "That person was not found in this organization." },
466
+ session_share_level_invalid: { family: "session_share", status: 400, message: "A share level is either follow or send." },
467
+ workspace_backing_conflict: { status: 409, message: "Workspace is provisioned in the cloud and cannot be placed on a machine" },
468
+ invalid_session_list_cursor: { status: 400, message: "Session list cursor does not match this query" },
469
+ BAD_REQUEST: { status: 400, message: "Invalid request" },
470
+ INTERNAL_SERVER_ERROR: { status: 500, message: "Internal error" },
471
+ CONTROL_PLANE_UNAVAILABLE: { status: 503, message: "Control Plane services are not configured" },
472
+ credential_verification_unsupported: { status: 400, message: "Credential provider does not support verification" },
473
+ credential_shape_invalid: { status: 400, message: "Credential secret has an unsupported shape" },
474
+ credential_provider_unavailable: { status: 502, retryable: true, message: "Credential provider request failed" },
475
+ credential_verification_failed: { status: 502, message: "Credential provider verification failed" },
476
+ credential_redirect_denied: { status: 502, message: "Sandbox provider redirects are not allowed" },
477
+ credential_endpoint_invalid: { status: 400, message: "Invalid credential provider endpoint" },
478
+ host_attestation_denied: { status: 403, message: "host_attestation_denied" },
479
+ signature_replayed: { status: 409, message: "signature_replayed" },
480
+ host_enrollment_not_found: { status: 404, message: "host_enrollment_not_found" },
481
+ enrollment_revoked: { status: 403, message: "enrollment_revoked" },
482
+ enrollment_paused: { status: 403, message: "enrollment_paused" },
483
+ enrollment_owner_ineligible: { status: 403, message: "enrollment_owner_ineligible" },
484
+ enrollment_key_version_mismatch: { status: 403, message: "enrollment_key_version_mismatch" },
485
+ invitation_invalid: { status: 403, message: "invitation_invalid" },
486
+ invitation_expired: { status: 410, message: "invitation_expired" },
487
+ invitation_revoked: { status: 410, message: "invitation_revoked" },
488
+ invitation_redeemed: { status: 409, message: "invitation_redeemed" },
489
+ invitation_host_conflict: { status: 409, message: "invitation_host_conflict" },
490
+ enrollment_generation_superseded: { status: 409, message: "enrollment_generation_superseded" },
491
+ host_assignment_outside_scope: { status: 400, message: "host_assignment_outside_scope" },
492
+ host_sealing_key_undeclared: { status: 409, message: "host_sealing_key_undeclared" },
493
+ host_provider_config_revision_stale: { status: 409, message: "host_provider_config_revision_stale" },
494
+ identity_conflict: { status: 409, message: "identity_conflict" },
495
+ registration_transition_denied: { status: 403, message: "registration_transition_denied" },
496
+ session_reservation_spent: { status: 409, message: "This session's create was undone; reserve it again to retry" },
497
+ actor_authorization_denied: { status: 403, message: "actor_authorization_denied" }
498
+ };
499
+ function encodeApiError(error, defaults = {}) {
500
+ const row = asRecord2(error);
501
+ return {
502
+ error: {
503
+ code: typeof row?.code === "string" && row.code ? row.code : defaults.code ?? "internal_error",
504
+ message: typeof row?.message === "string" && row.message ? row.message : defaults.message ?? "Internal error",
505
+ retryable: row?.retryable === true
506
+ }
507
+ };
508
+ }
509
+ function publicApiErrorShape(code) {
510
+ const entry = PUBLIC_API_ERRORS[code];
511
+ return { status: entry.status, retryable: entry.retryable === true };
512
+ }
513
+ function publicApiFailure(code, message = PUBLIC_API_ERRORS[code].message) {
514
+ return Object.assign(new Error(message), { code, ...publicApiErrorShape(code) });
515
+ }
516
+
517
+ // src/host-tunnel-failure.ts
518
+ function hostTunnelFailure(code) {
519
+ return publicApiFailure(code);
520
+ }
521
+ function hostTunnelFailureResponse(error) {
522
+ const code = asRecord(error)?.code;
523
+ const failure = hostTunnelFailure(code === "host_tunnel_timeout" || code === "host_tunnel_response_body_too_large" ? code : "host_tunnel_unavailable");
524
+ return Response.json(encodeApiError(failure), { status: failure.status });
525
+ }
526
+
527
+ // src/cloudflare.ts
528
+ import { trimToUndefined as trimToUndefined4 } from "@claxedo/helpers/string";
529
+
530
+ // src/http.ts
531
+ import { bearerToken, trimToUndefined } from "@claxedo/helpers/string";
532
+ function errorBody(code, message) {
533
+ return {
534
+ error: {
535
+ code,
536
+ message
537
+ }
538
+ };
539
+ }
540
+ function hostIdFromTunnelPath(pathname) {
541
+ return /^\/host-tunnels\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
542
+ }
543
+ function workspaceIdsFromSearch(url) {
544
+ return [...new Set([
545
+ ...url.searchParams.getAll("workspaceId"),
546
+ ...url.searchParams.getAll("workspace_id")
547
+ ].map(trimToUndefined).filter((item) => !!item))];
548
+ }
549
+
550
+ // src/cloudflare.ts
224
551
  import {
225
- makeTunnelPing,
226
552
  makeTunnelPong,
227
553
  TUNNEL_PROTOCOL_VERSION,
228
554
  validateTunnelMessage
229
555
  } from "@claxedo/workspace-relay-protocol";
230
556
 
231
557
  // src/server.ts
232
- import { Hono } from "hono";
233
- import { cors } from "hono/cors";
234
- import { exportJWK } from "jose";
558
+ import { sessionScopeReaches } from "@claxedo/workspace-relay-protocol";
235
559
 
236
- // src/cors-origins.ts
237
- var DEFAULT_RELAY_APP_ORIGINS = [
238
- "http://localhost:*",
239
- "http://127.0.0.1:*",
240
- "https://claxedo.com",
241
- "https://*.claxedo.com"
242
- ];
243
- function parseAllowedOrigins(raw) {
244
- const entries = (raw ?? "").split(",").map((item) => item.trim()).filter(Boolean);
245
- return entries.length ? entries : void 0;
560
+ // src/host-tunnel-forwarding.ts
561
+ function isHostTunnelTarget(target) {
562
+ return target.backing === "local-worktree";
246
563
  }
247
- function createOriginMatcher(patterns) {
248
- const exact = /* @__PURE__ */ new Set();
249
- const httpsSuffixes = [];
250
- const httpPortPrefixes = [];
251
- for (const pattern of patterns) {
252
- if (pattern.startsWith("https://*.")) {
253
- httpsSuffixes.push(pattern.slice("https://*".length));
254
- } else if (pattern.startsWith("http://") && pattern.endsWith(":*")) {
255
- httpPortPrefixes.push(pattern.slice(0, -1));
256
- } else {
257
- exact.add(pattern);
258
- }
259
- }
260
- return (origin) => {
261
- if (exact.has(origin)) return true;
262
- if (origin.startsWith("https://")) {
263
- return httpsSuffixes.some(
264
- (suffix) => origin.endsWith(suffix) && origin.length > "https://".length + suffix.length
265
- );
266
- }
267
- if (origin.startsWith("http://")) {
268
- return httpPortPrefixes.some(
269
- (prefix) => origin.startsWith(prefix) && /^\d+$/.test(origin.slice(prefix.length))
270
- );
271
- }
272
- return false;
564
+
565
+ // src/relay-target.ts
566
+ function parseWorkspaceRelayTarget(input) {
567
+ if (!isRecord(input)) return void 0;
568
+ const row = input;
569
+ const { workspaceId, hostId, baseUrl } = row;
570
+ if (typeof workspaceId !== "string" || typeof hostId !== "string" || typeof baseUrl !== "string") return void 0;
571
+ if (row.access !== void 0) return void 0;
572
+ if (!isRelayBacking(row.backing)) return void 0;
573
+ if (!isAllowedRelayTargetBaseUrl({ baseUrl, backing: row.backing })) return void 0;
574
+ return {
575
+ workspaceId,
576
+ hostId,
577
+ baseUrl,
578
+ backing: row.backing
273
579
  };
274
580
  }
581
+ function parseAbsoluteHttpUrl(baseUrl) {
582
+ let url;
583
+ try {
584
+ url = new URL(baseUrl);
585
+ } catch {
586
+ return void 0;
587
+ }
588
+ return url.protocol === "http:" || url.protocol === "https:" ? url : void 0;
589
+ }
590
+ function isLoopbackTargetHostname(hostname) {
591
+ const host = hostname.toLowerCase();
592
+ if (host === "localhost" || host === "[::1]" || host.endsWith(".localhost")) return true;
593
+ const ipv4 = /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
594
+ return !!ipv4 && ipv4.slice(1).every((octet) => Number(octet) <= 255);
595
+ }
596
+ function isAllowedRelayTargetBaseUrl(target) {
597
+ if (target.backing === "durable-object") return target.baseUrl === "";
598
+ if (isHostTunnelTarget(target)) {
599
+ if (target.baseUrl === "") return true;
600
+ return parseAbsoluteHttpUrl(target.baseUrl) !== void 0;
601
+ }
602
+ const url = parseAbsoluteHttpUrl(target.baseUrl);
603
+ if (!url) return false;
604
+ return url.protocol === "https:" || isLoopbackTargetHostname(url.hostname);
605
+ }
275
606
 
276
- // src/http.ts
277
- function bearerToken(header) {
278
- if (!header) return;
279
- const match = /^Bearer\s+(.+)$/i.exec(header.trim());
280
- return match?.[1]?.trim() || void 0;
607
+ // src/relay-audit.ts
608
+ var auditSamplers = /* @__PURE__ */ new WeakMap();
609
+ function getOrCreateAuditSampler(options) {
610
+ let state = auditSamplers.get(options);
611
+ if (!state) {
612
+ const sampleRate = clampSampleRate(options.auditAcceptSampleRate);
613
+ state = {
614
+ suppressedCount: 0,
615
+ timer: void 0,
616
+ flushIntervalMs: options.auditFlushIntervalMs ?? 6e4,
617
+ random: options.random ?? Math.random,
618
+ sampleRate
619
+ };
620
+ auditSamplers.set(options, state);
621
+ }
622
+ return state;
281
623
  }
282
- function errorBody(code, message) {
283
- return {
284
- error: {
285
- code,
286
- message
624
+ function clampSampleRate(rate) {
625
+ if (rate === void 0) return 1;
626
+ if (!Number.isFinite(rate)) return 1;
627
+ if (rate < 0) return 0;
628
+ if (rate > 1) return 1;
629
+ return rate;
630
+ }
631
+ function ensureFlushTimer(options, state) {
632
+ if (state.timer) return;
633
+ if (state.sampleRate >= 1) return;
634
+ if (state.flushIntervalMs <= 0) return;
635
+ const timer = setInterval(() => {
636
+ flushSuppressedSummary(options, state);
637
+ }, state.flushIntervalMs);
638
+ if (typeof timer.unref === "function") {
639
+ timer.unref();
640
+ }
641
+ state.timer = timer;
642
+ }
643
+ function flushSuppressedSummary(options, state) {
644
+ const count = state.suppressedCount;
645
+ if (count <= 0) return;
646
+ state.suppressedCount = 0;
647
+ void options.audit?.({
648
+ action: "relay.request.suppressed_summary",
649
+ result: "allow",
650
+ method: "INTERNAL",
651
+ path: "/",
652
+ suppressedCount: count
653
+ });
654
+ }
655
+ async function audit(options, event) {
656
+ if (event.action === "relay.request.accepted") {
657
+ const state = getOrCreateAuditSampler(options);
658
+ if (state.sampleRate < 1) {
659
+ ensureFlushTimer(options, state);
660
+ if (state.random() >= state.sampleRate) {
661
+ state.suppressedCount += 1;
662
+ return;
663
+ }
287
664
  }
288
- };
665
+ }
666
+ await options.audit?.(event);
289
667
  }
290
668
 
291
669
  // src/server.ts
670
+ function hostTunnelIncumbentOutranks(incumbentGeneration, candidateGeneration) {
671
+ return incumbentGeneration > candidateGeneration;
672
+ }
292
673
  var RELAY_HOST_TOKEN_CACHE_MAX_ENTRIES = 4096;
293
674
  var RUNTIME_ACCESS_TOKEN_CACHE_MAX_ENTRIES = 8192;
294
- var RESOLVER_CACHE_MAX_ENTRIES = 8192;
295
675
  var RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS_DEFAULT = 1e4;
676
+ var RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
296
677
  var relayHostTokenCaches = /* @__PURE__ */ new WeakMap();
297
678
  var runtimeAccessTokenCaches = /* @__PURE__ */ new WeakMap();
298
- function pruneExpiringCache(cache, now, maxEntries) {
299
- for (const [key, entry] of cache) {
300
- if (entry.expiresAt <= now) cache.delete(key);
679
+ async function checkHostTunnelGeneration(lookup, claims) {
680
+ if (!lookup) {
681
+ return {
682
+ ok: false,
683
+ retryable: false,
684
+ code: "host_generation_unverifiable",
685
+ reason: "Host tunnel generation cannot be verified by a relay without a host-generation resolver"
686
+ };
301
687
  }
302
- while (cache.size >= maxEntries) {
303
- const key = cache.keys().next().value;
304
- if (!key) return;
305
- cache.delete(key);
688
+ let result;
689
+ try {
690
+ result = await lookup({ enrollmentId: claims.enrollment_id, generation: claims.generation });
691
+ } catch (err) {
692
+ return {
693
+ ok: false,
694
+ retryable: true,
695
+ code: "host_generation_lookup_unavailable",
696
+ reason: `Host generation lookup is unavailable: ${err instanceof Error ? err.message : String(err)}`
697
+ };
306
698
  }
307
- }
308
- function createCachedRevocationClient(inner, options = {}) {
309
- const ttlMs = options.ttlMs ?? 1e4;
310
- const now = options.now ?? Date.now;
311
- const cache = /* @__PURE__ */ new Map();
312
- return async (args) => {
313
- const at = now();
314
- const entry = cache.get(args.jti);
315
- if (entry && entry.expiresAt > at) {
316
- if (entry.result) return entry.result;
317
- if (entry.promise) return await entry.promise;
318
- }
319
- const promise = inner(args).then((result) => {
320
- pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
321
- cache.set(args.jti, { result, expiresAt: now() + ttlMs });
322
- return result;
323
- }).catch((err) => {
324
- cache.delete(args.jti);
325
- throw err;
326
- });
327
- cache.set(args.jti, { promise, expiresAt: at + ttlMs });
328
- return await promise;
329
- };
330
- }
331
- function createCachedTargetClient(inner, options = {}) {
332
- const ttlMs = options.ttlMs ?? 5e3;
333
- const now = options.now ?? Date.now;
334
- const cache = /* @__PURE__ */ new Map();
335
- return async (args) => {
336
- const key = `${args.workspaceId}\0${args.hostId}`;
337
- const at = now();
338
- const entry = cache.get(key);
339
- if (entry && entry.expiresAt > at) {
340
- if (entry.result) return entry.result;
341
- if (entry.promise) return await entry.promise;
342
- }
343
- const promise = inner(args).then((result) => {
344
- pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
345
- if (result) cache.set(key, { result, expiresAt: now() + ttlMs });
346
- else cache.delete(key);
347
- return result;
348
- }).catch((err) => {
349
- cache.delete(key);
350
- throw err;
351
- });
352
- cache.set(key, { promise, expiresAt: at + ttlMs });
353
- return await promise;
354
- };
699
+ if (!result) {
700
+ return { ok: false, retryable: false, code: "host_enrollment_unknown", reason: "Host enrollment is unknown" };
701
+ }
702
+ if (result.revoked) {
703
+ return { ok: false, retryable: false, code: "host_enrollment_revoked", reason: "Host enrollment was revoked" };
704
+ }
705
+ if (result.generation > claims.generation) {
706
+ return { ok: false, retryable: false, code: "host_generation_superseded", reason: "Host tunnel generation was superseded" };
707
+ }
708
+ if (result.generation < claims.generation) {
709
+ return { ok: false, retryable: false, code: "host_generation_unknown", reason: "Host tunnel generation is ahead of the control plane" };
710
+ }
711
+ return { ok: true };
355
712
  }
356
713
  function protocolToken(header) {
357
714
  return header?.split(",").map((item) => item.trim()).find((item) => item.startsWith("claxedo-rat."))?.slice("claxedo-rat.".length);
@@ -389,53 +746,38 @@ function isDangerousInboundHeader(name) {
389
746
  return false;
390
747
  }
391
748
  function forwardHeaders(input, relayHostToken, workspaceId, options = {}) {
392
- const headers2 = new Headers(input);
393
- headers2.delete("host");
749
+ const headers = new Headers(input);
750
+ headers.delete("host");
394
751
  const inboundNames = /* @__PURE__ */ new Set();
395
- headers2.forEach((_value, name) => inboundNames.add(name));
752
+ headers.forEach((_value, name) => inboundNames.add(name));
396
753
  for (const name of inboundNames) {
397
- if (isDangerousInboundHeader(name)) headers2.delete(name);
398
- }
399
- if (options.userHosted) headers2.delete("cookie");
400
- headers2.set("accept-encoding", "identity");
401
- headers2.delete("authorization");
402
- headers2.delete("Authorization");
403
- headers2.set("Authorization", `Bearer ${relayHostToken}`);
404
- headers2.set("x-workspace-id", workspaceId);
405
- headers2.set("X-Daytona-Skip-Preview-Warning", "true");
406
- headers2.set("X-Daytona-Skip-Last-Activity-Update", "true");
407
- for (const [name, value] of Object.entries(options.upstreamHeaders ?? {})) {
408
- const trimmed = value.trim();
409
- if (trimmed) headers2.set(name, trimmed);
410
- }
411
- headers2.set("x-forwarded-by", "workspace-relay");
412
- return headers2;
754
+ if (isDangerousInboundHeader(name)) headers.delete(name);
755
+ }
756
+ if (options.hostTunnel) headers.delete("cookie");
757
+ headers.set("accept-encoding", "identity");
758
+ headers.delete("authorization");
759
+ headers.delete("Authorization");
760
+ headers.set("Authorization", `Bearer ${relayHostToken}`);
761
+ headers.set("x-workspace-id", workspaceId);
762
+ headers.set("x-forwarded-by", "workspace-relay");
763
+ return headers;
413
764
  }
414
765
  function targetUrl(target, path, search) {
415
- const url = new URL(path.replace(/^\/+/, ""), target.baseUrl.endsWith("/") ? target.baseUrl : `${target.baseUrl}/`);
766
+ const base = new URL(target.baseUrl.endsWith("/") ? target.baseUrl : `${target.baseUrl}/`);
767
+ const url = new URL(base);
768
+ url.pathname = `${base.pathname}${path.replace(/^\/+/, "")}`;
416
769
  url.search = search;
770
+ url.hash = "";
417
771
  return url;
418
772
  }
419
- var RELAY_VIEWER_DENIED_PATH = /^\/api\/wr\/pty(?:\/|$)/;
420
773
  function relayRuntimePath(request) {
421
- return new URL(request.url).pathname.replace(/^\/workspaces\/[^/]+\/?/, "/");
422
- }
423
- function relayAuthorizationPath(path) {
424
774
  try {
425
- return decodeURIComponent(path);
775
+ const decoded = decodeURIComponent(new URL(request.url).pathname.replace(/^\/workspaces\/[^/]+\/?/, "/")).replace(/^\/+/, "/");
776
+ return new URL(decoded.replace(/^\/+/, ""), "http://relay.invalid/").pathname;
426
777
  } catch {
427
778
  return void 0;
428
779
  }
429
780
  }
430
- function roleAllowsRelayRequest(role, method, path) {
431
- if (role === "owner" || role === "admin" || role === "editor") return true;
432
- if (role === "viewer") {
433
- const authorizationPath = relayAuthorizationPath(path);
434
- if (!authorizationPath || RELAY_VIEWER_DENIED_PATH.test(authorizationPath)) return false;
435
- return method === "GET" || method === "HEAD" || method === "OPTIONS";
436
- }
437
- return false;
438
- }
439
781
  function relayHostTokenCacheTtlMs(options, claims) {
440
782
  const configured = options.relayHostTokenCacheTtlMs ?? 3e4;
441
783
  if (!Number.isFinite(configured) || configured <= 0) return 0;
@@ -445,12 +787,13 @@ function relayHostTokenCacheTtlMs(options, claims) {
445
787
  function relayHostTokenCacheKey(options, claims, target) {
446
788
  return [
447
789
  claims.jti,
448
- claims.sub,
790
+ claims.principal_kind,
791
+ claims.actor_id,
792
+ claims.actor_kind,
449
793
  claims.org_id,
450
794
  claims.role,
451
795
  claims.workspace_id,
452
796
  claims.host_id,
453
- target.access,
454
797
  target.backing,
455
798
  options.relayHostMintKid ?? ""
456
799
  ].join("\0");
@@ -529,6 +872,13 @@ function pruneRelayHostTokenCache(cache, now) {
529
872
  cache.delete(key);
530
873
  }
531
874
  }
875
+ function relayHostMintInput(options, claims, target) {
876
+ return {
877
+ ...relayHostInputFromRuntimeClaims(claims),
878
+ ...target,
879
+ ...options.relayHostMintKid ? { kid: options.relayHostMintKid } : {}
880
+ };
881
+ }
532
882
  async function relayHostTokenFor(options, claims, target, trace) {
533
883
  const ttlMs = relayHostTokenCacheTtlMs(options, claims);
534
884
  if (ttlMs <= 0) {
@@ -549,13 +899,11 @@ async function relayHostTokenFor(options, claims, target, trace) {
549
899
  let promise;
550
900
  try {
551
901
  const token = await trace.span("rht-mint", async () => {
552
- promise = mintRelayHostToken({
553
- subject: claims.sub,
554
- orgId: claims.org_id,
555
- role: claims.role,
556
- ...target,
557
- ...options.relayHostMintKid ? { kid: options.relayHostMintKid } : {}
558
- }, options.relayHostSigningKey, options.relayHostAlgorithm);
902
+ promise = mintRelayHostToken(
903
+ relayHostMintInput(options, claims, target),
904
+ options.relayHostSigningKey,
905
+ options.relayHostAlgorithm
906
+ );
559
907
  pruneRelayHostTokenCache(cache, now);
560
908
  cache.set(key, { promise, expiresAt: now + ttlMs });
561
909
  return await promise;
@@ -573,13 +921,11 @@ async function relayHostTokenFor(options, claims, target, trace) {
573
921
  async function uncachedRelayHostTokenFor(options, claims, target, trace) {
574
922
  return await trace.span(
575
923
  "rht-mint",
576
- async () => await mintRelayHostToken({
577
- subject: claims.sub,
578
- orgId: claims.org_id,
579
- role: claims.role,
580
- ...target,
581
- ...options.relayHostMintKid ? { kid: options.relayHostMintKid } : {}
582
- }, options.relayHostSigningKey, options.relayHostAlgorithm)
924
+ async () => await mintRelayHostToken(
925
+ relayHostMintInput(options, claims, target),
926
+ options.relayHostSigningKey,
927
+ options.relayHostAlgorithm
928
+ )
583
929
  );
584
930
  }
585
931
  function workspaceRelayTargetUrl(target, path, search) {
@@ -600,146 +946,22 @@ function workspaceRelayForwardRequestInit(request, relayHostToken, workspaceId,
600
946
  if (request.method === "GET" || request.method === "HEAD") delete init.body;
601
947
  return init;
602
948
  }
603
- var auditSamplers = /* @__PURE__ */ new WeakMap();
604
- function getOrCreateAuditSampler(options) {
605
- let state = auditSamplers.get(options);
606
- if (!state) {
607
- const sampleRate = clampSampleRate(options.auditAcceptSampleRate);
608
- state = {
609
- suppressedCount: 0,
610
- timer: void 0,
611
- flushIntervalMs: options.auditFlushIntervalMs ?? 6e4,
612
- random: options.random ?? Math.random,
613
- sampleRate
614
- };
615
- auditSamplers.set(options, state);
616
- }
617
- return state;
618
- }
619
- function clampSampleRate(rate) {
620
- if (rate === void 0) return 1;
621
- if (!Number.isFinite(rate)) return 1;
622
- if (rate < 0) return 0;
623
- if (rate > 1) return 1;
624
- return rate;
625
- }
626
- function ensureFlushTimer(options, state) {
627
- if (state.timer) return;
628
- if (state.sampleRate >= 1) return;
629
- if (state.flushIntervalMs <= 0) return;
630
- const timer = setInterval(() => {
631
- flushSuppressedSummary(options, state);
632
- }, state.flushIntervalMs);
633
- if (typeof timer.unref === "function") {
634
- timer.unref();
635
- }
636
- state.timer = timer;
637
- }
638
- function flushSuppressedSummary(options, state) {
639
- const count = state.suppressedCount;
640
- if (count <= 0) return;
641
- state.suppressedCount = 0;
642
- void options.audit?.({
643
- action: "relay.request.suppressed_summary",
644
- result: "allow",
645
- method: "INTERNAL",
646
- path: "/",
647
- suppressedCount: count
648
- });
649
- }
650
- function disposeAuditSampler(options) {
651
- const state = auditSamplers.get(options);
652
- if (!state) return;
653
- if (state.timer) {
654
- clearInterval(state.timer);
655
- state.timer = void 0;
656
- }
657
- flushSuppressedSummary(options, state);
658
- auditSamplers.delete(options);
659
- }
660
- function createWorkspaceRelayTrace() {
661
- const phases = [];
662
- return {
663
- phases,
664
- async span(name, run) {
665
- const startedAt = performance.now();
666
- try {
667
- return await run();
668
- } finally {
669
- phases.push({ name, ms: performance.now() - startedAt });
670
- }
671
- }
672
- };
673
- }
674
- function roundedMs(value) {
675
- return Math.round(value * 100) / 100;
676
- }
677
- function workspaceRelayServerTiming(trace) {
678
- return trace.phases.map((phase) => `${phase.name};dur=${roundedMs(phase.ms)}`).join(", ");
679
- }
680
- function workspaceRelayTimingResponse(response, trace) {
681
- const value = workspaceRelayServerTiming(trace);
682
- if (!value) return response;
683
- const headers2 = new Headers(response.headers);
684
- const prev = headers2.get("server-timing");
685
- headers2.set("server-timing", prev ? `${prev}, ${value}` : value);
686
- return new Response(response.body, {
687
- status: response.status,
688
- statusText: response.statusText,
689
- headers: headers2
690
- });
691
- }
692
- async function audit(options, event) {
693
- if (event.action === "relay.request.accepted") {
694
- const state = getOrCreateAuditSampler(options);
695
- if (state.sampleRate < 1) {
696
- ensureFlushTimer(options, state);
697
- if (state.random() >= state.sampleRate) {
698
- state.suppressedCount += 1;
699
- return;
700
- }
701
- }
702
- }
703
- await options.audit?.(event);
704
- }
705
- var defaultOriginMatcher = createOriginMatcher(DEFAULT_RELAY_APP_ORIGINS);
706
- var originMatchers = /* @__PURE__ */ new WeakMap();
707
- function originMatcherFor(options) {
708
- if (!options.allowedOrigins) return defaultOriginMatcher;
709
- let matcher = originMatchers.get(options);
710
- if (!matcher) {
711
- matcher = createOriginMatcher(options.allowedOrigins);
712
- originMatchers.set(options, matcher);
713
- }
714
- return matcher;
715
- }
716
- function denyCorsHeaders(request, originAllowed = defaultOriginMatcher) {
717
- const origin = request.headers.get("origin");
718
- if (!origin) return void 0;
719
- if (!originAllowed(origin)) return void 0;
720
- return {
721
- "access-control-allow-origin": origin,
722
- "access-control-allow-headers": "Accept, Authorization, Content-Type, Last-Event-ID, X-Fetch-Bypass-Throttle, X-Daytona-Skip-Preview-Warning, X-Workspace-Id, X-OpenCode-Directory, X-Claxedo-Runner, X-Claxedo-Model, X-Claxedo-Draft-Id, X-Claxedo-Binary",
723
- "access-control-allow-methods": "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS"
724
- };
725
- }
726
949
  async function deny(options, input) {
727
950
  const url = new URL(input.request.url);
728
951
  await audit(options, {
729
952
  action: "relay.request.denied",
730
953
  result: "deny",
731
954
  reason: input.code,
732
- subject: input.claims?.sub,
955
+ actorId: input.claims?.actor_id,
956
+ actorKind: input.claims?.actor_kind,
957
+ principalKind: input.claims?.principal_kind,
733
958
  ...input.claims ? { role: input.claims.role } : {},
734
959
  workspaceId: input.claims?.workspace_id,
735
960
  hostId: input.claims?.host_id,
736
961
  method: input.request.method,
737
962
  path: url.pathname
738
963
  });
739
- return Response.json(errorBody(input.code, input.message), {
740
- status: input.status,
741
- headers: denyCorsHeaders(input.request, originMatcherFor(options))
742
- });
964
+ return Response.json(errorBody(input.code, input.message), { status: input.status });
743
965
  }
744
966
  async function authorizeWorkspaceRelayRequest(options, request, workspaceId, trace) {
745
967
  const token = runtimeAccessToken(request);
@@ -761,6 +983,8 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
761
983
  "rat-verify",
762
984
  async () => await cachedRuntimeAccessTokenClaims(options, token, workspaceId)
763
985
  );
986
+ const targetLookup = span("target-resolve", async () => await options.resolveTarget(claims));
987
+ targetLookup.catch(() => void 0);
764
988
  const active = await span("rat-active", async () => {
765
989
  return await options.isRuntimeAccessTokenActive?.(claims) ?? { active: true };
766
990
  });
@@ -778,21 +1002,21 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
778
1002
  };
779
1003
  }
780
1004
  const path = relayRuntimePath(request);
781
- if (!roleAllowsRelayRequest(claims.role, request.method, path)) {
1005
+ if (!path || !sessionScopeReaches(claims.session_id, path, new URL(request.url).search)) {
782
1006
  return {
783
1007
  ok: false,
784
- code: "relay_role_denied",
1008
+ code: "relay_scope_denied",
785
1009
  response: await deny(options, {
786
- code: "relay_role_denied",
787
- message: "Workspace role does not allow this relay request",
1010
+ code: "relay_scope_denied",
1011
+ message: "Runtime Access Token scope does not reach this relay request",
788
1012
  status: 403,
789
1013
  claims,
790
1014
  request
791
1015
  })
792
1016
  };
793
1017
  }
794
- const target = await span("target-resolve", async () => await options.resolveTarget(claims));
795
- if (!target || target.workspaceId !== claims.workspace_id || target.hostId !== claims.host_id) {
1018
+ const target = await targetLookup;
1019
+ if (!target || target.workspaceId !== claims.workspace_id || target.hostId !== claims.host_id || !isAllowedRelayTargetBaseUrl(target)) {
796
1020
  return {
797
1021
  ok: false,
798
1022
  code: "relay_target_unavailable",
@@ -805,13 +1029,13 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
805
1029
  })
806
1030
  };
807
1031
  }
808
- if (target.access === "user-hosted" && !options.directory?.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
1032
+ if (isHostTunnelTarget(target) && !options.directory?.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
809
1033
  return {
810
1034
  ok: false,
811
- code: "user_hosted_app_offline",
1035
+ code: "host_tunnel_offline",
812
1036
  response: await deny(options, {
813
- code: "user_hosted_app_offline",
814
- message: "User-hosted workspace is offline",
1037
+ code: "host_tunnel_offline",
1038
+ message: "The machine serving this workspace is offline",
815
1039
  status: 503,
816
1040
  claims,
817
1041
  request
@@ -821,7 +1045,9 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
821
1045
  await span("audit", async () => await audit(options, {
822
1046
  action: "relay.request.accepted",
823
1047
  result: "allow",
824
- subject: claims.sub,
1048
+ actorId: claims.actor_id,
1049
+ actorKind: claims.actor_kind,
1050
+ principalKind: claims.principal_kind,
825
1051
  role: claims.role,
826
1052
  workspaceId: claims.workspace_id,
827
1053
  hostId: claims.host_id,
@@ -852,1431 +1078,25 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
852
1078
  };
853
1079
  }
854
1080
  }
855
- async function forwardWorkspaceRelayRequest(request, target, relayHostToken, path, requestFetch, timeoutMs, trace, originAllowed = defaultOriginMatcher) {
856
- const controller = new AbortController();
857
- const timer = setTimeout(() => controller.abort(), timeoutMs);
858
- const span = (name, run) => trace ? trace.span(name, run) : run();
859
- try {
860
- const upstream = await span("upstream-fetch", async () => await requestFetch(
861
- targetUrl(target, path, new URL(request.url).search),
862
- workspaceRelayForwardRequestInit(request, relayHostToken, target.workspaceId, {
863
- signal: controller.signal,
864
- upstreamHeaders: target.upstreamHeaders
865
- })
866
- ));
867
- const headers2 = new Headers(upstream.headers);
868
- headers2.delete("access-control-allow-origin");
869
- headers2.delete("access-control-allow-credentials");
870
- headers2.delete("access-control-allow-headers");
871
- headers2.delete("access-control-allow-methods");
872
- headers2.delete("access-control-expose-headers");
873
- headers2.delete("access-control-max-age");
874
- for (const [key, value] of Object.entries(denyCorsHeaders(request, originAllowed) ?? {})) {
875
- headers2.set(key, value);
876
- }
877
- return new Response(upstream.body, {
878
- status: upstream.status,
879
- statusText: upstream.statusText,
880
- headers: headers2
881
- });
882
- } catch (err) {
883
- const aborted = err instanceof Error && err.name === "AbortError";
884
- return Response.json(
885
- errorBody(
886
- aborted ? "upstream_timeout" : "upstream_unavailable",
887
- aborted ? "Workspace upstream timed out" : "Workspace upstream is unavailable"
888
- ),
889
- { status: aborted ? 504 : 503, headers: denyCorsHeaders(request, originAllowed) }
890
- );
891
- } finally {
892
- clearTimeout(timer);
893
- }
894
- }
895
- function getAuditSuppressedCount(options) {
896
- return auditSamplers.get(options)?.suppressedCount ?? 0;
897
- }
898
- var LOOPBACK_REMOTES = /* @__PURE__ */ new Set([
899
- "127.0.0.1",
900
- "::1",
901
- "::ffff:127.0.0.1"
902
- ]);
903
- function isLoopbackAddress(address) {
904
- if (!address) return false;
905
- const trimmed = address.trim();
906
- if (LOOPBACK_REMOTES.has(trimmed)) return true;
907
- if (/^127\./.test(trimmed)) return true;
908
- if (/^::ffff:127\./i.test(trimmed)) return true;
909
- return false;
910
- }
911
- function checkMetricsAuth(options, request) {
912
- if (options.metricsToken) {
913
- const presented = bearerToken(request.headers.get("authorization") ?? void 0);
914
- if (!presented || presented !== options.metricsToken) {
915
- return {
916
- ok: false,
917
- status: 401,
918
- code: "metrics_unauthorized",
919
- message: "Metrics endpoint requires a valid bearer token"
920
- };
921
- }
922
- return { ok: true };
923
- }
924
- if (!options.metricsRemoteAddress) {
925
- return {
926
- ok: false,
927
- status: 401,
928
- code: "metrics_unauthorized",
929
- message: "Metrics endpoint is restricted to loopback or bearer-token clients"
930
- };
931
- }
932
- const address = options.metricsRemoteAddress(request);
933
- if (isLoopbackAddress(address)) return { ok: true };
934
- return {
935
- ok: false,
936
- status: 401,
937
- code: "metrics_unauthorized",
938
- message: "Metrics endpoint is restricted to loopback or bearer-token clients"
939
- };
940
- }
941
- function buildMetricsBody(options) {
942
- const sources = options.metricsSources ?? {};
943
- const fragmentation = sources.fragmentation?.() ?? { fragmentsBuffered: 0, oversizedClosed: 0 };
944
- const slowConsumer = sources.slowConsumer?.() ?? {
945
- overflowEvents: 0,
946
- timerFired: 0,
947
- droppedRequests: 0
948
- };
949
- const activeHostCount = options.directory?.size?.() ?? 0;
950
- const suppressedAccepts = getAuditSuppressedCount(options);
951
- const body = {
952
- fragmentation,
953
- slowConsumer,
954
- directory: { activeHostCount },
955
- audit: { suppressedAccepts }
956
- };
957
- if (sources.drainPending) {
958
- body.drain = { pendingCount: sources.drainPending() };
959
- }
960
- return body;
961
- }
962
- function createWorkspaceRelay(options) {
963
- const app = new Hono();
964
- const originAllowed = originMatcherFor(options);
965
- app.use("*", cors({
966
- origin: (origin) => {
967
- if (!origin) return void 0;
968
- return originAllowed(origin) ? origin : void 0;
969
- },
970
- allowHeaders: [
971
- "Accept",
972
- "Authorization",
973
- "Content-Type",
974
- "X-Daytona-Skip-Preview-Warning",
975
- "X-Workspace-Id",
976
- "X-OpenCode-Directory",
977
- "X-Claxedo-Runner",
978
- "X-Claxedo-Model",
979
- "X-Claxedo-Draft-Id",
980
- "X-Claxedo-Binary"
981
- ],
982
- allowMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
983
- }));
984
- app.get("/health", (c) => {
985
- if (options.isDraining?.()) {
986
- return c.json(
987
- { ok: false, service: "workspace-relay", draining: true },
988
- 503
989
- );
990
- }
991
- return c.json({ ok: true, service: "workspace-relay" });
992
- });
993
- app.get("/.well-known/jwks.json", async (c) => {
994
- const sources = options.relayHostPublicKeys ?? [];
995
- if (sources.length === 0) {
996
- return c.json(
997
- {
998
- error: {
999
- code: "jwks_no_keys_configured",
1000
- message: "No Relay Host Token public keys are configured for the JWKS endpoint"
1001
- }
1002
- },
1003
- 503
1004
- );
1005
- }
1006
- let keys;
1007
- try {
1008
- keys = await Promise.all(sources.map(async (source) => {
1009
- const jwk = await exportJWK(source.publicKey);
1010
- return {
1011
- ...jwk,
1012
- kid: source.kid,
1013
- alg: "EdDSA",
1014
- use: "sig"
1015
- };
1016
- }));
1017
- } catch (err) {
1018
- return c.json(
1019
- {
1020
- error: {
1021
- code: "jwks_invalid_key",
1022
- message: "A configured RHT public key could not be exported as a JWK",
1023
- detail: err instanceof Error ? err.message : String(err)
1024
- }
1025
- },
1026
- 500
1027
- );
1028
- }
1029
- c.header("cache-control", "public, max-age=300");
1030
- return c.json({ keys });
1031
- });
1032
- app.disposeAuditSampler = () => disposeAuditSampler(options);
1033
- app.get("/metrics", (c) => {
1034
- const authResult = checkMetricsAuth(options, c.req.raw);
1035
- if (!authResult.ok) {
1036
- return c.json(
1037
- errorBody(authResult.code, authResult.message),
1038
- authResult.status
1039
- );
1040
- }
1041
- return c.json(buildMetricsBody(options));
1042
- });
1043
- app.all("/workspaces/:workspaceId/*", async (c) => {
1044
- const trace = createWorkspaceRelayTrace();
1045
- if (options.isDraining?.()) {
1046
- return Response.json(
1047
- errorBody("relay_draining", "Workspace relay is shutting down; try another instance"),
1048
- { status: 503, headers: denyCorsHeaders(c.req.raw, originAllowed) }
1049
- );
1050
- }
1051
- const response = await trace.span("relay-total", async () => {
1052
- const relay = await authorizeWorkspaceRelayRequest(options, c.req.raw, c.req.param("workspaceId"), trace);
1053
- if (!relay.ok) return relay.response;
1054
- return await forwardWorkspaceRelayRequest(
1055
- new Request(c.req.raw),
1056
- relay.request.target,
1057
- relay.request.relayHostToken,
1058
- relay.request.path,
1059
- options.fetch ?? fetch,
1060
- options.forwardTimeoutMs ?? 3e4,
1061
- trace,
1062
- originAllowed
1063
- );
1064
- });
1065
- return workspaceRelayTimingResponse(response, trace);
1066
- });
1067
- return app;
1068
- }
1069
-
1070
- // src/bun.ts
1071
- var TUNNEL_PENDING_HTTP_CAP = 32;
1072
- var TUNNEL_CHANNEL_CAP = 16;
1073
- var HOST_TUNNEL_REGISTRATION_RECONNECT_CAP = 5;
1074
- var HOST_TUNNEL_REGISTRATION_RECONNECT_WINDOW_MS = 6e4;
1075
- var WS_MAX_PAYLOAD_LENGTH_BYTES = 16 * 1024 * 1024;
1076
- var TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
1077
- var TUNNEL_HTTP_RESPONSE_TIMEOUT_MS_DEFAULT = 3e4;
1078
- var DIRECT_HTTP_TIMEOUT_MS_DEFAULT = 3e4;
1079
- var UPSTREAM_WS_OPEN_TIMEOUT_MS_DEFAULT = 1e4;
1080
- var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT = 64;
1081
- var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
1082
- var WS_BUFFERED_AMOUNT_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
1083
- var relayBufferedBytes = (socket) => {
1084
- const method = socket.getBufferedAmount;
1085
- if (typeof method === "function") {
1086
- const measured = method.call(socket);
1087
- if (typeof measured === "number" && Number.isFinite(measured)) return measured;
1088
- return void 0;
1089
- }
1090
- const property = socket.bufferedAmount;
1091
- return typeof property === "number" && Number.isFinite(property) ? property : void 0;
1092
- };
1093
- var relayOverBackpressureLimit = (socket, limitBytes) => {
1094
- const queued = relayBufferedBytes(socket);
1095
- return queued !== void 0 && queued > limitBytes;
1096
- };
1097
- var HOST_TUNNEL_MAX_MISSED_PONGS_DEFAULT = 2;
1098
- var TUNNEL_MESSAGE_BUFFER_CAP_BYTES = 4 * 1024 * 1024;
1099
- var SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT = 8 * 1024 * 1024;
1100
- var SLOW_CONSUMER_TIMEOUT_MS_DEFAULT = 3e4;
1101
- var HOST_TUNNEL_STATE_DEBOUNCE_MS_DEFAULT = 250;
1102
- function createFragmentationStats() {
1103
- return {
1104
- fragmentsBuffered: 0,
1105
- oversizedClosed: 0
1106
- };
1107
- }
1108
- function createSlowConsumerStats() {
1109
- return {
1110
- overflowEvents: 0,
1111
- timerFired: 0,
1112
- droppedRequests: 0
1113
- };
1114
- }
1115
- function readFragmentationStats(stats) {
1116
- return {
1117
- fragmentsBuffered: stats.fragmentsBuffered,
1118
- oversizedClosed: stats.oversizedClosed
1119
- };
1120
- }
1121
- function resetFragmentationStats(stats) {
1122
- stats.fragmentsBuffered = 0;
1123
- stats.oversizedClosed = 0;
1124
- }
1125
- function readSlowConsumerStats(stats) {
1126
- return { ...stats };
1127
- }
1128
- function resetSlowConsumerStats(stats) {
1129
- stats.overflowEvents = 0;
1130
- stats.timerFired = 0;
1131
- stats.droppedRequests = 0;
1132
- }
1133
- function createDirectHttpLimiter(limit) {
1134
- if (!Number.isInteger(limit) || !limit || limit <= 0) return void 0;
1135
- const max = limit;
1136
- let active = 0;
1137
- const queue = [];
1138
- function drain() {
1139
- if (active >= max) return;
1140
- const next = queue.shift();
1141
- if (!next) return;
1142
- active++;
1143
- next();
1144
- }
1145
- return {
1146
- async acquire() {
1147
- if (active < max) {
1148
- active++;
1149
- } else {
1150
- await new Promise((resolve) => queue.push(resolve));
1151
- }
1152
- let released = false;
1153
- return () => {
1154
- if (released) return;
1155
- released = true;
1156
- active = Math.max(0, active - 1);
1157
- drain();
1158
- };
1159
- }
1160
- };
1161
- }
1162
- function jsonError(code, message, status) {
1163
- return new Response(JSON.stringify({ error: { code, message } }), {
1164
- status,
1165
- headers: { "content-type": "application/json" }
1166
- });
1167
- }
1168
- function corsJsonError(request, code, message, status) {
1169
- const headers2 = relayCorsHeaders(request);
1170
- headers2.set("content-type", "application/json");
1171
- return new Response(JSON.stringify({ error: { code, message } }), { status, headers: headers2 });
1172
- }
1173
- function preOpenFrameBytes(message) {
1174
- return typeof message === "string" ? Buffer.byteLength(message) : message.byteLength;
1175
- }
1176
- function safeCloseCode(input, fallback = 1011) {
1177
- if (!Number.isInteger(input)) return fallback;
1178
- const code = input;
1179
- if (code === 1e3) return code;
1180
- if (code >= 1001 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006) return code;
1181
- if (code >= 3e3 && code <= 4999) return code;
1182
- return fallback;
1183
- }
1184
- function closeWebSocket(ws, code, reason, fallback = 1011) {
1185
- try {
1186
- ws.close(safeCloseCode(code, fallback), reason);
1187
- } catch {
1188
- try {
1189
- ws.close(fallback, reason);
1190
- } catch {
1191
- }
1192
- }
1193
- }
1194
- function upstreamCloseReason(event) {
1195
- if (event.reason.trim()) return event.reason;
1196
- if (event.code === 1e3) return "";
1197
- if (event.code === 1005 || event.code === 1006) return "Upstream WebSocket closed abnormally";
1198
- return `Upstream WebSocket closed unexpectedly with code ${event.code}`;
1199
- }
1200
- function workspaceIdFromPath(pathname) {
1201
- return /^\/workspaces\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
1202
- }
1203
- function hostIdFromTunnelPath(pathname) {
1204
- return /^\/host-tunnels\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
1205
- }
1206
- function websocketRequest(request) {
1207
- return request.headers.get("upgrade")?.toLowerCase() === "websocket";
1208
- }
1209
- function roundedMs2(input) {
1210
- return Math.round(input * 100) / 100;
1211
- }
1212
- function relayWebSocketTraceEnabled(request) {
1213
- return request.headers.get("x-claxedo-relay-ws-trace") === "1";
1214
- }
1215
- function sendRelayWebSocketTrace(ws) {
1216
- const trace = ws.data.trace;
1217
- if (!trace || trace.emitted) return;
1218
- trace.emitted = true;
1219
- ws.send(JSON.stringify({
1220
- type: "relay.trace",
1221
- wsUpstreamOpenMs: trace.upstreamOpenMs === void 0 ? void 0 : roundedMs2(trace.upstreamOpenMs),
1222
- queuedFrames: trace.queuedFrames,
1223
- maxQueuedDelayMs: roundedMs2(trace.maxQueuedDelayMs)
1224
- }));
1225
- }
1226
- function relayWebSocketPayload(input) {
1227
- if (typeof input === "string") return input;
1228
- if (input instanceof ArrayBuffer) return input;
1229
- if (ArrayBuffer.isView(input)) {
1230
- const copy = new Uint8Array(input.byteLength);
1231
- copy.set(new Uint8Array(input.buffer, input.byteOffset, input.byteLength));
1232
- return copy;
1233
- }
1234
- }
1235
- function headersRecord(headers2) {
1236
- const result = {};
1237
- headers2.forEach((value, key) => {
1238
- result[key] = value;
1239
- });
1240
- return result;
1241
- }
1242
- function headers(input) {
1243
- const result = new Headers();
1244
- for (const [key, value] of Object.entries(input)) {
1245
- result.set(key, value);
1246
- }
1247
- return result;
1248
- }
1249
- function isEventStream(input) {
1250
- return Object.entries(input).some(
1251
- ([key, value]) => key.toLowerCase() === "content-type" && value.toLowerCase().includes("text/event-stream")
1252
- );
1253
- }
1254
- var defaultRelayOriginMatcher = createOriginMatcher(DEFAULT_RELAY_APP_ORIGINS);
1255
- function allowedCorsOrigin(origin) {
1256
- if (!origin) return;
1257
- if (defaultRelayOriginMatcher(origin)) return origin;
1258
- }
1259
- function requireAllowedOrigin(request) {
1260
- const origin = request.headers.get("origin");
1261
- if (allowedCorsOrigin(origin)) return null;
1262
- return jsonError(
1263
- "origin_not_allowed",
1264
- "Origin is not in the allowlist",
1265
- 403
1266
- );
1267
- }
1268
- function relayCorsHeaders(request, input = new Headers()) {
1269
- const result = new Headers(input);
1270
- result.delete("access-control-allow-origin");
1271
- result.delete("access-control-allow-credentials");
1272
- result.delete("access-control-allow-headers");
1273
- result.delete("access-control-allow-methods");
1274
- result.delete("access-control-expose-headers");
1275
- result.delete("access-control-max-age");
1276
- const origin = allowedCorsOrigin(request.headers.get("origin"));
1277
- if (origin) {
1278
- result.set("access-control-allow-origin", origin);
1279
- result.set("access-control-allow-headers", "Accept, Authorization, Content-Type, Last-Event-ID, X-Fetch-Bypass-Throttle, X-Daytona-Skip-Preview-Warning, X-Workspace-Id, X-OpenCode-Directory, X-Claxedo-Runner, X-Claxedo-Model, X-Claxedo-Draft-Id, X-Claxedo-Binary");
1280
- result.set("access-control-allow-methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
1281
- }
1282
- return result;
1283
- }
1284
- function encoded(input) {
1285
- return Buffer.from(input).toString("base64");
1286
- }
1287
- function decoded(input) {
1288
- return new Uint8Array(Buffer.from(input, "base64"));
1289
- }
1290
- function encodedFrame(message) {
1291
- if (typeof message === "string") {
1292
- return {
1293
- binary: false,
1294
- data_base64: Buffer.from(message).toString("base64")
1295
- };
1296
- }
1297
- return {
1298
- binary: true,
1299
- data_base64: Buffer.from(message).toString("base64")
1300
- };
1301
- }
1302
- function decodedFrame(frame) {
1303
- const body = Buffer.from(frame.data_base64, "base64");
1304
- return frame.binary ? new Uint8Array(body) : body.toString("utf8");
1305
- }
1306
- function tunnelMessage(ws, input, stats) {
1307
- if (typeof input !== "string") return;
1308
- const combined = ws.data.messageBuffer.length > 0 ? ws.data.messageBuffer + input : input;
1309
- let parsed;
1310
- try {
1311
- parsed = JSON.parse(combined);
1312
- } catch {
1313
- if (combined.length > TUNNEL_MESSAGE_BUFFER_CAP_BYTES) {
1314
- stats.oversizedClosed += 1;
1315
- ws.data.messageBuffer = "";
1316
- ws.close(1009, "Tunnel message buffer exceeded 4 MB");
1317
- return;
1318
- }
1319
- stats.fragmentsBuffered += 1;
1320
- ws.data.messageBuffer = combined;
1321
- return;
1322
- }
1323
- ws.data.messageBuffer = "";
1324
- const validated = validateTunnelMessage(parsed);
1325
- if (validated.ok) return validated.message;
1326
- if (validated.reason === "protocol_mismatch") {
1327
- ws.close(1002, `Tunnel protocol mismatch: expected ${validated.expected_protocol}`);
1328
- }
1329
- return;
1330
- }
1331
- function sendTunnelPing(ws) {
1332
- if (ws.readyState !== WebSocket.OPEN) return;
1333
- ws.data.missedPongs += 1;
1334
- ws.send(JSON.stringify(makeTunnelPing()));
1335
- }
1336
- function clearPendingTimers(entry) {
1337
- clearTimeout(entry.timeout);
1338
- if (entry.slowConsumerTimeout) {
1339
- clearTimeout(entry.slowConsumerTimeout);
1340
- entry.slowConsumerTimeout = void 0;
1341
- }
1342
- }
1343
- function failPendingHttpResponse(input) {
1344
- clearPendingTimers(input.entry);
1345
- input.entry.pendingChunks.length = 0;
1346
- input.entry.bytesQueued = 0;
1347
- try {
1348
- input.entry.controller.error(input.error);
1349
- } catch {
1350
- }
1351
- if (!input.entry.responseStarted) {
1352
- if (input.response) {
1353
- input.entry.resolve(input.response);
1354
- return;
1355
- }
1356
- input.entry.reject(input.error);
1357
- }
1358
- }
1359
- function cleanupHostTunnelSocket(input) {
1360
- if (input.ws.data.heartbeat) {
1361
- clearInterval(input.ws.data.heartbeat);
1362
- input.ws.data.heartbeat = void 0;
1363
- }
1364
- for (const pending of input.ws.data.pending.values()) {
1365
- failPendingHttpResponse({
1366
- entry: pending,
1367
- response: input.request ? corsJsonError(input.request, "user_hosted_app_offline", "User-hosted workspace is offline", 503) : jsonError("user_hosted_app_offline", "User-hosted workspace is offline", 503),
1368
- error: new Error("User-hosted tunnel disconnected")
1369
- });
1370
- }
1371
- input.ws.data.pending.clear();
1372
- for (const channel of input.ws.data.channels.values()) {
1373
- if (input.closeChannels) closeWebSocket(channel, 1011, "User-hosted tunnel disconnected");
1374
- }
1375
- input.ws.data.channels.clear();
1376
- if (!input.disconnectDirectory) return;
1377
- if (input.hostTunnels.get(input.ws.data.hostId) === input.ws) {
1378
- input.hostTunnels.delete(input.ws.data.hostId);
1379
- input.options.directory?.disconnectHost(input.ws.data.hostId);
1380
- scheduleHostTunnelStateChange(input.hostTunnelStateDebounce, input.options, {
1381
- hostId: input.ws.data.hostId,
1382
- workspaceId: input.ws.data.workspaceIds[0],
1383
- path: `/host-tunnels/${input.ws.data.hostId}`,
1384
- state: "disconnected",
1385
- debounceMs: input.bunOptions.hostTunnelStateDebounceMs ?? HOST_TUNNEL_STATE_DEBOUNCE_MS_DEFAULT
1386
- });
1387
- }
1388
- }
1389
- async function audit2(options, input) {
1390
- await options.audit?.({
1391
- action: input.action,
1392
- result: "allow",
1393
- hostId: input.hostId,
1394
- workspaceId: input.workspaceId,
1395
- method: "WEBSOCKET",
1396
- path: input.path
1397
- });
1398
- }
1399
- function scheduleHostTunnelStateChange(state, options, input) {
1400
- const existing = state.get(input.hostId);
1401
- const entry = existing ?? {
1402
- intendedState: "disconnected",
1403
- lastWritten: "disconnected",
1404
- flushTimer: void 0,
1405
- pending: { workspaceId: input.workspaceId, path: input.path }
1406
- };
1407
- entry.intendedState = input.state;
1408
- entry.pending = { workspaceId: input.workspaceId, path: input.path };
1409
- if (entry.flushTimer) clearTimeout(entry.flushTimer);
1410
- entry.flushTimer = setTimeout(() => {
1411
- entry.flushTimer = void 0;
1412
- if (entry.intendedState === entry.lastWritten) {
1413
- if (entry.intendedState === "disconnected") {
1414
- state.delete(input.hostId);
1415
- }
1416
- return;
1417
- }
1418
- entry.lastWritten = entry.intendedState;
1419
- void audit2(options, {
1420
- action: entry.intendedState === "connected" ? "host_tunnel.connected" : "host_tunnel.disconnected",
1421
- hostId: input.hostId,
1422
- workspaceId: entry.pending.workspaceId,
1423
- path: entry.pending.path
1424
- });
1425
- }, input.debounceMs);
1426
- state.set(input.hostId, entry);
1427
- }
1428
- function drainPendingChunks(entry) {
1429
- while (entry.pendingChunks.length > 0) {
1430
- const desired = entry.controller.desiredSize;
1431
- if (desired !== null && desired <= 0) break;
1432
- const next = entry.pendingChunks.shift();
1433
- entry.bytesQueued -= next.byteLength;
1434
- try {
1435
- entry.controller.enqueue(next);
1436
- } catch {
1437
- entry.pendingChunks.length = 0;
1438
- entry.bytesQueued = 0;
1439
- break;
1440
- }
1441
- }
1442
- if (entry.pendingChunks.length === 0 && entry.slowConsumerTimeout) {
1443
- clearTimeout(entry.slowConsumerTimeout);
1444
- entry.slowConsumerTimeout = void 0;
1445
- }
1446
- }
1447
- function enqueueChunkWithBackpressure(input) {
1448
- const { entry, chunk } = input;
1449
- const desired = entry.controller.desiredSize;
1450
- const overflowing = desired !== null && desired <= 0;
1451
- if (overflowing || entry.pendingChunks.length > 0) {
1452
- entry.pendingChunks.push(chunk);
1453
- entry.bytesQueued += chunk.byteLength;
1454
- if (!entry.slowConsumerTimeout) {
1455
- input.slowConsumerStats.overflowEvents += 1;
1456
- entry.slowConsumerTimeout = setTimeout(() => {
1457
- input.slowConsumerStats.timerFired += 1;
1458
- const error = new Error("slow_consumer_timeout: downstream consumer did not drain in time");
1459
- try {
1460
- entry.controller.error(error);
1461
- } catch {
1462
- }
1463
- clearTimeout(entry.timeout);
1464
- entry.pendingChunks.length = 0;
1465
- entry.bytesQueued = 0;
1466
- if (!entry.responseStarted) {
1467
- entry.resolve(jsonError("slow_consumer_timeout", "Downstream consumer did not drain in time", 503));
1468
- }
1469
- input.ws.data.pending.delete(input.requestId);
1470
- input.slowConsumerStats.droppedRequests += 1;
1471
- }, input.slowConsumerTimeoutMs);
1472
- }
1473
- return;
1474
- }
1475
- try {
1476
- entry.controller.enqueue(chunk);
1477
- } catch {
1478
- }
1479
- }
1480
- var __slowConsumerInternalsForTest = {
1481
- enqueueChunkWithBackpressure,
1482
- createSlowConsumerStats
1483
- };
1484
- async function tunnelHttpRequest(input) {
1485
- if (input.ws.readyState !== WebSocket.OPEN) {
1486
- return new Response("User-hosted workspace is offline", { status: 503 });
1487
- }
1488
- if (input.ws.data.pending.size >= TUNNEL_PENDING_HTTP_CAP) {
1489
- return jsonError(
1490
- "too_many_in_flight",
1491
- "User-hosted tunnel has too many in-flight HTTP requests",
1492
- 429
1493
- );
1494
- }
1495
- const requestId = crypto.randomUUID();
1496
- let controller;
1497
- const stream = new ReadableStream({
1498
- start(next) {
1499
- controller = next;
1500
- },
1501
- pull() {
1502
- const entry = input.ws.data.pending.get(requestId);
1503
- if (!entry) return;
1504
- drainPendingChunks(entry);
1505
- },
1506
- cancel() {
1507
- const entry = input.ws.data.pending.get(requestId);
1508
- if (!entry) return;
1509
- input.ws.data.pending.delete(requestId);
1510
- clearPendingTimers(entry);
1511
- entry.pendingChunks.length = 0;
1512
- entry.bytesQueued = 0;
1513
- if (input.ws.readyState === WebSocket.OPEN) {
1514
- input.ws.send(JSON.stringify({
1515
- type: "http.response.flow",
1516
- protocol: TUNNEL_PROTOCOL_VERSION,
1517
- request_id: requestId,
1518
- paused: false,
1519
- reason: "closed"
1520
- }));
1521
- }
1522
- }
1523
- }, new ByteLengthQueuingStrategy({ highWaterMark: input.slowConsumerHighWaterMarkBytes }));
1524
- const pending = new Promise((resolve, reject) => {
1525
- const timeout = setTimeout(() => {
1526
- const entry = input.ws.data.pending.get(requestId);
1527
- if (!entry) return;
1528
- input.ws.data.pending.delete(requestId);
1529
- if (entry.responseStarted) {
1530
- clearPendingTimers(entry);
1531
- entry.pendingChunks.length = 0;
1532
- entry.bytesQueued = 0;
1533
- try {
1534
- entry.controller.close();
1535
- } catch {
1536
- }
1537
- return;
1538
- }
1539
- const error = new Error("User-hosted tunnel response timed out");
1540
- failPendingHttpResponse({
1541
- entry,
1542
- response: corsJsonError(input.request, "user_hosted_tunnel_timeout", "User-hosted tunnel response timed out", 504),
1543
- error
1544
- });
1545
- }, input.responseTimeoutMs);
1546
- input.ws.data.pending.set(requestId, {
1547
- controller,
1548
- stream,
1549
- resolve,
1550
- reject,
1551
- timeout,
1552
- pendingChunks: [],
1553
- bytesQueued: 0,
1554
- responseStarted: false
1555
- });
1556
- });
1557
- const body = input.request.method === "GET" || input.request.method === "HEAD" ? void 0 : await readBoundedBody(input.request, input.requestBodyMaxBytes);
1558
- if (body && "response" in body) {
1559
- const entry = input.ws.data.pending.get(requestId);
1560
- if (entry) clearPendingTimers(entry);
1561
- input.ws.data.pending.delete(requestId);
1562
- return body.response;
1563
- }
1564
- input.ws.send(JSON.stringify({
1565
- type: "http.request",
1566
- protocol: TUNNEL_PROTOCOL_VERSION,
1567
- request_id: requestId,
1568
- workspace_id: input.workspaceId,
1569
- method: input.request.method,
1570
- path: input.path,
1571
- headers: headersRecord(workspaceRelayForwardHeaders(
1572
- input.request.headers,
1573
- input.relayHostToken,
1574
- input.workspaceId,
1575
- // T26: user-hosted tunnel — strip Cookie to avoid leaking browser
1576
- // cookies to the host process running on the user's laptop.
1577
- { userHosted: true }
1578
- )),
1579
- ...body?.bodyBase64 ? { body_base64: body.bodyBase64 } : {},
1580
- end: true
1581
- }));
1582
- return await pending;
1583
- }
1584
- async function readBoundedBody(request, maxBytes) {
1585
- if (!request.body) return void 0;
1586
- const reader = request.body.getReader();
1587
- const chunks = [];
1588
- let total = 0;
1589
- for (; ; ) {
1590
- const next = await reader.read();
1591
- if (next.done) break;
1592
- total += next.value.byteLength;
1593
- if (total > maxBytes) {
1594
- try {
1595
- await reader.cancel();
1596
- } catch {
1597
- }
1598
- return {
1599
- response: corsJsonError(request, "request_body_too_large", "Tunnel request body exceeds the relay limit", 413)
1600
- };
1601
- }
1602
- chunks.push(next.value);
1603
- }
1604
- const body = new Uint8Array(total);
1605
- chunks.reduce((offset, chunk) => {
1606
- body.set(chunk, offset);
1607
- return offset + chunk.byteLength;
1608
- }, 0);
1609
- return { bodyBase64: encoded(body.buffer) };
1610
- }
1611
- async function directHttpRequest(input) {
1612
- const controller = new AbortController();
1613
- const timer = setTimeout(() => controller.abort(), input.timeoutMs);
1614
- const span = (name, run) => input.trace ? input.trace.span(name, run) : run();
1615
- try {
1616
- const init = workspaceRelayForwardRequestInit(
1617
- input.request,
1618
- input.relayHostToken,
1619
- input.workspaceId,
1620
- {
1621
- // T26: cloud-vm direct path. Cookies pass through (default) — workspace
1622
- // dashboards in the VM may legitimately need session cookies.
1623
- userHosted: false,
1624
- upstreamHeaders: input.upstreamHeaders,
1625
- signal: controller.signal
1626
- }
1627
- );
1628
- if (input.request.method !== "GET" && input.request.method !== "HEAD" && input.request.body) {
1629
- init.body = await input.request.arrayBuffer();
1630
- }
1631
- const limiter = input.limiter;
1632
- const release = limiter ? await span("direct-http-queue", () => limiter.acquire()) : void 0;
1633
- try {
1634
- const upstream = await span("upstream-fetch", async () => await fetch(input.targetUrl, init));
1635
- return new Response(upstream.body, {
1636
- status: upstream.status,
1637
- statusText: upstream.statusText,
1638
- headers: relayCorsHeaders(input.request, upstream.headers)
1639
- });
1640
- } finally {
1641
- release?.();
1642
- }
1643
- } catch (err) {
1644
- const aborted = err instanceof Error && err.name === "AbortError";
1645
- return corsJsonError(
1646
- input.request,
1647
- aborted ? "upstream_timeout" : "upstream_unavailable",
1648
- aborted ? "Workspace upstream timed out" : "Workspace upstream is unavailable",
1649
- aborted ? 504 : 503
1650
- );
1651
- } finally {
1652
- clearTimeout(timer);
1653
- }
1654
- }
1655
- function hostTunnel(hostTunnels, hostId) {
1656
- const tunnel = hostTunnels.get(hostId);
1657
- if (tunnel?.readyState === WebSocket.OPEN) return tunnel;
1658
- return void 0;
1659
- }
1660
- async function authorizeHostTunnel(options, bunOptions, request, input) {
1661
- if (bunOptions.authorizeHostTunnel) return await bunOptions.authorizeHostTunnel(request, input);
1662
- const token = bearerToken(request.headers.get("authorization"));
1663
- if (!token) return false;
1664
- try {
1665
- await verifyHostTunnelToken(token, options.runtimeAccessKey, input);
1666
- return true;
1667
- } catch (err) {
1668
- if (err instanceof WorkspaceRelayAuthError) return false;
1669
- throw err;
1670
- }
1671
- }
1672
- function createWorkspaceRelayBun(options, bunOptions = {}) {
1673
- const hostTunnels = /* @__PURE__ */ new Map();
1674
- const relayClients = /* @__PURE__ */ new Set();
1675
- const hostTunnelRegistrations = /* @__PURE__ */ new Map();
1676
- const directHttpLimiter = createDirectHttpLimiter(bunOptions.directHttpConcurrency);
1677
- const hostTunnelStateDebounce = /* @__PURE__ */ new Map();
1678
- const fragmentationStats = createFragmentationStats();
1679
- const slowConsumerStats = createSlowConsumerStats();
1680
- const telemetry = {
1681
- getFragmentationStats: () => readFragmentationStats(fragmentationStats),
1682
- resetFragmentationStats: () => resetFragmentationStats(fragmentationStats),
1683
- getSlowConsumerStats: () => readSlowConsumerStats(slowConsumerStats),
1684
- resetSlowConsumerStats: () => resetSlowConsumerStats(slowConsumerStats)
1685
- };
1686
- let draining = false;
1687
- const drainController = {
1688
- isDraining: () => draining,
1689
- setDraining: (value) => {
1690
- draining = value;
1691
- if (!value) return;
1692
- for (const tunnel of hostTunnels.values()) {
1693
- closeWebSocket(tunnel, 1012, "Workspace relay is draining", 1012);
1694
- }
1695
- for (const client of relayClients) {
1696
- closeWebSocket(client, 1012, "Workspace relay is draining", 1012);
1697
- }
1698
- },
1699
- pendingCount: () => {
1700
- let total = 0;
1701
- for (const tunnel of hostTunnels.values()) {
1702
- total += tunnel.data.pending.size;
1703
- }
1704
- return total;
1705
- },
1706
- waitForDrain: async (timeoutMs) => {
1707
- const deadline = Date.now() + Math.max(0, timeoutMs);
1708
- const pollMs = 50;
1709
- while (true) {
1710
- const remaining = drainController.pendingCount();
1711
- if (remaining === 0) return { drained: true, remaining: 0 };
1712
- const now = Date.now();
1713
- if (now >= deadline) return { drained: false, remaining };
1714
- const sleep = Math.min(pollMs, Math.max(1, deadline - now));
1715
- await new Promise((resolve) => setTimeout(resolve, sleep));
1716
- }
1717
- }
1718
- };
1719
- const mergedMetricsSources = {
1720
- fragmentation: options.metricsSources?.fragmentation ?? telemetry.getFragmentationStats,
1721
- slowConsumer: options.metricsSources?.slowConsumer ?? telemetry.getSlowConsumerStats,
1722
- drainPending: options.metricsSources?.drainPending ?? drainController.pendingCount
1723
- };
1724
- const requestRemoteAddress = /* @__PURE__ */ new WeakMap();
1725
- const metricsRemoteAddressResolver = options.metricsRemoteAddress ?? ((request) => requestRemoteAddress.get(request));
1726
- const app = createWorkspaceRelay({
1727
- ...options,
1728
- isDraining: drainController.isDraining,
1729
- metricsSources: mergedMetricsSources,
1730
- metricsRemoteAddress: metricsRemoteAddressResolver
1731
- });
1732
- function getRegistrationTracker(hostId) {
1733
- let tracker = hostTunnelRegistrations.get(hostId);
1734
- if (!tracker) {
1735
- tracker = { recent: [] };
1736
- hostTunnelRegistrations.set(hostId, tracker);
1737
- }
1738
- return tracker;
1739
- }
1740
- function pruneReconnects(tracker, now) {
1741
- const cutoff = now - HOST_TUNNEL_REGISTRATION_RECONNECT_WINDOW_MS;
1742
- while (tracker.recent.length && tracker.recent[0] < cutoff) {
1743
- tracker.recent.shift();
1744
- }
1745
- }
1746
- return {
1747
- async fetch(request, server) {
1748
- const url = new URL(request.url);
1749
- const workspaceId = workspaceIdFromPath(url.pathname);
1750
- const hostId = hostIdFromTunnelPath(url.pathname);
1751
- try {
1752
- const ip = server.requestIP(request)?.address;
1753
- if (ip) requestRemoteAddress.set(request, ip);
1754
- } catch {
1755
- }
1756
- if (url.pathname === "/health") {
1757
- return Response.json(
1758
- draining ? { ok: false, service: "workspace-relay", draining: true } : { ok: true, service: "workspace-relay" },
1759
- { status: draining ? 503 : 200 }
1760
- );
1761
- }
1762
- if (draining && websocketRequest(request) && hostId) {
1763
- return jsonError(
1764
- "relay_draining",
1765
- "Workspace relay is shutting down; new tunnel registrations are not accepted",
1766
- 503
1767
- );
1768
- }
1769
- if (draining && workspaceId) {
1770
- const headers2 = relayCorsHeaders(request);
1771
- const body = JSON.stringify({
1772
- error: {
1773
- code: "relay_draining",
1774
- message: "Workspace relay is shutting down; try another instance"
1775
- }
1776
- });
1777
- headers2.set("content-type", "application/json");
1778
- return new Response(body, { status: 503, headers: headers2 });
1779
- }
1780
- if (websocketRequest(request) && hostId) {
1781
- const workspaceIds = url.searchParams.getAll("workspaceId");
1782
- if (!workspaceIds.length || !options.directory) {
1783
- return new Response("Host tunnel registration is unavailable", { status: 503 });
1784
- }
1785
- if (!await authorizeHostTunnel(options, bunOptions, request, { hostId, workspaceIds })) {
1786
- return new Response("Host tunnel registration denied", { status: 403 });
1787
- }
1788
- const tracker = getRegistrationTracker(hostId);
1789
- const now = Date.now();
1790
- pruneReconnects(tracker, now);
1791
- if (tracker.recent.length >= HOST_TUNNEL_REGISTRATION_RECONNECT_CAP) {
1792
- return jsonError(
1793
- "too_many_host_tunnel_reconnects",
1794
- "Too many host-tunnel reconnects for this host within the last 60 seconds",
1795
- 429
1796
- );
1797
- }
1798
- if (server.upgrade(request, {
1799
- data: {
1800
- kind: "host-tunnel",
1801
- hostId,
1802
- workspaceIds,
1803
- pending: /* @__PURE__ */ new Map(),
1804
- channels: /* @__PURE__ */ new Map(),
1805
- missedPongs: 0,
1806
- messageBuffer: ""
1807
- }
1808
- })) {
1809
- tracker.recent.push(now);
1810
- return;
1811
- }
1812
- return new Response("WebSocket upgrade failed", { status: 400 });
1813
- }
1814
- if (!workspaceId) return app.request(request);
1815
- if (request.method === "OPTIONS") {
1816
- return new Response(null, {
1817
- status: 204,
1818
- headers: relayCorsHeaders(request)
1819
- });
1820
- }
1821
- if (!websocketRequest(request)) {
1822
- const trace = createWorkspaceRelayTrace();
1823
- const response = await trace.span("relay-total", async () => {
1824
- const relay2 = await authorizeWorkspaceRelayRequest(options, request, workspaceId, trace);
1825
- if (!relay2.ok) return relay2.response;
1826
- if (relay2.request.target.access === "user-hosted") {
1827
- const tunnel = hostTunnel(hostTunnels, relay2.request.target.hostId);
1828
- return tunnel ? await trace.span("tunnel-http", async () => await tunnelHttpRequest({
1829
- ws: tunnel,
1830
- request,
1831
- workspaceId: relay2.request.target.workspaceId,
1832
- path: `${relay2.request.path}${url.search}`,
1833
- relayHostToken: relay2.request.relayHostToken,
1834
- slowConsumerHighWaterMarkBytes: bunOptions.slowConsumerHighWaterMarkBytes ?? SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT,
1835
- slowConsumerTimeoutMs: bunOptions.slowConsumerTimeoutMs ?? SLOW_CONSUMER_TIMEOUT_MS_DEFAULT,
1836
- slowConsumerStats,
1837
- requestBodyMaxBytes: bunOptions.tunnelRequestBodyMaxBytes ?? TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT,
1838
- responseTimeoutMs: bunOptions.tunnelHttpResponseTimeoutMs ?? TUNNEL_HTTP_RESPONSE_TIMEOUT_MS_DEFAULT
1839
- })) : new Response("User-hosted workspace is offline", { status: 503 });
1840
- }
1841
- return await directHttpRequest({
1842
- request,
1843
- targetUrl: workspaceRelayTargetUrl(
1844
- relay2.request.target,
1845
- relay2.request.path,
1846
- url.search
1847
- ).toString(),
1848
- relayHostToken: relay2.request.relayHostToken,
1849
- workspaceId: relay2.request.target.workspaceId,
1850
- upstreamHeaders: relay2.request.target.upstreamHeaders,
1851
- timeoutMs: bunOptions.directHttpTimeoutMs ?? DIRECT_HTTP_TIMEOUT_MS_DEFAULT,
1852
- limiter: directHttpLimiter,
1853
- trace
1854
- });
1855
- });
1856
- return workspaceRelayTimingResponse(response, trace);
1857
- }
1858
- const relay = await authorizeWorkspaceRelayRequest(options, request, workspaceId);
1859
- if (!relay.ok) return relay.response;
1860
- const originDenied = requireAllowedOrigin(request);
1861
- if (originDenied) return originDenied;
1862
- if (relay.request.target.access === "user-hosted") {
1863
- const tunnel = hostTunnel(hostTunnels, relay.request.target.hostId);
1864
- if (!tunnel) {
1865
- return new Response("User-hosted workspace is offline", { status: 503 });
1866
- }
1867
- if (tunnel.data.channels.size >= TUNNEL_CHANNEL_CAP) {
1868
- return jsonError(
1869
- "too_many_channels",
1870
- "User-hosted tunnel has too many active WebSocket channels",
1871
- 503
1872
- );
1873
- }
1874
- if (server.upgrade(request, {
1875
- data: {
1876
- kind: "user-hosted-client",
1877
- hostId: relay.request.target.hostId,
1878
- workspaceId: relay.request.target.workspaceId,
1879
- channelId: crypto.randomUUID(),
1880
- path: `${relay.request.path}${url.search}`,
1881
- relayHostToken: relay.request.relayHostToken
1882
- }
1883
- })) {
1884
- return;
1885
- }
1886
- return new Response("WebSocket upgrade failed", { status: 400 });
1887
- }
1888
- if (server.upgrade(request, {
1889
- data: {
1890
- kind: "client",
1891
- upstreamUrl: workspaceRelayTargetUrl(
1892
- relay.request.target,
1893
- relay.request.path,
1894
- url.search
1895
- ).toString().replace(/^http/, "ws"),
1896
- headers: headersRecord(workspaceRelayForwardHeaders(
1897
- new Headers(),
1898
- relay.request.relayHostToken,
1899
- relay.request.target.workspaceId,
1900
- // T26: cloud-vm WS upgrade. Cookies pass through (default).
1901
- { userHosted: false, upstreamHeaders: relay.request.target.upstreamHeaders }
1902
- )),
1903
- queue: [],
1904
- ...relayWebSocketTraceEnabled(request) ? {
1905
- trace: {
1906
- acceptedAt: performance.now(),
1907
- queuedFrames: 0,
1908
- maxQueuedDelayMs: 0,
1909
- emitted: false
1910
- }
1911
- } : {}
1912
- }
1913
- })) {
1914
- return;
1915
- }
1916
- return new Response("WebSocket upgrade failed", { status: 400 });
1917
- },
1918
- websocket: {
1919
- maxPayloadLength: WS_MAX_PAYLOAD_LENGTH_BYTES,
1920
- message(ws, message) {
1921
- if (ws.data.kind === "host-tunnel") {
1922
- const parsed = tunnelMessage(
1923
- ws,
1924
- message,
1925
- fragmentationStats
1926
- );
1927
- if (parsed?.type === "ping") {
1928
- options.directory?.recordPong(ws.data.hostId);
1929
- ws.send(JSON.stringify(makeTunnelPong(parsed)));
1930
- }
1931
- if (parsed?.type === "pong") {
1932
- ws.data.missedPongs = 0;
1933
- options.directory?.recordPong(ws.data.hostId);
1934
- }
1935
- if (parsed?.type === "host.registration.update") {
1936
- const update = parsed;
1937
- const workspaceIds = [...new Set(update.workspace_ids)];
1938
- const hostSocket = ws;
1939
- const hostId = hostSocket.data.hostId;
1940
- const authorizationRequest = new Request(
1941
- `http://relay.local/host-tunnels/${encodeURIComponent(hostId)}`,
1942
- { headers: { authorization: `Bearer ${update.token}` } }
1943
- );
1944
- void authorizeHostTunnel(options, bunOptions, authorizationRequest, {
1945
- hostId,
1946
- workspaceIds
1947
- }).then((authorized) => {
1948
- if (!authorized) {
1949
- closeWebSocket(hostSocket, 1008, "Host tunnel registration update denied", 1008);
1950
- return;
1951
- }
1952
- if (hostTunnels.get(hostId) !== hostSocket || hostSocket.readyState !== WebSocket.OPEN) return;
1953
- hostSocket.data.workspaceIds = workspaceIds;
1954
- options.directory?.registerHostTunnel({ hostId, workspaceIds });
1955
- }).catch(() => closeWebSocket(hostSocket, 1008, "Host tunnel registration update denied", 1008));
1956
- return;
1957
- }
1958
- if (parsed?.type === "error") {
1959
- if (parsed.request_id) {
1960
- const pending = ws.data.pending.get(parsed.request_id);
1961
- if (pending) {
1962
- ws.data.pending.delete(parsed.request_id);
1963
- failPendingHttpResponse({
1964
- entry: pending,
1965
- response: jsonError(parsed.code, parsed.message, 502),
1966
- error: new Error(parsed.message)
1967
- });
1968
- }
1969
- }
1970
- }
1971
- if (parsed?.type === "http.response.start") {
1972
- const pending = ws.data.pending.get(parsed.request_id);
1973
- if (pending) {
1974
- const response = parsed;
1975
- pending.responseStarted = true;
1976
- if (isEventStream(response.headers)) clearTimeout(pending.timeout);
1977
- pending.resolve(new Response(pending.stream, {
1978
- status: response.status,
1979
- headers: headers(response.headers)
1980
- }));
1981
- }
1982
- }
1983
- if (parsed?.type === "http.response.chunk") {
1984
- const entry = ws.data.pending.get(parsed.request_id);
1985
- if (entry) {
1986
- enqueueChunkWithBackpressure({
1987
- ws,
1988
- requestId: parsed.request_id,
1989
- entry,
1990
- chunk: decoded(parsed.body_base64),
1991
- slowConsumerTimeoutMs: bunOptions.slowConsumerTimeoutMs ?? SLOW_CONSUMER_TIMEOUT_MS_DEFAULT,
1992
- slowConsumerStats
1993
- });
1994
- }
1995
- }
1996
- if (parsed?.type === "http.response.end") {
1997
- const pending = ws.data.pending.get(parsed.request_id);
1998
- if (pending) {
1999
- clearTimeout(pending.timeout);
2000
- if (pending.slowConsumerTimeout) {
2001
- clearTimeout(pending.slowConsumerTimeout);
2002
- pending.slowConsumerTimeout = void 0;
2003
- }
2004
- drainPendingChunks(pending);
2005
- if (pending.pendingChunks.length > 0) {
2006
- for (const chunk of pending.pendingChunks) {
2007
- try {
2008
- pending.controller.enqueue(chunk);
2009
- } catch {
2010
- break;
2011
- }
2012
- }
2013
- pending.pendingChunks.length = 0;
2014
- pending.bytesQueued = 0;
2015
- }
2016
- try {
2017
- pending.controller.close();
2018
- } catch {
2019
- }
2020
- ws.data.pending.delete(parsed.request_id);
2021
- }
2022
- }
2023
- if (parsed?.type === "ws.frame") {
2024
- const frame = parsed;
2025
- const channel = ws.data.channels.get(frame.channel_id);
2026
- if (!channel || channel.readyState !== WebSocket.OPEN) {
2027
- ws.data.channels.delete(frame.channel_id);
2028
- return;
2029
- }
2030
- if (relayOverBackpressureLimit(channel, bunOptions.webSocketBufferedAmountMaxBytes ?? WS_BUFFERED_AMOUNT_MAX_BYTES_DEFAULT)) {
2031
- ws.data.channels.delete(frame.channel_id);
2032
- closeWebSocket(channel, 1011, "Client WebSocket backpressure limit exceeded");
2033
- return;
2034
- }
2035
- channel.send(decodedFrame(frame));
2036
- }
2037
- if (parsed?.type === "ws.close") {
2038
- const close = parsed;
2039
- const channel = ws.data.channels.get(close.channel_id);
2040
- if (channel) {
2041
- ws.data.channels.delete(close.channel_id);
2042
- closeWebSocket(channel, close.code, close.reason);
2043
- }
2044
- }
2045
- return;
2046
- }
2047
- if (ws.data.kind === "user-hosted-client") {
2048
- const tunnel = hostTunnel(hostTunnels, ws.data.hostId);
2049
- if (!tunnel) {
2050
- ws.close(1011, "User-hosted tunnel disconnected");
2051
- return;
2052
- }
2053
- if (relayOverBackpressureLimit(tunnel, bunOptions.webSocketBufferedAmountMaxBytes ?? WS_BUFFERED_AMOUNT_MAX_BYTES_DEFAULT)) {
2054
- closeWebSocket(ws, 1011, "Host tunnel backpressure limit exceeded");
2055
- return;
2056
- }
2057
- tunnel.send(JSON.stringify({
2058
- type: "ws.frame",
2059
- protocol: TUNNEL_PROTOCOL_VERSION,
2060
- channel_id: ws.data.channelId,
2061
- ...encodedFrame(message)
2062
- }));
2063
- return;
2064
- }
2065
- if (ws.data.upstream?.readyState === WebSocket.OPEN) {
2066
- ws.data.upstream.send(message);
2067
- return;
2068
- }
2069
- const queuedBytes = ws.data.queuedBytes ?? 0;
2070
- const frameBytes = preOpenFrameBytes(message);
2071
- if (ws.data.queue.length >= (bunOptions.upstreamWebSocketPreOpenQueueMaxFrames ?? UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT) && queuedBytes + frameBytes > (bunOptions.upstreamWebSocketPreOpenQueueMaxBytes ?? UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT)) {
2072
- closeWebSocket(ws, 1011, "Upstream WebSocket queue limit exceeded");
2073
- return;
2074
- }
2075
- ws.data.queuedBytes = queuedBytes + frameBytes;
2076
- ws.data.queue.push({ payload: message, queuedAt: performance.now() });
2077
- },
2078
- open(ws) {
2079
- if (ws.data.kind === "host-tunnel") {
2080
- const previous = hostTunnels.get(ws.data.hostId);
2081
- if (previous && previous !== ws) {
2082
- cleanupHostTunnelSocket({
2083
- ws: previous,
2084
- hostTunnels,
2085
- hostTunnelStateDebounce,
2086
- options,
2087
- bunOptions,
2088
- disconnectDirectory: false,
2089
- closeChannels: true
2090
- });
2091
- closeWebSocket(previous, 1012, "Host tunnel replaced by a newer connection", 1012);
2092
- }
2093
- hostTunnels.set(ws.data.hostId, ws);
2094
- options.directory?.registerHostTunnel({
2095
- hostId: ws.data.hostId,
2096
- workspaceIds: ws.data.workspaceIds
2097
- });
2098
- ws.data.heartbeat = setInterval(() => {
2099
- const hostWs = ws;
2100
- if (hostWs.data.missedPongs > (bunOptions.hostTunnelMaxMissedPongs ?? HOST_TUNNEL_MAX_MISSED_PONGS_DEFAULT)) {
2101
- closeWebSocket(ws, 1001, "Host tunnel heartbeat timed out", 1001);
2102
- return;
2103
- }
2104
- sendTunnelPing(hostWs);
2105
- }, bunOptions.hostTunnelPingIntervalMs ?? 15e3);
2106
- scheduleHostTunnelStateChange(hostTunnelStateDebounce, options, {
2107
- hostId: ws.data.hostId,
2108
- workspaceId: ws.data.workspaceIds[0],
2109
- path: `/host-tunnels/${ws.data.hostId}`,
2110
- state: "connected",
2111
- debounceMs: bunOptions.hostTunnelStateDebounceMs ?? HOST_TUNNEL_STATE_DEBOUNCE_MS_DEFAULT
2112
- });
2113
- return;
2114
- }
2115
- if (ws.data.kind === "user-hosted-client") {
2116
- const tunnel = hostTunnel(hostTunnels, ws.data.hostId);
2117
- if (!tunnel) {
2118
- ws.close(1011, "User-hosted tunnel disconnected");
2119
- return;
2120
- }
2121
- tunnel.data.channels.set(ws.data.channelId, ws);
2122
- if (tunnel.readyState !== WebSocket.OPEN) {
2123
- closeWebSocket(ws, 1011, "User-hosted tunnel disconnected");
2124
- return;
2125
- }
2126
- tunnel.send(JSON.stringify({
2127
- type: "ws.open",
2128
- protocol: TUNNEL_PROTOCOL_VERSION,
2129
- channel_id: ws.data.channelId,
2130
- workspace_id: ws.data.workspaceId,
2131
- path: ws.data.path,
2132
- headers: headersRecord(workspaceRelayForwardHeaders(
2133
- new Headers(),
2134
- ws.data.relayHostToken,
2135
- ws.data.workspaceId,
2136
- // T26: user-hosted tunnel WS upgrade — strip Cookie. Headers are
2137
- // empty here today, but pass the flag for consistency in case
2138
- // upstream client headers are forwarded in the future.
2139
- { userHosted: true }
2140
- ))
2141
- }));
2142
- return;
2143
- }
2144
- relayClients.add(ws);
2145
- const data = ws.data;
2146
- const UpstreamWebSocket = bunOptions.upstreamWebSocket ?? WebSocket;
2147
- if (data.trace) data.trace.upstreamStartedAt = performance.now();
2148
- const upstream = new UpstreamWebSocket(data.upstreamUrl, {
2149
- headers: data.headers
2150
- });
2151
- upstream.binaryType = "arraybuffer";
2152
- data.upstream = upstream;
2153
- data.upstreamOpenTimer = setTimeout(() => {
2154
- if (upstream.readyState === WebSocket.OPEN) return;
2155
- closeWebSocket(upstream, 1001, "Upstream WebSocket open timed out", 1001);
2156
- closeWebSocket(ws, 1011, "Upstream WebSocket open timed out");
2157
- }, bunOptions.upstreamWebSocketOpenTimeoutMs ?? UPSTREAM_WS_OPEN_TIMEOUT_MS_DEFAULT);
2158
- upstream.onopen = () => {
2159
- const openedAt = performance.now();
2160
- if (data.upstreamOpenTimer) {
2161
- clearTimeout(data.upstreamOpenTimer);
2162
- data.upstreamOpenTimer = void 0;
2163
- }
2164
- if (data.trace) {
2165
- data.trace.upstreamOpenMs = openedAt - (data.trace.upstreamStartedAt ?? data.trace.acceptedAt);
2166
- data.trace.queuedFrames = data.queue.length;
2167
- data.trace.maxQueuedDelayMs = data.queue.reduce(
2168
- (max, item) => Math.max(max, openedAt - item.queuedAt),
2169
- 0
2170
- );
2171
- sendRelayWebSocketTrace(ws);
2172
- }
2173
- for (const item of data.queue.splice(0)) upstream.send(item.payload);
2174
- data.queuedBytes = 0;
2175
- };
2176
- upstream.onmessage = (event) => {
2177
- const payload = relayWebSocketPayload(event.data);
2178
- if (payload !== void 0) ws.send(payload);
2179
- };
2180
- upstream.onclose = (event) => {
2181
- if (data.upstreamOpenTimer) {
2182
- clearTimeout(data.upstreamOpenTimer);
2183
- data.upstreamOpenTimer = void 0;
2184
- }
2185
- closeWebSocket(ws, event.code, upstreamCloseReason(event));
2186
- };
2187
- upstream.onerror = () => {
2188
- if (data.upstreamOpenTimer) {
2189
- clearTimeout(data.upstreamOpenTimer);
2190
- data.upstreamOpenTimer = void 0;
2191
- }
2192
- closeWebSocket(ws, 1011, "Upstream WebSocket connection failed");
2193
- };
2194
- },
2195
- close(ws, code, reason) {
2196
- if (ws.data.kind === "host-tunnel") {
2197
- cleanupHostTunnelSocket({
2198
- ws,
2199
- hostTunnels,
2200
- hostTunnelStateDebounce,
2201
- options,
2202
- bunOptions,
2203
- disconnectDirectory: true,
2204
- closeChannels: true
2205
- });
2206
- const tracker = hostTunnelRegistrations.get(ws.data.hostId);
2207
- if (tracker) {
2208
- pruneReconnects(tracker, Date.now());
2209
- if (tracker.recent.length === 0) hostTunnelRegistrations.delete(ws.data.hostId);
2210
- }
2211
- return;
2212
- }
2213
- if (ws.data.kind === "user-hosted-client") {
2214
- const tunnel = hostTunnel(hostTunnels, ws.data.hostId);
2215
- tunnel?.data.channels.delete(ws.data.channelId);
2216
- if (tunnel?.readyState === WebSocket.OPEN) {
2217
- tunnel.send(JSON.stringify({
2218
- type: "ws.close",
2219
- protocol: TUNNEL_PROTOCOL_VERSION,
2220
- channel_id: ws.data.channelId,
2221
- code: safeCloseCode(code, 1e3),
2222
- reason
2223
- }));
2224
- }
2225
- return;
2226
- }
2227
- relayClients.delete(ws);
2228
- if (ws.data.upstreamOpenTimer) clearTimeout(ws.data.upstreamOpenTimer);
2229
- if (ws.data.upstream) closeWebSocket(ws.data.upstream, code, reason, 1e3);
2230
- }
2231
- },
2232
- drain: drainController,
2233
- telemetry
2234
- };
2235
- }
2236
-
2237
- // src/cloudflare.ts
2238
- import {
2239
- makeTunnelPong as makeTunnelPong2,
2240
- TUNNEL_PROTOCOL_VERSION as TUNNEL_PROTOCOL_VERSION2,
2241
- validateTunnelMessage as validateTunnelMessage2
2242
- } from "@claxedo/workspace-relay-protocol";
2243
1081
 
2244
1082
  // src/directory.ts
1083
+ function presenceKey(hostId, workspaceId) {
1084
+ return `${hostId}\0${workspaceId}`;
1085
+ }
2245
1086
  function createWorkspaceRelayDirectory(options = {}) {
2246
1087
  const ttlMs = Math.max(1, options.ttlMs ?? 45e3);
2247
1088
  const now = options.now ?? Date.now;
2248
- const sweepIntervalMs = options.sweepIntervalMs ?? 3e4;
2249
- const hosts = /* @__PURE__ */ new Map();
2250
- const alive = (presence) => {
2251
- if (!presence) return;
1089
+ const entries = /* @__PURE__ */ new Map();
1090
+ const alive = (key) => {
1091
+ const presence = entries.get(key);
1092
+ if (!presence) return void 0;
2252
1093
  if (presence.expiresAt <= now()) {
2253
- hosts.delete(presence.hostId);
2254
- return;
1094
+ entries.delete(key);
1095
+ return void 0;
2255
1096
  }
2256
1097
  return presence;
2257
1098
  };
2258
- const touch = (presence) => {
2259
- const timestamp = now();
2260
- const next = {
2261
- ...presence,
2262
- lastPongAt: timestamp,
2263
- expiresAt: timestamp + ttlMs
2264
- };
2265
- hosts.set(next.hostId, next);
2266
- return next;
2267
- };
2268
- const sweep = () => {
2269
- const at = now();
2270
- for (const [hostId, presence] of hosts) {
2271
- if (presence.expiresAt <= at) {
2272
- hosts.delete(hostId);
2273
- }
2274
- }
2275
- };
2276
- let intervalHandle;
2277
- if (sweepIntervalMs > 0) {
2278
- intervalHandle = setInterval(sweep, sweepIntervalMs);
2279
- }
1099
+ const keysOf = (hostId) => [...entries].filter(([, presence]) => presence.hostId === hostId).map(([key]) => key);
2280
1100
  return {
2281
1101
  registerHostTunnel(input) {
2282
1102
  const timestamp = now();
@@ -2287,46 +1107,160 @@ function createWorkspaceRelayDirectory(options = {}) {
2287
1107
  lastPongAt: timestamp,
2288
1108
  expiresAt: timestamp + ttlMs
2289
1109
  };
2290
- hosts.set(next.hostId, next);
1110
+ for (const workspaceId of next.workspaceIds) entries.set(presenceKey(next.hostId, workspaceId), next);
2291
1111
  return next;
2292
1112
  },
2293
1113
  recordPong(hostId) {
2294
- const presence = alive(hosts.get(hostId));
2295
- return presence ? touch(presence) : void 0;
1114
+ const timestamp = now();
1115
+ let touched;
1116
+ for (const key of keysOf(hostId)) {
1117
+ const presence = alive(key);
1118
+ if (!presence) continue;
1119
+ const next = { ...presence, lastPongAt: timestamp, expiresAt: timestamp + ttlMs };
1120
+ entries.set(key, next);
1121
+ touched ??= next;
1122
+ }
1123
+ return touched;
2296
1124
  },
2297
1125
  disconnectHost(hostId) {
2298
- hosts.delete(hostId);
1126
+ for (const key of keysOf(hostId)) entries.delete(key);
2299
1127
  },
2300
1128
  activeHost(input) {
2301
- const presence = alive(hosts.get(input.hostId));
2302
- if (!presence?.workspaceIds.includes(input.workspaceId)) return;
2303
- return presence;
2304
- },
2305
- sweep,
2306
- size() {
2307
- const at = now();
2308
- let count = 0;
2309
- for (const presence of hosts.values()) {
2310
- if (presence.expiresAt > at) count += 1;
2311
- }
2312
- return count;
2313
- },
2314
- dispose() {
2315
- if (intervalHandle !== void 0) {
2316
- clearInterval(intervalHandle);
2317
- intervalHandle = void 0;
2318
- }
1129
+ return alive(presenceKey(input.hostId, input.workspaceId));
1130
+ }
1131
+ };
1132
+ }
1133
+
1134
+ // src/cors-origins.ts
1135
+ var DEFAULT_RELAY_APP_ORIGINS = [
1136
+ "https://claxedo.com",
1137
+ "https://*.claxedo.com"
1138
+ ];
1139
+ var RELAY_ALLOWED_REQUEST_HEADERS = [
1140
+ "Accept",
1141
+ "Authorization",
1142
+ "Content-Type",
1143
+ "Last-Event-ID",
1144
+ "Traceparent",
1145
+ "Tracestate",
1146
+ "X-Fetch-Bypass-Throttle",
1147
+ "X-Workspace-Id",
1148
+ "X-OpenCode-Directory",
1149
+ "X-Claxedo-Runner",
1150
+ "X-Claxedo-Model",
1151
+ "X-Claxedo-Draft-Id",
1152
+ "X-Claxedo-Session-Registration-Operation",
1153
+ "X-Claxedo-Binary"
1154
+ ].join(", ");
1155
+ function parseAllowedOrigins(raw) {
1156
+ const entries = (raw ?? "").split(",").map((item) => item.trim()).filter(Boolean);
1157
+ return entries.length ? entries : void 0;
1158
+ }
1159
+ function createOriginMatcher(patterns) {
1160
+ const exact = /* @__PURE__ */ new Set();
1161
+ const httpsSuffixes = [];
1162
+ const httpPortPrefixes = [];
1163
+ for (const pattern of patterns) {
1164
+ if (pattern.startsWith("https://*.")) {
1165
+ httpsSuffixes.push(pattern.slice("https://*".length));
1166
+ } else if (pattern.startsWith("http://") && pattern.endsWith(":*")) {
1167
+ httpPortPrefixes.push(pattern.slice(0, -1));
1168
+ } else {
1169
+ exact.add(pattern);
1170
+ }
1171
+ }
1172
+ return (origin) => {
1173
+ if (exact.has(origin)) return true;
1174
+ if (origin.startsWith("https://")) {
1175
+ return httpsSuffixes.some(
1176
+ (suffix) => origin.endsWith(suffix) && origin.length > "https://".length + suffix.length
1177
+ );
1178
+ }
1179
+ if (origin.startsWith("http://")) {
1180
+ return httpPortPrefixes.some(
1181
+ (prefix) => origin.startsWith(prefix) && /^\d+$/.test(origin.slice(prefix.length))
1182
+ );
2319
1183
  }
1184
+ return false;
1185
+ };
1186
+ }
1187
+
1188
+ // src/relay-cors.ts
1189
+ var RELAY_EXPOSED_RESPONSE_HEADERS = "X-Event-Cursor, X-Next-Cursor, X-Max-Event-Ordinal, Server-Timing, X-Claxedo-Trace-Id, X-Claxedo-Relay-Location-Hint";
1190
+ var configuredAppOriginAllowed = () => false;
1191
+ function setWorkspaceRelayAppOrigins(raw) {
1192
+ configuredAppOriginAllowed = raw?.trim() ? createOriginMatcher(parseAllowedOrigins(raw) ?? []) : () => false;
1193
+ }
1194
+ var defaultOriginAllowed = createOriginMatcher(DEFAULT_RELAY_APP_ORIGINS);
1195
+ var baseOriginAllowed = defaultOriginAllowed;
1196
+ function setWorkspaceRelayAllowedOrigins(raw) {
1197
+ const patterns = parseAllowedOrigins(raw);
1198
+ baseOriginAllowed = patterns ? createOriginMatcher(patterns) : defaultOriginAllowed;
1199
+ }
1200
+ function allowedCorsOrigin(origin) {
1201
+ if (!origin) return void 0;
1202
+ return baseOriginAllowed(origin) || configuredAppOriginAllowed(origin) ? origin : void 0;
1203
+ }
1204
+ function workspaceUpgradeOriginDenied(request) {
1205
+ const origin = request.headers.get("origin");
1206
+ if (origin === null || allowedCorsOrigin(origin)) return null;
1207
+ return Response.json(errorBody("origin_not_allowed", "Origin is not in the allowlist"), { status: 403 });
1208
+ }
1209
+ function corsHeaders(request) {
1210
+ const origin = allowedCorsOrigin(request.headers.get("origin"));
1211
+ if (!origin) return void 0;
1212
+ return {
1213
+ "access-control-allow-origin": origin,
1214
+ "access-control-allow-methods": "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS",
1215
+ "access-control-allow-headers": RELAY_ALLOWED_REQUEST_HEADERS,
1216
+ "access-control-expose-headers": RELAY_EXPOSED_RESPONSE_HEADERS,
1217
+ "access-control-max-age": "86400",
1218
+ "timing-allow-origin": origin,
1219
+ vary: "Origin"
2320
1220
  };
2321
1221
  }
2322
- function disposeWorkspaceRelayDirectory(directory) {
2323
- directory.dispose();
1222
+ function corsPreflight(request) {
1223
+ const headers = corsHeaders(request);
1224
+ return new Response(null, { status: headers ? 204 : 403, ...headers ? { headers } : {} });
1225
+ }
1226
+ var UPSTREAM_OWNED_RESPONSE_HEADERS = [
1227
+ "set-cookie",
1228
+ "access-control-allow-origin",
1229
+ "access-control-allow-credentials",
1230
+ "access-control-allow-headers",
1231
+ "access-control-allow-methods",
1232
+ "access-control-expose-headers",
1233
+ "access-control-max-age"
1234
+ ];
1235
+ var RELAY_DOCUMENT_LOCKDOWN_HEADERS = {
1236
+ "x-content-type-options": "nosniff",
1237
+ "content-security-policy": "sandbox"
1238
+ };
1239
+ function withRelayResponseHeaders(request, response) {
1240
+ if (response.status === 101 || "webSocket" in response && response.webSocket) return response;
1241
+ const next = new Headers(response.headers);
1242
+ for (const name of UPSTREAM_OWNED_RESPONSE_HEADERS) next.delete(name);
1243
+ for (const [key, value] of Object.entries({ ...RELAY_DOCUMENT_LOCKDOWN_HEADERS, ...corsHeaders(request) })) next.set(key, value);
1244
+ return new Response(response.body, {
1245
+ status: response.status,
1246
+ statusText: response.statusText,
1247
+ headers: next
1248
+ });
2324
1249
  }
2325
1250
 
2326
- // src/cloudflare.ts
1251
+ // src/session-host-forwarding.ts
1252
+ import { sessionHostRootOf as sessionHostRootOf2 } from "@claxedo/workspace-relay-protocol";
1253
+
1254
+ // src/relay-location.ts
1255
+ import { trimToUndefined as trimToUndefined2 } from "@claxedo/helpers/string";
1256
+ import { sessionHostRootOf } from "@claxedo/workspace-relay-protocol";
2327
1257
  var DEFAULT_RELAY_LOCATION_HINT = "apac";
1258
+ var SESSION_HOST_LOCATION_HINT = "enam";
2328
1259
  var RELAY_LOCATION_HINTS = ["wnam", "enam", "sam", "weur", "eeur", "apac", "oc", "afr", "me"];
2329
1260
  var relayLocationHintSet = new Set(RELAY_LOCATION_HINTS);
1261
+ function isRelayLocationHint(input) {
1262
+ return relayLocationHintSet.has(input);
1263
+ }
2330
1264
  var RELAY_REGION_TO_LOCATION_HINT = {
2331
1265
  // Canonical Claxedo regions.
2332
1266
  "apac-south": "apac",
@@ -2363,46 +1297,157 @@ function locationHintForCountry(country) {
2363
1297
  }
2364
1298
  var RELAY_REGION_HEADER = "x-claxedo-workspace-region";
2365
1299
  function relayLocationHint(input) {
2366
- const configured = clean(input.configured) ?? DEFAULT_RELAY_LOCATION_HINT;
2367
- const region = clean(input.region);
1300
+ const configured = trimToUndefined2(input.configured) ?? DEFAULT_RELAY_LOCATION_HINT;
1301
+ const region = trimToUndefined2(input.region);
2368
1302
  if (region) {
2369
- const direct = relayLocationHintSet.has(region.toLowerCase()) ? region.toLowerCase() : void 0;
2370
- const mapped = direct ?? RELAY_REGION_TO_LOCATION_HINT[region.toLowerCase()];
1303
+ const lowered = region.toLowerCase();
1304
+ const direct = isRelayLocationHint(lowered) ? lowered : void 0;
1305
+ const mapped = direct ?? RELAY_REGION_TO_LOCATION_HINT[lowered];
2371
1306
  if (mapped) return mapped;
2372
1307
  }
2373
- const country = clean(input.country);
1308
+ const country = trimToUndefined2(input.country);
2374
1309
  if (country) {
2375
1310
  const mapped = locationHintForCountry(country);
2376
1311
  if (mapped) return mapped;
2377
1312
  }
2378
1313
  return configured;
2379
1314
  }
1315
+ var RELAY_REGION_QUERY_PARAMS = ["region", "homeRegion"];
2380
1316
  function relayRequestRegion(request) {
2381
1317
  const url = new URL(request.url);
2382
- return clean(request.headers.get(RELAY_REGION_HEADER)) ?? clean(url.searchParams.get("region")) ?? clean(url.searchParams.get("homeRegion"));
1318
+ return trimToUndefined2(request.headers.get(RELAY_REGION_HEADER)) ?? trimToUndefined2(url.searchParams.get(RELAY_REGION_QUERY_PARAMS[0])) ?? trimToUndefined2(url.searchParams.get(RELAY_REGION_QUERY_PARAMS[1]));
1319
+ }
1320
+ async function relayRoomPlacement(request, input) {
1321
+ const configured = trimToUndefined2(input.configured);
1322
+ const region = relayRequestRegion(request);
1323
+ const country = relayRequestCountry(request);
1324
+ const floor = relayLocationHint(configured ? { configured } : {});
1325
+ const declared = relayLocationHint({
1326
+ ...configured ? { configured } : {},
1327
+ ...region ? { region } : {},
1328
+ ...country ? { country } : {}
1329
+ });
1330
+ const headers = new Headers(request.headers);
1331
+ headers.delete(RELAY_REGION_HEADER);
1332
+ const credential = await input.credential();
1333
+ const sessionHost = credential !== void 0 && sessionHostRootOf(credential.hostId) !== void 0;
1334
+ return {
1335
+ locationHint: sessionHost ? SESSION_HOST_LOCATION_HINT : credential || declared === floor ? declared : floor,
1336
+ url: withoutQueryParams(request.url, RELAY_REGION_QUERY_PARAMS),
1337
+ headers
1338
+ };
1339
+ }
1340
+ function withoutQueryParams(url, names) {
1341
+ const target = new URL(url);
1342
+ if (!names.some((name) => target.searchParams.has(name))) return url;
1343
+ const kept = target.search.slice(1).split("&").filter((pair) => !names.includes(pair.split("=")[0] ?? ""));
1344
+ target.search = kept.length ? `?${kept.join("&")}` : "";
1345
+ return target.toString();
2383
1346
  }
2384
1347
  function relayRequestCountry(request) {
2385
1348
  const cf = request.cf;
2386
1349
  return typeof cf?.country === "string" ? cf.country : void 0;
2387
1350
  }
1351
+
1352
+ // src/session-host-forwarding.ts
1353
+ var SESSION_HOST_LOCATION = { locationHint: SESSION_HOST_LOCATION_HINT };
1354
+ var SESSION_HOST_ORIGIN = "https://session-host.invalid";
1355
+ function unavailable() {
1356
+ return Response.json(errorBody("session_host_unavailable", "The session host is unavailable"), { status: 503 });
1357
+ }
1358
+ function isSessionHostTarget(target) {
1359
+ return target.backing === "durable-object";
1360
+ }
1361
+ async function forwardSessionHostHttp(namespace, request, authorized) {
1362
+ const root = sessionHostRootOf2(authorized.target.hostId);
1363
+ if (!namespace || !root) return unavailable();
1364
+ const url = workspaceRelayTargetUrl({ ...authorized.target, baseUrl: SESSION_HOST_ORIGIN }, authorized.path, new URL(request.url).search);
1365
+ const init = workspaceRelayForwardRequestInit(request, authorized.relayHostToken, authorized.target.workspaceId, { hostTunnel: true });
1366
+ let upstream;
1367
+ try {
1368
+ upstream = await namespace.getByName(root, SESSION_HOST_LOCATION).fetch(url.toString(), init);
1369
+ } catch {
1370
+ return unavailable();
1371
+ }
1372
+ return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: upstream.headers });
1373
+ }
1374
+
1375
+ // src/gateway-placement.ts
1376
+ import { createRemoteJWKSet, importSPKI } from "jose";
1377
+ import { trimToUndefined as trimToUndefined3 } from "@claxedo/helpers/string";
1378
+ var placementAuthKeys = /* @__PURE__ */ new Map();
1379
+ function placementAuthKey(env) {
1380
+ const jwksUrl = typeof env.CLAXEDO_CONTROL_PLANE_JWKS_URL === "string" ? trimToUndefined3(env.CLAXEDO_CONTROL_PLANE_JWKS_URL) : void 0;
1381
+ const pem = (typeof env.CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM === "string" ? trimToUndefined3(env.CLAXEDO_RUNTIME_ACCESS_TOKEN_PUBLIC_KEY_PEM) : void 0)?.replaceAll("\\n", "\n");
1382
+ const id = jwksUrl ? `jwks:${jwksUrl}` : pem ? `pem:${pem}` : void 0;
1383
+ if (!id) return void 0;
1384
+ const cached = placementAuthKeys.get(id);
1385
+ if (cached) return cached;
1386
+ if (placementAuthKeys.size >= 32) placementAuthKeys.clear();
1387
+ const loaded = (async () => {
1388
+ try {
1389
+ if (jwksUrl) return createRemoteJWKSet(new URL(jwksUrl));
1390
+ if (!pem) return void 0;
1391
+ return await importSPKI(pem, "EdDSA");
1392
+ } catch {
1393
+ return void 0;
1394
+ }
1395
+ })();
1396
+ placementAuthKeys.set(id, loaded);
1397
+ return loaded;
1398
+ }
1399
+ function runtimeAccessTokenFrom(request) {
1400
+ return bearerToken(request.headers.get("authorization") ?? void 0) ?? request.headers.get("sec-websocket-protocol")?.split(",").map((item) => item.trim()).find((item) => item.startsWith("claxedo-rat."))?.slice("claxedo-rat.".length);
1401
+ }
1402
+ async function relayPlacementCredential(request, env, url, workspaceId) {
1403
+ const key = await placementAuthKey(env);
1404
+ if (!key) return void 0;
1405
+ try {
1406
+ if (url.pathname.startsWith("/host-tunnels/")) {
1407
+ const hostId = trimToUndefined3(hostIdFromTunnelPath(url.pathname));
1408
+ const token2 = bearerToken(request.headers.get("authorization"));
1409
+ if (!hostId || !token2) return void 0;
1410
+ await verifyHostTunnelToken(token2, key, { hostId, workspaceIds: workspaceIdsFromSearch(url) });
1411
+ return { hostId };
1412
+ }
1413
+ const token = runtimeAccessTokenFrom(request);
1414
+ if (!token) return void 0;
1415
+ const claims = await verifyRuntimeAccessToken(token, key, { workspaceId });
1416
+ return { hostId: claims.host_id };
1417
+ } catch {
1418
+ return void 0;
1419
+ }
1420
+ }
1421
+ function workspaceRelayRoomPlacement(request, env, workspaceId) {
1422
+ return relayRoomPlacement(request, {
1423
+ ...typeof env.CLAXEDO_RELAY_LOCATION_HINT === "string" ? { configured: env.CLAXEDO_RELAY_LOCATION_HINT } : {},
1424
+ credential: () => relayPlacementCredential(request, env, new URL(request.url), workspaceId)
1425
+ });
1426
+ }
1427
+
1428
+ // src/cloudflare.ts
2388
1429
  var TUNNEL_PENDING_HTTP_CAP_DEFAULT = 32;
2389
1430
  var TUNNEL_STARTED_STREAM_CAP_DEFAULT = 16;
2390
1431
  var TUNNEL_CHANNEL_CAP_DEFAULT = 16;
2391
- var TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT2 = 16 * 1024 * 1024;
1432
+ var TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
2392
1433
  var TUNNEL_RESPONSE_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
2393
- var SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT2 = 8 * 1024 * 1024;
2394
- var SLOW_CONSUMER_TIMEOUT_MS_DEFAULT2 = 3e4;
2395
- var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT2 = 64;
2396
- var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT2 = 8 * 1024 * 1024;
2397
- var RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
1434
+ var SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT = 8 * 1024 * 1024;
1435
+ var SLOW_CONSUMER_TIMEOUT_MS_DEFAULT = 3e4;
1436
+ var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT = 8192;
1437
+ var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
1438
+ var UPSTREAM_WS_PRE_OPEN_QUEUE_FRAME_OVERHEAD_BYTES = 64;
2398
1439
  var WORKSPACE_TARGET_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
2399
1440
  var RESOLVER_OUTAGE_GRACE_ATTEMPTS_DEFAULT = 3;
2400
1441
  var HIBERNATED_REVOCATION_ALARM_INTERVAL_MS_DEFAULT = 3e4;
1442
+ var HOST_GENERATION_CHECK_INTERVAL_MS_DEFAULT = 3e4;
2401
1443
  var TRACE_ID_HEADER = "x-claxedo-trace-id";
2402
1444
  var TRACE_FORCE_HEADER = "x-claxedo-relay-trace";
2403
- function clean(input) {
2404
- const value = input?.trim();
2405
- return value ? value : void 0;
1445
+ var TRACE_SAMPLED_RESPONSE_HEADER = "x-claxedo-relay-sampled";
1446
+ function unrefTimer(timer) {
1447
+ if (typeof timer.unref === "function") timer.unref();
1448
+ }
1449
+ function isHostTunnelClient(client) {
1450
+ return "channelId" in client && typeof client.channelId === "string";
2406
1451
  }
2407
1452
  function clampRate(input) {
2408
1453
  if (input === void 0 || !Number.isFinite(input)) return 0;
@@ -2411,8 +1456,8 @@ function clampRate(input) {
2411
1456
  return input;
2412
1457
  }
2413
1458
  function traceRequest(request, routeKind, options) {
2414
- const id = clean(request.headers.get(TRACE_ID_HEADER)) ?? crypto.randomUUID();
2415
- const forceSecret = clean(options.traceForceHeaderSecret);
1459
+ const id = trimToUndefined4(request.headers.get(TRACE_ID_HEADER)) ?? crypto.randomUUID();
1460
+ const forceSecret = trimToUndefined4(options.traceForceHeaderSecret);
2416
1461
  const forced = !!forceSecret && request.headers.get(TRACE_FORCE_HEADER) === forceSecret;
2417
1462
  const rate = clampRate(options.traceSampleRate);
2418
1463
  return {
@@ -2431,37 +1476,38 @@ async function traceSpan(trace, name, fn) {
2431
1476
  trace?.phases.push({ name, ms: performance.now() - startedAt });
2432
1477
  }
2433
1478
  }
2434
- function tracedRequest(request, trace) {
2435
- const headers2 = new Headers(request.headers);
2436
- headers2.set(TRACE_ID_HEADER, trace.id);
2437
- if (trace.sampled) headers2.set("x-claxedo-trace-sampled", "1");
2438
- else headers2.delete("x-claxedo-trace-sampled");
2439
- return new Request(request.url, {
1479
+ function tracedRequest(request, trace, forwarded = request) {
1480
+ const headers = new Headers(forwarded.headers);
1481
+ headers.set(TRACE_ID_HEADER, trace.id);
1482
+ if (trace.sampled) headers.set("x-claxedo-trace-sampled", "1");
1483
+ else headers.delete("x-claxedo-trace-sampled");
1484
+ return new Request(forwarded.url, {
2440
1485
  method: request.method,
2441
- headers: headers2,
1486
+ headers,
2442
1487
  body: request.method === "GET" || request.method === "HEAD" ? void 0 : request.body,
2443
1488
  redirect: request.redirect,
2444
1489
  duplex: "half"
2445
1490
  });
2446
1491
  }
2447
- function roundedMs3(value) {
1492
+ function roundedMs(value) {
2448
1493
  return Math.round(value * 100) / 100;
2449
1494
  }
2450
1495
  function serverTiming(trace) {
2451
- return trace.phases.map((phase) => `${phase.name};dur=${roundedMs3(phase.ms)}`).join(", ");
1496
+ return trace.phases.map((phase) => `${phase.name};dur=${roundedMs(phase.ms)}`).join(", ");
2452
1497
  }
2453
1498
  function withTraceHeaders(response, trace) {
2454
- const headers2 = new Headers(response.headers);
2455
- headers2.set(TRACE_ID_HEADER, trace.id);
1499
+ const headers = new Headers(response.headers);
1500
+ headers.set(TRACE_ID_HEADER, trace.id);
2456
1501
  const timing = trace.sampled ? serverTiming(trace) : "";
2457
1502
  if (timing) {
2458
- const existing = headers2.get("server-timing");
2459
- headers2.set("server-timing", existing ? `${existing}, ${timing}` : timing);
1503
+ const existing = headers.get("server-timing");
1504
+ headers.set("server-timing", existing ? `${existing}, ${timing}` : timing);
1505
+ headers.set(TRACE_SAMPLED_RESPONSE_HEADER, "1");
2460
1506
  }
2461
1507
  return new Response(response.body, {
2462
1508
  status: response.status,
2463
1509
  statusText: response.statusText,
2464
- headers: headers2
1510
+ headers
2465
1511
  });
2466
1512
  }
2467
1513
  function logTrace(options, request, workspaceId, trace, response) {
@@ -2476,8 +1522,8 @@ function logTrace(options, request, workspaceId, trace, response) {
2476
1522
  method: request.method,
2477
1523
  path: url.pathname,
2478
1524
  status: response.status,
2479
- totalMs: roundedMs3(totalMs),
2480
- phases: Object.fromEntries(trace.phases.map((phase) => [phase.name, roundedMs3(phase.ms)]))
1525
+ totalMs: roundedMs(totalMs),
1526
+ phases: Object.fromEntries(trace.phases.map((phase) => [phase.name, roundedMs(phase.ms)]))
2481
1527
  };
2482
1528
  if (options.traceLog) {
2483
1529
  options.traceLog(event);
@@ -2488,57 +1534,45 @@ function logTrace(options, request, workspaceId, trace, response) {
2488
1534
  function workspaceIdFromRelayPath(pathname) {
2489
1535
  return /^\/workspaces\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
2490
1536
  }
2491
- function hostIdFromTunnelPath2(pathname) {
2492
- return /^\/host-tunnels\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
2493
- }
2494
- function websocketRequest2(request) {
1537
+ function websocketRequest(request) {
2495
1538
  return request.headers.get("upgrade")?.toLowerCase() === "websocket";
2496
1539
  }
2497
- function relayWebSocketTraceEnabled2(request) {
1540
+ function relayWebSocketTraceEnabled(request) {
2498
1541
  return request.headers.get("x-claxedo-relay-ws-trace") === "1";
2499
1542
  }
2500
- function workspaceIdsFromSearch(url) {
2501
- return [...new Set([
2502
- ...url.searchParams.getAll("workspaceId"),
2503
- ...url.searchParams.getAll("workspace_id")
2504
- ].map(clean).filter((item) => !!item))];
2505
- }
2506
1543
  function randomToken() {
2507
1544
  return crypto.randomUUID();
2508
1545
  }
2509
- function headerRecord(headers2) {
2510
- const result = {};
2511
- headers2.forEach((value, key) => {
1546
+ function headerRecord(headers) {
1547
+ const result = /* @__PURE__ */ Object.create(null);
1548
+ headers.forEach((value, key) => {
2512
1549
  result[key] = value;
2513
1550
  });
2514
1551
  return result;
2515
1552
  }
2516
1553
  function headersFromRecord(input) {
2517
- const headers2 = new Headers();
2518
- for (const [key, value] of Object.entries(input)) headers2.set(key, value);
2519
- return headers2;
1554
+ const headers = new Headers();
1555
+ for (const [key, value] of Object.entries(input)) headers.set(key, value);
1556
+ return headers;
2520
1557
  }
2521
1558
  var BASE64_CHUNK_BYTES = 32768;
2522
1559
  function bytesToBase64(bytes) {
2523
1560
  let binary = "";
2524
1561
  for (let i = 0; i < bytes.length; i += BASE64_CHUNK_BYTES) {
2525
- binary += String.fromCharCode.apply(
2526
- null,
2527
- bytes.subarray(i, i + BASE64_CHUNK_BYTES)
2528
- );
1562
+ binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_BYTES));
2529
1563
  }
2530
1564
  return btoa(binary);
2531
1565
  }
2532
1566
  function base64ToBytes(input) {
2533
1567
  return Uint8Array.from(atob(input), (char) => char.charCodeAt(0));
2534
1568
  }
2535
- function preOpenFrameBytes2(input) {
2536
- if (typeof input === "string") return new TextEncoder().encode(input).byteLength;
2537
- return input.byteLength;
1569
+ function preOpenFrameBytes(input) {
1570
+ const payload = typeof input === "string" ? new TextEncoder().encode(input).byteLength : input.byteLength;
1571
+ return payload + UPSTREAM_WS_PRE_OPEN_QUEUE_FRAME_OVERHEAD_BYTES;
2538
1572
  }
2539
- function blobLike(input) {
2540
- if (!input || typeof input !== "object") return void 0;
2541
- return typeof input.arrayBuffer === "function" ? input : void 0;
1573
+ function isBlobLike(input) {
1574
+ if (!input || typeof input !== "object") return false;
1575
+ return "arrayBuffer" in input && typeof input.arrayBuffer === "function";
2542
1576
  }
2543
1577
  function viewBytes(input) {
2544
1578
  return new Uint8Array(input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength));
@@ -2568,9 +1602,8 @@ function socketFrame(input) {
2568
1602
  data_base64: bytesToBase64(viewBytes(input))
2569
1603
  };
2570
1604
  }
2571
- const blob = blobLike(input);
2572
- if (!blob) return void 0;
2573
- return blob.arrayBuffer().then((buffer) => ({
1605
+ if (!isBlobLike(input)) return void 0;
1606
+ return input.arrayBuffer().then((buffer) => ({
2574
1607
  binary: true,
2575
1608
  data_base64: bytesToBase64(new Uint8Array(buffer))
2576
1609
  }));
@@ -2580,9 +1613,8 @@ function socketPayload(input) {
2580
1613
  if (input instanceof ArrayBuffer) return input;
2581
1614
  if (input instanceof Uint8Array) return input;
2582
1615
  if (ArrayBuffer.isView(input)) return viewBytes(input);
2583
- const blob = blobLike(input);
2584
- if (!blob) return void 0;
2585
- return blob.arrayBuffer();
1616
+ if (!isBlobLike(input)) return void 0;
1617
+ return input.arrayBuffer();
2586
1618
  }
2587
1619
  function orderedFrameDelivery(deliver) {
2588
1620
  let tail;
@@ -2648,7 +1680,7 @@ async function readRequestBodyCapped(request, maxBytes) {
2648
1680
  }
2649
1681
  return concatBytes(chunks);
2650
1682
  }
2651
- function safeCloseCode2(code, fallback = 1011) {
1683
+ function safeCloseCode(code, fallback = 1011) {
2652
1684
  if (code === void 0) return fallback;
2653
1685
  if (code >= 1e3 && code <= 4999 && ![1004, 1005, 1006].includes(code)) return code;
2654
1686
  return fallback;
@@ -2658,63 +1690,25 @@ function workspaceRelayDurableObjectRoomName(workspaceId) {
2658
1690
  }
2659
1691
  function workspaceRelayDurableObjectWorkspaceId(request) {
2660
1692
  const url = new URL(request.url);
2661
- const workspaceId = clean(workspaceIdFromRelayPath(url.pathname));
1693
+ const workspaceId = trimToUndefined4(workspaceIdFromRelayPath(url.pathname));
2662
1694
  if (workspaceId) return decodeURIComponent(workspaceId);
2663
- if (!hostIdFromTunnelPath2(url.pathname)) return;
2664
- return clean(url.searchParams.get("workspaceId")) ?? clean(url.searchParams.get("workspace_id"));
1695
+ if (!hostIdFromTunnelPath(url.pathname)) return void 0;
1696
+ return trimToUndefined4(url.searchParams.get("workspaceId")) ?? trimToUndefined4(url.searchParams.get("workspace_id"));
2665
1697
  }
2666
1698
  function namespaceFromEnv(env, options) {
2667
1699
  if (options.namespace) return options.namespace;
2668
1700
  const binding = env[options.bindingName ?? "WORKSPACE_RELAY_ROOM"];
2669
- return binding && typeof binding === "object" ? binding : void 0;
1701
+ return isDurableObjectNamespace(binding) ? binding : void 0;
1702
+ }
1703
+ function isDurableObjectNamespace(value) {
1704
+ if (!value || typeof value !== "object") return false;
1705
+ return "idFromName" in value && typeof value.idFromName === "function" && "get" in value && typeof value.get === "function";
2670
1706
  }
2671
1707
  function json(code, message, status) {
2672
1708
  return Response.json(errorBody(code, message), { status });
2673
1709
  }
2674
- var configuredAppOriginAllowed = () => false;
2675
- function setWorkspaceRelayAppOrigins(raw) {
2676
- configuredAppOriginAllowed = raw?.trim() ? createOriginMatcher(parseAllowedOrigins(raw) ?? []) : () => false;
2677
- }
2678
- var defaultOriginAllowed = createOriginMatcher(DEFAULT_RELAY_APP_ORIGINS);
2679
- var baseOriginAllowed = defaultOriginAllowed;
2680
- function setWorkspaceRelayAllowedOrigins(raw) {
2681
- const patterns = parseAllowedOrigins(raw);
2682
- baseOriginAllowed = patterns ? createOriginMatcher(patterns) : defaultOriginAllowed;
2683
- }
2684
- function allowedCorsOrigin2(origin) {
2685
- if (!origin) return;
2686
- if (baseOriginAllowed(origin)) return origin;
2687
- if (configuredAppOriginAllowed(origin)) return origin;
2688
- }
2689
- function corsHeaders(request) {
2690
- const origin = allowedCorsOrigin2(request.headers.get("origin"));
2691
- if (!origin) return;
2692
- return {
2693
- "access-control-allow-origin": origin,
2694
- "access-control-allow-methods": "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS",
2695
- "access-control-allow-headers": "Accept, Authorization, Content-Type, Last-Event-ID, X-Fetch-Bypass-Throttle, X-Daytona-Skip-Preview-Warning, X-Workspace-Id, X-OpenCode-Directory, X-Claxedo-Runner, X-Claxedo-Model, X-Claxedo-Draft-Id, X-Claxedo-Binary",
2696
- "access-control-max-age": "86400",
2697
- vary: "Origin"
2698
- };
2699
- }
2700
- function corsPreflight(request) {
2701
- const headers2 = corsHeaders(request);
2702
- return new Response(null, { status: headers2 ? 204 : 403, ...headers2 ? { headers: headers2 } : {} });
2703
- }
2704
- function withCors(request, response) {
2705
- const headers2 = corsHeaders(request);
2706
- if (!headers2) return response;
2707
- if (response.status === 101 || response.webSocket) return response;
2708
- const next = new Headers(response.headers);
2709
- for (const [key, value] of Object.entries(headers2)) next.set(key, value);
2710
- return new Response(response.body, {
2711
- status: response.status,
2712
- statusText: response.statusText,
2713
- headers: next
2714
- });
2715
- }
2716
1710
  function defaultWebSocketPair() {
2717
- const pair = new globalThis.WebSocketPair();
1711
+ const pair = new WebSocketPair();
2718
1712
  return {
2719
1713
  client: pair[0],
2720
1714
  server: pair[1]
@@ -2722,7 +1716,7 @@ function defaultWebSocketPair() {
2722
1716
  }
2723
1717
  function selectedSubprotocol(request) {
2724
1718
  const offered = request.headers.get("sec-websocket-protocol")?.split(",").map((item) => item.trim()).filter(Boolean);
2725
- if (!offered?.length) return;
1719
+ if (!offered?.length) return void 0;
2726
1720
  return offered.find((item) => item.startsWith("claxedo-rat.")) ?? offered[0];
2727
1721
  }
2728
1722
  function upgradeResponse(socket, request) {
@@ -2749,7 +1743,7 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
2749
1743
  return {
2750
1744
  async fetch(request, env = {}) {
2751
1745
  const gatewayStartedAt = performance.now();
2752
- const gatewayTraceId = clean(request.headers.get(TRACE_ID_HEADER)) ?? crypto.randomUUID();
1746
+ const gatewayTraceId = trimToUndefined4(request.headers.get(TRACE_ID_HEADER)) ?? crypto.randomUUID();
2753
1747
  const url = new URL(request.url);
2754
1748
  if (request.method === "OPTIONS" && (url.pathname.startsWith("/workspaces/") || url.pathname.startsWith("/host-tunnels/"))) {
2755
1749
  return corsPreflight(request);
@@ -2757,13 +1751,6 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
2757
1751
  if (url.pathname === "/health") {
2758
1752
  return Response.json({ ok: true, service: "workspace-relay", mode: "cloudflare-durable-object" });
2759
1753
  }
2760
- const requestRegion = relayRequestRegion(request);
2761
- const requestCountry = relayRequestCountry(request);
2762
- const locationHint = relayLocationHint({
2763
- ...typeof env.CLAXEDO_RELAY_LOCATION_HINT === "string" ? { configured: env.CLAXEDO_RELAY_LOCATION_HINT } : {},
2764
- ...requestRegion ? { region: requestRegion } : {},
2765
- ...requestCountry ? { country: requestCountry } : {}
2766
- });
2767
1754
  const namespace = namespaceFromEnv(env, options);
2768
1755
  if (!namespace) {
2769
1756
  return json(
@@ -2787,6 +1774,8 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
2787
1774
  url.pathname.startsWith("/workspaces/") || url.pathname.startsWith("/host-tunnels/") ? 400 : 404
2788
1775
  );
2789
1776
  }
1777
+ const placement = await workspaceRelayRoomPlacement(request, env, workspaceId);
1778
+ const locationHint = placement.locationHint;
2790
1779
  const routeFinishedAt = performance.now();
2791
1780
  const response = await namespace.get(namespace.idFromName(workspaceRelayDurableObjectRoomName(workspaceId)), {
2792
1781
  locationHint
@@ -2796,37 +1785,39 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
2796
1785
  startedAt: gatewayStartedAt,
2797
1786
  routeKind: "gateway",
2798
1787
  phases: []
2799
- }));
1788
+ }, placement));
2800
1789
  const fetchFinishedAt = performance.now();
2801
- if (response.status === 101 || response.webSocket) {
2802
- return url.pathname.startsWith("/workspaces/") ? withCors(request, response) : response;
1790
+ if (response.status === 101 || "webSocket" in response && response.webSocket) {
1791
+ return url.pathname.startsWith("/workspaces/") ? withRelayResponseHeaders(request, response) : response;
2803
1792
  }
2804
- const headers2 = new Headers(response.headers);
2805
- headers2.set(TRACE_ID_HEADER, headers2.get(TRACE_ID_HEADER) ?? gatewayTraceId);
2806
- headers2.set("x-claxedo-relay-location-hint", locationHint);
1793
+ const headers = new Headers(response.headers);
1794
+ headers.set(TRACE_ID_HEADER, headers.get(TRACE_ID_HEADER) ?? gatewayTraceId);
1795
+ headers.set("x-claxedo-relay-location-hint", locationHint);
2807
1796
  const gatewayResponseStartedAt = performance.now();
2808
1797
  const tracedResponse = new Response(response.body, {
2809
1798
  status: response.status,
2810
1799
  statusText: response.statusText,
2811
- headers: headers2
1800
+ headers
2812
1801
  });
2813
1802
  const gatewayFinishedAt = performance.now();
2814
- const roomTiming = tracedResponse.headers.get("server-timing");
2815
- if (roomTiming) {
1803
+ const sampled = tracedResponse.headers.get(TRACE_SAMPLED_RESPONSE_HEADER) === "1";
1804
+ tracedResponse.headers.delete(TRACE_SAMPLED_RESPONSE_HEADER);
1805
+ const roomTiming = tracedResponse.headers.get("server-timing") ?? "";
1806
+ if (sampled) {
2816
1807
  const gatewayTiming = [
2817
- `relay-gateway-route;dur=${roundedMs3(routeFinishedAt - gatewayStartedAt)}`,
2818
- `relay-gateway-do-fetch;dur=${roundedMs3(fetchFinishedAt - routeFinishedAt)}`,
2819
- `relay-gateway-response;dur=${roundedMs3(gatewayFinishedAt - gatewayResponseStartedAt)}`,
2820
- `relay-gateway;dur=${roundedMs3(gatewayFinishedAt - gatewayStartedAt)}`
1808
+ `relay-gateway-route;dur=${roundedMs(routeFinishedAt - gatewayStartedAt)}`,
1809
+ `relay-gateway-do-fetch;dur=${roundedMs(fetchFinishedAt - routeFinishedAt)}`,
1810
+ `relay-gateway-response;dur=${roundedMs(gatewayFinishedAt - gatewayResponseStartedAt)}`,
1811
+ `relay-gateway;dur=${roundedMs(gatewayFinishedAt - gatewayStartedAt)}`
2821
1812
  ].join(", ");
2822
1813
  tracedResponse.headers.set("server-timing", `${roomTiming}, ${gatewayTiming}`);
2823
1814
  }
2824
- return url.pathname.startsWith("/workspaces/") ? withCors(request, tracedResponse) : tracedResponse;
1815
+ return url.pathname.startsWith("/workspaces/") ? withRelayResponseHeaders(request, tracedResponse) : tracedResponse;
2825
1816
  }
2826
1817
  };
2827
1818
  }
2828
1819
  function createWorkspaceRelayDurableObjectRoom(options) {
2829
- const directory = options.directory ?? createWorkspaceRelayDirectory({ sweepIntervalMs: 0 });
1820
+ const directory = options.directory ?? createWorkspaceRelayDirectory();
2830
1821
  const relayOptions = { ...options, directory };
2831
1822
  const hostTunnels = /* @__PURE__ */ new Map();
2832
1823
  const clients = /* @__PURE__ */ new Set();
@@ -2867,6 +1858,8 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2867
1858
  }
2868
1859
  };
2869
1860
  const cleanupTunnelResources = (tunnel, reason) => {
1861
+ if (tunnel.generationWatcher) clearInterval(tunnel.generationWatcher);
1862
+ tunnel.generationWatcher = void 0;
2870
1863
  for (const pending of tunnel.pending.values()) {
2871
1864
  clearTimeout(pending.timeout);
2872
1865
  if (pending.stream?.slowConsumerTimeout) clearTimeout(pending.stream.slowConsumerTimeout);
@@ -2899,10 +1892,15 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2899
1892
  const attachment = socketAttachment(socket);
2900
1893
  if (!attachment) continue;
2901
1894
  if (attachment.kind === "host-tunnel") {
1895
+ const incumbent = hostTunnels.get(attachment.hostId);
1896
+ if (incumbent && incumbent.connectedAt > attachment.connectedAt) continue;
2902
1897
  hostTunnels.set(attachment.hostId, {
2903
1898
  hostId: attachment.hostId,
2904
1899
  workspaceIds: attachment.workspaceIds,
2905
1900
  connectedAt: attachment.connectedAt,
1901
+ enrollmentId: attachment.enrollmentId,
1902
+ generation: attachment.generation,
1903
+ generationCheckFailures: attachment.generationCheckFailures ?? 0,
2906
1904
  socket,
2907
1905
  pending: /* @__PURE__ */ new Map(),
2908
1906
  channels: /* @__PURE__ */ new Map()
@@ -2918,7 +1916,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2918
1916
  for (const row of clientRows) {
2919
1917
  const tunnel = hostTunnels.get(row.attachment.target.hostId);
2920
1918
  if (!tunnel) {
2921
- closeSocket(row.socket, 1011, "User-hosted tunnel unavailable");
1919
+ closeSocket(row.socket, 1011, "Host tunnel unavailable");
2922
1920
  continue;
2923
1921
  }
2924
1922
  const client = {
@@ -2953,7 +1951,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2953
1951
  const resolverUnreachable = (code) => code === "relay_revocation_resolver_unavailable";
2954
1952
  const watchRuntimeAccessToken = (client, onInactive) => {
2955
1953
  const intervalMs = options.runtimeAccessTokenActiveCheckIntervalMs ?? RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT;
2956
- if (intervalMs <= 0) return;
1954
+ if (intervalMs <= 0) return void 0;
2957
1955
  const now = options.now ?? Date.now;
2958
1956
  let consecutiveUnreachable = 0;
2959
1957
  const timer = setInterval(() => {
@@ -2969,7 +1967,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2969
1967
  clearInterval(timer);
2970
1968
  onInactive(reason);
2971
1969
  };
2972
- void Promise.resolve(options.isRuntimeAccessTokenActive(client.request.claims)).then((active) => {
1970
+ void Promise.resolve().then(() => options.isRuntimeAccessTokenActive(client.request.claims)).then((active) => {
2973
1971
  if (active.active) {
2974
1972
  consecutiveUnreachable = 0;
2975
1973
  return;
@@ -2983,25 +1981,23 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2983
1981
  onInactive(active.reason);
2984
1982
  }).catch(() => tolerate("Runtime Access Token active check failed"));
2985
1983
  }, intervalMs);
2986
- if (typeof timer.unref === "function") {
2987
- timer.unref();
2988
- }
1984
+ unrefTimer(timer);
2989
1985
  return timer;
2990
1986
  };
2991
- const watchUserHostedTarget = (client, onInactive) => {
1987
+ const watchHostTunnelTarget = (client, onInactive) => {
2992
1988
  const intervalMs = options.workspaceTargetActiveCheckIntervalMs ?? WORKSPACE_TARGET_ACTIVE_CHECK_INTERVAL_MS_DEFAULT;
2993
- if (intervalMs <= 0) return;
1989
+ if (intervalMs <= 0) return void 0;
2994
1990
  const target = client.request.target;
2995
1991
  let consecutiveUnreachable = 0;
2996
1992
  const timer = setInterval(() => {
2997
1993
  void Promise.resolve(options.resolveTarget(client.request.claims)).then((activeTarget) => {
2998
- if (activeTarget?.access === "user-hosted" && activeTarget.workspaceId === target.workspaceId && activeTarget.hostId === target.hostId && directory.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
1994
+ if (activeTarget?.backing === "local-worktree" && activeTarget.workspaceId === target.workspaceId && activeTarget.hostId === target.hostId && directory.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
2999
1995
  consecutiveUnreachable = 0;
3000
1996
  return;
3001
1997
  }
3002
1998
  consecutiveUnreachable = 0;
3003
1999
  clearInterval(timer);
3004
- onInactive("User-hosted workspace is offline");
2000
+ onInactive("The machine serving this workspace is offline");
3005
2001
  }).catch(() => {
3006
2002
  consecutiveUnreachable += 1;
3007
2003
  if (consecutiveUnreachable < resolverGraceAttempts) return;
@@ -3009,11 +2005,53 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3009
2005
  onInactive("Workspace relay target active check failed");
3010
2006
  });
3011
2007
  }, intervalMs);
3012
- if (typeof timer.unref === "function") {
3013
- timer.unref();
3014
- }
2008
+ unrefTimer(timer);
3015
2009
  return timer;
3016
2010
  };
2011
+ const hostTunnelAttachment = (tunnel) => ({
2012
+ kind: "host-tunnel",
2013
+ hostId: tunnel.hostId,
2014
+ workspaceIds: tunnel.workspaceIds,
2015
+ connectedAt: tunnel.connectedAt,
2016
+ enrollmentId: tunnel.enrollmentId,
2017
+ generation: tunnel.generation,
2018
+ ...tunnel.generationCheckFailures ? { generationCheckFailures: tunnel.generationCheckFailures } : {}
2019
+ });
2020
+ const recheckHostGeneration = async (tunnel) => {
2021
+ if (hostTunnels.get(tunnel.hostId) !== tunnel) return;
2022
+ const decision = await checkHostTunnelGeneration(options.resolveHostGeneration, {
2023
+ enrollment_id: tunnel.enrollmentId,
2024
+ generation: tunnel.generation
2025
+ });
2026
+ if (hostTunnels.get(tunnel.hostId) !== tunnel) return;
2027
+ if (decision.ok) {
2028
+ if (tunnel.generationCheckFailures === 0) return;
2029
+ tunnel.generationCheckFailures = 0;
2030
+ tunnel.socket.serializeAttachment?.(hostTunnelAttachment(tunnel));
2031
+ return;
2032
+ }
2033
+ if (decision.retryable) {
2034
+ tunnel.generationCheckFailures += 1;
2035
+ if (tunnel.generationCheckFailures < resolverGraceAttempts) {
2036
+ tunnel.socket.serializeAttachment?.(hostTunnelAttachment(tunnel));
2037
+ return;
2038
+ }
2039
+ cleanupHostTunnel(tunnel.hostId, tunnel.socket, "Host generation check unavailable");
2040
+ closeSocket(tunnel.socket, 1012, "Host generation check unavailable");
2041
+ return;
2042
+ }
2043
+ cleanupHostTunnel(tunnel.hostId, tunnel.socket, decision.reason);
2044
+ closeSocket(tunnel.socket, 1008, decision.reason);
2045
+ };
2046
+ const watchHostGeneration = (tunnel) => {
2047
+ const intervalMs = options.hostGenerationCheckIntervalMs ?? HOST_GENERATION_CHECK_INTERVAL_MS_DEFAULT;
2048
+ if (tunnel.generationWatcher || hibernation || intervalMs <= 0) return;
2049
+ tunnel.generationWatcher = setInterval(() => {
2050
+ void recheckHostGeneration(tunnel).catch(() => {
2051
+ });
2052
+ }, intervalMs);
2053
+ unrefTimer(tunnel.generationWatcher);
2054
+ };
3017
2055
  const clearSlowConsumerTimer = (pending) => {
3018
2056
  if (!pending.stream?.slowConsumerTimeout) return;
3019
2057
  clearTimeout(pending.stream.slowConsumerTimeout);
@@ -3028,7 +2066,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3028
2066
  pending.stream.flowPaused = paused;
3029
2067
  tunnel.socket.send?.(JSON.stringify({
3030
2068
  type: "http.response.flow",
3031
- protocol: TUNNEL_PROTOCOL_VERSION2,
2069
+ protocol: TUNNEL_PROTOCOL_VERSION,
3032
2070
  request_id: requestId,
3033
2071
  paused,
3034
2072
  reason
@@ -3049,7 +2087,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3049
2087
  if (pending.stream?.slowConsumerTimeout) return;
3050
2088
  pending.stream.slowConsumerTimeout = setTimeout(
3051
2089
  () => failSlowConsumer(tunnel, requestId, pending),
3052
- options.slowConsumerTimeoutMs ?? SLOW_CONSUMER_TIMEOUT_MS_DEFAULT2
2090
+ options.slowConsumerTimeoutMs ?? SLOW_CONSUMER_TIMEOUT_MS_DEFAULT
3053
2091
  );
3054
2092
  };
3055
2093
  const drainPendingStreamChunks = (tunnel, requestId, pending) => {
@@ -3103,7 +2141,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3103
2141
  failPendingTunnelResponse(tunnel, oldestId, oldest, new Error("stream_evicted: tunnel started-stream cap reached"));
3104
2142
  }
3105
2143
  };
3106
- const resolvePendingTunnelStream = (tunnel, requestId, pending, status, headers2) => {
2144
+ const resolvePendingTunnelStream = (tunnel, requestId, pending, status, headers) => {
3107
2145
  if (pending.stream) return;
3108
2146
  pending.startedSeq = ++startedSeqCounter;
3109
2147
  evictOldestStartedStream(tunnel, requestId);
@@ -3127,18 +2165,18 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3127
2165
  }
3128
2166
  tunnel.socket.send?.(JSON.stringify({
3129
2167
  type: "http.response.flow",
3130
- protocol: TUNNEL_PROTOCOL_VERSION2,
2168
+ protocol: TUNNEL_PROTOCOL_VERSION,
3131
2169
  request_id: requestId,
3132
2170
  paused: false,
3133
2171
  reason: "closed"
3134
2172
  }));
3135
2173
  }
3136
2174
  }, new ByteLengthQueuingStrategy({
3137
- highWaterMark: options.slowConsumerHighWaterMarkBytes ?? SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT2
2175
+ highWaterMark: options.slowConsumerHighWaterMarkBytes ?? SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT
3138
2176
  }));
3139
2177
  pending.resolve(new Response(stream, {
3140
2178
  status,
3141
- headers: headers2
2179
+ headers
3142
2180
  }));
3143
2181
  };
3144
2182
  const failPendingTunnelResponse = (tunnel, requestId, pending, error) => {
@@ -3184,18 +2222,18 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3184
2222
  const active = await options.isRuntimeAccessTokenActive(client.request.claims);
3185
2223
  return active.active ? { active: true } : { active: false, reason: active.reason };
3186
2224
  };
3187
- const userHostedTargetActive = async (client) => {
2225
+ const hostTunnelTargetActive = async (client) => {
3188
2226
  const target = client.request.target;
3189
2227
  const activeTarget = await options.resolveTarget(client.request.claims);
3190
- return !!(activeTarget?.access === "user-hosted" && activeTarget.workspaceId === target.workspaceId && activeTarget.hostId === target.hostId && directory.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId }));
2228
+ return !!(activeTarget?.backing === "local-worktree" && activeTarget.workspaceId === target.workspaceId && activeTarget.hostId === target.hostId && directory.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId }));
3191
2229
  };
3192
- const dropUserHostedChannel = (tunnel, channelId, client, code, reason) => {
2230
+ const dropHostTunnelChannel = (tunnel, channelId, client, code, reason) => {
3193
2231
  tunnel.channels.delete(channelId);
3194
2232
  clients.delete(client);
3195
2233
  closeSocket(client.socket, code, reason);
3196
2234
  sendSocket(tunnel.socket, JSON.stringify({
3197
2235
  type: "ws.close",
3198
- protocol: TUNNEL_PROTOCOL_VERSION2,
2236
+ protocol: TUNNEL_PROTOCOL_VERSION,
3199
2237
  channel_id: channelId,
3200
2238
  code,
3201
2239
  reason
@@ -3210,7 +2248,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3210
2248
  const scheduleHibernatedRevocationCheck = async () => {
3211
2249
  const intervalMs = hibernatedRevocationIntervalMs();
3212
2250
  if (!hibernation || !options.alarms || intervalMs <= 0) return;
3213
- if (clients.size === 0) return;
2251
+ if (clients.size === 0 && hostTunnels.size === 0) return;
3214
2252
  const now = options.now ?? Date.now;
3215
2253
  const at = now() + intervalMs;
3216
2254
  try {
@@ -3224,16 +2262,17 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3224
2262
  ensureHibernatedSocketsRebuilt();
3225
2263
  const now = options.now ?? Date.now;
3226
2264
  const revoke = (client, reason) => {
3227
- const channelId = client.channelId;
3228
- for (const tunnel of hostTunnels.values()) {
3229
- if (!channelId || tunnel.channels.get(channelId) !== client) continue;
3230
- dropUserHostedChannel(tunnel, channelId, client, 1008, reason);
3231
- return;
2265
+ if (isHostTunnelClient(client)) {
2266
+ for (const tunnel of hostTunnels.values()) {
2267
+ if (tunnel.channels.get(client.channelId) !== client) continue;
2268
+ dropHostTunnelChannel(tunnel, client.channelId, client, 1008, reason);
2269
+ return;
2270
+ }
3232
2271
  }
3233
2272
  closeSocket(client.socket, 1008, reason);
3234
2273
  clients.delete(client);
3235
2274
  };
3236
- for (const client of [...clients]) {
2275
+ for (const client of Array.from(clients)) {
3237
2276
  if (client.request.claims.exp * 1e3 <= now()) {
3238
2277
  revoke(client, "Runtime Access Token expired");
3239
2278
  continue;
@@ -3248,65 +2287,85 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3248
2287
  if (active.active || resolverUnreachable(active.code)) continue;
3249
2288
  revoke(client, active.reason);
3250
2289
  }
2290
+ for (const tunnel of Array.from(hostTunnels.values())) {
2291
+ await recheckHostGeneration(tunnel);
2292
+ }
3251
2293
  await scheduleHibernatedRevocationCheck();
3252
2294
  };
3253
- const activeUserHostedClient = (channelId, socket) => {
2295
+ const activeHostTunnelClient = (channelId, socket) => {
3254
2296
  for (const tunnel of hostTunnels.values()) {
3255
2297
  const client = tunnel.channels.get(channelId);
3256
2298
  if (client?.socket === socket) return { tunnel, client };
3257
2299
  }
2300
+ return void 0;
3258
2301
  };
3259
- const handleUserHostedClientMessage = async (channelId, socket, data) => {
3260
- const active = activeUserHostedClient(channelId, socket);
2302
+ const handleHostTunnelClientMessage = async (channelId, socket, data) => {
2303
+ const active = activeHostTunnelClient(channelId, socket);
3261
2304
  if (!active) return;
3262
2305
  const token = await runtimeAccessTokenActive(active.client);
3263
2306
  if (!token.active) {
3264
2307
  closeSocket(socket, 1008, token.reason);
3265
2308
  return;
3266
2309
  }
3267
- if (!await userHostedTargetActive(active.client)) {
3268
- closeSocket(socket, 1011, "User-hosted workspace is offline");
2310
+ if (!await hostTunnelTargetActive(active.client)) {
2311
+ closeSocket(socket, 1011, "The machine serving this workspace is offline");
3269
2312
  return;
3270
2313
  }
3271
2314
  const frame = await socketFrame(data);
3272
2315
  if (!frame) return;
3273
2316
  const sent = sendSocket(active.tunnel.socket, JSON.stringify({
3274
2317
  type: "ws.frame",
3275
- protocol: TUNNEL_PROTOCOL_VERSION2,
2318
+ protocol: TUNNEL_PROTOCOL_VERSION,
3276
2319
  channel_id: channelId,
3277
2320
  ...frame
3278
2321
  }));
3279
- if (!sent) closeSocket(socket, 1011, "User-hosted tunnel unavailable");
2322
+ if (!sent) closeSocket(socket, 1011, "Host tunnel unavailable");
2323
+ };
2324
+ const applyRegistrationUpdate = async (tunnel, workspaceIds, token) => {
2325
+ const hostId = tunnel.hostId;
2326
+ const refuse = (code, reason) => {
2327
+ cleanupHostTunnel(hostId, tunnel.socket, reason);
2328
+ closeSocket(tunnel.socket, code, reason);
2329
+ };
2330
+ let claims;
2331
+ try {
2332
+ claims = await verifyHostTunnelToken(token, options.runtimeAccessKey, { hostId, workspaceIds });
2333
+ } catch {
2334
+ if (hostTunnels.get(hostId) === tunnel) refuse(1008, "Host tunnel registration update denied");
2335
+ return;
2336
+ }
2337
+ if (hostTunnels.get(hostId) !== tunnel) return;
2338
+ if (hostTunnelIncumbentOutranks(tunnel.generation, claims.generation)) {
2339
+ refuse(1008, "Host tunnel registration update superseded");
2340
+ return;
2341
+ }
2342
+ const decision = await checkHostTunnelGeneration(options.resolveHostGeneration, claims);
2343
+ if (hostTunnels.get(hostId) !== tunnel) return;
2344
+ if (!decision.ok) {
2345
+ refuse(decision.retryable ? 1012 : 1008, decision.retryable ? "Host generation check unavailable" : decision.reason);
2346
+ return;
2347
+ }
2348
+ tunnel.workspaceIds = workspaceIds;
2349
+ tunnel.enrollmentId = claims.enrollment_id;
2350
+ tunnel.generation = claims.generation;
2351
+ tunnel.socket.serializeAttachment?.(hostTunnelAttachment(tunnel));
2352
+ directory.registerHostTunnel({ hostId, workspaceIds });
2353
+ watchHostGeneration(tunnel);
2354
+ void scheduleHibernatedRevocationCheck();
3280
2355
  };
3281
- const handleTunnelMessage = async (hostId, event) => {
2356
+ const handleTunnelMessage = async (hostId, socket, event) => {
3282
2357
  const row = parseTunnelMessageData(event.data);
3283
2358
  if (!row.ok) return;
3284
- const parsed = validateTunnelMessage2(row.value);
2359
+ const parsed = validateTunnelMessage(row.value);
3285
2360
  const tunnel = hostTunnels.get(hostId);
3286
- if (!parsed.ok || !tunnel) return;
2361
+ if (!parsed.ok || !tunnel || tunnel.socket !== socket) return;
3287
2362
  directory.recordPong(tunnel.hostId);
3288
2363
  if (parsed.message.type === "ping") {
3289
- tunnel.socket.send?.(JSON.stringify(makeTunnelPong2(parsed.message)));
2364
+ tunnel.socket.send?.(JSON.stringify(makeTunnelPong(parsed.message)));
3290
2365
  return;
3291
2366
  }
3292
2367
  if (parsed.message.type === "host.registration.update") {
3293
- const update = parsed.message;
3294
- const workspaceIds = [...new Set(update.workspace_ids)];
3295
- try {
3296
- await verifyHostTunnelToken(update.token, options.runtimeAccessKey, { hostId, workspaceIds });
3297
- } catch {
3298
- closeSocket(tunnel.socket, 1008, "Host tunnel registration update denied");
3299
- return;
3300
- }
3301
- if (hostTunnels.get(hostId) !== tunnel) return;
3302
- tunnel.workspaceIds = workspaceIds;
3303
- tunnel.socket.serializeAttachment?.({
3304
- kind: "host-tunnel",
3305
- hostId,
3306
- workspaceIds,
3307
- connectedAt: tunnel.connectedAt
3308
- });
3309
- directory.registerHostTunnel({ hostId, workspaceIds });
2368
+ await applyRegistrationUpdate(tunnel, [...new Set(parsed.message.workspace_ids)], parsed.message.token);
3310
2369
  return;
3311
2370
  }
3312
2371
  if (parsed.message.type === "error" && parsed.message.request_id) {
@@ -3331,7 +2390,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3331
2390
  if (!pending) return;
3332
2391
  const chunk = base64ToBytes(message.body_base64);
3333
2392
  if (pending.bytes + chunk.byteLength > (options.tunnelResponseBodyMaxBytes ?? TUNNEL_RESPONSE_BODY_MAX_BYTES_DEFAULT)) {
3334
- failPendingTunnelResponse(tunnel, message.request_id, pending, new Error("User-hosted response body exceeds the relay limit"));
2393
+ failPendingTunnelResponse(tunnel, message.request_id, pending, hostTunnelFailure("host_tunnel_response_body_too_large"));
3335
2394
  return;
3336
2395
  }
3337
2396
  pending.bytes += chunk.byteLength;
@@ -3377,7 +2436,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3377
2436
  return;
3378
2437
  }
3379
2438
  if (!sendSocket(channel.socket, tunnelFrame(message))) {
3380
- dropUserHostedChannel(tunnel, message.channel_id, channel, 1013, "Client WebSocket delivery failed");
2439
+ dropHostTunnelChannel(tunnel, message.channel_id, channel, 1013, "Client WebSocket delivery failed");
3381
2440
  }
3382
2441
  return;
3383
2442
  }
@@ -3387,7 +2446,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3387
2446
  if (!channel) return;
3388
2447
  tunnel.channels.delete(message.channel_id);
3389
2448
  clients.delete(channel);
3390
- closeSocket(channel.socket, safeCloseCode2(message.code), message.reason ?? "");
2449
+ closeSocket(channel.socket, safeCloseCode(message.code), message.reason ?? "");
3391
2450
  return;
3392
2451
  }
3393
2452
  if (parsed.message.type === "error" && parsed.message.channel_id) {
@@ -3415,7 +2474,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3415
2474
  hostTunnels.delete(hostId);
3416
2475
  directory.disconnectHost(hostId);
3417
2476
  };
3418
- const cleanupUserHostedClient = (channelId, socket, event) => {
2477
+ const cleanupHostTunnelClient = (channelId, socket, event) => {
3419
2478
  for (const tunnel of hostTunnels.values()) {
3420
2479
  const active = tunnel.channels.get(channelId);
3421
2480
  if (!active || active.socket !== socket) continue;
@@ -3423,17 +2482,17 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3423
2482
  clients.delete(active);
3424
2483
  sendSocket(tunnel.socket, JSON.stringify({
3425
2484
  type: "ws.close",
3426
- protocol: TUNNEL_PROTOCOL_VERSION2,
2485
+ protocol: TUNNEL_PROTOCOL_VERSION,
3427
2486
  channel_id: channelId,
3428
- code: safeCloseCode2(event?.code, 1e3),
2487
+ code: safeCloseCode(event?.code, 1e3),
3429
2488
  reason: event?.reason ?? "Client WebSocket closed"
3430
2489
  }));
3431
2490
  return;
3432
2491
  }
3433
2492
  };
3434
2493
  const admitHostTunnel = async (request, roomWorkspaceId, url) => {
3435
- if (!websocketRequest2(request)) return json("websocket_upgrade_required", "Workspace Relay requires a WebSocket upgrade", 426);
3436
- const hostId = clean(hostIdFromTunnelPath2(url.pathname));
2494
+ if (!websocketRequest(request)) return json("websocket_upgrade_required", "Workspace Relay requires a WebSocket upgrade", 426);
2495
+ const hostId = trimToUndefined4(hostIdFromTunnelPath(url.pathname));
3437
2496
  if (!hostId) return json("host_tunnel_host_required", "Host tunnel id is required", 400);
3438
2497
  const workspaceIds = workspaceIdsFromSearch(url);
3439
2498
  if (!workspaceIds.includes(roomWorkspaceId)) {
@@ -3441,54 +2500,69 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3441
2500
  }
3442
2501
  const token = bearerToken(request.headers.get("authorization"));
3443
2502
  if (!token) return json("host_tunnel_token_required", "Host Tunnel Token is required", 401);
2503
+ let claims;
3444
2504
  try {
3445
- await verifyHostTunnelToken(token, options.runtimeAccessKey, { hostId, workspaceIds });
2505
+ claims = await verifyHostTunnelToken(token, options.runtimeAccessKey, { hostId, workspaceIds });
3446
2506
  } catch (err) {
3447
2507
  if (err instanceof WorkspaceRelayAuthError) {
3448
2508
  return json(err.code, "Host Tunnel Token was denied", 403);
3449
2509
  }
3450
2510
  throw err;
3451
2511
  }
3452
- const connectedAt = options.now?.() ?? Date.now();
3453
- const pair = acceptSocket({
3454
- kind: "host-tunnel",
3455
- hostId,
3456
- workspaceIds,
3457
- connectedAt
3458
- });
2512
+ const decision = await checkHostTunnelGeneration(options.resolveHostGeneration, claims);
2513
+ if (!decision.ok) return json(decision.code, decision.reason, decision.retryable ? 503 : 403);
3459
2514
  const previous = hostTunnels.get(hostId);
3460
- if (previous) {
3461
- cleanupTunnelResources(previous, "Host tunnel replaced");
3462
- closeSocket(previous.socket, 1012, "Host tunnel replaced");
2515
+ if (previous && hostTunnelIncumbentOutranks(previous.generation, claims.generation)) {
2516
+ return json("host_generation_superseded", "Host tunnel generation was superseded", 403);
3463
2517
  }
3464
- hostTunnels.set(hostId, {
2518
+ const connectedAt = options.now?.() ?? Date.now();
2519
+ const fence = {
2520
+ enrollmentId: claims.enrollment_id,
2521
+ generation: claims.generation
2522
+ };
2523
+ const pair = acceptSocket({ kind: "host-tunnel", hostId, workspaceIds, connectedAt, ...fence });
2524
+ const tunnel = {
3465
2525
  hostId,
3466
2526
  workspaceIds,
3467
2527
  connectedAt,
2528
+ ...fence,
2529
+ generationCheckFailures: 0,
3468
2530
  socket: pair.server,
3469
2531
  pending: /* @__PURE__ */ new Map(),
3470
2532
  channels: /* @__PURE__ */ new Map()
3471
- });
2533
+ };
2534
+ if (previous) {
2535
+ cleanupTunnelResources(previous, "Host tunnel replaced");
2536
+ closeSocket(previous.socket, 1012, "Host tunnel replaced");
2537
+ }
2538
+ hostTunnels.set(hostId, tunnel);
3472
2539
  directory.registerHostTunnel({ hostId, workspaceIds });
2540
+ watchHostGeneration(tunnel);
2541
+ void scheduleHibernatedRevocationCheck();
3473
2542
  if (!hibernation) {
3474
2543
  pair.server.addEventListener?.("message", (event) => {
3475
2544
  if (!event || event.data === void 0) return;
3476
- void handleTunnelMessage(hostId, { data: event.data }).catch((err) => reportFrameHandlerFailure(pair.server, err));
2545
+ void handleTunnelMessage(hostId, pair.server, { data: event.data }).catch((err) => reportFrameHandlerFailure(pair.server, err));
3477
2546
  });
3478
- pair.server.addEventListener?.("close", () => cleanupHostTunnel(hostId, pair.server, "User-hosted tunnel disconnected"));
3479
- pair.server.addEventListener?.("error", () => cleanupHostTunnel(hostId, pair.server, "User-hosted tunnel disconnected"));
2547
+ pair.server.addEventListener?.("close", () => cleanupHostTunnel(hostId, pair.server, "Host tunnel disconnected"));
2548
+ pair.server.addEventListener?.("error", () => cleanupHostTunnel(hostId, pair.server, "Host tunnel disconnected"));
3480
2549
  }
3481
2550
  return upgradeResponse(pair.client, request);
3482
2551
  };
3483
2552
  const admitClient = async (request, workspaceId) => {
3484
- if (!websocketRequest2(request)) return json("websocket_upgrade_required", "Workspace Relay requires a WebSocket upgrade", 426);
2553
+ if (!websocketRequest(request)) return json("websocket_upgrade_required", "Workspace Relay requires a WebSocket upgrade", 426);
3485
2554
  const authorized = await authorizeWorkspaceRelayRequest(relayOptions, request, workspaceId);
3486
2555
  if (!authorized.ok) return authorized.response;
3487
- if (authorized.request.target.access !== "cloud") return admitUserHostedClient(request, authorized.request);
2556
+ const originDenied = workspaceUpgradeOriginDenied(request);
2557
+ if (originDenied) return originDenied;
2558
+ if (isHostTunnelTarget(authorized.request.target)) return admitHostTunnelClient(request, authorized.request);
2559
+ if (isSessionHostTarget(authorized.request.target)) {
2560
+ return json("durable_object_websocket_unsupported", "A session host serves its events over HTTP streams, not WebSockets", 400);
2561
+ }
3488
2562
  return await admitCloudClient(request, authorized.request);
3489
2563
  };
3490
2564
  const admitCloudClient = async (request, authorized) => {
3491
- const trace = relayWebSocketTraceEnabled2(request) ? { upstreamStartedAt: performance.now(), queueTimes: [], emitted: false } : void 0;
2565
+ const trace = relayWebSocketTraceEnabled(request) ? { upstreamStartedAt: performance.now(), queueTimes: [], emitted: false } : void 0;
3492
2566
  let upstream;
3493
2567
  try {
3494
2568
  upstream = await connectWebSocket(
@@ -3502,7 +2576,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3502
2576
  new Headers(),
3503
2577
  authorized.relayHostToken,
3504
2578
  authorized.target.workspaceId,
3505
- { userHosted: false, upstreamHeaders: authorized.target.upstreamHeaders }
2579
+ { hostTunnel: false }
3506
2580
  ))
3507
2581
  }
3508
2582
  );
@@ -3522,9 +2596,9 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3522
2596
  trace.emitted = true;
3523
2597
  sendSocket(pair.server, JSON.stringify({
3524
2598
  type: "relay.trace",
3525
- wsUpstreamOpenMs: roundedMs3(openedAt - trace.upstreamStartedAt),
2599
+ wsUpstreamOpenMs: roundedMs(openedAt - trace.upstreamStartedAt),
3526
2600
  queuedFrames: trace.queueTimes.length,
3527
- maxQueuedDelayMs: roundedMs3(trace.queueTimes.reduce((max, at) => Math.max(max, openedAt - at), 0))
2601
+ maxQueuedDelayMs: roundedMs(trace.queueTimes.reduce((max, at) => Math.max(max, openedAt - at), 0))
3528
2602
  }));
3529
2603
  };
3530
2604
  const activeTokenTimer = watchRuntimeAccessToken(client, (reason) => {
@@ -3535,14 +2609,14 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3535
2609
  closed = true;
3536
2610
  if (activeTokenTimer) clearInterval(activeTokenTimer);
3537
2611
  clients.delete(client);
3538
- closeSocket(upstream, safeCloseCode2(event?.code, 1e3), event?.reason ?? "Client WebSocket closed");
2612
+ closeSocket(upstream, safeCloseCode(event?.code, 1e3), event?.reason ?? "Client WebSocket closed");
3539
2613
  };
3540
2614
  const cleanupUpstream = (event) => {
3541
2615
  if (closed) return;
3542
2616
  closed = true;
3543
2617
  if (activeTokenTimer) clearInterval(activeTokenTimer);
3544
2618
  clients.delete(client);
3545
- closeSocket(pair.server, safeCloseCode2(event?.code), event?.reason ?? "");
2619
+ closeSocket(pair.server, safeCloseCode(event?.code), event?.reason ?? "");
3546
2620
  };
3547
2621
  const flushQueue = () => {
3548
2622
  upstreamOpen = true;
@@ -3557,8 +2631,8 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3557
2631
  };
3558
2632
  const deliverClientFrame = orderedFrameDelivery((payload) => {
3559
2633
  if (!upstreamOpen) {
3560
- const frameBytes = preOpenFrameBytes2(payload);
3561
- if (queue.length >= (options.upstreamWebSocketPreOpenQueueMaxFrames ?? UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT2) && queuedBytes + frameBytes > (options.upstreamWebSocketPreOpenQueueMaxBytes ?? UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT2)) {
2634
+ const frameBytes = preOpenFrameBytes(payload);
2635
+ if (queue.length >= (options.upstreamWebSocketPreOpenQueueMaxFrames ?? UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT) || queuedBytes + frameBytes > (options.upstreamWebSocketPreOpenQueueMaxBytes ?? UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT)) {
3562
2636
  closeSocket(pair.server, 1011, "Upstream WebSocket queue limit exceeded");
3563
2637
  cleanupClient({ code: 1011, reason: "Upstream WebSocket queue limit exceeded" });
3564
2638
  return;
@@ -3585,17 +2659,17 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3585
2659
  if (trace && upstreamOpen) emitTrace(upstreamConnectedAt);
3586
2660
  return upgradeResponse(pair.client, request);
3587
2661
  };
3588
- const admitUserHostedClient = (request, authorized) => {
2662
+ const admitHostTunnelClient = (request, authorized) => {
3589
2663
  const tunnel = hostTunnels.get(authorized.target.hostId);
3590
2664
  if (!tunnel || !directory.activeHost({ hostId: authorized.target.hostId, workspaceId: authorized.target.workspaceId })) {
3591
- return json("user_hosted_app_offline", "User-hosted workspace is offline", 503);
2665
+ return json("host_tunnel_offline", "The machine serving this workspace is offline", 503);
3592
2666
  }
3593
2667
  if (tunnel.channels.size >= (options.tunnelChannelCap ?? TUNNEL_CHANNEL_CAP_DEFAULT)) {
3594
- return json("too_many_channels", "User-hosted tunnel has too many active WebSocket channels", 503);
2668
+ return json("too_many_channels", "Host tunnel has too many active WebSocket channels", 503);
3595
2669
  }
3596
2670
  const channelId = randomToken();
3597
2671
  const pair = acceptSocket({
3598
- kind: "user-hosted-client",
2672
+ kind: "host-tunnel-client",
3599
2673
  channelId,
3600
2674
  claims: authorized.claims,
3601
2675
  target: authorized.target,
@@ -3612,7 +2686,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3612
2686
  const activeTokenTimer = hibernation ? void 0 : watchRuntimeAccessToken(client, (reason) => {
3613
2687
  closeSocket(pair.server, 1008, reason);
3614
2688
  });
3615
- const activeTargetTimer = hibernation ? void 0 : watchUserHostedTarget(client, (reason) => {
2689
+ const activeTargetTimer = hibernation ? void 0 : watchHostTunnelTarget(client, (reason) => {
3616
2690
  closeSocket(pair.server, 1011, reason);
3617
2691
  });
3618
2692
  if (hibernation) void scheduleHibernatedRevocationCheck();
@@ -3620,18 +2694,18 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3620
2694
  const cleanup = (event) => {
3621
2695
  if (activeTokenTimer) clearInterval(activeTokenTimer);
3622
2696
  if (activeTargetTimer) clearInterval(activeTargetTimer);
3623
- cleanupUserHostedClient(channelId, pair.server, event);
2697
+ cleanupHostTunnelClient(channelId, pair.server, event);
3624
2698
  };
3625
2699
  pair.server.addEventListener?.("message", (event) => {
3626
2700
  if (!event) return;
3627
- void handleUserHostedClientMessage(channelId, pair.server, event.data).catch((err) => reportFrameHandlerFailure(pair.server, err));
2701
+ void handleHostTunnelClientMessage(channelId, pair.server, event.data).catch((err) => reportFrameHandlerFailure(pair.server, err));
3628
2702
  });
3629
2703
  pair.server.addEventListener?.("close", cleanup);
3630
2704
  pair.server.addEventListener?.("error", cleanup);
3631
2705
  }
3632
2706
  const opened = sendSocket(tunnel.socket, JSON.stringify({
3633
2707
  type: "ws.open",
3634
- protocol: TUNNEL_PROTOCOL_VERSION2,
2708
+ protocol: TUNNEL_PROTOCOL_VERSION,
3635
2709
  channel_id: channelId,
3636
2710
  workspace_id: authorized.target.workspaceId,
3637
2711
  path: authorized.path,
@@ -3639,14 +2713,14 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3639
2713
  new Headers(),
3640
2714
  authorized.relayHostToken,
3641
2715
  authorized.target.workspaceId,
3642
- { userHosted: true }
2716
+ { hostTunnel: true }
3643
2717
  ))
3644
2718
  }));
3645
2719
  if (!opened) {
3646
2720
  tunnel.channels.delete(channelId);
3647
2721
  clients.delete(client);
3648
- closeSocket(pair.server, 1011, "User-hosted tunnel unavailable");
3649
- return json("user_hosted_app_offline", "User-hosted workspace is offline", 503);
2722
+ closeSocket(pair.server, 1011, "Host tunnel unavailable");
2723
+ return json("host_tunnel_offline", "The machine serving this workspace is offline", 503);
3650
2724
  }
3651
2725
  return upgradeResponse(pair.client, request);
3652
2726
  };
@@ -3659,9 +2733,13 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3659
2733
  })
3660
2734
  );
3661
2735
  if (!authorized.ok) return authorized.response;
3662
- if (authorized.request.target.access !== "cloud") {
3663
- trace && (trace.routeKind = "user-hosted-http");
3664
- return forwardUserHostedHttp(request, authorized.request, trace);
2736
+ if (isHostTunnelTarget(authorized.request.target)) {
2737
+ trace && (trace.routeKind = "host-tunnel-http");
2738
+ return forwardHostTunnelHttp(request, authorized.request, trace);
2739
+ }
2740
+ if (isSessionHostTarget(authorized.request.target)) {
2741
+ trace && (trace.routeKind = "session-host-http");
2742
+ return traceSpan(trace, "session-host-fetch", () => forwardSessionHostHttp(options.sessionHost, request, authorized.request));
3665
2743
  }
3666
2744
  trace && (trace.routeKind = "cloud-http");
3667
2745
  const controller = new AbortController();
@@ -3674,8 +2752,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3674
2752
  new URL(request.url).search
3675
2753
  ),
3676
2754
  workspaceRelayForwardRequestInit(request, authorized.request.relayHostToken, workspaceId, {
3677
- signal: controller.signal,
3678
- upstreamHeaders: authorized.request.target.upstreamHeaders
2755
+ signal: controller.signal
3679
2756
  })
3680
2757
  ));
3681
2758
  return new Response(upstream.body, {
@@ -3696,27 +2773,27 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3696
2773
  clearTimeout(timer);
3697
2774
  }
3698
2775
  };
3699
- const forwardUserHostedHttp = async (request, authorized, trace) => {
2776
+ const forwardHostTunnelHttp = async (request, authorized, trace) => {
3700
2777
  const tunnel = hostTunnels.get(authorized.target.hostId);
3701
2778
  if (!tunnel || !directory.activeHost({ hostId: authorized.target.hostId, workspaceId: authorized.target.workspaceId })) {
3702
- return json("user_hosted_app_offline", "User-hosted workspace is offline", 503);
2779
+ return json("host_tunnel_offline", "The machine serving this workspace is offline", 503);
3703
2780
  }
3704
2781
  const awaitingStart = [...tunnel.pending.values()].filter((entry) => entry.startedSeq === void 0).length;
3705
2782
  if (awaitingStart >= (options.tunnelPendingHttpCap ?? TUNNEL_PENDING_HTTP_CAP_DEFAULT)) {
3706
- return json("too_many_pending_requests", "User-hosted tunnel has too many pending HTTP requests", 503);
2783
+ return json("too_many_pending_requests", "Host tunnel has too many pending HTTP requests", 503);
3707
2784
  }
3708
2785
  const requestId = randomToken();
3709
2786
  const init = workspaceRelayForwardRequestInit(request, authorized.relayHostToken, authorized.target.workspaceId, {
3710
- userHosted: true
2787
+ hostTunnel: true
3711
2788
  });
3712
- const bodyMaxBytes = options.tunnelRequestBodyMaxBytes ?? TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT2;
2789
+ const bodyMaxBytes = options.tunnelRequestBodyMaxBytes ?? TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT;
3713
2790
  const contentLength = Number(request.headers.get("content-length") ?? Number.NaN);
3714
2791
  if (Number.isFinite(contentLength) && contentLength > bodyMaxBytes) {
3715
- return json("request_body_too_large", "User-hosted request body exceeds the relay limit", 413);
2792
+ return json("request_body_too_large", "Host tunnel request body exceeds the relay limit", 413);
3716
2793
  }
3717
2794
  const bodyBytes = request.method === "GET" || request.method === "HEAD" ? void 0 : await traceSpan(trace, "request-body", () => readRequestBodyCapped(request, bodyMaxBytes));
3718
2795
  if (bodyBytes === "too_large") {
3719
- return json("request_body_too_large", "User-hosted request body exceeds the relay limit", 413);
2796
+ return json("request_body_too_large", "Host tunnel request body exceeds the relay limit", 413);
3720
2797
  }
3721
2798
  const body = bodyBytes ? bytesToBase64(bodyBytes) : void 0;
3722
2799
  const pending = new Promise((resolve, reject) => {
@@ -3724,12 +2801,12 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3724
2801
  tunnel.pending.delete(requestId);
3725
2802
  tunnel.socket.send?.(JSON.stringify({
3726
2803
  type: "http.response.flow",
3727
- protocol: TUNNEL_PROTOCOL_VERSION2,
2804
+ protocol: TUNNEL_PROTOCOL_VERSION,
3728
2805
  request_id: requestId,
3729
2806
  paused: false,
3730
2807
  reason: "closed"
3731
2808
  }));
3732
- reject(new Error("User-hosted tunnel timed out"));
2809
+ reject(hostTunnelFailure("host_tunnel_timeout"));
3733
2810
  }, options.forwardTimeoutMs ?? 3e4);
3734
2811
  tunnel.pending.set(requestId, {
3735
2812
  chunks: [],
@@ -3741,7 +2818,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3741
2818
  });
3742
2819
  tunnel.socket.send?.(JSON.stringify({
3743
2820
  type: "http.request",
3744
- protocol: TUNNEL_PROTOCOL_VERSION2,
2821
+ protocol: TUNNEL_PROTOCOL_VERSION,
3745
2822
  request_id: requestId,
3746
2823
  workspace_id: authorized.target.workspaceId,
3747
2824
  method: request.method,
@@ -3753,15 +2830,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3753
2830
  try {
3754
2831
  return await traceSpan(trace, "tunnel-wait", () => pending);
3755
2832
  } catch (err) {
3756
- const timeout = err instanceof Error && err.message.includes("timed out");
3757
- const responseTooLarge = err instanceof Error && err.message.includes("response body exceeds");
3758
- return Response.json(
3759
- errorBody(
3760
- timeout ? "user_hosted_tunnel_timeout" : responseTooLarge ? "user_hosted_response_body_too_large" : "user_hosted_tunnel_unavailable",
3761
- timeout ? "User-hosted tunnel timed out" : responseTooLarge ? "User-hosted response body exceeds the relay limit" : "User-hosted tunnel is unavailable"
3762
- ),
3763
- { status: responseTooLarge ? 413 : 503 }
3764
- );
2833
+ return hostTunnelFailureResponse(err);
3765
2834
  }
3766
2835
  };
3767
2836
  return {
@@ -3788,7 +2857,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3788
2857
  if (!roomWorkspaceId) return json("relay_workspace_required", "Workspace Relay requests must include a workspace id", 400);
3789
2858
  if (url.pathname.startsWith("/host-tunnels/")) return admitHostTunnel(request, roomWorkspaceId, url);
3790
2859
  if (url.pathname.startsWith("/workspaces/")) {
3791
- if (websocketRequest2(request)) return admitClient(request, roomWorkspaceId);
2860
+ if (websocketRequest(request)) return admitClient(request, roomWorkspaceId);
3792
2861
  const trace = traceRequest(request, "workspace-http", options);
3793
2862
  const roomTotalStartedAt = performance.now();
3794
2863
  const nextRequest = tracedRequest(request, trace);
@@ -3796,7 +2865,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3796
2865
  trace.phases.push({ name: "relay-room-total", ms: performance.now() - roomTotalStartedAt });
3797
2866
  const tracedResponse = withTraceHeaders(response, trace);
3798
2867
  logTrace(options, nextRequest, roomWorkspaceId, trace, tracedResponse);
3799
- return withCors(request, tracedResponse);
2868
+ return withRelayResponseHeaders(request, tracedResponse);
3800
2869
  }
3801
2870
  return json("relay_route_not_found", "Workspace Relay route not found", 404);
3802
2871
  },
@@ -3806,17 +2875,17 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3806
2875
  const attachment = socketAttachment(socket);
3807
2876
  if (!attachment) return;
3808
2877
  if (attachment.kind === "host-tunnel") {
3809
- await handleTunnelMessage(attachment.hostId, { data: message });
2878
+ await handleTunnelMessage(attachment.hostId, socket, { data: message });
3810
2879
  return;
3811
2880
  }
3812
- await handleUserHostedClientMessage(attachment.channelId, socket, message);
2881
+ await handleHostTunnelClientMessage(attachment.channelId, socket, message);
3813
2882
  } catch (err) {
3814
2883
  reportFrameHandlerFailure(socket, err);
3815
2884
  }
3816
2885
  },
3817
2886
  /**
3818
2887
  * Durable Object alarm entry point. Runs the hibernation-safe revocation
3819
- * sweep (W6b.2). The Worker must forward `alarm()` here.
2888
+ * sweep. The Worker must forward `alarm()` here.
3820
2889
  */
3821
2890
  async alarm() {
3822
2891
  try {
@@ -3830,20 +2899,20 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3830
2899
  const attachment = socketAttachment(socket);
3831
2900
  if (!attachment) return;
3832
2901
  if (attachment.kind === "host-tunnel") {
3833
- cleanupHostTunnel(attachment.hostId, socket, "User-hosted tunnel disconnected");
2902
+ cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected");
3834
2903
  return;
3835
2904
  }
3836
- cleanupUserHostedClient(attachment.channelId, socket, { code, reason });
2905
+ cleanupHostTunnelClient(attachment.channelId, socket, { code, reason });
3837
2906
  },
3838
2907
  webSocketError(socket) {
3839
2908
  ensureHibernatedSocketsRebuilt();
3840
2909
  const attachment = socketAttachment(socket);
3841
2910
  if (!attachment) return;
3842
2911
  if (attachment.kind === "host-tunnel") {
3843
- cleanupHostTunnel(attachment.hostId, socket, "User-hosted tunnel disconnected");
2912
+ cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected");
3844
2913
  return;
3845
2914
  }
3846
- cleanupUserHostedClient(attachment.channelId, socket, { code: 1011, reason: "WebSocket error" });
2915
+ cleanupHostTunnelClient(attachment.channelId, socket, { code: 1011, reason: "WebSocket error" });
3847
2916
  },
3848
2917
  state() {
3849
2918
  ensureHibernatedSocketsRebuilt();
@@ -3857,55 +2926,222 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3857
2926
  drain: drainController
3858
2927
  };
3859
2928
  }
2929
+
2930
+ // ../claxedo-helpers/dist/deadline.mjs
2931
+ async function withDeadlineSignal(timeoutMs, work, parent) {
2932
+ const deadline = new AbortController();
2933
+ const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
2934
+ try {
2935
+ return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
2936
+ } finally {
2937
+ clearTimeout(timer);
2938
+ }
2939
+ }
2940
+
2941
+ // src/resolver-clients.ts
2942
+ function parseRuntimeAccessTokenActiveResult(input) {
2943
+ if (!isRecord(input)) return void 0;
2944
+ const row = input;
2945
+ if (row.active === true) return { active: true };
2946
+ if (row.active !== false) return void 0;
2947
+ if (typeof row.code !== "string" || typeof row.reason !== "string") return void 0;
2948
+ return { active: false, code: row.code, reason: row.reason };
2949
+ }
2950
+ function parseHostGenerationResult(input) {
2951
+ if (!isRecord(input)) return void 0;
2952
+ const row = input;
2953
+ if (typeof row.enrollmentId !== "string" || !row.enrollmentId.trim()) return void 0;
2954
+ if (!isHostGeneration(row.generation) || typeof row.revoked !== "boolean") return void 0;
2955
+ return { enrollmentId: row.enrollmentId, generation: row.generation, revoked: row.revoked };
2956
+ }
2957
+ var HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT = 5e3;
2958
+ var HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE = "relay_resolver_enrollment_not_found";
2959
+ function createHostGenerationResolverLookup(url, options) {
2960
+ const fetcher = options.fetch ?? ((target, init) => fetch(target, init));
2961
+ const timeoutMs = options.timeoutMs ?? HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT;
2962
+ return async ({ enrollmentId }) => {
2963
+ const target = new URL(url);
2964
+ target.searchParams.set("enrollmentId", enrollmentId);
2965
+ return withDeadlineSignal(timeoutMs, async (signal) => {
2966
+ const res = await fetcher(target, { headers: options.headers, signal });
2967
+ if (res.status === 404) {
2968
+ const body = await res.json().catch(() => void 0);
2969
+ const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
2970
+ if (error?.code === HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE) return void 0;
2971
+ throw new Error(`relay host-generation resolver failed: 404 (no host-generation route at ${url})`);
2972
+ }
2973
+ if (!res.ok) throw new Error(`relay host-generation resolver failed: ${res.status} ${await res.text()}`);
2974
+ const result = parseHostGenerationResult(await res.json());
2975
+ if (!result) throw new Error("relay host-generation resolver returned a malformed result");
2976
+ return result;
2977
+ });
2978
+ };
2979
+ }
2980
+ var RESOLVER_CACHE_MAX_ENTRIES = 8192;
2981
+ var REVOCATION_CACHE_TTL_MS_DEFAULT = 1e4;
2982
+ var TARGET_CACHE_TTL_MS_DEFAULT = 1e4;
2983
+ var DURABLE_OBJECT_TARGET_CACHE_TTL_MS_DEFAULT = 30 * 6e4;
2984
+ function pruneExpiringCache(cache, now, maxEntries) {
2985
+ for (const [key, entry] of cache) {
2986
+ if (entry.expiresAt <= now) cache.delete(key);
2987
+ }
2988
+ while (cache.size >= maxEntries) {
2989
+ const key = cache.keys().next().value;
2990
+ if (!key) return;
2991
+ cache.delete(key);
2992
+ }
2993
+ }
2994
+ function createCachedRevocationClient(inner, options = {}) {
2995
+ const ttlMs = options.ttlMs ?? REVOCATION_CACHE_TTL_MS_DEFAULT;
2996
+ const now = options.now ?? Date.now;
2997
+ const cache = /* @__PURE__ */ new Map();
2998
+ return async (args) => {
2999
+ const at = now();
3000
+ const entry = cache.get(args.jti);
3001
+ if (entry && entry.expiresAt > at) {
3002
+ if (entry.result) return entry.result;
3003
+ if (entry.promise) return await entry.promise;
3004
+ }
3005
+ const promise = inner(args).then((result) => {
3006
+ pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
3007
+ cache.set(args.jti, { result, expiresAt: now() + ttlMs });
3008
+ return result;
3009
+ }).catch((err) => {
3010
+ cache.delete(args.jti);
3011
+ throw err;
3012
+ });
3013
+ cache.set(args.jti, { promise, expiresAt: at + ttlMs });
3014
+ return await promise;
3015
+ };
3016
+ }
3017
+ function runtimeAccessTokenRevocationDelayMs(input = {}) {
3018
+ const configuredTtl = input.revocationCacheTtlMs ?? REVOCATION_CACHE_TTL_MS_DEFAULT;
3019
+ const ttlMs = Number.isFinite(configuredTtl) && configuredTtl > 0 ? configuredTtl : 0;
3020
+ if (input.activeCheckIntervalMs === void 0) return ttlMs;
3021
+ if (!Number.isFinite(input.activeCheckIntervalMs) || input.activeCheckIntervalMs <= 0) {
3022
+ return Number.POSITIVE_INFINITY;
3023
+ }
3024
+ return ttlMs + input.activeCheckIntervalMs;
3025
+ }
3026
+ function createCachedHostGenerationClient(inner, options = {}) {
3027
+ const ttlMs = options.ttlMs ?? 1e4;
3028
+ const now = options.now ?? Date.now;
3029
+ const cache = /* @__PURE__ */ new Map();
3030
+ return async (args) => {
3031
+ for (; ; ) {
3032
+ const at = now();
3033
+ const entry = cache.get(args.enrollmentId);
3034
+ if (entry && entry.expiresAt > at) {
3035
+ if (entry.result && entry.result.generation >= args.generation) return entry.result;
3036
+ if (!entry.result && entry.promise) {
3037
+ const shared = await entry.promise;
3038
+ if (!shared || shared.generation >= args.generation) return shared;
3039
+ continue;
3040
+ }
3041
+ }
3042
+ const promise = inner(args).then((result) => {
3043
+ pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
3044
+ if (result) cache.set(args.enrollmentId, { result, expiresAt: now() + ttlMs });
3045
+ else cache.delete(args.enrollmentId);
3046
+ return result;
3047
+ }).catch((err) => {
3048
+ cache.delete(args.enrollmentId);
3049
+ throw err;
3050
+ });
3051
+ cache.set(args.enrollmentId, { promise, expiresAt: at + ttlMs });
3052
+ return await promise;
3053
+ }
3054
+ };
3055
+ }
3056
+ function createCachedTargetClient(inner, options = {}) {
3057
+ const ttlMs = options.ttlMs ?? TARGET_CACHE_TTL_MS_DEFAULT;
3058
+ const durableObjectTtlMs = options.durableObjectTtlMs ?? DURABLE_OBJECT_TARGET_CACHE_TTL_MS_DEFAULT;
3059
+ const now = options.now ?? Date.now;
3060
+ const cache = /* @__PURE__ */ new Map();
3061
+ return async (args) => {
3062
+ const key = `${args.workspaceId}\0${args.hostId}\0${args.routingId ?? ""}`;
3063
+ const at = now();
3064
+ const entry = cache.get(key);
3065
+ if (entry && entry.expiresAt > at) {
3066
+ if (entry.target) return entry.target;
3067
+ if (entry.promise) return await entry.promise;
3068
+ }
3069
+ const promise = inner(args).then((target) => {
3070
+ pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
3071
+ if (target) cache.set(key, { target, expiresAt: now() + (target.backing === "durable-object" ? durableObjectTtlMs : ttlMs) });
3072
+ else cache.delete(key);
3073
+ return target;
3074
+ }).catch((err) => {
3075
+ cache.delete(key);
3076
+ throw err;
3077
+ });
3078
+ cache.set(key, { promise, expiresAt: at + ttlMs });
3079
+ return await promise;
3080
+ };
3081
+ }
3860
3082
  export {
3861
3083
  DEFAULT_RELAY_APP_ORIGINS,
3862
3084
  DEFAULT_RELAY_LOCATION_HINT,
3085
+ RELAY_ALLOWED_REQUEST_HEADERS,
3863
3086
  RELAY_LOCATION_HINTS,
3864
3087
  RELAY_REGION_HEADER,
3088
+ RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT,
3089
+ RUNTIME_ACCESS_TOKEN_ISSUED_AT_FLOOR_SECONDS,
3090
+ SESSION_HOST_LOCATION_HINT,
3865
3091
  WorkspaceRelayAuthError,
3866
- __slowConsumerInternalsForTest,
3867
3092
  authorizeWorkspaceRelayRequest,
3868
3093
  bytesToBase64,
3094
+ checkHostTunnelGeneration,
3095
+ corsPreflight,
3096
+ createCachedHostGenerationClient,
3869
3097
  createCachedRevocationClient,
3870
3098
  createCachedTargetClient,
3099
+ createHostGenerationResolverLookup,
3871
3100
  createOriginMatcher,
3872
- createWorkspaceRelay,
3873
- createWorkspaceRelayBun,
3874
3101
  createWorkspaceRelayDirectory,
3875
3102
  createWorkspaceRelayDurableObjectGateway,
3876
3103
  createWorkspaceRelayDurableObjectRoom,
3877
- createWorkspaceRelayTrace,
3878
- disposeAuditSampler,
3879
- disposeWorkspaceRelayDirectory,
3880
- forwardWorkspaceRelayRequest,
3881
- getAuditSuppressedCount,
3104
+ deriveRelayHostKid,
3105
+ deriveRelayHostPublicKey,
3106
+ hostTunnelIncumbentOutranks,
3882
3107
  hostTunnelTokenAudience,
3883
- isRelayClaimPair,
3108
+ isAllowedRelayTargetBaseUrl,
3109
+ isHostGeneration,
3110
+ isRelayBacking,
3884
3111
  mintHostTunnelToken,
3885
3112
  mintRelayHostToken,
3886
3113
  mintRuntimeAccessToken,
3887
3114
  parseAllowedOrigins,
3888
- relayBufferedBytes,
3115
+ parseRuntimeAccessTokenActiveResult,
3116
+ parseWorkspaceRelayTarget,
3117
+ pruneExpiringCache,
3889
3118
  relayFrameDiagnostics,
3119
+ relayHostInputFromRuntimeClaims,
3890
3120
  relayHostTokenAudience,
3891
3121
  relayHostTokenIssuer,
3892
3122
  relayLocationHint,
3893
- relayOverBackpressureLimit,
3894
3123
  relayRequestCountry,
3895
3124
  relayRequestRegion,
3125
+ relayRoomPlacement,
3896
3126
  runtimeAccessTokenAudience,
3897
3127
  runtimeAccessTokenIssuer,
3128
+ runtimeAccessTokenRevocationDelayMs,
3898
3129
  setWorkspaceRelayAllowedOrigins,
3899
3130
  setWorkspaceRelayAppOrigins,
3131
+ toChannelIdentityClaim,
3132
+ tokenPurposeClaims,
3133
+ tokenScopeClaims,
3134
+ tokenScopePayload,
3900
3135
  validateRuntimeAccessTokenClaims,
3901
3136
  verifyHostTunnelToken,
3902
3137
  verifyRelayHostToken,
3903
3138
  verifyRuntimeAccessToken,
3139
+ withRelayResponseHeaders,
3904
3140
  workspaceRelayDurableObjectRoomName,
3905
3141
  workspaceRelayDurableObjectWorkspaceId,
3906
3142
  workspaceRelayForwardHeaders,
3907
3143
  workspaceRelayForwardRequestInit,
3908
- workspaceRelayServerTiming,
3144
+ workspaceRelayRoomPlacement,
3909
3145
  workspaceRelayTargetUrl,
3910
- workspaceRelayTimingResponse
3146
+ workspaceUpgradeOriginDenied
3911
3147
  };