@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/README.md +92 -223
- package/dist/auth.d.ts +94 -35
- package/dist/cloudflare.d.ts +17 -57
- package/dist/cors-origins.d.ts +2 -0
- package/dist/directory.d.ts +5 -38
- package/dist/gateway-placement.d.ts +7 -0
- package/dist/host-tunnel-failure.d.ts +7 -0
- package/dist/host-tunnel-forwarding.d.ts +29 -0
- package/dist/http.d.ts +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +940 -870
- package/dist/relay-audit.d.ts +4 -0
- package/dist/relay-cors.d.ts +12 -0
- package/dist/relay-location.d.ts +93 -0
- package/dist/relay-target.d.ts +25 -0
- package/dist/relay-trace.d.ts +54 -0
- package/dist/resolver-clients.d.ts +116 -0
- package/dist/server.d.ts +27 -286
- package/dist/session-host-forwarding.d.ts +22 -0
- package/dist/test-support/unfenced-host-token.d.ts +6 -0
- package/dist/worker.d.ts +3 -3
- package/dist/workerd-fixture/compatibility.d.ts +2 -0
- package/package.json +8 -27
- package/dist/bun.d.ts +0 -225
- package/dist/bun.mjs +0 -2492
- package/dist/main.d.ts +0 -255
- package/dist/main.mjs +0 -23713
- package/dist/synthetic.d.ts +0 -60
- package/dist/upstream-websocket.d.ts +0 -34
- package/dist/user-hosted-forwarding.d.ts +0 -40
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
|
|
84
|
-
return input.
|
|
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 (
|
|
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
|
-
|
|
186
|
-
|
|
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 (!
|
|
203
|
-
throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Relay Host Token
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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/
|
|
372
|
-
function
|
|
373
|
-
return target.
|
|
562
|
+
// src/host-tunnel-forwarding.ts
|
|
563
|
+
function isHostTunnelTarget(target) {
|
|
564
|
+
return target.backing === "local-worktree";
|
|
374
565
|
}
|
|
375
566
|
|
|
376
|
-
// src/
|
|
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
|
|
571
|
+
const { workspaceId, hostId, baseUrl } = row;
|
|
381
572
|
if (typeof workspaceId !== "string" || typeof hostId !== "string" || typeof baseUrl !== "string") return void 0;
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
if (!
|
|
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
|
-
|
|
390
|
-
...pair
|
|
580
|
+
backing: row.backing
|
|
391
581
|
};
|
|
392
582
|
}
|
|
393
|
-
function
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
|
|
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
|
|
430
|
-
if (
|
|
431
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
439
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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 || !
|
|
1007
|
+
if (!path || !sessionScopeReaches(claims.session_id, path, new URL(request.url).search)) {
|
|
1030
1008
|
return {
|
|
1031
1009
|
ok: false,
|
|
1032
|
-
code: "
|
|
1010
|
+
code: "relay_scope_denied",
|
|
1033
1011
|
response: await deny(options, {
|
|
1034
|
-
code: "
|
|
1035
|
-
message: "
|
|
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
|
|
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 (
|
|
1034
|
+
if (isHostTunnelTarget(target) && !options.directory?.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
|
|
1057
1035
|
return {
|
|
1058
1036
|
ok: false,
|
|
1059
|
-
code: "
|
|
1037
|
+
code: "host_tunnel_offline",
|
|
1060
1038
|
response: await deny(options, {
|
|
1061
|
-
code: "
|
|
1062
|
-
message: "
|
|
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,
|
|
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
|
|
1115
|
+
recordPong(hostId) {
|
|
1368
1116
|
const timestamp = now();
|
|
1369
1117
|
let touched;
|
|
1370
|
-
for (const key of keysOf(hostId
|
|
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
|
|
1380
|
-
for (const key of keysOf(hostId
|
|
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
|
-
|
|
1403
|
-
|
|
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/
|
|
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 =
|
|
1450
|
-
const 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 =
|
|
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
|
|
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
|
-
|
|
1473
|
-
|
|
1474
|
-
var
|
|
1475
|
-
var
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
-
|
|
1492
|
-
|
|
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 =
|
|
1502
|
-
const forceSecret =
|
|
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(
|
|
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(
|
|
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
|
|
1475
|
+
function roundedMs(value) {
|
|
1535
1476
|
return Math.round(value * 100) / 100;
|
|
1536
1477
|
}
|
|
1537
|
-
function serverTiming(
|
|
1538
|
-
return
|
|
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:
|
|
1567
|
-
phases: Object.fromEntries(trace.phases.map((phase) => [phase.name,
|
|
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
|
-
|
|
1621
|
-
return
|
|
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 =
|
|
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
|
|
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 =
|
|
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/") ?
|
|
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
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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/") ?
|
|
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(
|
|
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
|
-
|
|
1994
|
-
|
|
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, "
|
|
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
|
|
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?.
|
|
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("
|
|
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
|
-
|
|
2109
|
-
|
|
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 (
|
|
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 ||
|
|
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
|
|
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?.
|
|
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
|
|
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 &&
|
|
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 (
|
|
2273
|
+
if (isHostTunnelClient(client)) {
|
|
2359
2274
|
for (const tunnel of hostTunnels.values()) {
|
|
2360
2275
|
if (tunnel.channels.get(client.channelId) !== client) continue;
|
|
2361
|
-
|
|
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
|
|
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
|
|
2396
|
-
const active =
|
|
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
|
|
2404
|
-
closeSocket(socket, 1011, "
|
|
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, "
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
2614
|
-
|
|
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, "
|
|
2641
|
-
pair.server.addEventListener?.("error", () => cleanupHostTunnel(hostId, pair.server, "
|
|
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
|
-
|
|
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
|
-
{
|
|
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,
|
|
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,
|
|
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)
|
|
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
|
|
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("
|
|
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", "
|
|
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: "
|
|
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 :
|
|
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
|
-
|
|
2700
|
+
cleanupHostTunnelClient(channelId, pair.server, event);
|
|
2786
2701
|
};
|
|
2787
2702
|
pair.server.addEventListener?.("message", (event) => {
|
|
2788
2703
|
if (!event) return;
|
|
2789
|
-
void
|
|
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
|
-
{
|
|
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, "
|
|
2811
|
-
return json("
|
|
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 (
|
|
2825
|
-
trace && (trace.routeKind = "
|
|
2826
|
-
return
|
|
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
|
|
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("
|
|
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", "
|
|
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
|
-
|
|
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", "
|
|
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", "
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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, "
|
|
2906
|
+
cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected");
|
|
2996
2907
|
return;
|
|
2997
2908
|
}
|
|
2998
|
-
|
|
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, "
|
|
2916
|
+
cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected");
|
|
3006
2917
|
return;
|
|
3007
2918
|
}
|
|
3008
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3148
|
+
workspaceRelayRoomPlacement,
|
|
3079
3149
|
workspaceRelayTargetUrl,
|
|
3080
|
-
|
|
3150
|
+
workspaceUpgradeOriginDenied
|
|
3081
3151
|
};
|