@claxedo/workspace-relay 0.8.0 → 0.10.1

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
@@ -5,6 +5,9 @@ import { SignJWT, errors, exportJWK, importJWK, jwtVerify } from "jose";
5
5
  function isRecord(value) {
6
6
  return value !== null && typeof value === "object" && !Array.isArray(value);
7
7
  }
8
+ function asRecord(value) {
9
+ return isRecord(value) ? value : void 0;
10
+ }
8
11
  function isFiniteNumber(value) {
9
12
  return typeof value === "number" && Number.isFinite(value);
10
13
  }
@@ -16,12 +19,21 @@ function numberClaim(payload, key) {
16
19
  }
17
20
 
18
21
  // src/auth.ts
22
+ import { CURRENT_CHANNEL_IDENTITY_VERSION } from "@claxedo/workspace-relay-protocol";
19
23
  var algorithms = ["EdDSA", "ES256", "RS256"];
20
24
  var runtimeAccessTokenIssuer = "claxedo-control-plane";
21
25
  var runtimeAccessTokenAudience = "workspace-relay";
22
26
  var hostTunnelTokenAudience = "workspace-relay-host-tunnel";
23
27
  var relayHostTokenIssuer = "workspace-relay";
24
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
+ }
25
37
  var WorkspaceRelayAuthError = class extends Error {
26
38
  constructor(code, message) {
27
39
  super(message);
@@ -53,7 +65,6 @@ function isHostGeneration(value) {
53
65
  function hostGenerationClaims(payload) {
54
66
  const enrollment_id = stringClaim(payload, "enrollment_id");
55
67
  const generation = payload.generation;
56
- if (generation === void 0) return enrollment_id ? { enrollment_id } : {};
57
68
  if (!isHostGeneration(generation) || !enrollment_id) return null;
58
69
  return { enrollment_id, generation };
59
70
  }
@@ -80,8 +91,40 @@ function actorProfilePayload(input) {
80
91
  ...input.actorAvatarUrl ? { actor_avatar_url: input.actorAvatarUrl } : {}
81
92
  };
82
93
  }
83
- function isRelayClaimPair(input) {
84
- return input.access === "cloud" && input.backing === "cloud-vm" || input.access === "user-hosted" && input.backing === "local-worktree";
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";
85
128
  }
86
129
  function checkHostTunnelTarget(payload, expected) {
87
130
  if (stringClaim(payload, "host_id") !== expected.hostId) {
@@ -142,12 +185,17 @@ async function mintRuntimeAccessToken(input, key, alg) {
142
185
  return await new SignJWT({
143
186
  principal_kind: input.principalKind,
144
187
  actor_id: input.actorId,
188
+ ...input.userId ? { user_id: input.userId } : {},
145
189
  actor_kind: input.actorKind,
146
190
  ...actorProfilePayload(input),
191
+ ...channelIdentityPayload(input),
147
192
  org_id: input.orgId,
148
193
  workspace_id: input.workspaceId,
149
194
  host_id: input.hostId,
150
- role: input.role
195
+ role: input.role,
196
+ ...tokenScopePayload(input.sessionId),
197
+ ...input.purpose ? { purpose: input.purpose } : {},
198
+ ...input.routingId !== void 0 ? { routing_id: input.routingId } : {}
151
199
  }).setProtectedHeader({ alg: requireAlgorithm(alg) }).setIssuer(runtimeAccessTokenIssuer).setAudience(runtimeAccessTokenAudience).setIssuedAt(now).setExpirationTime(now + (input.ttlSeconds ?? 30 * 60)).setJti(input.jti ?? jti()).sign(key);
152
200
  }
153
201
  async function verifyRuntimeAccessToken(token, key, expected) {
@@ -160,8 +208,27 @@ async function verifyRuntimeAccessToken(token, key, expected) {
160
208
  if (!claims) {
161
209
  throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Runtime Access Token claims are incomplete");
162
210
  }
211
+ checkIssuedAtFloor(claims.iat);
163
212
  return claims;
164
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
+ }
165
232
  function validateRuntimeAccessTokenClaims(input, expected) {
166
233
  const payload = input;
167
234
  if (stringClaim(payload, "iss") !== runtimeAccessTokenIssuer || stringClaim(payload, "aud") !== runtimeAccessTokenAudience) {
@@ -172,18 +239,20 @@ function validateRuntimeAccessTokenClaims(input, expected) {
172
239
  if (!claims) {
173
240
  throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Runtime Access Token claims are incomplete");
174
241
  }
242
+ checkIssuedAtFloor(claims.iat);
243
+ checkTokenTimeClaims(payload, claims.exp, "Runtime Access Token");
175
244
  return claims;
176
245
  }
177
246
  async function mintHostTunnelToken(input, key, alg) {
178
247
  const now = seconds(input.now);
179
- if (input.generation !== void 0 && (!isHostGeneration(input.generation) || !input.enrollmentId)) {
248
+ if (!isHostGeneration(input.generation) || !input.enrollmentId?.trim()) {
180
249
  throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Host Tunnel Token generation requires a non-negative integer and an enrollment id");
181
250
  }
182
251
  return await new SignJWT({
183
252
  host_id: input.hostId,
184
253
  workspace_ids: input.workspaceIds,
185
- ...input.enrollmentId ? { enrollment_id: input.enrollmentId } : {},
186
- ...input.generation !== void 0 ? { generation: input.generation } : {}
254
+ enrollment_id: input.enrollmentId,
255
+ generation: input.generation
187
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);
188
257
  }
189
258
  async function verifyHostTunnelToken(token, key, expected) {
@@ -198,9 +267,34 @@ async function verifyHostTunnelToken(token, key, expected) {
198
267
  }
199
268
  return claims;
200
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
+ }
201
295
  async function mintRelayHostToken(input, key, alg) {
202
- if (!isRelayClaimPair(input)) {
203
- 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");
204
298
  }
205
299
  const now = seconds(input.now);
206
300
  const protectedHeader = { alg: requireAlgorithm(alg) };
@@ -208,13 +302,16 @@ async function mintRelayHostToken(input, key, alg) {
208
302
  return await new SignJWT({
209
303
  principal_kind: input.principalKind,
210
304
  actor_id: input.actorId,
305
+ ...input.userId ? { user_id: input.userId } : {},
211
306
  actor_kind: input.actorKind,
212
307
  ...actorProfilePayload(input),
308
+ ...channelIdentityPayload(input),
213
309
  org_id: input.orgId,
214
310
  workspace_id: input.workspaceId,
215
311
  host_id: input.hostId,
216
312
  role: input.role,
217
- access: input.access,
313
+ ...tokenScopePayload(input.sessionId),
314
+ ...input.purpose ? { purpose: input.purpose } : {},
218
315
  backing: input.backing,
219
316
  parent_jti: input.parentJti
220
317
  }).setProtectedHeader(protectedHeader).setIssuer(relayHostTokenIssuer).setAudience(relayHostTokenAudience).setIssuedAt(now).setExpirationTime(now + (input.ttlSeconds ?? 60)).setJti(input.jti ?? jti()).sign(key);
@@ -236,48 +333,70 @@ function runtimeClaims(payload) {
236
333
  const iat = numberClaim(payload, "iat");
237
334
  const principal_kind = stringClaim(payload, "principal_kind");
238
335
  const actor_id = stringClaim(payload, "actor_id");
336
+ const user_id = stringClaim(payload, "user_id");
239
337
  const actor_kind = stringClaim(payload, "actor_kind");
240
338
  const jti2 = stringClaim(payload, "jti");
241
339
  const org_id = stringClaim(payload, "org_id");
242
340
  const workspace_id = stringClaim(payload, "workspace_id");
243
341
  const host_id = stringClaim(payload, "host_id");
244
342
  const role = roleClaim(payload);
245
- if (!exp || !iat || !jti2 || !org_id || !workspace_id || !host_id || !role || !actor_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;
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;
246
346
  const actorProfile = actorProfileClaims(payload);
247
347
  if (!actorProfile) return void 0;
348
+ const channel_identity = channelIdentityClaims(payload);
349
+ const routing_id = stringClaim(payload, "routing_id");
248
350
  return {
249
351
  iss: runtimeAccessTokenIssuer,
250
352
  aud: runtimeAccessTokenAudience,
251
353
  principal_kind,
252
354
  actor_id,
355
+ ...user_id ? { user_id } : {},
253
356
  actor_kind,
254
357
  ...actorProfile,
358
+ ...channel_identity ? { channel_identity } : {},
359
+ ...routing_id !== void 0 ? { routing_id } : {},
255
360
  org_id,
256
361
  workspace_id,
257
362
  host_id,
258
363
  role,
364
+ ...reach,
365
+ ...purpose,
259
366
  exp,
260
367
  iat,
261
368
  jti: jti2
262
369
  };
263
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
+ }
264
384
  function relayHostClaims(payload) {
265
385
  const base = runtimeClaims({
266
386
  ...payload,
267
387
  iss: runtimeAccessTokenIssuer,
268
388
  aud: runtimeAccessTokenAudience
269
389
  });
270
- const access = stringClaim(payload, "access");
390
+ if (payload.access !== void 0) return void 0;
271
391
  const backing = stringClaim(payload, "backing");
272
392
  const parent_jti = stringClaim(payload, "parent_jti");
273
- const pair = { access, backing };
274
- if (!base || !parent_jti || !isRelayClaimPair(pair)) return void 0;
393
+ if (!base || !parent_jti || !isRelayBacking(backing)) return void 0;
275
394
  return {
276
395
  ...base,
277
396
  iss: relayHostTokenIssuer,
278
397
  aud: relayHostTokenAudience,
279
398
  parent_jti,
280
- ...pair
399
+ backing
281
400
  };
282
401
  }
283
402
  function hostTunnelClaims(payload) {
@@ -302,11 +421,114 @@ function hostTunnelClaims(payload) {
302
421
  };
303
422
  }
304
423
 
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
+
305
527
  // src/cloudflare.ts
306
- import { trimToUndefined } from "@claxedo/helpers/string";
528
+ import { trimToUndefined as trimToUndefined5 } from "@claxedo/helpers/string";
307
529
 
308
530
  // src/http.ts
309
- import { bearerToken } from "@claxedo/helpers/string";
531
+ import { bearerToken, trimToUndefined } from "@claxedo/helpers/string";
310
532
  function errorBody(code, message) {
311
533
  return {
312
534
  error: {
@@ -315,200 +537,148 @@ function errorBody(code, message) {
315
537
  }
316
538
  };
317
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
+ }
318
549
 
319
550
  // src/cloudflare.ts
320
551
  import {
552
+ closeCode,
553
+ closeReason,
321
554
  makeTunnelPong,
322
555
  TUNNEL_PROTOCOL_VERSION,
323
556
  validateTunnelMessage
324
557
  } from "@claxedo/workspace-relay-protocol";
325
558
 
326
559
  // src/server.ts
327
- import { Hono } from "hono";
328
- import { cors } from "hono/cors";
329
- import { exportJWK as exportJWK2 } from "jose";
330
-
331
- // src/cors-origins.ts
332
- var DEFAULT_RELAY_APP_ORIGINS = [
333
- "http://localhost:*",
334
- "http://127.0.0.1:*",
335
- "https://claxedo.com",
336
- "https://*.claxedo.com"
337
- ];
338
- function parseAllowedOrigins(raw) {
339
- const entries = (raw ?? "").split(",").map((item) => item.trim()).filter(Boolean);
340
- return entries.length ? entries : void 0;
341
- }
342
- function createOriginMatcher(patterns) {
343
- const exact = /* @__PURE__ */ new Set();
344
- const httpsSuffixes = [];
345
- const httpPortPrefixes = [];
346
- for (const pattern of patterns) {
347
- if (pattern.startsWith("https://*.")) {
348
- httpsSuffixes.push(pattern.slice("https://*".length));
349
- } else if (pattern.startsWith("http://") && pattern.endsWith(":*")) {
350
- httpPortPrefixes.push(pattern.slice(0, -1));
351
- } else {
352
- exact.add(pattern);
353
- }
354
- }
355
- return (origin) => {
356
- if (exact.has(origin)) return true;
357
- if (origin.startsWith("https://")) {
358
- return httpsSuffixes.some(
359
- (suffix) => origin.endsWith(suffix) && origin.length > "https://".length + suffix.length
360
- );
361
- }
362
- if (origin.startsWith("http://")) {
363
- return httpPortPrefixes.some(
364
- (prefix) => origin.startsWith(prefix) && /^\d+$/.test(origin.slice(prefix.length))
365
- );
366
- }
367
- return false;
368
- };
369
- }
560
+ import { sessionScopeReaches } from "@claxedo/workspace-relay-protocol";
370
561
 
371
- // src/user-hosted-forwarding.ts
372
- function isUserHostedTarget(target) {
373
- return target.access === "user-hosted";
562
+ // src/host-tunnel-forwarding.ts
563
+ function isHostTunnelTarget(target) {
564
+ return target.backing === "local-worktree";
374
565
  }
375
566
 
376
- // src/server.ts
567
+ // src/relay-target.ts
377
568
  function parseWorkspaceRelayTarget(input) {
378
569
  if (!isRecord(input)) return void 0;
379
570
  const row = input;
380
- const { workspaceId, hostId, baseUrl, upstreamHeaders } = row;
571
+ const { workspaceId, hostId, baseUrl } = row;
381
572
  if (typeof workspaceId !== "string" || typeof hostId !== "string" || typeof baseUrl !== "string") return void 0;
382
- if (upstreamHeaders !== void 0 && !isStringRecord(upstreamHeaders)) return void 0;
383
- const pair = relayClaimPair(row.access, row.backing);
384
- if (!pair) return void 0;
573
+ if (row.access !== void 0) return void 0;
574
+ if (!isRelayBacking(row.backing)) return void 0;
575
+ if (!isAllowedRelayTargetBaseUrl({ baseUrl, backing: row.backing })) return void 0;
385
576
  return {
386
577
  workspaceId,
387
578
  hostId,
388
579
  baseUrl,
389
- ...upstreamHeaders ? { upstreamHeaders } : {},
390
- ...pair
580
+ backing: row.backing
391
581
  };
392
582
  }
393
- function parseRuntimeAccessTokenActiveResult(input) {
394
- if (!isRecord(input)) return void 0;
395
- const row = input;
396
- if (row.active === true) return { active: true };
397
- if (row.active !== false) return void 0;
398
- if (typeof row.code !== "string" || typeof row.reason !== "string") return void 0;
399
- return { active: false, code: row.code, reason: row.reason };
400
- }
401
- function parseHostGenerationResult(input) {
402
- if (!isRecord(input)) return void 0;
403
- const row = input;
404
- if (typeof row.enrollmentId !== "string" || !row.enrollmentId.trim()) return void 0;
405
- if (!isHostGeneration(row.generation) || typeof row.revoked !== "boolean") return void 0;
406
- return { enrollmentId: row.enrollmentId, generation: row.generation, revoked: row.revoked };
583
+ function parseAbsoluteHttpUrl(baseUrl) {
584
+ let url;
585
+ try {
586
+ url = new URL(baseUrl);
587
+ } catch {
588
+ return void 0;
589
+ }
590
+ return url.protocol === "http:" || url.protocol === "https:" ? url : void 0;
407
591
  }
408
- var HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT = 5e3;
409
- var HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE = "relay_resolver_enrollment_not_found";
410
- function createHostGenerationResolverLookup(url, options) {
411
- const fetcher = options.fetch ?? ((target, init) => fetch(target, init));
412
- const timeoutMs = options.timeoutMs ?? HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT;
413
- return async ({ enrollmentId }) => {
414
- const target = new URL(url);
415
- target.searchParams.set("enrollmentId", enrollmentId);
416
- const res = await fetcher(target, { headers: options.headers, signal: AbortSignal.timeout(timeoutMs) });
417
- if (res.status === 404) {
418
- const body = await res.json().catch(() => void 0);
419
- const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
420
- if (error?.code === HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE) return void 0;
421
- throw new Error(`relay host-generation resolver failed: 404 (no host-generation route at ${url})`);
422
- }
423
- if (!res.ok) throw new Error(`relay host-generation resolver failed: ${res.status} ${await res.text()}`);
424
- const result = parseHostGenerationResult(await res.json());
425
- if (!result) throw new Error("relay host-generation resolver returned a malformed result");
426
- return result;
427
- };
592
+ function isLoopbackTargetHostname(hostname) {
593
+ const host = hostname.toLowerCase();
594
+ if (host === "localhost" || host === "[::1]" || host.endsWith(".localhost")) return true;
595
+ const ipv4 = /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
596
+ return !!ipv4 && ipv4.slice(1).every((octet) => Number(octet) <= 255);
428
597
  }
429
- function hostTunnelIncumbentOutranks(incumbentGeneration, candidateGeneration) {
430
- if (incumbentGeneration === void 0) return false;
431
- return candidateGeneration === void 0 || incumbentGeneration > candidateGeneration;
598
+ function isAllowedRelayTargetBaseUrl(target) {
599
+ if (target.backing === "durable-object") return target.baseUrl === "";
600
+ if (isHostTunnelTarget(target)) {
601
+ if (target.baseUrl === "") return true;
602
+ return parseAbsoluteHttpUrl(target.baseUrl) !== void 0;
603
+ }
604
+ const url = parseAbsoluteHttpUrl(target.baseUrl);
605
+ if (!url) return false;
606
+ return url.protocol === "https:" || isLoopbackTargetHostname(url.hostname);
432
607
  }
433
- function relayClaimPair(access, backing) {
434
- if (access === "cloud" && backing === "cloud-vm") return { access, backing };
435
- if (access === "user-hosted" && backing === "local-worktree") return { access, backing };
436
- return void 0;
608
+
609
+ // src/relay-audit.ts
610
+ var auditSamplers = /* @__PURE__ */ new WeakMap();
611
+ function getOrCreateAuditSampler(options) {
612
+ let state = auditSamplers.get(options);
613
+ if (!state) {
614
+ const sampleRate = clampSampleRate(options.auditAcceptSampleRate);
615
+ state = {
616
+ suppressedCount: 0,
617
+ timer: void 0,
618
+ flushIntervalMs: options.auditFlushIntervalMs ?? 6e4,
619
+ random: options.random ?? Math.random,
620
+ sampleRate
621
+ };
622
+ auditSamplers.set(options, state);
623
+ }
624
+ return state;
437
625
  }
438
- function isStringRecord(input) {
439
- return isRecord(input) && Object.values(input).every((value) => typeof value === "string");
626
+ function clampSampleRate(rate) {
627
+ if (rate === void 0) return 1;
628
+ if (!Number.isFinite(rate)) return 1;
629
+ if (rate < 0) return 0;
630
+ if (rate > 1) return 1;
631
+ return rate;
440
632
  }
441
- var RELAY_HOST_TOKEN_CACHE_MAX_ENTRIES = 4096;
442
- var RUNTIME_ACCESS_TOKEN_CACHE_MAX_ENTRIES = 8192;
443
- var RESOLVER_CACHE_MAX_ENTRIES = 8192;
444
- var RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS_DEFAULT = 1e4;
445
- var relayHostTokenCaches = /* @__PURE__ */ new WeakMap();
446
- var runtimeAccessTokenCaches = /* @__PURE__ */ new WeakMap();
447
- function pruneExpiringCache(cache, now, maxEntries) {
448
- for (const [key, entry] of cache) {
449
- if (entry.expiresAt <= now) cache.delete(key);
450
- }
451
- while (cache.size >= maxEntries) {
452
- const key = cache.keys().next().value;
453
- if (!key) return;
454
- cache.delete(key);
633
+ function ensureFlushTimer(options, state) {
634
+ if (state.timer) return;
635
+ if (state.sampleRate >= 1) return;
636
+ if (state.flushIntervalMs <= 0) return;
637
+ const timer = setInterval(() => {
638
+ flushSuppressedSummary(options, state);
639
+ }, state.flushIntervalMs);
640
+ if (typeof timer.unref === "function") {
641
+ timer.unref();
455
642
  }
643
+ state.timer = timer;
456
644
  }
457
- function createCachedRevocationClient(inner, options = {}) {
458
- const ttlMs = options.ttlMs ?? 1e4;
459
- const now = options.now ?? Date.now;
460
- const cache = /* @__PURE__ */ new Map();
461
- return async (args) => {
462
- const at = now();
463
- const entry = cache.get(args.jti);
464
- if (entry && entry.expiresAt > at) {
465
- if (entry.result) return entry.result;
466
- if (entry.promise) return await entry.promise;
467
- }
468
- const promise = inner(args).then((result) => {
469
- pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
470
- cache.set(args.jti, { result, expiresAt: now() + ttlMs });
471
- return result;
472
- }).catch((err) => {
473
- cache.delete(args.jti);
474
- throw err;
475
- });
476
- cache.set(args.jti, { promise, expiresAt: at + ttlMs });
477
- return await promise;
478
- };
645
+ function flushSuppressedSummary(options, state) {
646
+ const count = state.suppressedCount;
647
+ if (count <= 0) return;
648
+ state.suppressedCount = 0;
649
+ void options.audit?.({
650
+ action: "relay.request.suppressed_summary",
651
+ result: "allow",
652
+ method: "INTERNAL",
653
+ path: "/",
654
+ suppressedCount: count
655
+ });
479
656
  }
480
- function createCachedHostGenerationClient(inner, options = {}) {
481
- const ttlMs = options.ttlMs ?? 1e4;
482
- const now = options.now ?? Date.now;
483
- const cache = /* @__PURE__ */ new Map();
484
- return async (args) => {
485
- for (; ; ) {
486
- const at = now();
487
- const entry = cache.get(args.enrollmentId);
488
- if (entry && entry.expiresAt > at) {
489
- if (entry.result && entry.result.generation >= args.generation) return entry.result;
490
- if (!entry.result && entry.promise) {
491
- const shared = await entry.promise;
492
- if (!shared || shared.generation >= args.generation) return shared;
493
- continue;
494
- }
657
+ async function audit(options, event) {
658
+ if (event.action === "relay.request.accepted") {
659
+ const state = getOrCreateAuditSampler(options);
660
+ if (state.sampleRate < 1) {
661
+ ensureFlushTimer(options, state);
662
+ if (state.random() >= state.sampleRate) {
663
+ state.suppressedCount += 1;
664
+ return;
495
665
  }
496
- const promise = inner(args).then((result) => {
497
- pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
498
- if (result) cache.set(args.enrollmentId, { result, expiresAt: now() + ttlMs });
499
- else cache.delete(args.enrollmentId);
500
- return result;
501
- }).catch((err) => {
502
- cache.delete(args.enrollmentId);
503
- throw err;
504
- });
505
- cache.set(args.enrollmentId, { promise, expiresAt: at + ttlMs });
506
- return await promise;
507
666
  }
508
- };
667
+ }
668
+ await options.audit?.(event);
669
+ }
670
+
671
+ // src/server.ts
672
+ function hostTunnelIncumbentOutranks(incumbentGeneration, candidateGeneration) {
673
+ return incumbentGeneration > candidateGeneration;
509
674
  }
675
+ var RELAY_HOST_TOKEN_CACHE_MAX_ENTRIES = 4096;
676
+ var RUNTIME_ACCESS_TOKEN_CACHE_MAX_ENTRIES = 8192;
677
+ var RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS_DEFAULT = 1e4;
678
+ var RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
679
+ var relayHostTokenCaches = /* @__PURE__ */ new WeakMap();
680
+ var runtimeAccessTokenCaches = /* @__PURE__ */ new WeakMap();
510
681
  async function checkHostTunnelGeneration(lookup, claims) {
511
- if (claims.generation === void 0 || !claims.enrollment_id) return { ok: true };
512
682
  if (!lookup) {
513
683
  return {
514
684
  ok: false,
@@ -542,31 +712,6 @@ async function checkHostTunnelGeneration(lookup, claims) {
542
712
  }
543
713
  return { ok: true };
544
714
  }
545
- function createCachedTargetClient(inner, options = {}) {
546
- const ttlMs = options.ttlMs ?? 5e3;
547
- const now = options.now ?? Date.now;
548
- const cache = /* @__PURE__ */ new Map();
549
- return async (args) => {
550
- const key = `${args.workspaceId}\0${args.hostId}`;
551
- const at = now();
552
- const entry = cache.get(key);
553
- if (entry && entry.expiresAt > at) {
554
- if (entry.result) return entry.result;
555
- if (entry.promise) return await entry.promise;
556
- }
557
- const promise = inner(args).then((result) => {
558
- pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
559
- if (result) cache.set(key, { result, expiresAt: now() + ttlMs });
560
- else cache.delete(key);
561
- return result;
562
- }).catch((err) => {
563
- cache.delete(key);
564
- throw err;
565
- });
566
- cache.set(key, { promise, expiresAt: at + ttlMs });
567
- return await promise;
568
- };
569
- }
570
715
  function protocolToken(header) {
571
716
  return header?.split(",").map((item) => item.trim()).find((item) => item.startsWith("claxedo-rat."))?.slice("claxedo-rat.".length);
572
717
  }
@@ -610,27 +755,23 @@ function forwardHeaders(input, relayHostToken, workspaceId, options = {}) {
610
755
  for (const name of inboundNames) {
611
756
  if (isDangerousInboundHeader(name)) headers.delete(name);
612
757
  }
613
- if (options.userHosted) headers.delete("cookie");
758
+ if (options.hostTunnel) headers.delete("cookie");
614
759
  headers.set("accept-encoding", "identity");
615
760
  headers.delete("authorization");
616
761
  headers.delete("Authorization");
617
762
  headers.set("Authorization", `Bearer ${relayHostToken}`);
618
763
  headers.set("x-workspace-id", workspaceId);
619
- headers.set("X-Daytona-Skip-Preview-Warning", "true");
620
- headers.set("X-Daytona-Skip-Last-Activity-Update", "true");
621
- for (const [name, value] of Object.entries(options.upstreamHeaders ?? {})) {
622
- const trimmed = value.trim();
623
- if (trimmed) headers.set(name, trimmed);
624
- }
625
764
  headers.set("x-forwarded-by", "workspace-relay");
626
765
  return headers;
627
766
  }
628
767
  function targetUrl(target, path, search) {
629
- const url = new URL(path.replace(/^\/+/, ""), target.baseUrl.endsWith("/") ? target.baseUrl : `${target.baseUrl}/`);
768
+ const base = new URL(target.baseUrl.endsWith("/") ? target.baseUrl : `${target.baseUrl}/`);
769
+ const url = new URL(base);
770
+ url.pathname = `${base.pathname}${path.replace(/^\/+/, "")}`;
630
771
  url.search = search;
772
+ url.hash = "";
631
773
  return url;
632
774
  }
633
- var RELAY_VIEWER_DENIED_PATH = /^\/api\/wr\/pty(?:\/|$)/;
634
775
  function relayRuntimePath(request) {
635
776
  try {
636
777
  const decoded = decodeURIComponent(new URL(request.url).pathname.replace(/^\/workspaces\/[^/]+\/?/, "/")).replace(/^\/+/, "/");
@@ -639,14 +780,6 @@ function relayRuntimePath(request) {
639
780
  return void 0;
640
781
  }
641
782
  }
642
- function roleAllowsRelayRequest(role, method, path) {
643
- if (role === "owner" || role === "admin" || role === "editor") return true;
644
- if (role === "viewer") {
645
- if (RELAY_VIEWER_DENIED_PATH.test(path)) return false;
646
- return method === "GET" || method === "HEAD" || method === "OPTIONS";
647
- }
648
- return false;
649
- }
650
783
  function relayHostTokenCacheTtlMs(options, claims) {
651
784
  const configured = options.relayHostTokenCacheTtlMs ?? 3e4;
652
785
  if (!Number.isFinite(configured) || configured <= 0) return 0;
@@ -663,7 +796,6 @@ function relayHostTokenCacheKey(options, claims, target) {
663
796
  claims.role,
664
797
  claims.workspace_id,
665
798
  claims.host_id,
666
- target.access,
667
799
  target.backing,
668
800
  options.relayHostMintKid ?? ""
669
801
  ].join("\0");
@@ -742,6 +874,13 @@ function pruneRelayHostTokenCache(cache, now) {
742
874
  cache.delete(key);
743
875
  }
744
876
  }
877
+ function relayHostMintInput(options, claims, target) {
878
+ return {
879
+ ...relayHostInputFromRuntimeClaims(claims),
880
+ ...target,
881
+ ...options.relayHostMintKid ? { kid: options.relayHostMintKid } : {}
882
+ };
883
+ }
745
884
  async function relayHostTokenFor(options, claims, target, trace) {
746
885
  const ttlMs = relayHostTokenCacheTtlMs(options, claims);
747
886
  if (ttlMs <= 0) {
@@ -762,21 +901,11 @@ async function relayHostTokenFor(options, claims, target, trace) {
762
901
  let promise;
763
902
  try {
764
903
  const token = await trace.span("rht-mint", async () => {
765
- promise = mintRelayHostToken({
766
- principalKind: claims.principal_kind,
767
- actorId: claims.actor_id,
768
- actorKind: claims.actor_kind,
769
- parentJti: claims.jti,
770
- ...claims.actor_public_id && claims.actor_name ? {
771
- actorPublicId: claims.actor_public_id,
772
- actorName: claims.actor_name,
773
- ...claims.actor_avatar_url ? { actorAvatarUrl: claims.actor_avatar_url } : {}
774
- } : {},
775
- orgId: claims.org_id,
776
- role: claims.role,
777
- ...target,
778
- ...options.relayHostMintKid ? { kid: options.relayHostMintKid } : {}
779
- }, options.relayHostSigningKey, options.relayHostAlgorithm);
904
+ promise = mintRelayHostToken(
905
+ relayHostMintInput(options, claims, target),
906
+ options.relayHostSigningKey,
907
+ options.relayHostAlgorithm
908
+ );
780
909
  pruneRelayHostTokenCache(cache, now);
781
910
  cache.set(key, { promise, expiresAt: now + ttlMs });
782
911
  return await promise;
@@ -794,21 +923,11 @@ async function relayHostTokenFor(options, claims, target, trace) {
794
923
  async function uncachedRelayHostTokenFor(options, claims, target, trace) {
795
924
  return await trace.span(
796
925
  "rht-mint",
797
- async () => await mintRelayHostToken({
798
- principalKind: claims.principal_kind,
799
- actorId: claims.actor_id,
800
- actorKind: claims.actor_kind,
801
- parentJti: claims.jti,
802
- ...claims.actor_public_id && claims.actor_name ? {
803
- actorPublicId: claims.actor_public_id,
804
- actorName: claims.actor_name,
805
- ...claims.actor_avatar_url ? { actorAvatarUrl: claims.actor_avatar_url } : {}
806
- } : {},
807
- orgId: claims.org_id,
808
- role: claims.role,
809
- ...target,
810
- ...options.relayHostMintKid ? { kid: options.relayHostMintKid } : {}
811
- }, options.relayHostSigningKey, options.relayHostAlgorithm)
926
+ async () => await mintRelayHostToken(
927
+ relayHostMintInput(options, claims, target),
928
+ options.relayHostSigningKey,
929
+ options.relayHostAlgorithm
930
+ )
812
931
  );
813
932
  }
814
933
  function workspaceRelayTargetUrl(target, path, search) {
@@ -829,146 +948,6 @@ function workspaceRelayForwardRequestInit(request, relayHostToken, workspaceId,
829
948
  if (request.method === "GET" || request.method === "HEAD") delete init.body;
830
949
  return init;
831
950
  }
832
- var auditSamplers = /* @__PURE__ */ new WeakMap();
833
- function getOrCreateAuditSampler(options) {
834
- let state = auditSamplers.get(options);
835
- if (!state) {
836
- const sampleRate = clampSampleRate(options.auditAcceptSampleRate);
837
- state = {
838
- suppressedCount: 0,
839
- timer: void 0,
840
- flushIntervalMs: options.auditFlushIntervalMs ?? 6e4,
841
- random: options.random ?? Math.random,
842
- sampleRate
843
- };
844
- auditSamplers.set(options, state);
845
- }
846
- return state;
847
- }
848
- function clampSampleRate(rate) {
849
- if (rate === void 0) return 1;
850
- if (!Number.isFinite(rate)) return 1;
851
- if (rate < 0) return 0;
852
- if (rate > 1) return 1;
853
- return rate;
854
- }
855
- function ensureFlushTimer(options, state) {
856
- if (state.timer) return;
857
- if (state.sampleRate >= 1) return;
858
- if (state.flushIntervalMs <= 0) return;
859
- const timer = setInterval(() => {
860
- flushSuppressedSummary(options, state);
861
- }, state.flushIntervalMs);
862
- if (typeof timer.unref === "function") {
863
- timer.unref();
864
- }
865
- state.timer = timer;
866
- }
867
- function flushSuppressedSummary(options, state) {
868
- const count = state.suppressedCount;
869
- if (count <= 0) return;
870
- state.suppressedCount = 0;
871
- void options.audit?.({
872
- action: "relay.request.suppressed_summary",
873
- result: "allow",
874
- method: "INTERNAL",
875
- path: "/",
876
- suppressedCount: count
877
- });
878
- }
879
- function disposeAuditSampler(options) {
880
- const state = auditSamplers.get(options);
881
- if (!state) return;
882
- if (state.timer) {
883
- clearInterval(state.timer);
884
- state.timer = void 0;
885
- }
886
- flushSuppressedSummary(options, state);
887
- auditSamplers.delete(options);
888
- }
889
- function createWorkspaceRelayTrace() {
890
- const phases = [];
891
- return {
892
- phases,
893
- async span(name, run) {
894
- const startedAt = performance.now();
895
- try {
896
- return await run();
897
- } finally {
898
- phases.push({ name, ms: performance.now() - startedAt });
899
- }
900
- }
901
- };
902
- }
903
- function roundedMs(value) {
904
- return Math.round(value * 100) / 100;
905
- }
906
- function workspaceRelayServerTiming(trace) {
907
- return trace.phases.map((phase) => `${phase.name};dur=${roundedMs(phase.ms)}`).join(", ");
908
- }
909
- function workspaceRelayTimingResponse(response, trace) {
910
- const value = workspaceRelayServerTiming(trace);
911
- if (!value) return response;
912
- const headers = new Headers(response.headers);
913
- const prev = headers.get("server-timing");
914
- headers.set("server-timing", prev ? `${prev}, ${value}` : value);
915
- return new Response(response.body, {
916
- status: response.status,
917
- statusText: response.statusText,
918
- headers
919
- });
920
- }
921
- async function audit(options, event) {
922
- if (event.action === "relay.request.accepted") {
923
- const state = getOrCreateAuditSampler(options);
924
- if (state.sampleRate < 1) {
925
- ensureFlushTimer(options, state);
926
- if (state.random() >= state.sampleRate) {
927
- state.suppressedCount += 1;
928
- return;
929
- }
930
- }
931
- }
932
- await options.audit?.(event);
933
- }
934
- var defaultOriginMatcher = createOriginMatcher(DEFAULT_RELAY_APP_ORIGINS);
935
- var originMatchers = /* @__PURE__ */ new WeakMap();
936
- function originMatcherFor(options) {
937
- if (!options.allowedOrigins) return defaultOriginMatcher;
938
- let matcher = originMatchers.get(options);
939
- if (!matcher) {
940
- matcher = createOriginMatcher(options.allowedOrigins);
941
- originMatchers.set(options, matcher);
942
- }
943
- return matcher;
944
- }
945
- var RELAY_ALLOWED_REQUEST_HEADER_LIST = [
946
- "Accept",
947
- "Authorization",
948
- "Content-Type",
949
- "Last-Event-ID",
950
- "Traceparent",
951
- "Tracestate",
952
- "X-Fetch-Bypass-Throttle",
953
- "X-Daytona-Skip-Preview-Warning",
954
- "X-Workspace-Id",
955
- "X-OpenCode-Directory",
956
- "X-Claxedo-Runner",
957
- "X-Claxedo-Model",
958
- "X-Claxedo-Draft-Id",
959
- "X-Claxedo-Binary"
960
- ];
961
- var RELAY_ALLOWED_REQUEST_HEADERS = RELAY_ALLOWED_REQUEST_HEADER_LIST.join(", ");
962
- function denyCorsHeaders(request, originAllowed = defaultOriginMatcher) {
963
- const origin = request.headers.get("origin");
964
- if (!origin) return void 0;
965
- if (!originAllowed(origin)) return void 0;
966
- return {
967
- "access-control-allow-origin": origin,
968
- "access-control-allow-headers": RELAY_ALLOWED_REQUEST_HEADERS,
969
- "access-control-allow-methods": "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS"
970
- };
971
- }
972
951
  async function deny(options, input) {
973
952
  const url = new URL(input.request.url);
974
953
  await audit(options, {
@@ -984,10 +963,7 @@ async function deny(options, input) {
984
963
  method: input.request.method,
985
964
  path: url.pathname
986
965
  });
987
- return Response.json(errorBody(input.code, input.message), {
988
- status: input.status,
989
- headers: denyCorsHeaders(input.request, originMatcherFor(options))
990
- });
966
+ return Response.json(errorBody(input.code, input.message), { status: input.status });
991
967
  }
992
968
  async function authorizeWorkspaceRelayRequest(options, request, workspaceId, trace) {
993
969
  const token = runtimeAccessToken(request);
@@ -1009,6 +985,8 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
1009
985
  "rat-verify",
1010
986
  async () => await cachedRuntimeAccessTokenClaims(options, token, workspaceId)
1011
987
  );
988
+ const targetLookup = span("target-resolve", async () => await options.resolveTarget(claims));
989
+ targetLookup.catch(() => void 0);
1012
990
  const active = await span("rat-active", async () => {
1013
991
  return await options.isRuntimeAccessTokenActive?.(claims) ?? { active: true };
1014
992
  });
@@ -1026,21 +1004,21 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
1026
1004
  };
1027
1005
  }
1028
1006
  const path = relayRuntimePath(request);
1029
- if (!path || !roleAllowsRelayRequest(claims.role, request.method, path)) {
1007
+ if (!path || !sessionScopeReaches(claims.session_id, path, new URL(request.url).search)) {
1030
1008
  return {
1031
1009
  ok: false,
1032
- code: "relay_role_denied",
1010
+ code: "relay_scope_denied",
1033
1011
  response: await deny(options, {
1034
- code: "relay_role_denied",
1035
- message: "Workspace role does not allow this relay request",
1012
+ code: "relay_scope_denied",
1013
+ message: "Runtime Access Token scope does not reach this relay request",
1036
1014
  status: 403,
1037
1015
  claims,
1038
1016
  request
1039
1017
  })
1040
1018
  };
1041
1019
  }
1042
- const target = await span("target-resolve", async () => await options.resolveTarget(claims));
1043
- if (!target || target.workspaceId !== claims.workspace_id || target.hostId !== claims.host_id) {
1020
+ const target = await targetLookup;
1021
+ if (!target || target.workspaceId !== claims.workspace_id || target.hostId !== claims.host_id || !isAllowedRelayTargetBaseUrl(target)) {
1044
1022
  return {
1045
1023
  ok: false,
1046
1024
  code: "relay_target_unavailable",
@@ -1053,13 +1031,13 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
1053
1031
  })
1054
1032
  };
1055
1033
  }
1056
- if (isUserHostedTarget(target) && !options.directory?.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
1034
+ if (isHostTunnelTarget(target) && !options.directory?.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
1057
1035
  return {
1058
1036
  ok: false,
1059
- code: "user_hosted_app_offline",
1037
+ code: "host_tunnel_offline",
1060
1038
  response: await deny(options, {
1061
- code: "user_hosted_app_offline",
1062
- message: "User-hosted workspace is offline",
1039
+ code: "host_tunnel_offline",
1040
+ message: "The machine serving this workspace is offline",
1063
1041
  status: 503,
1064
1042
  claims,
1065
1043
  request
@@ -1102,220 +1080,6 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
1102
1080
  };
1103
1081
  }
1104
1082
  }
1105
- async function forwardWorkspaceRelayRequest(request, target, relayHostToken, path, requestFetch, timeoutMs, trace, originAllowed = defaultOriginMatcher) {
1106
- const controller = new AbortController();
1107
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1108
- const span = (name, run) => trace ? trace.span(name, run) : run();
1109
- try {
1110
- const upstream = await span("upstream-fetch", async () => await requestFetch(
1111
- targetUrl(target, path, new URL(request.url).search),
1112
- workspaceRelayForwardRequestInit(request, relayHostToken, target.workspaceId, {
1113
- signal: controller.signal,
1114
- upstreamHeaders: target.upstreamHeaders
1115
- })
1116
- ));
1117
- const headers = new Headers(upstream.headers);
1118
- headers.delete("access-control-allow-origin");
1119
- headers.delete("access-control-allow-credentials");
1120
- headers.delete("access-control-allow-headers");
1121
- headers.delete("access-control-allow-methods");
1122
- headers.delete("access-control-expose-headers");
1123
- headers.delete("access-control-max-age");
1124
- for (const [key, value] of Object.entries(denyCorsHeaders(request, originAllowed) ?? {})) {
1125
- headers.set(key, value);
1126
- }
1127
- return new Response(upstream.body, {
1128
- status: upstream.status,
1129
- statusText: upstream.statusText,
1130
- headers
1131
- });
1132
- } catch (err) {
1133
- const aborted = err instanceof Error && err.name === "AbortError";
1134
- return Response.json(
1135
- errorBody(
1136
- aborted ? "upstream_timeout" : "upstream_unavailable",
1137
- aborted ? "Workspace upstream timed out" : "Workspace upstream is unavailable"
1138
- ),
1139
- { status: aborted ? 504 : 503, headers: denyCorsHeaders(request, originAllowed) }
1140
- );
1141
- } finally {
1142
- clearTimeout(timer);
1143
- }
1144
- }
1145
- function getAuditSuppressedCount(options) {
1146
- return auditSamplers.get(options)?.suppressedCount ?? 0;
1147
- }
1148
- var LOOPBACK_REMOTES = /* @__PURE__ */ new Set([
1149
- "127.0.0.1",
1150
- "::1",
1151
- "::ffff:127.0.0.1"
1152
- ]);
1153
- function isLoopbackAddress(address) {
1154
- if (!address) return false;
1155
- const trimmed = address.trim();
1156
- if (LOOPBACK_REMOTES.has(trimmed)) return true;
1157
- if (trimmed.startsWith("127.")) return true;
1158
- if (/^::ffff:127\./i.test(trimmed)) return true;
1159
- return false;
1160
- }
1161
- function checkMetricsAuth(options, request) {
1162
- if (options.metricsToken) {
1163
- const presented = bearerToken(request.headers.get("authorization") ?? void 0);
1164
- if (!presented || presented !== options.metricsToken) {
1165
- return {
1166
- ok: false,
1167
- status: 401,
1168
- code: "metrics_unauthorized",
1169
- message: "Metrics endpoint requires a valid bearer token"
1170
- };
1171
- }
1172
- return { ok: true };
1173
- }
1174
- if (!options.metricsRemoteAddress) {
1175
- return {
1176
- ok: false,
1177
- status: 401,
1178
- code: "metrics_unauthorized",
1179
- message: "Metrics endpoint is restricted to loopback or bearer-token clients"
1180
- };
1181
- }
1182
- const address = options.metricsRemoteAddress(request);
1183
- if (isLoopbackAddress(address)) return { ok: true };
1184
- return {
1185
- ok: false,
1186
- status: 401,
1187
- code: "metrics_unauthorized",
1188
- message: "Metrics endpoint is restricted to loopback or bearer-token clients"
1189
- };
1190
- }
1191
- function buildMetricsBody(options) {
1192
- const sources = options.metricsSources ?? {};
1193
- const fragmentation = sources.fragmentation?.() ?? { fragmentsBuffered: 0, oversizedClosed: 0 };
1194
- const slowConsumer = sources.slowConsumer?.() ?? {
1195
- overflowEvents: 0,
1196
- timerFired: 0,
1197
- droppedRequests: 0
1198
- };
1199
- const activeHostCount = options.directory?.size?.() ?? 0;
1200
- const suppressedAccepts = getAuditSuppressedCount(options);
1201
- const body = {
1202
- fragmentation,
1203
- slowConsumer,
1204
- directory: { activeHostCount },
1205
- audit: { suppressedAccepts }
1206
- };
1207
- if (sources.drainPending) {
1208
- body.drain = { pendingCount: sources.drainPending() };
1209
- }
1210
- return body;
1211
- }
1212
- function createWorkspaceRelay(options) {
1213
- const app = Object.assign(new Hono(), {
1214
- disposeAuditSampler: () => disposeAuditSampler(options)
1215
- });
1216
- const originAllowed = originMatcherFor(options);
1217
- app.use("*", async (c, next) => {
1218
- await next();
1219
- const origin = c.res.headers.get("access-control-allow-origin");
1220
- if (origin && origin !== "*" && !c.res.headers.has("timing-allow-origin")) {
1221
- c.res.headers.set("timing-allow-origin", origin);
1222
- }
1223
- });
1224
- app.use("*", cors({
1225
- origin: (origin) => {
1226
- if (!origin) return void 0;
1227
- return originAllowed(origin) ? origin : void 0;
1228
- },
1229
- // Spread from the shared list, not hand-maintained here: a duplicate
1230
- // array drifts out of sync and silently drops headers like
1231
- // `Last-Event-ID` or `X-Fetch-Bypass-Throttle`.
1232
- allowHeaders: [...RELAY_ALLOWED_REQUEST_HEADER_LIST],
1233
- allowMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
1234
- }));
1235
- app.get("/health", (c) => {
1236
- if (options.isDraining?.()) {
1237
- return c.json(
1238
- { ok: false, service: "workspace-relay", draining: true },
1239
- 503
1240
- );
1241
- }
1242
- return c.json({ ok: true, service: "workspace-relay" });
1243
- });
1244
- app.get("/.well-known/jwks.json", async (c) => {
1245
- const sources = options.relayHostPublicKeys ?? [];
1246
- if (sources.length === 0) {
1247
- return c.json(
1248
- {
1249
- error: {
1250
- code: "jwks_no_keys_configured",
1251
- message: "No Relay Host Token public keys are configured for the JWKS endpoint"
1252
- }
1253
- },
1254
- 503
1255
- );
1256
- }
1257
- let keys;
1258
- try {
1259
- keys = await Promise.all(sources.map(async (source) => {
1260
- const jwk = await exportJWK2(source.publicKey);
1261
- return {
1262
- ...jwk,
1263
- kid: source.kid,
1264
- alg: "EdDSA",
1265
- use: "sig"
1266
- };
1267
- }));
1268
- } catch (err) {
1269
- return c.json(
1270
- {
1271
- error: {
1272
- code: "jwks_invalid_key",
1273
- message: "A configured RHT public key could not be exported as a JWK",
1274
- detail: err instanceof Error ? err.message : String(err)
1275
- }
1276
- },
1277
- 500
1278
- );
1279
- }
1280
- c.header("cache-control", "public, max-age=300");
1281
- return c.json({ keys });
1282
- });
1283
- app.get("/metrics", (c) => {
1284
- const authResult = checkMetricsAuth(options, c.req.raw);
1285
- if (!authResult.ok) {
1286
- return c.json(
1287
- errorBody(authResult.code, authResult.message),
1288
- authResult.status
1289
- );
1290
- }
1291
- return c.json(buildMetricsBody(options));
1292
- });
1293
- app.all("/workspaces/:workspaceId/*", async (c) => {
1294
- const trace = createWorkspaceRelayTrace();
1295
- if (options.isDraining?.()) {
1296
- return Response.json(
1297
- errorBody("relay_draining", "Workspace relay is shutting down; try another instance"),
1298
- { status: 503, headers: denyCorsHeaders(c.req.raw, originAllowed) }
1299
- );
1300
- }
1301
- const response = await trace.span("relay-total", async () => {
1302
- const relay = await authorizeWorkspaceRelayRequest(options, c.req.raw, c.req.param("workspaceId"), trace);
1303
- if (!relay.ok) return relay.response;
1304
- return await forwardWorkspaceRelayRequest(
1305
- new Request(c.req.raw),
1306
- relay.request.target,
1307
- relay.request.relayHostToken,
1308
- relay.request.path,
1309
- options.fetch ?? fetch,
1310
- options.forwardTimeoutMs ?? 3e4,
1311
- trace,
1312
- originAllowed
1313
- );
1314
- });
1315
- return workspaceRelayTimingResponse(response, trace);
1316
- });
1317
- return app;
1318
- }
1319
1083
 
1320
1084
  // src/directory.ts
1321
1085
  function presenceKey(hostId, workspaceId) {
@@ -1324,7 +1088,6 @@ function presenceKey(hostId, workspaceId) {
1324
1088
  function createWorkspaceRelayDirectory(options = {}) {
1325
1089
  const ttlMs = Math.max(1, options.ttlMs ?? 45e3);
1326
1090
  const now = options.now ?? Date.now;
1327
- const sweepIntervalMs = options.sweepIntervalMs ?? 3e4;
1328
1091
  const entries = /* @__PURE__ */ new Map();
1329
1092
  const alive = (key) => {
1330
1093
  const presence = entries.get(key);
@@ -1335,22 +1098,7 @@ function createWorkspaceRelayDirectory(options = {}) {
1335
1098
  }
1336
1099
  return presence;
1337
1100
  };
1338
- const keysOf = (hostId, workspaceIds) => {
1339
- if (workspaceIds) return [...new Set(workspaceIds)].map((workspaceId) => presenceKey(hostId, workspaceId));
1340
- return [...entries].filter(([, presence]) => presence.hostId === hostId).map(([key]) => key);
1341
- };
1342
- const sweep = () => {
1343
- const at = now();
1344
- for (const [key, presence] of entries) {
1345
- if (presence.expiresAt <= at) {
1346
- entries.delete(key);
1347
- }
1348
- }
1349
- };
1350
- let intervalHandle;
1351
- if (sweepIntervalMs > 0) {
1352
- intervalHandle = setInterval(sweep, sweepIntervalMs);
1353
- }
1101
+ const keysOf = (hostId) => [...entries].filter(([, presence]) => presence.hostId === hostId).map(([key]) => key);
1354
1102
  return {
1355
1103
  registerHostTunnel(input) {
1356
1104
  const timestamp = now();
@@ -1364,10 +1112,10 @@ function createWorkspaceRelayDirectory(options = {}) {
1364
1112
  for (const workspaceId of next.workspaceIds) entries.set(presenceKey(next.hostId, workspaceId), next);
1365
1113
  return next;
1366
1114
  },
1367
- recordPong(hostId, workspaceIds) {
1115
+ recordPong(hostId) {
1368
1116
  const timestamp = now();
1369
1117
  let touched;
1370
- for (const key of keysOf(hostId, workspaceIds)) {
1118
+ for (const key of keysOf(hostId)) {
1371
1119
  const presence = alive(key);
1372
1120
  if (!presence) continue;
1373
1121
  const next = { ...presence, lastPongAt: timestamp, expiresAt: timestamp + ttlMs };
@@ -1376,35 +1124,140 @@ function createWorkspaceRelayDirectory(options = {}) {
1376
1124
  }
1377
1125
  return touched;
1378
1126
  },
1379
- disconnectHost(hostId, workspaceIds) {
1380
- for (const key of keysOf(hostId, workspaceIds)) entries.delete(key);
1127
+ disconnectHost(hostId) {
1128
+ for (const key of keysOf(hostId)) entries.delete(key);
1381
1129
  },
1382
1130
  activeHost(input) {
1383
1131
  return alive(presenceKey(input.hostId, input.workspaceId));
1384
- },
1385
- sweep,
1386
- size() {
1387
- const at = now();
1388
- const hosts = /* @__PURE__ */ new Set();
1389
- for (const presence of entries.values()) {
1390
- if (presence.expiresAt > at) hosts.add(presence.hostId);
1391
- }
1392
- return hosts.size;
1393
- },
1394
- dispose() {
1395
- if (intervalHandle !== void 0) {
1396
- clearInterval(intervalHandle);
1397
- intervalHandle = void 0;
1398
- }
1399
1132
  }
1400
1133
  };
1401
1134
  }
1402
- function disposeWorkspaceRelayDirectory(directory) {
1403
- directory.dispose();
1135
+
1136
+ // src/cors-origins.ts
1137
+ var DEFAULT_RELAY_APP_ORIGINS = [
1138
+ "https://claxedo.com",
1139
+ "https://*.claxedo.com"
1140
+ ];
1141
+ var RELAY_ALLOWED_REQUEST_HEADERS = [
1142
+ "Accept",
1143
+ "Authorization",
1144
+ "Content-Type",
1145
+ "Last-Event-ID",
1146
+ "Traceparent",
1147
+ "Tracestate",
1148
+ "X-Fetch-Bypass-Throttle",
1149
+ "X-Workspace-Id",
1150
+ "X-OpenCode-Directory",
1151
+ "X-Claxedo-Runner",
1152
+ "X-Claxedo-Model",
1153
+ "X-Claxedo-Draft-Id",
1154
+ "X-Claxedo-Session-Registration-Operation",
1155
+ "X-Claxedo-Binary"
1156
+ ].join(", ");
1157
+ function parseAllowedOrigins(raw) {
1158
+ const entries = (raw ?? "").split(",").map((item) => item.trim()).filter(Boolean);
1159
+ return entries.length ? entries : void 0;
1160
+ }
1161
+ function createOriginMatcher(patterns) {
1162
+ const exact = /* @__PURE__ */ new Set();
1163
+ const httpsSuffixes = [];
1164
+ const httpPortPrefixes = [];
1165
+ for (const pattern of patterns) {
1166
+ if (pattern.startsWith("https://*.")) {
1167
+ httpsSuffixes.push(pattern.slice("https://*".length));
1168
+ } else if (pattern.startsWith("http://") && pattern.endsWith(":*")) {
1169
+ httpPortPrefixes.push(pattern.slice(0, -1));
1170
+ } else {
1171
+ exact.add(pattern);
1172
+ }
1173
+ }
1174
+ return (origin) => {
1175
+ if (exact.has(origin)) return true;
1176
+ if (origin.startsWith("https://")) {
1177
+ return httpsSuffixes.some(
1178
+ (suffix) => origin.endsWith(suffix) && origin.length > "https://".length + suffix.length
1179
+ );
1180
+ }
1181
+ if (origin.startsWith("http://")) {
1182
+ return httpPortPrefixes.some(
1183
+ (prefix) => origin.startsWith(prefix) && /^\d+$/.test(origin.slice(prefix.length))
1184
+ );
1185
+ }
1186
+ return false;
1187
+ };
1188
+ }
1189
+
1190
+ // src/relay-cors.ts
1191
+ 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";
1192
+ var configuredAppOriginAllowed = () => false;
1193
+ function setWorkspaceRelayAppOrigins(raw) {
1194
+ configuredAppOriginAllowed = raw?.trim() ? createOriginMatcher(parseAllowedOrigins(raw) ?? []) : () => false;
1195
+ }
1196
+ var defaultOriginAllowed = createOriginMatcher(DEFAULT_RELAY_APP_ORIGINS);
1197
+ var baseOriginAllowed = defaultOriginAllowed;
1198
+ function setWorkspaceRelayAllowedOrigins(raw) {
1199
+ const patterns = parseAllowedOrigins(raw);
1200
+ baseOriginAllowed = patterns ? createOriginMatcher(patterns) : defaultOriginAllowed;
1201
+ }
1202
+ function allowedCorsOrigin(origin) {
1203
+ if (!origin) return void 0;
1204
+ return baseOriginAllowed(origin) || configuredAppOriginAllowed(origin) ? origin : void 0;
1205
+ }
1206
+ function workspaceUpgradeOriginDenied(request) {
1207
+ const origin = request.headers.get("origin");
1208
+ if (origin === null || allowedCorsOrigin(origin)) return null;
1209
+ return Response.json(errorBody("origin_not_allowed", "Origin is not in the allowlist"), { status: 403 });
1210
+ }
1211
+ function corsHeaders(request) {
1212
+ const origin = allowedCorsOrigin(request.headers.get("origin"));
1213
+ if (!origin) return void 0;
1214
+ return {
1215
+ "access-control-allow-origin": origin,
1216
+ "access-control-allow-methods": "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS",
1217
+ "access-control-allow-headers": RELAY_ALLOWED_REQUEST_HEADERS,
1218
+ "access-control-expose-headers": RELAY_EXPOSED_RESPONSE_HEADERS,
1219
+ "access-control-max-age": "86400",
1220
+ "timing-allow-origin": origin,
1221
+ vary: "Origin"
1222
+ };
1223
+ }
1224
+ function corsPreflight(request) {
1225
+ const headers = corsHeaders(request);
1226
+ return new Response(null, { status: headers ? 204 : 403, ...headers ? { headers } : {} });
1227
+ }
1228
+ var UPSTREAM_OWNED_RESPONSE_HEADERS = [
1229
+ "set-cookie",
1230
+ "access-control-allow-origin",
1231
+ "access-control-allow-credentials",
1232
+ "access-control-allow-headers",
1233
+ "access-control-allow-methods",
1234
+ "access-control-expose-headers",
1235
+ "access-control-max-age"
1236
+ ];
1237
+ var RELAY_DOCUMENT_LOCKDOWN_HEADERS = {
1238
+ "x-content-type-options": "nosniff",
1239
+ "content-security-policy": "sandbox"
1240
+ };
1241
+ function withRelayResponseHeaders(request, response) {
1242
+ if (response.status === 101 || "webSocket" in response && response.webSocket) return response;
1243
+ const next = new Headers(response.headers);
1244
+ for (const name of UPSTREAM_OWNED_RESPONSE_HEADERS) next.delete(name);
1245
+ for (const [key, value] of Object.entries({ ...RELAY_DOCUMENT_LOCKDOWN_HEADERS, ...corsHeaders(request) })) next.set(key, value);
1246
+ return new Response(response.body, {
1247
+ status: response.status,
1248
+ statusText: response.statusText,
1249
+ headers: next
1250
+ });
1404
1251
  }
1405
1252
 
1406
- // src/cloudflare.ts
1253
+ // src/session-host-forwarding.ts
1254
+ import { sessionHostRootOf as sessionHostRootOf2 } from "@claxedo/workspace-relay-protocol";
1255
+
1256
+ // src/relay-location.ts
1257
+ import { trimToUndefined as trimToUndefined2 } from "@claxedo/helpers/string";
1258
+ import { sessionHostRootOf } from "@claxedo/workspace-relay-protocol";
1407
1259
  var DEFAULT_RELAY_LOCATION_HINT = "apac";
1260
+ var SESSION_HOST_LOCATION_HINT = "enam";
1408
1261
  var RELAY_LOCATION_HINTS = ["wnam", "enam", "sam", "weur", "eeur", "apac", "oc", "afr", "me"];
1409
1262
  var relayLocationHintSet = new Set(RELAY_LOCATION_HINTS);
1410
1263
  function isRelayLocationHint(input) {
@@ -1446,51 +1299,139 @@ function locationHintForCountry(country) {
1446
1299
  }
1447
1300
  var RELAY_REGION_HEADER = "x-claxedo-workspace-region";
1448
1301
  function relayLocationHint(input) {
1449
- const configured = trimToUndefined(input.configured) ?? DEFAULT_RELAY_LOCATION_HINT;
1450
- const region = trimToUndefined(input.region);
1302
+ const configured = trimToUndefined2(input.configured) ?? DEFAULT_RELAY_LOCATION_HINT;
1303
+ const region = trimToUndefined2(input.region);
1451
1304
  if (region) {
1452
1305
  const lowered = region.toLowerCase();
1453
1306
  const direct = isRelayLocationHint(lowered) ? lowered : void 0;
1454
1307
  const mapped = direct ?? RELAY_REGION_TO_LOCATION_HINT[lowered];
1455
1308
  if (mapped) return mapped;
1456
1309
  }
1457
- const country = trimToUndefined(input.country);
1310
+ const country = trimToUndefined2(input.country);
1458
1311
  if (country) {
1459
1312
  const mapped = locationHintForCountry(country);
1460
1313
  if (mapped) return mapped;
1461
1314
  }
1462
1315
  return configured;
1463
1316
  }
1317
+ var RELAY_REGION_QUERY_PARAMS = ["region", "homeRegion"];
1464
1318
  function relayRequestRegion(request) {
1465
1319
  const url = new URL(request.url);
1466
- return trimToUndefined(request.headers.get(RELAY_REGION_HEADER)) ?? trimToUndefined(url.searchParams.get("region")) ?? trimToUndefined(url.searchParams.get("homeRegion"));
1320
+ 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]));
1321
+ }
1322
+ async function relayRoomPlacement(request, input) {
1323
+ const configured = trimToUndefined2(input.configured);
1324
+ const region = relayRequestRegion(request);
1325
+ const country = relayRequestCountry(request);
1326
+ const floor = relayLocationHint(configured ? { configured } : {});
1327
+ const declared = relayLocationHint({
1328
+ ...configured ? { configured } : {},
1329
+ ...region ? { region } : {},
1330
+ ...country ? { country } : {}
1331
+ });
1332
+ const headers = new Headers(request.headers);
1333
+ headers.delete(RELAY_REGION_HEADER);
1334
+ const credential = await input.credential();
1335
+ const sessionHost = credential !== void 0 && sessionHostRootOf(credential.hostId) !== void 0;
1336
+ return {
1337
+ locationHint: sessionHost ? SESSION_HOST_LOCATION_HINT : credential || declared === floor ? declared : floor,
1338
+ url: withoutQueryParams(request.url, RELAY_REGION_QUERY_PARAMS),
1339
+ headers
1340
+ };
1341
+ }
1342
+ function withoutQueryParams(url, names) {
1343
+ const target = new URL(url);
1344
+ if (!names.some((name) => target.searchParams.has(name))) return url;
1345
+ const kept = target.search.slice(1).split("&").filter((pair) => !names.includes(pair.split("=")[0] ?? ""));
1346
+ target.search = kept.length ? `?${kept.join("&")}` : "";
1347
+ return target.toString();
1467
1348
  }
1468
1349
  function relayRequestCountry(request) {
1469
1350
  const cf = request.cf;
1470
1351
  return typeof cf?.country === "string" ? cf.country : void 0;
1471
1352
  }
1472
- var TUNNEL_PENDING_HTTP_CAP_DEFAULT = 32;
1473
- var TUNNEL_STARTED_STREAM_CAP_DEFAULT = 16;
1474
- var TUNNEL_CHANNEL_CAP_DEFAULT = 16;
1475
- var TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
1476
- var TUNNEL_RESPONSE_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
1477
- var SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT = 8 * 1024 * 1024;
1478
- var SLOW_CONSUMER_TIMEOUT_MS_DEFAULT = 3e4;
1479
- var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT = 64;
1480
- var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
1481
- var RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
1482
- var WORKSPACE_TARGET_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
1483
- var RESOLVER_OUTAGE_GRACE_ATTEMPTS_DEFAULT = 3;
1484
- var HIBERNATED_REVOCATION_ALARM_INTERVAL_MS_DEFAULT = 3e4;
1485
- var HOST_GENERATION_CHECK_INTERVAL_MS_DEFAULT = 3e4;
1486
- var TRACE_ID_HEADER = "x-claxedo-trace-id";
1487
- var TRACE_FORCE_HEADER = "x-claxedo-relay-trace";
1488
- function unrefTimer(timer) {
1489
- if (typeof timer.unref === "function") timer.unref();
1353
+
1354
+ // src/session-host-forwarding.ts
1355
+ var SESSION_HOST_LOCATION = { locationHint: SESSION_HOST_LOCATION_HINT };
1356
+ var SESSION_HOST_ORIGIN = "https://session-host.invalid";
1357
+ function unavailable() {
1358
+ return Response.json(errorBody("session_host_unavailable", "The session host is unavailable"), { status: 503 });
1359
+ }
1360
+ function isSessionHostTarget(target) {
1361
+ return target.backing === "durable-object";
1362
+ }
1363
+ async function forwardSessionHostHttp(namespace, request, authorized) {
1364
+ const root = sessionHostRootOf2(authorized.target.hostId);
1365
+ if (!namespace || !root) return unavailable();
1366
+ const url = workspaceRelayTargetUrl({ ...authorized.target, baseUrl: SESSION_HOST_ORIGIN }, authorized.path, new URL(request.url).search);
1367
+ const init = workspaceRelayForwardRequestInit(request, authorized.relayHostToken, authorized.target.workspaceId, { hostTunnel: true });
1368
+ let upstream;
1369
+ try {
1370
+ upstream = await namespace.getByName(root, SESSION_HOST_LOCATION).fetch(url.toString(), init);
1371
+ } catch {
1372
+ return unavailable();
1373
+ }
1374
+ return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: upstream.headers });
1490
1375
  }
1491
- function isUserHostedClient(client) {
1492
- return "channelId" in client && typeof client.channelId === "string";
1376
+
1377
+ // src/gateway-placement.ts
1378
+ import { createRemoteJWKSet, importSPKI } from "jose";
1379
+ import { trimToUndefined as trimToUndefined3 } from "@claxedo/helpers/string";
1380
+ var placementAuthKeys = /* @__PURE__ */ new Map();
1381
+ function placementAuthKey(env) {
1382
+ const jwksUrl = typeof env.CLAXEDO_CONTROL_PLANE_JWKS_URL === "string" ? trimToUndefined3(env.CLAXEDO_CONTROL_PLANE_JWKS_URL) : void 0;
1383
+ 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");
1384
+ const id = jwksUrl ? `jwks:${jwksUrl}` : pem ? `pem:${pem}` : void 0;
1385
+ if (!id) return void 0;
1386
+ const cached = placementAuthKeys.get(id);
1387
+ if (cached) return cached;
1388
+ if (placementAuthKeys.size >= 32) placementAuthKeys.clear();
1389
+ const loaded = (async () => {
1390
+ try {
1391
+ if (jwksUrl) return createRemoteJWKSet(new URL(jwksUrl));
1392
+ if (!pem) return void 0;
1393
+ return await importSPKI(pem, "EdDSA");
1394
+ } catch {
1395
+ return void 0;
1396
+ }
1397
+ })();
1398
+ placementAuthKeys.set(id, loaded);
1399
+ return loaded;
1400
+ }
1401
+ function runtimeAccessTokenFrom(request) {
1402
+ 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);
1403
+ }
1404
+ async function relayPlacementCredential(request, env, url, workspaceId) {
1405
+ const key = await placementAuthKey(env);
1406
+ if (!key) return void 0;
1407
+ try {
1408
+ if (url.pathname.startsWith("/host-tunnels/")) {
1409
+ const hostId = trimToUndefined3(hostIdFromTunnelPath(url.pathname));
1410
+ const token2 = bearerToken(request.headers.get("authorization"));
1411
+ if (!hostId || !token2) return void 0;
1412
+ await verifyHostTunnelToken(token2, key, { hostId, workspaceIds: workspaceIdsFromSearch(url) });
1413
+ return { hostId };
1414
+ }
1415
+ const token = runtimeAccessTokenFrom(request);
1416
+ if (!token) return void 0;
1417
+ const claims = await verifyRuntimeAccessToken(token, key, { workspaceId });
1418
+ return { hostId: claims.host_id };
1419
+ } catch {
1420
+ return void 0;
1421
+ }
1422
+ }
1423
+ function workspaceRelayRoomPlacement(request, env, workspaceId) {
1424
+ return relayRoomPlacement(request, {
1425
+ ...typeof env.CLAXEDO_RELAY_LOCATION_HINT === "string" ? { configured: env.CLAXEDO_RELAY_LOCATION_HINT } : {},
1426
+ credential: () => relayPlacementCredential(request, env, new URL(request.url), workspaceId)
1427
+ });
1493
1428
  }
1429
+
1430
+ // src/relay-trace.ts
1431
+ import { trimToUndefined as trimToUndefined4 } from "@claxedo/helpers/string";
1432
+ var TRACE_ID_HEADER = "x-claxedo-trace-id";
1433
+ var TRACE_FORCE_HEADER = "x-claxedo-relay-trace";
1434
+ var TRACE_SAMPLED_RESPONSE_HEADER = "x-claxedo-relay-sampled";
1494
1435
  function clampRate(input) {
1495
1436
  if (input === void 0 || !Number.isFinite(input)) return 0;
1496
1437
  if (input <= 0) return 0;
@@ -1498,8 +1439,8 @@ function clampRate(input) {
1498
1439
  return input;
1499
1440
  }
1500
1441
  function traceRequest(request, routeKind, options) {
1501
- const id = trimToUndefined(request.headers.get(TRACE_ID_HEADER)) ?? crypto.randomUUID();
1502
- const forceSecret = trimToUndefined(options.traceForceHeaderSecret);
1442
+ const id = trimToUndefined4(request.headers.get(TRACE_ID_HEADER)) ?? crypto.randomUUID();
1443
+ const forceSecret = trimToUndefined4(options.traceForceHeaderSecret);
1503
1444
  const forced = !!forceSecret && request.headers.get(TRACE_FORCE_HEADER) === forceSecret;
1504
1445
  const rate = clampRate(options.traceSampleRate);
1505
1446
  return {
@@ -1518,12 +1459,12 @@ async function traceSpan(trace, name, fn) {
1518
1459
  trace?.phases.push({ name, ms: performance.now() - startedAt });
1519
1460
  }
1520
1461
  }
1521
- function tracedRequest(request, trace) {
1522
- const headers = new Headers(request.headers);
1462
+ function tracedRequest(request, trace, forwarded = request) {
1463
+ const headers = new Headers(forwarded.headers);
1523
1464
  headers.set(TRACE_ID_HEADER, trace.id);
1524
1465
  if (trace.sampled) headers.set("x-claxedo-trace-sampled", "1");
1525
1466
  else headers.delete("x-claxedo-trace-sampled");
1526
- return new Request(request.url, {
1467
+ return new Request(forwarded.url, {
1527
1468
  method: request.method,
1528
1469
  headers,
1529
1470
  body: request.method === "GET" || request.method === "HEAD" ? void 0 : request.body,
@@ -1531,19 +1472,28 @@ function tracedRequest(request, trace) {
1531
1472
  duplex: "half"
1532
1473
  });
1533
1474
  }
1534
- function roundedMs2(value) {
1475
+ function roundedMs(value) {
1535
1476
  return Math.round(value * 100) / 100;
1536
1477
  }
1537
- function serverTiming(trace) {
1538
- return trace.phases.map((phase) => `${phase.name};dur=${roundedMs2(phase.ms)}`).join(", ");
1478
+ function serverTiming(phases) {
1479
+ return phases.map((phase) => `${phase.name};dur=${roundedMs(phase.ms)}`).join(", ");
1480
+ }
1481
+ function webSocketTraceFrame(upstreamStartedAt, queueTimes, openedAt) {
1482
+ return {
1483
+ type: "relay.trace",
1484
+ wsUpstreamOpenMs: roundedMs(openedAt - upstreamStartedAt),
1485
+ queuedFrames: queueTimes.length,
1486
+ maxQueuedDelayMs: roundedMs(queueTimes.reduce((max, at) => Math.max(max, openedAt - at), 0))
1487
+ };
1539
1488
  }
1540
1489
  function withTraceHeaders(response, trace) {
1541
1490
  const headers = new Headers(response.headers);
1542
1491
  headers.set(TRACE_ID_HEADER, trace.id);
1543
- const timing = trace.sampled ? serverTiming(trace) : "";
1492
+ const timing = trace.sampled ? serverTiming(trace.phases) : "";
1544
1493
  if (timing) {
1545
1494
  const existing = headers.get("server-timing");
1546
1495
  headers.set("server-timing", existing ? `${existing}, ${timing}` : timing);
1496
+ headers.set(TRACE_SAMPLED_RESPONSE_HEADER, "1");
1547
1497
  }
1548
1498
  return new Response(response.body, {
1549
1499
  status: response.status,
@@ -1563,8 +1513,8 @@ function logTrace(options, request, workspaceId, trace, response) {
1563
1513
  method: request.method,
1564
1514
  path: url.pathname,
1565
1515
  status: response.status,
1566
- totalMs: roundedMs2(totalMs),
1567
- phases: Object.fromEntries(trace.phases.map((phase) => [phase.name, roundedMs2(phase.ms)]))
1516
+ totalMs: roundedMs(totalMs),
1517
+ phases: Object.fromEntries(trace.phases.map((phase) => [phase.name, roundedMs(phase.ms)]))
1568
1518
  };
1569
1519
  if (options.traceLog) {
1570
1520
  options.traceLog(event);
@@ -1572,29 +1522,42 @@ function logTrace(options, request, workspaceId, trace, response) {
1572
1522
  }
1573
1523
  console.log(JSON.stringify(event));
1574
1524
  }
1525
+
1526
+ // src/cloudflare.ts
1527
+ var TUNNEL_PENDING_HTTP_CAP_DEFAULT = 32;
1528
+ var TUNNEL_STARTED_STREAM_CAP_DEFAULT = 16;
1529
+ var TUNNEL_CHANNEL_CAP_DEFAULT = 16;
1530
+ var TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
1531
+ var TUNNEL_RESPONSE_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
1532
+ var SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT = 8 * 1024 * 1024;
1533
+ var SLOW_CONSUMER_TIMEOUT_MS_DEFAULT = 3e4;
1534
+ var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT = 8192;
1535
+ var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
1536
+ var UPSTREAM_WS_PRE_OPEN_QUEUE_FRAME_OVERHEAD_BYTES = 64;
1537
+ var WORKSPACE_TARGET_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
1538
+ var RESOLVER_OUTAGE_GRACE_ATTEMPTS_DEFAULT = 3;
1539
+ var HIBERNATED_REVOCATION_ALARM_INTERVAL_MS_DEFAULT = 3e4;
1540
+ var HOST_GENERATION_CHECK_INTERVAL_MS_DEFAULT = 3e4;
1541
+ function unrefTimer(timer) {
1542
+ if (typeof timer.unref === "function") timer.unref();
1543
+ }
1544
+ function isHostTunnelClient(client) {
1545
+ return "channelId" in client && typeof client.channelId === "string";
1546
+ }
1575
1547
  function workspaceIdFromRelayPath(pathname) {
1576
1548
  return /^\/workspaces\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
1577
1549
  }
1578
- function hostIdFromTunnelPath(pathname) {
1579
- return /^\/host-tunnels\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
1580
- }
1581
1550
  function websocketRequest(request) {
1582
1551
  return request.headers.get("upgrade")?.toLowerCase() === "websocket";
1583
1552
  }
1584
1553
  function relayWebSocketTraceEnabled(request) {
1585
1554
  return request.headers.get("x-claxedo-relay-ws-trace") === "1";
1586
1555
  }
1587
- function workspaceIdsFromSearch(url) {
1588
- return [...new Set([
1589
- ...url.searchParams.getAll("workspaceId"),
1590
- ...url.searchParams.getAll("workspace_id")
1591
- ].map(trimToUndefined).filter((item) => !!item))];
1592
- }
1593
1556
  function randomToken() {
1594
1557
  return crypto.randomUUID();
1595
1558
  }
1596
1559
  function headerRecord(headers) {
1597
- const result = {};
1560
+ const result = /* @__PURE__ */ Object.create(null);
1598
1561
  headers.forEach((value, key) => {
1599
1562
  result[key] = value;
1600
1563
  });
@@ -1617,8 +1580,8 @@ function base64ToBytes(input) {
1617
1580
  return Uint8Array.from(atob(input), (char) => char.charCodeAt(0));
1618
1581
  }
1619
1582
  function preOpenFrameBytes(input) {
1620
- if (typeof input === "string") return new TextEncoder().encode(input).byteLength;
1621
- return input.byteLength;
1583
+ const payload = typeof input === "string" ? new TextEncoder().encode(input).byteLength : input.byteLength;
1584
+ return payload + UPSTREAM_WS_PRE_OPEN_QUEUE_FRAME_OVERHEAD_BYTES;
1622
1585
  }
1623
1586
  function isBlobLike(input) {
1624
1587
  if (!input || typeof input !== "object") return false;
@@ -1730,20 +1693,15 @@ async function readRequestBodyCapped(request, maxBytes) {
1730
1693
  }
1731
1694
  return concatBytes(chunks);
1732
1695
  }
1733
- function safeCloseCode(code, fallback = 1011) {
1734
- if (code === void 0) return fallback;
1735
- if (code >= 1e3 && code <= 4999 && ![1004, 1005, 1006].includes(code)) return code;
1736
- return fallback;
1737
- }
1738
1696
  function workspaceRelayDurableObjectRoomName(workspaceId) {
1739
1697
  return `workspace:${workspaceId}`;
1740
1698
  }
1741
1699
  function workspaceRelayDurableObjectWorkspaceId(request) {
1742
1700
  const url = new URL(request.url);
1743
- const workspaceId = trimToUndefined(workspaceIdFromRelayPath(url.pathname));
1701
+ const workspaceId = trimToUndefined5(workspaceIdFromRelayPath(url.pathname));
1744
1702
  if (workspaceId) return decodeURIComponent(workspaceId);
1745
1703
  if (!hostIdFromTunnelPath(url.pathname)) return void 0;
1746
- return trimToUndefined(url.searchParams.get("workspaceId")) ?? trimToUndefined(url.searchParams.get("workspace_id"));
1704
+ return trimToUndefined5(url.searchParams.get("workspaceId")) ?? trimToUndefined5(url.searchParams.get("workspace_id"));
1747
1705
  }
1748
1706
  function namespaceFromEnv(env, options) {
1749
1707
  if (options.namespace) return options.namespace;
@@ -1757,47 +1715,6 @@ function isDurableObjectNamespace(value) {
1757
1715
  function json(code, message, status) {
1758
1716
  return Response.json(errorBody(code, message), { status });
1759
1717
  }
1760
- var configuredAppOriginAllowed = () => false;
1761
- function setWorkspaceRelayAppOrigins(raw) {
1762
- configuredAppOriginAllowed = raw?.trim() ? createOriginMatcher(parseAllowedOrigins(raw) ?? []) : () => false;
1763
- }
1764
- var defaultOriginAllowed = createOriginMatcher(DEFAULT_RELAY_APP_ORIGINS);
1765
- var baseOriginAllowed = defaultOriginAllowed;
1766
- function setWorkspaceRelayAllowedOrigins(raw) {
1767
- const patterns = parseAllowedOrigins(raw);
1768
- baseOriginAllowed = patterns ? createOriginMatcher(patterns) : defaultOriginAllowed;
1769
- }
1770
- function allowedCorsOrigin(origin) {
1771
- if (!origin) return void 0;
1772
- return baseOriginAllowed(origin) || configuredAppOriginAllowed(origin) ? origin : void 0;
1773
- }
1774
- function corsHeaders(request) {
1775
- const origin = allowedCorsOrigin(request.headers.get("origin"));
1776
- if (!origin) return void 0;
1777
- return {
1778
- "access-control-allow-origin": origin,
1779
- "access-control-allow-methods": "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS",
1780
- "access-control-allow-headers": RELAY_ALLOWED_REQUEST_HEADERS,
1781
- "access-control-max-age": "86400",
1782
- vary: "Origin"
1783
- };
1784
- }
1785
- function corsPreflight(request) {
1786
- const headers = corsHeaders(request);
1787
- return new Response(null, { status: headers ? 204 : 403, ...headers ? { headers } : {} });
1788
- }
1789
- function withCors(request, response) {
1790
- const headers = corsHeaders(request);
1791
- if (!headers) return response;
1792
- if (response.status === 101 || "webSocket" in response && response.webSocket) return response;
1793
- const next = new Headers(response.headers);
1794
- for (const [key, value] of Object.entries(headers)) next.set(key, value);
1795
- return new Response(response.body, {
1796
- status: response.status,
1797
- statusText: response.statusText,
1798
- headers: next
1799
- });
1800
- }
1801
1718
  function defaultWebSocketPair() {
1802
1719
  const pair = new WebSocketPair();
1803
1720
  return {
@@ -1834,7 +1751,7 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
1834
1751
  return {
1835
1752
  async fetch(request, env = {}) {
1836
1753
  const gatewayStartedAt = performance.now();
1837
- const gatewayTraceId = trimToUndefined(request.headers.get(TRACE_ID_HEADER)) ?? crypto.randomUUID();
1754
+ const gatewayTraceId = trimToUndefined5(request.headers.get(TRACE_ID_HEADER)) ?? crypto.randomUUID();
1838
1755
  const url = new URL(request.url);
1839
1756
  if (request.method === "OPTIONS" && (url.pathname.startsWith("/workspaces/") || url.pathname.startsWith("/host-tunnels/"))) {
1840
1757
  return corsPreflight(request);
@@ -1842,13 +1759,6 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
1842
1759
  if (url.pathname === "/health") {
1843
1760
  return Response.json({ ok: true, service: "workspace-relay", mode: "cloudflare-durable-object" });
1844
1761
  }
1845
- const requestRegion = relayRequestRegion(request);
1846
- const requestCountry = relayRequestCountry(request);
1847
- const locationHint = relayLocationHint({
1848
- ...typeof env.CLAXEDO_RELAY_LOCATION_HINT === "string" ? { configured: env.CLAXEDO_RELAY_LOCATION_HINT } : {},
1849
- ...requestRegion ? { region: requestRegion } : {},
1850
- ...requestCountry ? { country: requestCountry } : {}
1851
- });
1852
1762
  const namespace = namespaceFromEnv(env, options);
1853
1763
  if (!namespace) {
1854
1764
  return json(
@@ -1872,6 +1782,8 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
1872
1782
  url.pathname.startsWith("/workspaces/") || url.pathname.startsWith("/host-tunnels/") ? 400 : 404
1873
1783
  );
1874
1784
  }
1785
+ const placement = await workspaceRelayRoomPlacement(request, env, workspaceId);
1786
+ const locationHint = placement.locationHint;
1875
1787
  const routeFinishedAt = performance.now();
1876
1788
  const response = await namespace.get(namespace.idFromName(workspaceRelayDurableObjectRoomName(workspaceId)), {
1877
1789
  locationHint
@@ -1881,10 +1793,10 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
1881
1793
  startedAt: gatewayStartedAt,
1882
1794
  routeKind: "gateway",
1883
1795
  phases: []
1884
- }));
1796
+ }, placement));
1885
1797
  const fetchFinishedAt = performance.now();
1886
1798
  if (response.status === 101 || "webSocket" in response && response.webSocket) {
1887
- return url.pathname.startsWith("/workspaces/") ? withCors(request, response) : response;
1799
+ return url.pathname.startsWith("/workspaces/") ? withRelayResponseHeaders(request, response) : response;
1888
1800
  }
1889
1801
  const headers = new Headers(response.headers);
1890
1802
  headers.set(TRACE_ID_HEADER, headers.get(TRACE_ID_HEADER) ?? gatewayTraceId);
@@ -1896,22 +1808,24 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
1896
1808
  headers
1897
1809
  });
1898
1810
  const gatewayFinishedAt = performance.now();
1899
- const roomTiming = tracedResponse.headers.get("server-timing");
1900
- if (roomTiming) {
1901
- const gatewayTiming = [
1902
- `relay-gateway-route;dur=${roundedMs2(routeFinishedAt - gatewayStartedAt)}`,
1903
- `relay-gateway-do-fetch;dur=${roundedMs2(fetchFinishedAt - routeFinishedAt)}`,
1904
- `relay-gateway-response;dur=${roundedMs2(gatewayFinishedAt - gatewayResponseStartedAt)}`,
1905
- `relay-gateway;dur=${roundedMs2(gatewayFinishedAt - gatewayStartedAt)}`
1906
- ].join(", ");
1811
+ const sampled = tracedResponse.headers.get(TRACE_SAMPLED_RESPONSE_HEADER) === "1";
1812
+ tracedResponse.headers.delete(TRACE_SAMPLED_RESPONSE_HEADER);
1813
+ const roomTiming = tracedResponse.headers.get("server-timing") ?? "";
1814
+ if (sampled) {
1815
+ const gatewayTiming = serverTiming([
1816
+ { name: "relay-gateway-route", ms: routeFinishedAt - gatewayStartedAt },
1817
+ { name: "relay-gateway-do-fetch", ms: fetchFinishedAt - routeFinishedAt },
1818
+ { name: "relay-gateway-response", ms: gatewayFinishedAt - gatewayResponseStartedAt },
1819
+ { name: "relay-gateway", ms: gatewayFinishedAt - gatewayStartedAt }
1820
+ ]);
1907
1821
  tracedResponse.headers.set("server-timing", `${roomTiming}, ${gatewayTiming}`);
1908
1822
  }
1909
- return url.pathname.startsWith("/workspaces/") ? withCors(request, tracedResponse) : tracedResponse;
1823
+ return url.pathname.startsWith("/workspaces/") ? withRelayResponseHeaders(request, tracedResponse) : tracedResponse;
1910
1824
  }
1911
1825
  };
1912
1826
  }
1913
1827
  function createWorkspaceRelayDurableObjectRoom(options) {
1914
- const directory = options.directory ?? createWorkspaceRelayDirectory({ sweepIntervalMs: 0 });
1828
+ const directory = options.directory ?? createWorkspaceRelayDirectory();
1915
1829
  const relayOptions = { ...options, directory };
1916
1830
  const hostTunnels = /* @__PURE__ */ new Map();
1917
1831
  const clients = /* @__PURE__ */ new Set();
@@ -1939,7 +1853,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
1939
1853
  };
1940
1854
  const closeSocket = (socket, code, reason) => {
1941
1855
  try {
1942
- socket.close?.(code, reason);
1856
+ socket.close?.(code, closeReason(reason));
1943
1857
  } catch {
1944
1858
  }
1945
1859
  };
@@ -1986,12 +1900,14 @@ function createWorkspaceRelayDurableObjectRoom(options) {
1986
1900
  const attachment = socketAttachment(socket);
1987
1901
  if (!attachment) continue;
1988
1902
  if (attachment.kind === "host-tunnel") {
1903
+ const incumbent = hostTunnels.get(attachment.hostId);
1904
+ if (incumbent && incumbent.connectedAt > attachment.connectedAt) continue;
1989
1905
  hostTunnels.set(attachment.hostId, {
1990
1906
  hostId: attachment.hostId,
1991
1907
  workspaceIds: attachment.workspaceIds,
1992
1908
  connectedAt: attachment.connectedAt,
1993
- ...attachment.enrollmentId ? { enrollmentId: attachment.enrollmentId } : {},
1994
- ...attachment.generation !== void 0 ? { generation: attachment.generation } : {},
1909
+ enrollmentId: attachment.enrollmentId,
1910
+ generation: attachment.generation,
1995
1911
  generationCheckFailures: attachment.generationCheckFailures ?? 0,
1996
1912
  socket,
1997
1913
  pending: /* @__PURE__ */ new Map(),
@@ -2008,7 +1924,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2008
1924
  for (const row of clientRows) {
2009
1925
  const tunnel = hostTunnels.get(row.attachment.target.hostId);
2010
1926
  if (!tunnel) {
2011
- closeSocket(row.socket, 1011, "User-hosted tunnel unavailable");
1927
+ closeSocket(row.socket, 1011, "Host tunnel unavailable");
2012
1928
  continue;
2013
1929
  }
2014
1930
  const client = {
@@ -2059,7 +1975,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2059
1975
  clearInterval(timer);
2060
1976
  onInactive(reason);
2061
1977
  };
2062
- void Promise.resolve(options.isRuntimeAccessTokenActive(client.request.claims)).then((active) => {
1978
+ void Promise.resolve().then(() => options.isRuntimeAccessTokenActive(client.request.claims)).then((active) => {
2063
1979
  if (active.active) {
2064
1980
  consecutiveUnreachable = 0;
2065
1981
  return;
@@ -2076,20 +1992,20 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2076
1992
  unrefTimer(timer);
2077
1993
  return timer;
2078
1994
  };
2079
- const watchUserHostedTarget = (client, onInactive) => {
1995
+ const watchHostTunnelTarget = (client, onInactive) => {
2080
1996
  const intervalMs = options.workspaceTargetActiveCheckIntervalMs ?? WORKSPACE_TARGET_ACTIVE_CHECK_INTERVAL_MS_DEFAULT;
2081
1997
  if (intervalMs <= 0) return void 0;
2082
1998
  const target = client.request.target;
2083
1999
  let consecutiveUnreachable = 0;
2084
2000
  const timer = setInterval(() => {
2085
2001
  void Promise.resolve(options.resolveTarget(client.request.claims)).then((activeTarget) => {
2086
- if (activeTarget?.access === "user-hosted" && activeTarget.workspaceId === target.workspaceId && activeTarget.hostId === target.hostId && directory.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
2002
+ if (activeTarget?.backing === "local-worktree" && activeTarget.workspaceId === target.workspaceId && activeTarget.hostId === target.hostId && directory.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
2087
2003
  consecutiveUnreachable = 0;
2088
2004
  return;
2089
2005
  }
2090
2006
  consecutiveUnreachable = 0;
2091
2007
  clearInterval(timer);
2092
- onInactive("User-hosted workspace is offline");
2008
+ onInactive("The machine serving this workspace is offline");
2093
2009
  }).catch(() => {
2094
2010
  consecutiveUnreachable += 1;
2095
2011
  if (consecutiveUnreachable < resolverGraceAttempts) return;
@@ -2105,13 +2021,12 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2105
2021
  hostId: tunnel.hostId,
2106
2022
  workspaceIds: tunnel.workspaceIds,
2107
2023
  connectedAt: tunnel.connectedAt,
2108
- ...tunnel.enrollmentId ? { enrollmentId: tunnel.enrollmentId } : {},
2109
- ...tunnel.generation !== void 0 ? { generation: tunnel.generation } : {},
2024
+ enrollmentId: tunnel.enrollmentId,
2025
+ generation: tunnel.generation,
2110
2026
  ...tunnel.generationCheckFailures ? { generationCheckFailures: tunnel.generationCheckFailures } : {}
2111
2027
  });
2112
- const fenced = (tunnel) => Boolean(options.resolveHostGeneration) && tunnel.generation !== void 0;
2113
2028
  const recheckHostGeneration = async (tunnel) => {
2114
- if (!fenced(tunnel) || hostTunnels.get(tunnel.hostId) !== tunnel) return;
2029
+ if (hostTunnels.get(tunnel.hostId) !== tunnel) return;
2115
2030
  const decision = await checkHostTunnelGeneration(options.resolveHostGeneration, {
2116
2031
  enrollment_id: tunnel.enrollmentId,
2117
2032
  generation: tunnel.generation
@@ -2138,7 +2053,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2138
2053
  };
2139
2054
  const watchHostGeneration = (tunnel) => {
2140
2055
  const intervalMs = options.hostGenerationCheckIntervalMs ?? HOST_GENERATION_CHECK_INTERVAL_MS_DEFAULT;
2141
- if (tunnel.generationWatcher || hibernation || !fenced(tunnel) || intervalMs <= 0) return;
2056
+ if (tunnel.generationWatcher || hibernation || intervalMs <= 0) return;
2142
2057
  tunnel.generationWatcher = setInterval(() => {
2143
2058
  void recheckHostGeneration(tunnel).catch(() => {
2144
2059
  });
@@ -2315,12 +2230,12 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2315
2230
  const active = await options.isRuntimeAccessTokenActive(client.request.claims);
2316
2231
  return active.active ? { active: true } : { active: false, reason: active.reason };
2317
2232
  };
2318
- const userHostedTargetActive = async (client) => {
2233
+ const hostTunnelTargetActive = async (client) => {
2319
2234
  const target = client.request.target;
2320
2235
  const activeTarget = await options.resolveTarget(client.request.claims);
2321
- return !!(activeTarget?.access === "user-hosted" && activeTarget.workspaceId === target.workspaceId && activeTarget.hostId === target.hostId && directory.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId }));
2236
+ return !!(activeTarget?.backing === "local-worktree" && activeTarget.workspaceId === target.workspaceId && activeTarget.hostId === target.hostId && directory.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId }));
2322
2237
  };
2323
- const dropUserHostedChannel = (tunnel, channelId, client, code, reason) => {
2238
+ const dropHostTunnelChannel = (tunnel, channelId, client, code, reason) => {
2324
2239
  tunnel.channels.delete(channelId);
2325
2240
  clients.delete(client);
2326
2241
  closeSocket(client.socket, code, reason);
@@ -2341,7 +2256,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2341
2256
  const scheduleHibernatedRevocationCheck = async () => {
2342
2257
  const intervalMs = hibernatedRevocationIntervalMs();
2343
2258
  if (!hibernation || !options.alarms || intervalMs <= 0) return;
2344
- if (clients.size === 0 && ![...hostTunnels.values()].some(fenced)) return;
2259
+ if (clients.size === 0 && hostTunnels.size === 0) return;
2345
2260
  const now = options.now ?? Date.now;
2346
2261
  const at = now() + intervalMs;
2347
2262
  try {
@@ -2355,10 +2270,10 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2355
2270
  ensureHibernatedSocketsRebuilt();
2356
2271
  const now = options.now ?? Date.now;
2357
2272
  const revoke = (client, reason) => {
2358
- if (isUserHostedClient(client)) {
2273
+ if (isHostTunnelClient(client)) {
2359
2274
  for (const tunnel of hostTunnels.values()) {
2360
2275
  if (tunnel.channels.get(client.channelId) !== client) continue;
2361
- dropUserHostedChannel(tunnel, client.channelId, client, 1008, reason);
2276
+ dropHostTunnelChannel(tunnel, client.channelId, client, 1008, reason);
2362
2277
  return;
2363
2278
  }
2364
2279
  }
@@ -2385,23 +2300,23 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2385
2300
  }
2386
2301
  await scheduleHibernatedRevocationCheck();
2387
2302
  };
2388
- const activeUserHostedClient = (channelId, socket) => {
2303
+ const activeHostTunnelClient = (channelId, socket) => {
2389
2304
  for (const tunnel of hostTunnels.values()) {
2390
2305
  const client = tunnel.channels.get(channelId);
2391
2306
  if (client?.socket === socket) return { tunnel, client };
2392
2307
  }
2393
2308
  return void 0;
2394
2309
  };
2395
- const handleUserHostedClientMessage = async (channelId, socket, data) => {
2396
- const active = activeUserHostedClient(channelId, socket);
2310
+ const handleHostTunnelClientMessage = async (channelId, socket, data) => {
2311
+ const active = activeHostTunnelClient(channelId, socket);
2397
2312
  if (!active) return;
2398
2313
  const token = await runtimeAccessTokenActive(active.client);
2399
2314
  if (!token.active) {
2400
2315
  closeSocket(socket, 1008, token.reason);
2401
2316
  return;
2402
2317
  }
2403
- if (!await userHostedTargetActive(active.client)) {
2404
- closeSocket(socket, 1011, "User-hosted workspace is offline");
2318
+ if (!await hostTunnelTargetActive(active.client)) {
2319
+ closeSocket(socket, 1011, "The machine serving this workspace is offline");
2405
2320
  return;
2406
2321
  }
2407
2322
  const frame = await socketFrame(data);
@@ -2412,7 +2327,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2412
2327
  channel_id: channelId,
2413
2328
  ...frame
2414
2329
  }));
2415
- if (!sent) closeSocket(socket, 1011, "User-hosted tunnel unavailable");
2330
+ if (!sent) closeSocket(socket, 1011, "Host tunnel unavailable");
2416
2331
  };
2417
2332
  const applyRegistrationUpdate = async (tunnel, workspaceIds, token) => {
2418
2333
  const hostId = tunnel.hostId;
@@ -2446,12 +2361,12 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2446
2361
  watchHostGeneration(tunnel);
2447
2362
  void scheduleHibernatedRevocationCheck();
2448
2363
  };
2449
- const handleTunnelMessage = async (hostId, event) => {
2364
+ const handleTunnelMessage = async (hostId, socket, event) => {
2450
2365
  const row = parseTunnelMessageData(event.data);
2451
2366
  if (!row.ok) return;
2452
2367
  const parsed = validateTunnelMessage(row.value);
2453
2368
  const tunnel = hostTunnels.get(hostId);
2454
- if (!parsed.ok || !tunnel) return;
2369
+ if (!parsed.ok || !tunnel || tunnel.socket !== socket) return;
2455
2370
  directory.recordPong(tunnel.hostId);
2456
2371
  if (parsed.message.type === "ping") {
2457
2372
  tunnel.socket.send?.(JSON.stringify(makeTunnelPong(parsed.message)));
@@ -2483,7 +2398,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2483
2398
  if (!pending) return;
2484
2399
  const chunk = base64ToBytes(message.body_base64);
2485
2400
  if (pending.bytes + chunk.byteLength > (options.tunnelResponseBodyMaxBytes ?? TUNNEL_RESPONSE_BODY_MAX_BYTES_DEFAULT)) {
2486
- failPendingTunnelResponse(tunnel, message.request_id, pending, new Error("User-hosted response body exceeds the relay limit"));
2401
+ failPendingTunnelResponse(tunnel, message.request_id, pending, hostTunnelFailure("host_tunnel_response_body_too_large"));
2487
2402
  return;
2488
2403
  }
2489
2404
  pending.bytes += chunk.byteLength;
@@ -2529,7 +2444,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2529
2444
  return;
2530
2445
  }
2531
2446
  if (!sendSocket(channel.socket, tunnelFrame(message))) {
2532
- dropUserHostedChannel(tunnel, message.channel_id, channel, 1013, "Client WebSocket delivery failed");
2447
+ dropHostTunnelChannel(tunnel, message.channel_id, channel, 1013, "Client WebSocket delivery failed");
2533
2448
  }
2534
2449
  return;
2535
2450
  }
@@ -2539,7 +2454,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2539
2454
  if (!channel) return;
2540
2455
  tunnel.channels.delete(message.channel_id);
2541
2456
  clients.delete(channel);
2542
- closeSocket(channel.socket, safeCloseCode(message.code), message.reason ?? "");
2457
+ closeSocket(channel.socket, closeCode(message.code), message.reason ?? "");
2543
2458
  return;
2544
2459
  }
2545
2460
  if (parsed.message.type === "error" && parsed.message.channel_id) {
@@ -2567,7 +2482,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2567
2482
  hostTunnels.delete(hostId);
2568
2483
  directory.disconnectHost(hostId);
2569
2484
  };
2570
- const cleanupUserHostedClient = (channelId, socket, event) => {
2485
+ const cleanupHostTunnelClient = (channelId, socket, event) => {
2571
2486
  for (const tunnel of hostTunnels.values()) {
2572
2487
  const active = tunnel.channels.get(channelId);
2573
2488
  if (!active || active.socket !== socket) continue;
@@ -2577,7 +2492,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2577
2492
  type: "ws.close",
2578
2493
  protocol: TUNNEL_PROTOCOL_VERSION,
2579
2494
  channel_id: channelId,
2580
- code: safeCloseCode(event?.code, 1e3),
2495
+ code: closeCode(event?.code, 1e3),
2581
2496
  reason: event?.reason ?? "Client WebSocket closed"
2582
2497
  }));
2583
2498
  return;
@@ -2585,7 +2500,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2585
2500
  };
2586
2501
  const admitHostTunnel = async (request, roomWorkspaceId, url) => {
2587
2502
  if (!websocketRequest(request)) return json("websocket_upgrade_required", "Workspace Relay requires a WebSocket upgrade", 426);
2588
- const hostId = trimToUndefined(hostIdFromTunnelPath(url.pathname));
2503
+ const hostId = trimToUndefined5(hostIdFromTunnelPath(url.pathname));
2589
2504
  if (!hostId) return json("host_tunnel_host_required", "Host tunnel id is required", 400);
2590
2505
  const workspaceIds = workspaceIdsFromSearch(url);
2591
2506
  if (!workspaceIds.includes(roomWorkspaceId)) {
@@ -2610,8 +2525,8 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2610
2525
  }
2611
2526
  const connectedAt = options.now?.() ?? Date.now();
2612
2527
  const fence = {
2613
- ...claims.enrollment_id ? { enrollmentId: claims.enrollment_id } : {},
2614
- ...claims.generation !== void 0 ? { generation: claims.generation } : {}
2528
+ enrollmentId: claims.enrollment_id,
2529
+ generation: claims.generation
2615
2530
  };
2616
2531
  const pair = acceptSocket({ kind: "host-tunnel", hostId, workspaceIds, connectedAt, ...fence });
2617
2532
  const tunnel = {
@@ -2635,10 +2550,10 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2635
2550
  if (!hibernation) {
2636
2551
  pair.server.addEventListener?.("message", (event) => {
2637
2552
  if (!event || event.data === void 0) return;
2638
- void handleTunnelMessage(hostId, { data: event.data }).catch((err) => reportFrameHandlerFailure(pair.server, err));
2553
+ void handleTunnelMessage(hostId, pair.server, { data: event.data }).catch((err) => reportFrameHandlerFailure(pair.server, err));
2639
2554
  });
2640
- pair.server.addEventListener?.("close", () => cleanupHostTunnel(hostId, pair.server, "User-hosted tunnel disconnected"));
2641
- pair.server.addEventListener?.("error", () => cleanupHostTunnel(hostId, pair.server, "User-hosted tunnel disconnected"));
2555
+ pair.server.addEventListener?.("close", () => cleanupHostTunnel(hostId, pair.server, "Host tunnel disconnected"));
2556
+ pair.server.addEventListener?.("error", () => cleanupHostTunnel(hostId, pair.server, "Host tunnel disconnected"));
2642
2557
  }
2643
2558
  return upgradeResponse(pair.client, request);
2644
2559
  };
@@ -2646,7 +2561,12 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2646
2561
  if (!websocketRequest(request)) return json("websocket_upgrade_required", "Workspace Relay requires a WebSocket upgrade", 426);
2647
2562
  const authorized = await authorizeWorkspaceRelayRequest(relayOptions, request, workspaceId);
2648
2563
  if (!authorized.ok) return authorized.response;
2649
- if (isUserHostedTarget(authorized.request.target)) return admitUserHostedClient(request, authorized.request);
2564
+ const originDenied = workspaceUpgradeOriginDenied(request);
2565
+ if (originDenied) return originDenied;
2566
+ if (isHostTunnelTarget(authorized.request.target)) return admitHostTunnelClient(request, authorized.request);
2567
+ if (isSessionHostTarget(authorized.request.target)) {
2568
+ return json("durable_object_websocket_unsupported", "A session host serves its events over HTTP streams, not WebSockets", 400);
2569
+ }
2650
2570
  return await admitCloudClient(request, authorized.request);
2651
2571
  };
2652
2572
  const admitCloudClient = async (request, authorized) => {
@@ -2664,7 +2584,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2664
2584
  new Headers(),
2665
2585
  authorized.relayHostToken,
2666
2586
  authorized.target.workspaceId,
2667
- { userHosted: false, upstreamHeaders: authorized.target.upstreamHeaders }
2587
+ { hostTunnel: false }
2668
2588
  ))
2669
2589
  }
2670
2590
  );
@@ -2682,12 +2602,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2682
2602
  const emitTrace = (openedAt) => {
2683
2603
  if (!trace || trace.emitted) return;
2684
2604
  trace.emitted = true;
2685
- sendSocket(pair.server, JSON.stringify({
2686
- type: "relay.trace",
2687
- wsUpstreamOpenMs: roundedMs2(openedAt - trace.upstreamStartedAt),
2688
- queuedFrames: trace.queueTimes.length,
2689
- maxQueuedDelayMs: roundedMs2(trace.queueTimes.reduce((max, at) => Math.max(max, openedAt - at), 0))
2690
- }));
2605
+ sendSocket(pair.server, JSON.stringify(webSocketTraceFrame(trace.upstreamStartedAt, trace.queueTimes, openedAt)));
2691
2606
  };
2692
2607
  const activeTokenTimer = watchRuntimeAccessToken(client, (reason) => {
2693
2608
  closeSocket(pair.server, 1008, reason);
@@ -2697,14 +2612,14 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2697
2612
  closed = true;
2698
2613
  if (activeTokenTimer) clearInterval(activeTokenTimer);
2699
2614
  clients.delete(client);
2700
- closeSocket(upstream, safeCloseCode(event?.code, 1e3), event?.reason ?? "Client WebSocket closed");
2615
+ closeSocket(upstream, closeCode(event?.code, 1e3), event?.reason ?? "Client WebSocket closed");
2701
2616
  };
2702
2617
  const cleanupUpstream = (event) => {
2703
2618
  if (closed) return;
2704
2619
  closed = true;
2705
2620
  if (activeTokenTimer) clearInterval(activeTokenTimer);
2706
2621
  clients.delete(client);
2707
- closeSocket(pair.server, safeCloseCode(event?.code), event?.reason ?? "");
2622
+ closeSocket(pair.server, closeCode(event?.code), event?.reason ?? "");
2708
2623
  };
2709
2624
  const flushQueue = () => {
2710
2625
  upstreamOpen = true;
@@ -2720,7 +2635,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2720
2635
  const deliverClientFrame = orderedFrameDelivery((payload) => {
2721
2636
  if (!upstreamOpen) {
2722
2637
  const frameBytes = preOpenFrameBytes(payload);
2723
- 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)) {
2638
+ 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)) {
2724
2639
  closeSocket(pair.server, 1011, "Upstream WebSocket queue limit exceeded");
2725
2640
  cleanupClient({ code: 1011, reason: "Upstream WebSocket queue limit exceeded" });
2726
2641
  return;
@@ -2747,17 +2662,17 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2747
2662
  if (trace && upstreamOpen) emitTrace(upstreamConnectedAt);
2748
2663
  return upgradeResponse(pair.client, request);
2749
2664
  };
2750
- const admitUserHostedClient = (request, authorized) => {
2665
+ const admitHostTunnelClient = (request, authorized) => {
2751
2666
  const tunnel = hostTunnels.get(authorized.target.hostId);
2752
2667
  if (!tunnel || !directory.activeHost({ hostId: authorized.target.hostId, workspaceId: authorized.target.workspaceId })) {
2753
- return json("user_hosted_app_offline", "User-hosted workspace is offline", 503);
2668
+ return json("host_tunnel_offline", "The machine serving this workspace is offline", 503);
2754
2669
  }
2755
2670
  if (tunnel.channels.size >= (options.tunnelChannelCap ?? TUNNEL_CHANNEL_CAP_DEFAULT)) {
2756
- return json("too_many_channels", "User-hosted tunnel has too many active WebSocket channels", 503);
2671
+ return json("too_many_channels", "Host tunnel has too many active WebSocket channels", 503);
2757
2672
  }
2758
2673
  const channelId = randomToken();
2759
2674
  const pair = acceptSocket({
2760
- kind: "user-hosted-client",
2675
+ kind: "host-tunnel-client",
2761
2676
  channelId,
2762
2677
  claims: authorized.claims,
2763
2678
  target: authorized.target,
@@ -2774,7 +2689,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2774
2689
  const activeTokenTimer = hibernation ? void 0 : watchRuntimeAccessToken(client, (reason) => {
2775
2690
  closeSocket(pair.server, 1008, reason);
2776
2691
  });
2777
- const activeTargetTimer = hibernation ? void 0 : watchUserHostedTarget(client, (reason) => {
2692
+ const activeTargetTimer = hibernation ? void 0 : watchHostTunnelTarget(client, (reason) => {
2778
2693
  closeSocket(pair.server, 1011, reason);
2779
2694
  });
2780
2695
  if (hibernation) void scheduleHibernatedRevocationCheck();
@@ -2782,11 +2697,11 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2782
2697
  const cleanup = (event) => {
2783
2698
  if (activeTokenTimer) clearInterval(activeTokenTimer);
2784
2699
  if (activeTargetTimer) clearInterval(activeTargetTimer);
2785
- cleanupUserHostedClient(channelId, pair.server, event);
2700
+ cleanupHostTunnelClient(channelId, pair.server, event);
2786
2701
  };
2787
2702
  pair.server.addEventListener?.("message", (event) => {
2788
2703
  if (!event) return;
2789
- void handleUserHostedClientMessage(channelId, pair.server, event.data).catch((err) => reportFrameHandlerFailure(pair.server, err));
2704
+ void handleHostTunnelClientMessage(channelId, pair.server, event.data).catch((err) => reportFrameHandlerFailure(pair.server, err));
2790
2705
  });
2791
2706
  pair.server.addEventListener?.("close", cleanup);
2792
2707
  pair.server.addEventListener?.("error", cleanup);
@@ -2801,14 +2716,14 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2801
2716
  new Headers(),
2802
2717
  authorized.relayHostToken,
2803
2718
  authorized.target.workspaceId,
2804
- { userHosted: true }
2719
+ { hostTunnel: true }
2805
2720
  ))
2806
2721
  }));
2807
2722
  if (!opened) {
2808
2723
  tunnel.channels.delete(channelId);
2809
2724
  clients.delete(client);
2810
- closeSocket(pair.server, 1011, "User-hosted tunnel unavailable");
2811
- return json("user_hosted_app_offline", "User-hosted workspace is offline", 503);
2725
+ closeSocket(pair.server, 1011, "Host tunnel unavailable");
2726
+ return json("host_tunnel_offline", "The machine serving this workspace is offline", 503);
2812
2727
  }
2813
2728
  return upgradeResponse(pair.client, request);
2814
2729
  };
@@ -2821,9 +2736,13 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2821
2736
  })
2822
2737
  );
2823
2738
  if (!authorized.ok) return authorized.response;
2824
- if (isUserHostedTarget(authorized.request.target)) {
2825
- trace && (trace.routeKind = "user-hosted-http");
2826
- return forwardUserHostedHttp(request, authorized.request, trace);
2739
+ if (isHostTunnelTarget(authorized.request.target)) {
2740
+ trace && (trace.routeKind = "host-tunnel-http");
2741
+ return forwardHostTunnelHttp(request, authorized.request, trace);
2742
+ }
2743
+ if (isSessionHostTarget(authorized.request.target)) {
2744
+ trace && (trace.routeKind = "session-host-http");
2745
+ return traceSpan(trace, "session-host-fetch", () => forwardSessionHostHttp(options.sessionHost, request, authorized.request));
2827
2746
  }
2828
2747
  trace && (trace.routeKind = "cloud-http");
2829
2748
  const controller = new AbortController();
@@ -2836,8 +2755,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2836
2755
  new URL(request.url).search
2837
2756
  ),
2838
2757
  workspaceRelayForwardRequestInit(request, authorized.request.relayHostToken, workspaceId, {
2839
- signal: controller.signal,
2840
- upstreamHeaders: authorized.request.target.upstreamHeaders
2758
+ signal: controller.signal
2841
2759
  })
2842
2760
  ));
2843
2761
  return new Response(upstream.body, {
@@ -2858,27 +2776,27 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2858
2776
  clearTimeout(timer);
2859
2777
  }
2860
2778
  };
2861
- const forwardUserHostedHttp = async (request, authorized, trace) => {
2779
+ const forwardHostTunnelHttp = async (request, authorized, trace) => {
2862
2780
  const tunnel = hostTunnels.get(authorized.target.hostId);
2863
2781
  if (!tunnel || !directory.activeHost({ hostId: authorized.target.hostId, workspaceId: authorized.target.workspaceId })) {
2864
- return json("user_hosted_app_offline", "User-hosted workspace is offline", 503);
2782
+ return json("host_tunnel_offline", "The machine serving this workspace is offline", 503);
2865
2783
  }
2866
2784
  const awaitingStart = [...tunnel.pending.values()].filter((entry) => entry.startedSeq === void 0).length;
2867
2785
  if (awaitingStart >= (options.tunnelPendingHttpCap ?? TUNNEL_PENDING_HTTP_CAP_DEFAULT)) {
2868
- return json("too_many_pending_requests", "User-hosted tunnel has too many pending HTTP requests", 503);
2786
+ return json("too_many_pending_requests", "Host tunnel has too many pending HTTP requests", 503);
2869
2787
  }
2870
2788
  const requestId = randomToken();
2871
2789
  const init = workspaceRelayForwardRequestInit(request, authorized.relayHostToken, authorized.target.workspaceId, {
2872
- userHosted: true
2790
+ hostTunnel: true
2873
2791
  });
2874
2792
  const bodyMaxBytes = options.tunnelRequestBodyMaxBytes ?? TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT;
2875
2793
  const contentLength = Number(request.headers.get("content-length") ?? Number.NaN);
2876
2794
  if (Number.isFinite(contentLength) && contentLength > bodyMaxBytes) {
2877
- return json("request_body_too_large", "User-hosted request body exceeds the relay limit", 413);
2795
+ return json("request_body_too_large", "Host tunnel request body exceeds the relay limit", 413);
2878
2796
  }
2879
2797
  const bodyBytes = request.method === "GET" || request.method === "HEAD" ? void 0 : await traceSpan(trace, "request-body", () => readRequestBodyCapped(request, bodyMaxBytes));
2880
2798
  if (bodyBytes === "too_large") {
2881
- return json("request_body_too_large", "User-hosted request body exceeds the relay limit", 413);
2799
+ return json("request_body_too_large", "Host tunnel request body exceeds the relay limit", 413);
2882
2800
  }
2883
2801
  const body = bodyBytes ? bytesToBase64(bodyBytes) : void 0;
2884
2802
  const pending = new Promise((resolve, reject) => {
@@ -2891,7 +2809,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2891
2809
  paused: false,
2892
2810
  reason: "closed"
2893
2811
  }));
2894
- reject(new Error("User-hosted tunnel timed out"));
2812
+ reject(hostTunnelFailure("host_tunnel_timeout"));
2895
2813
  }, options.forwardTimeoutMs ?? 3e4);
2896
2814
  tunnel.pending.set(requestId, {
2897
2815
  chunks: [],
@@ -2915,15 +2833,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2915
2833
  try {
2916
2834
  return await traceSpan(trace, "tunnel-wait", () => pending);
2917
2835
  } catch (err) {
2918
- const timeout = err instanceof Error && err.message.includes("timed out");
2919
- const responseTooLarge = err instanceof Error && err.message.includes("response body exceeds");
2920
- return Response.json(
2921
- errorBody(
2922
- timeout ? "user_hosted_tunnel_timeout" : responseTooLarge ? "user_hosted_response_body_too_large" : "user_hosted_tunnel_unavailable",
2923
- timeout ? "User-hosted tunnel timed out" : responseTooLarge ? "User-hosted response body exceeds the relay limit" : "User-hosted tunnel is unavailable"
2924
- ),
2925
- { status: responseTooLarge ? 413 : 503 }
2926
- );
2836
+ return hostTunnelFailureResponse(err);
2927
2837
  }
2928
2838
  };
2929
2839
  return {
@@ -2958,7 +2868,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2958
2868
  trace.phases.push({ name: "relay-room-total", ms: performance.now() - roomTotalStartedAt });
2959
2869
  const tracedResponse = withTraceHeaders(response, trace);
2960
2870
  logTrace(options, nextRequest, roomWorkspaceId, trace, tracedResponse);
2961
- return withCors(request, tracedResponse);
2871
+ return withRelayResponseHeaders(request, tracedResponse);
2962
2872
  }
2963
2873
  return json("relay_route_not_found", "Workspace Relay route not found", 404);
2964
2874
  },
@@ -2968,10 +2878,10 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2968
2878
  const attachment = socketAttachment(socket);
2969
2879
  if (!attachment) return;
2970
2880
  if (attachment.kind === "host-tunnel") {
2971
- await handleTunnelMessage(attachment.hostId, { data: message });
2881
+ await handleTunnelMessage(attachment.hostId, socket, { data: message });
2972
2882
  return;
2973
2883
  }
2974
- await handleUserHostedClientMessage(attachment.channelId, socket, message);
2884
+ await handleHostTunnelClientMessage(attachment.channelId, socket, message);
2975
2885
  } catch (err) {
2976
2886
  reportFrameHandlerFailure(socket, err);
2977
2887
  }
@@ -2989,23 +2899,24 @@ function createWorkspaceRelayDurableObjectRoom(options) {
2989
2899
  },
2990
2900
  webSocketClose(socket, code, reason) {
2991
2901
  ensureHibernatedSocketsRebuilt();
2902
+ closeSocket(socket, closeCode(code, 1e3), reason);
2992
2903
  const attachment = socketAttachment(socket);
2993
2904
  if (!attachment) return;
2994
2905
  if (attachment.kind === "host-tunnel") {
2995
- cleanupHostTunnel(attachment.hostId, socket, "User-hosted tunnel disconnected");
2906
+ cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected");
2996
2907
  return;
2997
2908
  }
2998
- cleanupUserHostedClient(attachment.channelId, socket, { code, reason });
2909
+ cleanupHostTunnelClient(attachment.channelId, socket, { code, reason });
2999
2910
  },
3000
2911
  webSocketError(socket) {
3001
2912
  ensureHibernatedSocketsRebuilt();
3002
2913
  const attachment = socketAttachment(socket);
3003
2914
  if (!attachment) return;
3004
2915
  if (attachment.kind === "host-tunnel") {
3005
- cleanupHostTunnel(attachment.hostId, socket, "User-hosted tunnel disconnected");
2916
+ cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected");
3006
2917
  return;
3007
2918
  }
3008
- cleanupUserHostedClient(attachment.channelId, socket, { code: 1011, reason: "WebSocket error" });
2919
+ cleanupHostTunnelClient(attachment.channelId, socket, { code: 1011, reason: "WebSocket error" });
3009
2920
  },
3010
2921
  state() {
3011
2922
  ensureHibernatedSocketsRebuilt();
@@ -3019,63 +2930,222 @@ function createWorkspaceRelayDurableObjectRoom(options) {
3019
2930
  drain: drainController
3020
2931
  };
3021
2932
  }
2933
+
2934
+ // ../claxedo-helpers/dist/deadline.mjs
2935
+ async function withDeadlineSignal(timeoutMs, work, parent) {
2936
+ const deadline = new AbortController();
2937
+ const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
2938
+ try {
2939
+ return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
2940
+ } finally {
2941
+ clearTimeout(timer);
2942
+ }
2943
+ }
2944
+
2945
+ // src/resolver-clients.ts
2946
+ function parseRuntimeAccessTokenActiveResult(input) {
2947
+ if (!isRecord(input)) return void 0;
2948
+ const row = input;
2949
+ if (row.active === true) return { active: true };
2950
+ if (row.active !== false) return void 0;
2951
+ if (typeof row.code !== "string" || typeof row.reason !== "string") return void 0;
2952
+ return { active: false, code: row.code, reason: row.reason };
2953
+ }
2954
+ function parseHostGenerationResult(input) {
2955
+ if (!isRecord(input)) return void 0;
2956
+ const row = input;
2957
+ if (typeof row.enrollmentId !== "string" || !row.enrollmentId.trim()) return void 0;
2958
+ if (!isHostGeneration(row.generation) || typeof row.revoked !== "boolean") return void 0;
2959
+ return { enrollmentId: row.enrollmentId, generation: row.generation, revoked: row.revoked };
2960
+ }
2961
+ var HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT = 5e3;
2962
+ var HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE = "relay_resolver_enrollment_not_found";
2963
+ function createHostGenerationResolverLookup(url, options) {
2964
+ const fetcher = options.fetch ?? ((target, init) => fetch(target, init));
2965
+ const timeoutMs = options.timeoutMs ?? HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT;
2966
+ return async ({ enrollmentId }) => {
2967
+ const target = new URL(url);
2968
+ target.searchParams.set("enrollmentId", enrollmentId);
2969
+ return withDeadlineSignal(timeoutMs, async (signal) => {
2970
+ const res = await fetcher(target, { headers: options.headers, signal });
2971
+ if (res.status === 404) {
2972
+ const body = await res.json().catch(() => void 0);
2973
+ const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
2974
+ if (error?.code === HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE) return void 0;
2975
+ throw new Error(`relay host-generation resolver failed: 404 (no host-generation route at ${url})`);
2976
+ }
2977
+ if (!res.ok) throw new Error(`relay host-generation resolver failed: ${res.status} ${await res.text()}`);
2978
+ const result = parseHostGenerationResult(await res.json());
2979
+ if (!result) throw new Error("relay host-generation resolver returned a malformed result");
2980
+ return result;
2981
+ });
2982
+ };
2983
+ }
2984
+ var RESOLVER_CACHE_MAX_ENTRIES = 8192;
2985
+ var REVOCATION_CACHE_TTL_MS_DEFAULT = 1e4;
2986
+ var TARGET_CACHE_TTL_MS_DEFAULT = 1e4;
2987
+ var DURABLE_OBJECT_TARGET_CACHE_TTL_MS_DEFAULT = 30 * 6e4;
2988
+ function pruneExpiringCache(cache, now, maxEntries) {
2989
+ for (const [key, entry] of cache) {
2990
+ if (entry.expiresAt <= now) cache.delete(key);
2991
+ }
2992
+ while (cache.size >= maxEntries) {
2993
+ const key = cache.keys().next().value;
2994
+ if (!key) return;
2995
+ cache.delete(key);
2996
+ }
2997
+ }
2998
+ function createCachedRevocationClient(inner, options = {}) {
2999
+ const ttlMs = options.ttlMs ?? REVOCATION_CACHE_TTL_MS_DEFAULT;
3000
+ const now = options.now ?? Date.now;
3001
+ const cache = /* @__PURE__ */ new Map();
3002
+ return async (args) => {
3003
+ const at = now();
3004
+ const entry = cache.get(args.jti);
3005
+ if (entry && entry.expiresAt > at) {
3006
+ if (entry.result) return entry.result;
3007
+ if (entry.promise) return await entry.promise;
3008
+ }
3009
+ const promise = inner(args).then((result) => {
3010
+ pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
3011
+ cache.set(args.jti, { result, expiresAt: now() + ttlMs });
3012
+ return result;
3013
+ }).catch((err) => {
3014
+ cache.delete(args.jti);
3015
+ throw err;
3016
+ });
3017
+ cache.set(args.jti, { promise, expiresAt: at + ttlMs });
3018
+ return await promise;
3019
+ };
3020
+ }
3021
+ function runtimeAccessTokenRevocationDelayMs(input = {}) {
3022
+ const configuredTtl = input.revocationCacheTtlMs ?? REVOCATION_CACHE_TTL_MS_DEFAULT;
3023
+ const ttlMs = Number.isFinite(configuredTtl) && configuredTtl > 0 ? configuredTtl : 0;
3024
+ if (input.activeCheckIntervalMs === void 0) return ttlMs;
3025
+ if (!Number.isFinite(input.activeCheckIntervalMs) || input.activeCheckIntervalMs <= 0) {
3026
+ return Number.POSITIVE_INFINITY;
3027
+ }
3028
+ return ttlMs + input.activeCheckIntervalMs;
3029
+ }
3030
+ function createCachedHostGenerationClient(inner, options = {}) {
3031
+ const ttlMs = options.ttlMs ?? 1e4;
3032
+ const now = options.now ?? Date.now;
3033
+ const cache = /* @__PURE__ */ new Map();
3034
+ return async (args) => {
3035
+ for (; ; ) {
3036
+ const at = now();
3037
+ const entry = cache.get(args.enrollmentId);
3038
+ if (entry && entry.expiresAt > at) {
3039
+ if (entry.result && entry.result.generation >= args.generation) return entry.result;
3040
+ if (!entry.result && entry.promise) {
3041
+ const shared = await entry.promise;
3042
+ if (!shared || shared.generation >= args.generation) return shared;
3043
+ continue;
3044
+ }
3045
+ }
3046
+ const promise = inner(args).then((result) => {
3047
+ pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
3048
+ if (result) cache.set(args.enrollmentId, { result, expiresAt: now() + ttlMs });
3049
+ else cache.delete(args.enrollmentId);
3050
+ return result;
3051
+ }).catch((err) => {
3052
+ cache.delete(args.enrollmentId);
3053
+ throw err;
3054
+ });
3055
+ cache.set(args.enrollmentId, { promise, expiresAt: at + ttlMs });
3056
+ return await promise;
3057
+ }
3058
+ };
3059
+ }
3060
+ function createCachedTargetClient(inner, options = {}) {
3061
+ const ttlMs = options.ttlMs ?? TARGET_CACHE_TTL_MS_DEFAULT;
3062
+ const durableObjectTtlMs = options.durableObjectTtlMs ?? DURABLE_OBJECT_TARGET_CACHE_TTL_MS_DEFAULT;
3063
+ const now = options.now ?? Date.now;
3064
+ const cache = /* @__PURE__ */ new Map();
3065
+ return async (args) => {
3066
+ const key = `${args.workspaceId}\0${args.hostId}\0${args.routingId ?? ""}`;
3067
+ const at = now();
3068
+ const entry = cache.get(key);
3069
+ if (entry && entry.expiresAt > at) {
3070
+ if (entry.target) return entry.target;
3071
+ if (entry.promise) return await entry.promise;
3072
+ }
3073
+ const promise = inner(args).then((target) => {
3074
+ pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
3075
+ if (target) cache.set(key, { target, expiresAt: now() + (target.backing === "durable-object" ? durableObjectTtlMs : ttlMs) });
3076
+ else cache.delete(key);
3077
+ return target;
3078
+ }).catch((err) => {
3079
+ cache.delete(key);
3080
+ throw err;
3081
+ });
3082
+ cache.set(key, { promise, expiresAt: at + ttlMs });
3083
+ return await promise;
3084
+ };
3085
+ }
3022
3086
  export {
3023
3087
  DEFAULT_RELAY_APP_ORIGINS,
3024
3088
  DEFAULT_RELAY_LOCATION_HINT,
3025
3089
  RELAY_ALLOWED_REQUEST_HEADERS,
3026
- RELAY_ALLOWED_REQUEST_HEADER_LIST,
3027
3090
  RELAY_LOCATION_HINTS,
3028
3091
  RELAY_REGION_HEADER,
3092
+ RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT,
3093
+ RUNTIME_ACCESS_TOKEN_ISSUED_AT_FLOOR_SECONDS,
3094
+ SESSION_HOST_LOCATION_HINT,
3029
3095
  WorkspaceRelayAuthError,
3030
3096
  authorizeWorkspaceRelayRequest,
3031
3097
  bytesToBase64,
3032
3098
  checkHostTunnelGeneration,
3099
+ corsPreflight,
3033
3100
  createCachedHostGenerationClient,
3034
3101
  createCachedRevocationClient,
3035
3102
  createCachedTargetClient,
3036
3103
  createHostGenerationResolverLookup,
3037
3104
  createOriginMatcher,
3038
- createWorkspaceRelay,
3039
3105
  createWorkspaceRelayDirectory,
3040
3106
  createWorkspaceRelayDurableObjectGateway,
3041
3107
  createWorkspaceRelayDurableObjectRoom,
3042
- createWorkspaceRelayTrace,
3043
3108
  deriveRelayHostKid,
3044
3109
  deriveRelayHostPublicKey,
3045
- disposeAuditSampler,
3046
- disposeWorkspaceRelayDirectory,
3047
- forwardWorkspaceRelayRequest,
3048
- getAuditSuppressedCount,
3049
3110
  hostTunnelIncumbentOutranks,
3050
3111
  hostTunnelTokenAudience,
3112
+ isAllowedRelayTargetBaseUrl,
3051
3113
  isHostGeneration,
3052
- isRelayClaimPair,
3114
+ isRelayBacking,
3053
3115
  mintHostTunnelToken,
3054
3116
  mintRelayHostToken,
3055
3117
  mintRuntimeAccessToken,
3056
3118
  parseAllowedOrigins,
3057
- parseHostGenerationResult,
3058
3119
  parseRuntimeAccessTokenActiveResult,
3059
3120
  parseWorkspaceRelayTarget,
3121
+ pruneExpiringCache,
3060
3122
  relayFrameDiagnostics,
3123
+ relayHostInputFromRuntimeClaims,
3061
3124
  relayHostTokenAudience,
3062
3125
  relayHostTokenIssuer,
3063
3126
  relayLocationHint,
3064
3127
  relayRequestCountry,
3065
3128
  relayRequestRegion,
3129
+ relayRoomPlacement,
3066
3130
  runtimeAccessTokenAudience,
3067
3131
  runtimeAccessTokenIssuer,
3132
+ runtimeAccessTokenRevocationDelayMs,
3068
3133
  setWorkspaceRelayAllowedOrigins,
3069
3134
  setWorkspaceRelayAppOrigins,
3135
+ toChannelIdentityClaim,
3136
+ tokenPurposeClaims,
3137
+ tokenScopeClaims,
3138
+ tokenScopePayload,
3070
3139
  validateRuntimeAccessTokenClaims,
3071
3140
  verifyHostTunnelToken,
3072
3141
  verifyRelayHostToken,
3073
3142
  verifyRuntimeAccessToken,
3143
+ withRelayResponseHeaders,
3074
3144
  workspaceRelayDurableObjectRoomName,
3075
3145
  workspaceRelayDurableObjectWorkspaceId,
3076
3146
  workspaceRelayForwardHeaders,
3077
3147
  workspaceRelayForwardRequestInit,
3078
- workspaceRelayServerTiming,
3148
+ workspaceRelayRoomPlacement,
3079
3149
  workspaceRelayTargetUrl,
3080
- workspaceRelayTimingResponse
3150
+ workspaceUpgradeOriginDenied
3081
3151
  };