@claxedo/workspace-relay 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bun.d.ts +31 -0
- package/dist/cloudflare.d.ts +98 -0
- package/dist/index.mjs +309 -39
- package/dist/main.d.ts +47 -27
- package/dist/main.mjs +19692 -39183
- package/dist/worker.d.ts +12 -0
- package/dist/workerd-fixture/binary-frame-worker.d.ts +33 -0
- package/package.json +13 -6
package/dist/index.mjs
CHANGED
|
@@ -237,8 +237,6 @@ import { exportJWK } from "jose";
|
|
|
237
237
|
var DEFAULT_RELAY_APP_ORIGINS = [
|
|
238
238
|
"http://localhost:*",
|
|
239
239
|
"http://127.0.0.1:*",
|
|
240
|
-
"https://opencode.ai",
|
|
241
|
-
"https://*.opencode.ai",
|
|
242
240
|
"https://claxedo.com",
|
|
243
241
|
"https://*.claxedo.com"
|
|
244
242
|
];
|
|
@@ -1080,7 +1078,22 @@ var TUNNEL_HTTP_RESPONSE_TIMEOUT_MS_DEFAULT = 3e4;
|
|
|
1080
1078
|
var DIRECT_HTTP_TIMEOUT_MS_DEFAULT = 3e4;
|
|
1081
1079
|
var UPSTREAM_WS_OPEN_TIMEOUT_MS_DEFAULT = 1e4;
|
|
1082
1080
|
var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT = 64;
|
|
1081
|
+
var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
|
|
1083
1082
|
var WS_BUFFERED_AMOUNT_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
|
|
1083
|
+
var relayBufferedBytes = (socket) => {
|
|
1084
|
+
const method = socket.getBufferedAmount;
|
|
1085
|
+
if (typeof method === "function") {
|
|
1086
|
+
const measured = method.call(socket);
|
|
1087
|
+
if (typeof measured === "number" && Number.isFinite(measured)) return measured;
|
|
1088
|
+
return void 0;
|
|
1089
|
+
}
|
|
1090
|
+
const property = socket.bufferedAmount;
|
|
1091
|
+
return typeof property === "number" && Number.isFinite(property) ? property : void 0;
|
|
1092
|
+
};
|
|
1093
|
+
var relayOverBackpressureLimit = (socket, limitBytes) => {
|
|
1094
|
+
const queued = relayBufferedBytes(socket);
|
|
1095
|
+
return queued !== void 0 && queued > limitBytes;
|
|
1096
|
+
};
|
|
1084
1097
|
var HOST_TUNNEL_MAX_MISSED_PONGS_DEFAULT = 2;
|
|
1085
1098
|
var TUNNEL_MESSAGE_BUFFER_CAP_BYTES = 4 * 1024 * 1024;
|
|
1086
1099
|
var SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT = 8 * 1024 * 1024;
|
|
@@ -1157,6 +1170,9 @@ function corsJsonError(request, code, message, status) {
|
|
|
1157
1170
|
headers2.set("content-type", "application/json");
|
|
1158
1171
|
return new Response(JSON.stringify({ error: { code, message } }), { status, headers: headers2 });
|
|
1159
1172
|
}
|
|
1173
|
+
function preOpenFrameBytes(message) {
|
|
1174
|
+
return typeof message === "string" ? Buffer.byteLength(message) : message.byteLength;
|
|
1175
|
+
}
|
|
1160
1176
|
function safeCloseCode(input, fallback = 1011) {
|
|
1161
1177
|
if (!Number.isInteger(input)) return fallback;
|
|
1162
1178
|
const code = input;
|
|
@@ -1235,11 +1251,10 @@ function isEventStream(input) {
|
|
|
1235
1251
|
([key, value]) => key.toLowerCase() === "content-type" && value.toLowerCase().includes("text/event-stream")
|
|
1236
1252
|
);
|
|
1237
1253
|
}
|
|
1254
|
+
var defaultRelayOriginMatcher = createOriginMatcher(DEFAULT_RELAY_APP_ORIGINS);
|
|
1238
1255
|
function allowedCorsOrigin(origin) {
|
|
1239
1256
|
if (!origin) return;
|
|
1240
|
-
if (origin
|
|
1241
|
-
if (origin.startsWith("http://127.0.0.1:")) return origin;
|
|
1242
|
-
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(origin)) return origin;
|
|
1257
|
+
if (defaultRelayOriginMatcher(origin)) return origin;
|
|
1243
1258
|
}
|
|
1244
1259
|
function requireAllowedOrigin(request) {
|
|
1245
1260
|
const origin = request.headers.get("origin");
|
|
@@ -2012,7 +2027,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
2012
2027
|
ws.data.channels.delete(frame.channel_id);
|
|
2013
2028
|
return;
|
|
2014
2029
|
}
|
|
2015
|
-
if (channel
|
|
2030
|
+
if (relayOverBackpressureLimit(channel, bunOptions.webSocketBufferedAmountMaxBytes ?? WS_BUFFERED_AMOUNT_MAX_BYTES_DEFAULT)) {
|
|
2016
2031
|
ws.data.channels.delete(frame.channel_id);
|
|
2017
2032
|
closeWebSocket(channel, 1011, "Client WebSocket backpressure limit exceeded");
|
|
2018
2033
|
return;
|
|
@@ -2035,7 +2050,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
2035
2050
|
ws.close(1011, "User-hosted tunnel disconnected");
|
|
2036
2051
|
return;
|
|
2037
2052
|
}
|
|
2038
|
-
if (tunnel
|
|
2053
|
+
if (relayOverBackpressureLimit(tunnel, bunOptions.webSocketBufferedAmountMaxBytes ?? WS_BUFFERED_AMOUNT_MAX_BYTES_DEFAULT)) {
|
|
2039
2054
|
closeWebSocket(ws, 1011, "Host tunnel backpressure limit exceeded");
|
|
2040
2055
|
return;
|
|
2041
2056
|
}
|
|
@@ -2051,10 +2066,13 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
2051
2066
|
ws.data.upstream.send(message);
|
|
2052
2067
|
return;
|
|
2053
2068
|
}
|
|
2054
|
-
|
|
2069
|
+
const queuedBytes = ws.data.queuedBytes ?? 0;
|
|
2070
|
+
const frameBytes = preOpenFrameBytes(message);
|
|
2071
|
+
if (ws.data.queue.length >= (bunOptions.upstreamWebSocketPreOpenQueueMaxFrames ?? UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT) && queuedBytes + frameBytes > (bunOptions.upstreamWebSocketPreOpenQueueMaxBytes ?? UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT)) {
|
|
2055
2072
|
closeWebSocket(ws, 1011, "Upstream WebSocket queue limit exceeded");
|
|
2056
2073
|
return;
|
|
2057
2074
|
}
|
|
2075
|
+
ws.data.queuedBytes = queuedBytes + frameBytes;
|
|
2058
2076
|
ws.data.queue.push({ payload: message, queuedAt: performance.now() });
|
|
2059
2077
|
},
|
|
2060
2078
|
open(ws) {
|
|
@@ -2153,6 +2171,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
2153
2171
|
sendRelayWebSocketTrace(ws);
|
|
2154
2172
|
}
|
|
2155
2173
|
for (const item of data.queue.splice(0)) upstream.send(item.payload);
|
|
2174
|
+
data.queuedBytes = 0;
|
|
2156
2175
|
};
|
|
2157
2176
|
upstream.onmessage = (event) => {
|
|
2158
2177
|
const payload = relayWebSocketPayload(event.data);
|
|
@@ -2306,6 +2325,66 @@ function disposeWorkspaceRelayDirectory(directory) {
|
|
|
2306
2325
|
|
|
2307
2326
|
// src/cloudflare.ts
|
|
2308
2327
|
var DEFAULT_RELAY_LOCATION_HINT = "apac";
|
|
2328
|
+
var RELAY_LOCATION_HINTS = ["wnam", "enam", "sam", "weur", "eeur", "apac", "oc", "afr", "me"];
|
|
2329
|
+
var relayLocationHintSet = new Set(RELAY_LOCATION_HINTS);
|
|
2330
|
+
var RELAY_REGION_TO_LOCATION_HINT = {
|
|
2331
|
+
// Canonical Claxedo regions.
|
|
2332
|
+
"apac-south": "apac",
|
|
2333
|
+
"apac-east": "apac",
|
|
2334
|
+
"eu-west": "weur",
|
|
2335
|
+
"us-east": "enam",
|
|
2336
|
+
"us-west": "wnam",
|
|
2337
|
+
// Provider spellings.
|
|
2338
|
+
"us-central": "wnam",
|
|
2339
|
+
"eu-central": "weur",
|
|
2340
|
+
"eu-east": "eeur",
|
|
2341
|
+
"ap-south": "apac",
|
|
2342
|
+
"ap-southeast": "apac",
|
|
2343
|
+
"ap-northeast": "apac",
|
|
2344
|
+
"sa-east": "sam",
|
|
2345
|
+
"af-south": "afr",
|
|
2346
|
+
"me-central": "me",
|
|
2347
|
+
"oceania": "oc",
|
|
2348
|
+
"au-southeast": "oc"
|
|
2349
|
+
};
|
|
2350
|
+
function locationHintForCountry(country) {
|
|
2351
|
+
const code = country.toUpperCase();
|
|
2352
|
+
if (["US", "CA", "MX"].includes(code)) return code === "US" || code === "CA" ? "enam" : "wnam";
|
|
2353
|
+
if (["BR", "AR", "CL", "CO", "PE", "UY", "PY", "BO", "EC", "VE"].includes(code)) return "sam";
|
|
2354
|
+
if (["GB", "IE", "FR", "ES", "PT", "NL", "BE", "DE", "CH", "AT", "IT", "DK", "NO", "SE", "FI", "IS", "LU"].includes(code)) return "weur";
|
|
2355
|
+
if (["PL", "CZ", "SK", "HU", "RO", "BG", "GR", "UA", "RS", "HR", "SI", "EE", "LV", "LT", "TR", "RU"].includes(code)) return "eeur";
|
|
2356
|
+
if (["IN", "PK", "BD", "LK", "NP", "SG", "MY", "TH", "VN", "PH", "ID", "CN", "HK", "TW", "JP", "KR"].includes(code)) return "apac";
|
|
2357
|
+
if (["AU", "NZ", "FJ", "PG"].includes(code)) return "oc";
|
|
2358
|
+
if (["ZA", "NG", "KE", "GH", "ET", "TZ", "UG", "EG", "MA", "DZ", "TN"].includes(code)) {
|
|
2359
|
+
return ["EG", "MA", "DZ", "TN"].includes(code) ? "me" : "afr";
|
|
2360
|
+
}
|
|
2361
|
+
if (["AE", "SA", "IL", "QA", "KW", "BH", "OM", "JO", "LB", "IQ", "IR"].includes(code)) return "me";
|
|
2362
|
+
return void 0;
|
|
2363
|
+
}
|
|
2364
|
+
var RELAY_REGION_HEADER = "x-claxedo-workspace-region";
|
|
2365
|
+
function relayLocationHint(input) {
|
|
2366
|
+
const configured = clean(input.configured) ?? DEFAULT_RELAY_LOCATION_HINT;
|
|
2367
|
+
const region = clean(input.region);
|
|
2368
|
+
if (region) {
|
|
2369
|
+
const direct = relayLocationHintSet.has(region.toLowerCase()) ? region.toLowerCase() : void 0;
|
|
2370
|
+
const mapped = direct ?? RELAY_REGION_TO_LOCATION_HINT[region.toLowerCase()];
|
|
2371
|
+
if (mapped) return mapped;
|
|
2372
|
+
}
|
|
2373
|
+
const country = clean(input.country);
|
|
2374
|
+
if (country) {
|
|
2375
|
+
const mapped = locationHintForCountry(country);
|
|
2376
|
+
if (mapped) return mapped;
|
|
2377
|
+
}
|
|
2378
|
+
return configured;
|
|
2379
|
+
}
|
|
2380
|
+
function relayRequestRegion(request) {
|
|
2381
|
+
const url = new URL(request.url);
|
|
2382
|
+
return clean(request.headers.get(RELAY_REGION_HEADER)) ?? clean(url.searchParams.get("region")) ?? clean(url.searchParams.get("homeRegion"));
|
|
2383
|
+
}
|
|
2384
|
+
function relayRequestCountry(request) {
|
|
2385
|
+
const cf = request.cf;
|
|
2386
|
+
return typeof cf?.country === "string" ? cf.country : void 0;
|
|
2387
|
+
}
|
|
2309
2388
|
var TUNNEL_PENDING_HTTP_CAP_DEFAULT = 32;
|
|
2310
2389
|
var TUNNEL_STARTED_STREAM_CAP_DEFAULT = 16;
|
|
2311
2390
|
var TUNNEL_CHANNEL_CAP_DEFAULT = 16;
|
|
@@ -2314,8 +2393,11 @@ var TUNNEL_RESPONSE_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
|
|
|
2314
2393
|
var SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT2 = 8 * 1024 * 1024;
|
|
2315
2394
|
var SLOW_CONSUMER_TIMEOUT_MS_DEFAULT2 = 3e4;
|
|
2316
2395
|
var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT2 = 64;
|
|
2396
|
+
var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT2 = 8 * 1024 * 1024;
|
|
2317
2397
|
var RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
|
|
2318
2398
|
var WORKSPACE_TARGET_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
|
|
2399
|
+
var RESOLVER_OUTAGE_GRACE_ATTEMPTS_DEFAULT = 3;
|
|
2400
|
+
var HIBERNATED_REVOCATION_ALARM_INTERVAL_MS_DEFAULT = 3e4;
|
|
2319
2401
|
var TRACE_ID_HEADER = "x-claxedo-trace-id";
|
|
2320
2402
|
var TRACE_FORCE_HEADER = "x-claxedo-relay-trace";
|
|
2321
2403
|
function clean(input) {
|
|
@@ -2436,14 +2518,31 @@ function headersFromRecord(input) {
|
|
|
2436
2518
|
for (const [key, value] of Object.entries(input)) headers2.set(key, value);
|
|
2437
2519
|
return headers2;
|
|
2438
2520
|
}
|
|
2521
|
+
var BASE64_CHUNK_BYTES = 32768;
|
|
2439
2522
|
function bytesToBase64(bytes) {
|
|
2440
2523
|
let binary = "";
|
|
2441
|
-
for (
|
|
2524
|
+
for (let i = 0; i < bytes.length; i += BASE64_CHUNK_BYTES) {
|
|
2525
|
+
binary += String.fromCharCode.apply(
|
|
2526
|
+
null,
|
|
2527
|
+
bytes.subarray(i, i + BASE64_CHUNK_BYTES)
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2442
2530
|
return btoa(binary);
|
|
2443
2531
|
}
|
|
2444
2532
|
function base64ToBytes(input) {
|
|
2445
2533
|
return Uint8Array.from(atob(input), (char) => char.charCodeAt(0));
|
|
2446
2534
|
}
|
|
2535
|
+
function preOpenFrameBytes2(input) {
|
|
2536
|
+
if (typeof input === "string") return new TextEncoder().encode(input).byteLength;
|
|
2537
|
+
return input.byteLength;
|
|
2538
|
+
}
|
|
2539
|
+
function blobLike(input) {
|
|
2540
|
+
if (!input || typeof input !== "object") return void 0;
|
|
2541
|
+
return typeof input.arrayBuffer === "function" ? input : void 0;
|
|
2542
|
+
}
|
|
2543
|
+
function viewBytes(input) {
|
|
2544
|
+
return new Uint8Array(input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength));
|
|
2545
|
+
}
|
|
2447
2546
|
function socketFrame(input) {
|
|
2448
2547
|
if (typeof input === "string") {
|
|
2449
2548
|
return {
|
|
@@ -2463,14 +2562,56 @@ function socketFrame(input) {
|
|
|
2463
2562
|
data_base64: bytesToBase64(input)
|
|
2464
2563
|
};
|
|
2465
2564
|
}
|
|
2565
|
+
if (ArrayBuffer.isView(input)) {
|
|
2566
|
+
return {
|
|
2567
|
+
binary: true,
|
|
2568
|
+
data_base64: bytesToBase64(viewBytes(input))
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
const blob = blobLike(input);
|
|
2572
|
+
if (!blob) return void 0;
|
|
2573
|
+
return blob.arrayBuffer().then((buffer) => ({
|
|
2574
|
+
binary: true,
|
|
2575
|
+
data_base64: bytesToBase64(new Uint8Array(buffer))
|
|
2576
|
+
}));
|
|
2466
2577
|
}
|
|
2467
2578
|
function socketPayload(input) {
|
|
2468
2579
|
if (typeof input === "string") return input;
|
|
2469
2580
|
if (input instanceof ArrayBuffer) return input;
|
|
2470
2581
|
if (input instanceof Uint8Array) return input;
|
|
2471
|
-
if (ArrayBuffer.isView(input))
|
|
2472
|
-
|
|
2473
|
-
|
|
2582
|
+
if (ArrayBuffer.isView(input)) return viewBytes(input);
|
|
2583
|
+
const blob = blobLike(input);
|
|
2584
|
+
if (!blob) return void 0;
|
|
2585
|
+
return blob.arrayBuffer();
|
|
2586
|
+
}
|
|
2587
|
+
function orderedFrameDelivery(deliver) {
|
|
2588
|
+
let tail;
|
|
2589
|
+
return (data) => {
|
|
2590
|
+
const payload = socketPayload(data);
|
|
2591
|
+
if (payload === void 0) return;
|
|
2592
|
+
if (tail === void 0 && !(payload instanceof Promise)) {
|
|
2593
|
+
deliver(payload);
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
const queued = (tail ?? Promise.resolve()).then(async () => deliver(await payload)).catch(() => {
|
|
2597
|
+
});
|
|
2598
|
+
tail = queued;
|
|
2599
|
+
void queued.then(() => {
|
|
2600
|
+
if (tail === queued) tail = void 0;
|
|
2601
|
+
});
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
async function relayFrameDiagnostics(data) {
|
|
2605
|
+
const frame = await socketFrame(data);
|
|
2606
|
+
const payload = await socketPayload(data);
|
|
2607
|
+
return {
|
|
2608
|
+
// `undefined` here is exactly the silent drop: every caller treats it as
|
|
2609
|
+
// "nothing to forward" and returns without a throw or a log.
|
|
2610
|
+
frameDropped: frame === void 0,
|
|
2611
|
+
payloadDropped: payload === void 0,
|
|
2612
|
+
...frame ? { binary: frame.binary, dataBase64: frame.data_base64 } : {},
|
|
2613
|
+
payloadBase64: payload === void 0 ? void 0 : typeof payload === "string" ? bytesToBase64(new TextEncoder().encode(payload)) : bytesToBase64(payload instanceof ArrayBuffer ? new Uint8Array(payload) : payload)
|
|
2614
|
+
};
|
|
2474
2615
|
}
|
|
2475
2616
|
function tunnelFrame(input) {
|
|
2476
2617
|
const bytes = base64ToBytes(input.data_base64);
|
|
@@ -2616,7 +2757,13 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
|
|
|
2616
2757
|
if (url.pathname === "/health") {
|
|
2617
2758
|
return Response.json({ ok: true, service: "workspace-relay", mode: "cloudflare-durable-object" });
|
|
2618
2759
|
}
|
|
2619
|
-
const
|
|
2760
|
+
const requestRegion = relayRequestRegion(request);
|
|
2761
|
+
const requestCountry = relayRequestCountry(request);
|
|
2762
|
+
const locationHint = relayLocationHint({
|
|
2763
|
+
...typeof env.CLAXEDO_RELAY_LOCATION_HINT === "string" ? { configured: env.CLAXEDO_RELAY_LOCATION_HINT } : {},
|
|
2764
|
+
...requestRegion ? { region: requestRegion } : {},
|
|
2765
|
+
...requestCountry ? { country: requestCountry } : {}
|
|
2766
|
+
});
|
|
2620
2767
|
const namespace = namespaceFromEnv(env, options);
|
|
2621
2768
|
if (!namespace) {
|
|
2622
2769
|
return json(
|
|
@@ -2642,7 +2789,7 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
|
|
|
2642
2789
|
}
|
|
2643
2790
|
const routeFinishedAt = performance.now();
|
|
2644
2791
|
const response = await namespace.get(namespace.idFromName(workspaceRelayDurableObjectRoomName(workspaceId)), {
|
|
2645
|
-
locationHint
|
|
2792
|
+
locationHint
|
|
2646
2793
|
}).fetch(tracedRequest(request, {
|
|
2647
2794
|
id: gatewayTraceId,
|
|
2648
2795
|
sampled: request.headers.get("x-claxedo-trace-sampled") === "1",
|
|
@@ -2656,7 +2803,7 @@ function createWorkspaceRelayDurableObjectGateway(options = {}) {
|
|
|
2656
2803
|
}
|
|
2657
2804
|
const headers2 = new Headers(response.headers);
|
|
2658
2805
|
headers2.set(TRACE_ID_HEADER, headers2.get(TRACE_ID_HEADER) ?? gatewayTraceId);
|
|
2659
|
-
headers2.set("x-claxedo-relay-location-hint",
|
|
2806
|
+
headers2.set("x-claxedo-relay-location-hint", locationHint);
|
|
2660
2807
|
const gatewayResponseStartedAt = performance.now();
|
|
2661
2808
|
const tracedResponse = new Response(response.body, {
|
|
2662
2809
|
status: response.status,
|
|
@@ -2741,6 +2888,10 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2741
2888
|
const rebuildHibernatedSockets = () => {
|
|
2742
2889
|
if (!hibernation) return;
|
|
2743
2890
|
hibernatedSocketsRebuilt = true;
|
|
2891
|
+
for (const tunnel of hostTunnels.values()) {
|
|
2892
|
+
if (tunnel.pending.size === 0) continue;
|
|
2893
|
+
cleanupTunnelResources(tunnel, "Workspace relay restarted while the request was in flight");
|
|
2894
|
+
}
|
|
2744
2895
|
hostTunnels.clear();
|
|
2745
2896
|
clients.clear();
|
|
2746
2897
|
const clientRows = [];
|
|
@@ -2798,10 +2949,13 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2798
2949
|
}
|
|
2799
2950
|
}
|
|
2800
2951
|
};
|
|
2952
|
+
const resolverGraceAttempts = options.resolverOutageGraceAttempts ?? RESOLVER_OUTAGE_GRACE_ATTEMPTS_DEFAULT;
|
|
2953
|
+
const resolverUnreachable = (code) => code === "relay_revocation_resolver_unavailable";
|
|
2801
2954
|
const watchRuntimeAccessToken = (client, onInactive) => {
|
|
2802
2955
|
const intervalMs = options.runtimeAccessTokenActiveCheckIntervalMs ?? RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT;
|
|
2803
2956
|
if (intervalMs <= 0) return;
|
|
2804
2957
|
const now = options.now ?? Date.now;
|
|
2958
|
+
let consecutiveUnreachable = 0;
|
|
2805
2959
|
const timer = setInterval(() => {
|
|
2806
2960
|
if (client.request.claims.exp * 1e3 <= now()) {
|
|
2807
2961
|
clearInterval(timer);
|
|
@@ -2809,14 +2963,25 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2809
2963
|
return;
|
|
2810
2964
|
}
|
|
2811
2965
|
if (!options.isRuntimeAccessTokenActive) return;
|
|
2966
|
+
const tolerate = (reason) => {
|
|
2967
|
+
consecutiveUnreachable += 1;
|
|
2968
|
+
if (consecutiveUnreachable < resolverGraceAttempts) return;
|
|
2969
|
+
clearInterval(timer);
|
|
2970
|
+
onInactive(reason);
|
|
2971
|
+
};
|
|
2812
2972
|
void Promise.resolve(options.isRuntimeAccessTokenActive(client.request.claims)).then((active) => {
|
|
2813
|
-
if (active.active)
|
|
2973
|
+
if (active.active) {
|
|
2974
|
+
consecutiveUnreachable = 0;
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
if (resolverUnreachable(active.code)) {
|
|
2978
|
+
tolerate(`Runtime Access Token active check unavailable: ${active.reason}`);
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
consecutiveUnreachable = 0;
|
|
2814
2982
|
clearInterval(timer);
|
|
2815
2983
|
onInactive(active.reason);
|
|
2816
|
-
}).catch(() =>
|
|
2817
|
-
clearInterval(timer);
|
|
2818
|
-
onInactive("Runtime Access Token active check failed");
|
|
2819
|
-
});
|
|
2984
|
+
}).catch(() => tolerate("Runtime Access Token active check failed"));
|
|
2820
2985
|
}, intervalMs);
|
|
2821
2986
|
if (typeof timer.unref === "function") {
|
|
2822
2987
|
timer.unref();
|
|
@@ -2827,14 +2992,19 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2827
2992
|
const intervalMs = options.workspaceTargetActiveCheckIntervalMs ?? WORKSPACE_TARGET_ACTIVE_CHECK_INTERVAL_MS_DEFAULT;
|
|
2828
2993
|
if (intervalMs <= 0) return;
|
|
2829
2994
|
const target = client.request.target;
|
|
2995
|
+
let consecutiveUnreachable = 0;
|
|
2830
2996
|
const timer = setInterval(() => {
|
|
2831
2997
|
void Promise.resolve(options.resolveTarget(client.request.claims)).then((activeTarget) => {
|
|
2832
2998
|
if (activeTarget?.access === "user-hosted" && activeTarget.workspaceId === target.workspaceId && activeTarget.hostId === target.hostId && directory.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
|
|
2999
|
+
consecutiveUnreachable = 0;
|
|
2833
3000
|
return;
|
|
2834
3001
|
}
|
|
3002
|
+
consecutiveUnreachable = 0;
|
|
2835
3003
|
clearInterval(timer);
|
|
2836
3004
|
onInactive("User-hosted workspace is offline");
|
|
2837
3005
|
}).catch(() => {
|
|
3006
|
+
consecutiveUnreachable += 1;
|
|
3007
|
+
if (consecutiveUnreachable < resolverGraceAttempts) return;
|
|
2838
3008
|
clearInterval(timer);
|
|
2839
3009
|
onInactive("Workspace relay target active check failed");
|
|
2840
3010
|
});
|
|
@@ -3019,6 +3189,67 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3019
3189
|
const activeTarget = await options.resolveTarget(client.request.claims);
|
|
3020
3190
|
return !!(activeTarget?.access === "user-hosted" && activeTarget.workspaceId === target.workspaceId && activeTarget.hostId === target.hostId && directory.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId }));
|
|
3021
3191
|
};
|
|
3192
|
+
const dropUserHostedChannel = (tunnel, channelId, client, code, reason) => {
|
|
3193
|
+
tunnel.channels.delete(channelId);
|
|
3194
|
+
clients.delete(client);
|
|
3195
|
+
closeSocket(client.socket, code, reason);
|
|
3196
|
+
sendSocket(tunnel.socket, JSON.stringify({
|
|
3197
|
+
type: "ws.close",
|
|
3198
|
+
protocol: TUNNEL_PROTOCOL_VERSION2,
|
|
3199
|
+
channel_id: channelId,
|
|
3200
|
+
code,
|
|
3201
|
+
reason
|
|
3202
|
+
}));
|
|
3203
|
+
};
|
|
3204
|
+
const reportFrameHandlerFailure = (socket, err) => {
|
|
3205
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3206
|
+
console.error(`[workspace-relay] frame handler failed: ${reason}`);
|
|
3207
|
+
closeSocket(socket, 1011, "Workspace relay failed to process a frame");
|
|
3208
|
+
};
|
|
3209
|
+
const hibernatedRevocationIntervalMs = () => options.hibernatedRevocationCheckIntervalMs ?? HIBERNATED_REVOCATION_ALARM_INTERVAL_MS_DEFAULT;
|
|
3210
|
+
const scheduleHibernatedRevocationCheck = async () => {
|
|
3211
|
+
const intervalMs = hibernatedRevocationIntervalMs();
|
|
3212
|
+
if (!hibernation || !options.alarms || intervalMs <= 0) return;
|
|
3213
|
+
if (clients.size === 0) return;
|
|
3214
|
+
const now = options.now ?? Date.now;
|
|
3215
|
+
const at = now() + intervalMs;
|
|
3216
|
+
try {
|
|
3217
|
+
const existing = await options.alarms.getAlarm();
|
|
3218
|
+
if (typeof existing === "number" && existing > 0 && existing <= at) return;
|
|
3219
|
+
await options.alarms.setAlarm(at);
|
|
3220
|
+
} catch {
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
const runHibernatedRevocationCheck = async () => {
|
|
3224
|
+
ensureHibernatedSocketsRebuilt();
|
|
3225
|
+
const now = options.now ?? Date.now;
|
|
3226
|
+
const revoke = (client, reason) => {
|
|
3227
|
+
const channelId = client.channelId;
|
|
3228
|
+
for (const tunnel of hostTunnels.values()) {
|
|
3229
|
+
if (!channelId || tunnel.channels.get(channelId) !== client) continue;
|
|
3230
|
+
dropUserHostedChannel(tunnel, channelId, client, 1008, reason);
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
closeSocket(client.socket, 1008, reason);
|
|
3234
|
+
clients.delete(client);
|
|
3235
|
+
};
|
|
3236
|
+
for (const client of [...clients]) {
|
|
3237
|
+
if (client.request.claims.exp * 1e3 <= now()) {
|
|
3238
|
+
revoke(client, "Runtime Access Token expired");
|
|
3239
|
+
continue;
|
|
3240
|
+
}
|
|
3241
|
+
if (!options.isRuntimeAccessTokenActive) continue;
|
|
3242
|
+
let active;
|
|
3243
|
+
try {
|
|
3244
|
+
active = await options.isRuntimeAccessTokenActive(client.request.claims);
|
|
3245
|
+
} catch {
|
|
3246
|
+
continue;
|
|
3247
|
+
}
|
|
3248
|
+
if (active.active || resolverUnreachable(active.code)) continue;
|
|
3249
|
+
revoke(client, active.reason);
|
|
3250
|
+
}
|
|
3251
|
+
await scheduleHibernatedRevocationCheck();
|
|
3252
|
+
};
|
|
3022
3253
|
const activeUserHostedClient = (channelId, socket) => {
|
|
3023
3254
|
for (const tunnel of hostTunnels.values()) {
|
|
3024
3255
|
const client = tunnel.channels.get(channelId);
|
|
@@ -3037,7 +3268,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3037
3268
|
closeSocket(socket, 1011, "User-hosted workspace is offline");
|
|
3038
3269
|
return;
|
|
3039
3270
|
}
|
|
3040
|
-
const frame = socketFrame(data);
|
|
3271
|
+
const frame = await socketFrame(data);
|
|
3041
3272
|
if (!frame) return;
|
|
3042
3273
|
const sent = sendSocket(active.tunnel.socket, JSON.stringify({
|
|
3043
3274
|
type: "ws.frame",
|
|
@@ -3145,7 +3376,9 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3145
3376
|
closeSocket(channel.socket, 1008, token.reason);
|
|
3146
3377
|
return;
|
|
3147
3378
|
}
|
|
3148
|
-
sendSocket(channel.socket, tunnelFrame(message))
|
|
3379
|
+
if (!sendSocket(channel.socket, tunnelFrame(message))) {
|
|
3380
|
+
dropUserHostedChannel(tunnel, message.channel_id, channel, 1013, "Client WebSocket delivery failed");
|
|
3381
|
+
}
|
|
3149
3382
|
return;
|
|
3150
3383
|
}
|
|
3151
3384
|
if (parsed.message.type === "ws.close") {
|
|
@@ -3239,7 +3472,8 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3239
3472
|
directory.registerHostTunnel({ hostId, workspaceIds });
|
|
3240
3473
|
if (!hibernation) {
|
|
3241
3474
|
pair.server.addEventListener?.("message", (event) => {
|
|
3242
|
-
if (event
|
|
3475
|
+
if (!event || event.data === void 0) return;
|
|
3476
|
+
void handleTunnelMessage(hostId, { data: event.data }).catch((err) => reportFrameHandlerFailure(pair.server, err));
|
|
3243
3477
|
});
|
|
3244
3478
|
pair.server.addEventListener?.("close", () => cleanupHostTunnel(hostId, pair.server, "User-hosted tunnel disconnected"));
|
|
3245
3479
|
pair.server.addEventListener?.("error", () => cleanupHostTunnel(hostId, pair.server, "User-hosted tunnel disconnected"));
|
|
@@ -3282,6 +3516,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3282
3516
|
let closed = false;
|
|
3283
3517
|
let upstreamOpen = upstream.readyState === void 0 || upstream.readyState === 1;
|
|
3284
3518
|
const queue = [];
|
|
3519
|
+
let queuedBytes = 0;
|
|
3285
3520
|
const emitTrace = (openedAt) => {
|
|
3286
3521
|
if (!trace || trace.emitted) return;
|
|
3287
3522
|
trace.emitted = true;
|
|
@@ -3312,6 +3547,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3312
3547
|
const flushQueue = () => {
|
|
3313
3548
|
upstreamOpen = true;
|
|
3314
3549
|
if (trace) emitTrace(performance.now());
|
|
3550
|
+
queuedBytes = 0;
|
|
3315
3551
|
for (const item of queue.splice(0)) {
|
|
3316
3552
|
if (!sendSocket(upstream, item)) {
|
|
3317
3553
|
closeSocket(pair.server, 1011, "Upstream WebSocket connection failed");
|
|
@@ -3319,28 +3555,31 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3319
3555
|
}
|
|
3320
3556
|
}
|
|
3321
3557
|
};
|
|
3322
|
-
|
|
3323
|
-
const payload = socketPayload(event?.data);
|
|
3324
|
-
if (payload === void 0) return;
|
|
3558
|
+
const deliverClientFrame = orderedFrameDelivery((payload) => {
|
|
3325
3559
|
if (!upstreamOpen) {
|
|
3326
|
-
|
|
3560
|
+
const frameBytes = preOpenFrameBytes2(payload);
|
|
3561
|
+
if (queue.length >= (options.upstreamWebSocketPreOpenQueueMaxFrames ?? UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT2) && queuedBytes + frameBytes > (options.upstreamWebSocketPreOpenQueueMaxBytes ?? UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT2)) {
|
|
3327
3562
|
closeSocket(pair.server, 1011, "Upstream WebSocket queue limit exceeded");
|
|
3328
3563
|
cleanupClient({ code: 1011, reason: "Upstream WebSocket queue limit exceeded" });
|
|
3329
3564
|
return;
|
|
3330
3565
|
}
|
|
3566
|
+
queuedBytes += frameBytes;
|
|
3331
3567
|
if (trace) trace.queueTimes.push(performance.now());
|
|
3332
3568
|
queue.push(payload);
|
|
3333
3569
|
return;
|
|
3334
3570
|
}
|
|
3335
3571
|
if (!sendSocket(upstream, payload)) closeSocket(pair.server, 1011, "Upstream WebSocket connection failed");
|
|
3336
3572
|
});
|
|
3573
|
+
pair.server.addEventListener?.("message", (event) => deliverClientFrame(event?.data));
|
|
3337
3574
|
pair.server.addEventListener?.("close", cleanupClient);
|
|
3338
3575
|
pair.server.addEventListener?.("error", cleanupClient);
|
|
3339
3576
|
upstream.addEventListener?.("open", flushQueue);
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3577
|
+
const deliverUpstreamFrame = orderedFrameDelivery((payload) => {
|
|
3578
|
+
if (!sendSocket(pair.server, payload)) {
|
|
3579
|
+
closeSocket(pair.server, 1011, "Client WebSocket send failed");
|
|
3580
|
+
}
|
|
3343
3581
|
});
|
|
3582
|
+
upstream.addEventListener?.("message", (event) => deliverUpstreamFrame(event?.data));
|
|
3344
3583
|
upstream.addEventListener?.("close", cleanupUpstream);
|
|
3345
3584
|
upstream.addEventListener?.("error", () => cleanupUpstream({ code: 1011, reason: "Upstream WebSocket connection failed" }));
|
|
3346
3585
|
if (trace && upstreamOpen) emitTrace(upstreamConnectedAt);
|
|
@@ -3376,6 +3615,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3376
3615
|
const activeTargetTimer = hibernation ? void 0 : watchUserHostedTarget(client, (reason) => {
|
|
3377
3616
|
closeSocket(pair.server, 1011, reason);
|
|
3378
3617
|
});
|
|
3618
|
+
if (hibernation) void scheduleHibernatedRevocationCheck();
|
|
3379
3619
|
if (!hibernation) {
|
|
3380
3620
|
const cleanup = (event) => {
|
|
3381
3621
|
if (activeTokenTimer) clearInterval(activeTokenTimer);
|
|
@@ -3384,12 +3624,12 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3384
3624
|
};
|
|
3385
3625
|
pair.server.addEventListener?.("message", (event) => {
|
|
3386
3626
|
if (!event) return;
|
|
3387
|
-
void handleUserHostedClientMessage(channelId, pair.server, event.data);
|
|
3627
|
+
void handleUserHostedClientMessage(channelId, pair.server, event.data).catch((err) => reportFrameHandlerFailure(pair.server, err));
|
|
3388
3628
|
});
|
|
3389
3629
|
pair.server.addEventListener?.("close", cleanup);
|
|
3390
3630
|
pair.server.addEventListener?.("error", cleanup);
|
|
3391
3631
|
}
|
|
3392
|
-
sendSocket(tunnel.socket, JSON.stringify({
|
|
3632
|
+
const opened = sendSocket(tunnel.socket, JSON.stringify({
|
|
3393
3633
|
type: "ws.open",
|
|
3394
3634
|
protocol: TUNNEL_PROTOCOL_VERSION2,
|
|
3395
3635
|
channel_id: channelId,
|
|
@@ -3402,6 +3642,12 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3402
3642
|
{ userHosted: true }
|
|
3403
3643
|
))
|
|
3404
3644
|
}));
|
|
3645
|
+
if (!opened) {
|
|
3646
|
+
tunnel.channels.delete(channelId);
|
|
3647
|
+
clients.delete(client);
|
|
3648
|
+
closeSocket(pair.server, 1011, "User-hosted tunnel unavailable");
|
|
3649
|
+
return json("user_hosted_app_offline", "User-hosted workspace is offline", 503);
|
|
3650
|
+
}
|
|
3405
3651
|
return upgradeResponse(pair.client, request);
|
|
3406
3652
|
};
|
|
3407
3653
|
const forwardCloudHttp = async (request, workspaceId, trace) => {
|
|
@@ -3555,14 +3801,29 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3555
3801
|
return json("relay_route_not_found", "Workspace Relay route not found", 404);
|
|
3556
3802
|
},
|
|
3557
3803
|
async webSocketMessage(socket, message) {
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3804
|
+
try {
|
|
3805
|
+
ensureHibernatedSocketsRebuilt();
|
|
3806
|
+
const attachment = socketAttachment(socket);
|
|
3807
|
+
if (!attachment) return;
|
|
3808
|
+
if (attachment.kind === "host-tunnel") {
|
|
3809
|
+
await handleTunnelMessage(attachment.hostId, { data: message });
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3812
|
+
await handleUserHostedClientMessage(attachment.channelId, socket, message);
|
|
3813
|
+
} catch (err) {
|
|
3814
|
+
reportFrameHandlerFailure(socket, err);
|
|
3815
|
+
}
|
|
3816
|
+
},
|
|
3817
|
+
/**
|
|
3818
|
+
* Durable Object alarm entry point. Runs the hibernation-safe revocation
|
|
3819
|
+
* sweep (W6b.2). The Worker must forward `alarm()` here.
|
|
3820
|
+
*/
|
|
3821
|
+
async alarm() {
|
|
3822
|
+
try {
|
|
3823
|
+
await runHibernatedRevocationCheck();
|
|
3824
|
+
} catch (err) {
|
|
3825
|
+
console.error(`[workspace-relay] revocation alarm failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3564
3826
|
}
|
|
3565
|
-
await handleUserHostedClientMessage(attachment.channelId, socket, message);
|
|
3566
3827
|
},
|
|
3567
3828
|
webSocketClose(socket, code, reason) {
|
|
3568
3829
|
ensureHibernatedSocketsRebuilt();
|
|
@@ -3599,9 +3860,12 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
3599
3860
|
export {
|
|
3600
3861
|
DEFAULT_RELAY_APP_ORIGINS,
|
|
3601
3862
|
DEFAULT_RELAY_LOCATION_HINT,
|
|
3863
|
+
RELAY_LOCATION_HINTS,
|
|
3864
|
+
RELAY_REGION_HEADER,
|
|
3602
3865
|
WorkspaceRelayAuthError,
|
|
3603
3866
|
__slowConsumerInternalsForTest,
|
|
3604
3867
|
authorizeWorkspaceRelayRequest,
|
|
3868
|
+
bytesToBase64,
|
|
3605
3869
|
createCachedRevocationClient,
|
|
3606
3870
|
createCachedTargetClient,
|
|
3607
3871
|
createOriginMatcher,
|
|
@@ -3621,8 +3885,14 @@ export {
|
|
|
3621
3885
|
mintRelayHostToken,
|
|
3622
3886
|
mintRuntimeAccessToken,
|
|
3623
3887
|
parseAllowedOrigins,
|
|
3888
|
+
relayBufferedBytes,
|
|
3889
|
+
relayFrameDiagnostics,
|
|
3624
3890
|
relayHostTokenAudience,
|
|
3625
3891
|
relayHostTokenIssuer,
|
|
3892
|
+
relayLocationHint,
|
|
3893
|
+
relayOverBackpressureLimit,
|
|
3894
|
+
relayRequestCountry,
|
|
3895
|
+
relayRequestRegion,
|
|
3626
3896
|
runtimeAccessTokenAudience,
|
|
3627
3897
|
runtimeAccessTokenIssuer,
|
|
3628
3898
|
setWorkspaceRelayAllowedOrigins,
|