@claxedo/workspace-relay 0.10.1 → 0.10.3
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 +68 -19
- package/dist/auth.d.ts +10 -12
- package/dist/cloudflare.d.ts +11 -30
- package/dist/host-tunnel-forwarding.d.ts +0 -2
- package/dist/http.d.ts +0 -2
- package/dist/index.mjs +541 -364
- package/dist/relay-bytes.d.ts +10 -0
- package/dist/relay-location.d.ts +19 -10
- package/dist/relay-log.d.ts +61 -0
- package/dist/relay-trace.d.ts +5 -0
- package/dist/resolver-clients.d.ts +19 -4
- package/dist/server.d.ts +6 -14
- package/dist/socket-frames.d.ts +47 -0
- package/dist/stream-standing.d.ts +24 -0
- package/dist/worker.d.ts +35 -15
- package/dist/workerd-fixture/boot.d.ts +5 -13
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -450,6 +450,11 @@ var PUBLIC_API_ERRORS = {
|
|
|
450
450
|
org_invitation_invalid: { family: "access", status: 409, message: "Invitation is invalid or unavailable" },
|
|
451
451
|
org_invitation_pending: { family: "access", status: 409, message: "An invitation to this address is already pending" },
|
|
452
452
|
org_invitation_rate_limited: { family: "access", status: 429, message: "Organization invitation limit exceeded" },
|
|
453
|
+
org_limit_reached: { family: "access", status: 403, message: "An account owns at most 3 organizations" },
|
|
454
|
+
org_creation_rate_limited: { family: "access", status: 429, message: "Organization creation limit exceeded" },
|
|
455
|
+
project_limit_reached: { family: "access", status: 402, message: "Free accounts have 3 projects. Get Cloud or Self-host for unlimited projects." },
|
|
456
|
+
seats_full: { family: "access", status: 402, message: "Add teammate seats to invite more people." },
|
|
457
|
+
email_verification_required: { family: "access", status: 403, message: "A verified email address is required" },
|
|
453
458
|
org_invitation_email_mismatch: { family: "access", status: 403, message: "Sign in with the invited verified email address" },
|
|
454
459
|
org_invitation_delivery_unavailable: { family: "access", status: 503, message: "Invitation delivery is unavailable" },
|
|
455
460
|
team_not_found: { family: "access", status: 404, message: "Team not found" },
|
|
@@ -459,6 +464,12 @@ var PUBLIC_API_ERRORS = {
|
|
|
459
464
|
project_member_not_found: { family: "access", status: 404, message: "Project member not found" },
|
|
460
465
|
project_member_owner_immutable: { family: "access", status: 409, message: "The project owner's access cannot be changed" },
|
|
461
466
|
resource_conflict: { family: "access", status: 409, message: "Organization or team authority changed concurrently" },
|
|
467
|
+
org_not_deletable: { family: "access", status: 409, message: "A personal organization goes with its account, and a deployment's with the deployment" },
|
|
468
|
+
org_has_workspaces: { family: "access", status: 409, message: "Delete the organization's workspaces first" },
|
|
469
|
+
account_owns_workspaces: { family: "access", status: 409, message: "Delete your workspaces first" },
|
|
470
|
+
account_founds_organization: { family: "access", status: 409, message: "Delete the organizations you founded first" },
|
|
471
|
+
account_has_cloud_subscription: { family: "access", status: 409, message: "Cancel Cloud before deleting this account" },
|
|
472
|
+
recent_sign_in_required: { family: "access", status: 403, message: "Sign in again to confirm this deletion" },
|
|
462
473
|
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
474
|
session_share_admin_required: { family: "session_share", status: 403, message: "Only the person who started this session can manage its People." },
|
|
464
475
|
session_share_target_required: { family: "session_share", status: 400, message: "A session is shared with exactly one person" },
|
|
@@ -482,11 +493,19 @@ var PUBLIC_API_ERRORS = {
|
|
|
482
493
|
enrollment_paused: { status: 403, message: "enrollment_paused" },
|
|
483
494
|
enrollment_owner_ineligible: { status: 403, message: "enrollment_owner_ineligible" },
|
|
484
495
|
enrollment_key_version_mismatch: { status: 403, message: "enrollment_key_version_mismatch" },
|
|
496
|
+
enrollment_denied: { status: 403, message: "enrollment_denied" },
|
|
497
|
+
enrollment_approval_expired: { status: 403, message: "enrollment_approval_expired" },
|
|
498
|
+
enrollment_awaiting_approval: { status: 403, message: "enrollment_awaiting_approval" },
|
|
485
499
|
invitation_invalid: { status: 403, message: "invitation_invalid" },
|
|
486
500
|
invitation_expired: { status: 410, message: "invitation_expired" },
|
|
487
501
|
invitation_revoked: { status: 410, message: "invitation_revoked" },
|
|
488
502
|
invitation_redeemed: { status: 409, message: "invitation_redeemed" },
|
|
489
503
|
invitation_host_conflict: { status: 409, message: "invitation_host_conflict" },
|
|
504
|
+
pairing_invalid: { status: 403, message: "pairing_invalid" },
|
|
505
|
+
pairing_expired: { status: 410, message: "pairing_expired" },
|
|
506
|
+
pairing_used: { status: 409, message: "pairing_used" },
|
|
507
|
+
pairing_owner_signed_out: { status: 410, message: "pairing_owner_signed_out" },
|
|
508
|
+
pairing_decided: { status: 409, message: "pairing_decided" },
|
|
490
509
|
enrollment_generation_superseded: { status: 409, message: "enrollment_generation_superseded" },
|
|
491
510
|
host_assignment_outside_scope: { status: 400, message: "host_assignment_outside_scope" },
|
|
492
511
|
host_sealing_key_undeclared: { status: 409, message: "host_sealing_key_undeclared" },
|
|
@@ -674,7 +693,7 @@ function hostTunnelIncumbentOutranks(incumbentGeneration, candidateGeneration) {
|
|
|
674
693
|
}
|
|
675
694
|
var RELAY_HOST_TOKEN_CACHE_MAX_ENTRIES = 4096;
|
|
676
695
|
var RUNTIME_ACCESS_TOKEN_CACHE_MAX_ENTRIES = 8192;
|
|
677
|
-
var RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS_DEFAULT =
|
|
696
|
+
var RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS_DEFAULT = 3e4;
|
|
678
697
|
var RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
|
|
679
698
|
var relayHostTokenCaches = /* @__PURE__ */ new WeakMap();
|
|
680
699
|
var runtimeAccessTokenCaches = /* @__PURE__ */ new WeakMap();
|
|
@@ -1081,6 +1100,246 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
|
|
|
1081
1100
|
}
|
|
1082
1101
|
}
|
|
1083
1102
|
|
|
1103
|
+
// ../claxedo-helpers/dist/deadline.mjs
|
|
1104
|
+
async function withDeadlineSignal(timeoutMs, work, parent) {
|
|
1105
|
+
const deadline = new AbortController();
|
|
1106
|
+
const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
|
|
1107
|
+
try {
|
|
1108
|
+
return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
|
|
1109
|
+
} finally {
|
|
1110
|
+
clearTimeout(timer);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/resolver-clients.ts
|
|
1115
|
+
function parseRuntimeAccessTokenActiveResult(input) {
|
|
1116
|
+
if (!isRecord(input)) return void 0;
|
|
1117
|
+
const row = input;
|
|
1118
|
+
if (row.active === true) return { active: true };
|
|
1119
|
+
if (row.active !== false) return void 0;
|
|
1120
|
+
if (typeof row.code !== "string" || typeof row.reason !== "string") return void 0;
|
|
1121
|
+
return { active: false, code: row.code, reason: row.reason };
|
|
1122
|
+
}
|
|
1123
|
+
function parseHostGenerationResult(input) {
|
|
1124
|
+
if (!isRecord(input)) return void 0;
|
|
1125
|
+
const row = input;
|
|
1126
|
+
if (typeof row.enrollmentId !== "string" || !row.enrollmentId.trim()) return void 0;
|
|
1127
|
+
if (!isHostGeneration(row.generation) || typeof row.revoked !== "boolean") return void 0;
|
|
1128
|
+
return { enrollmentId: row.enrollmentId, generation: row.generation, revoked: row.revoked };
|
|
1129
|
+
}
|
|
1130
|
+
var HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT = 5e3;
|
|
1131
|
+
var HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE = "relay_resolver_enrollment_not_found";
|
|
1132
|
+
function createHostGenerationResolverLookup(url, options) {
|
|
1133
|
+
const fetcher = options.fetch ?? ((target, init) => fetch(target, init));
|
|
1134
|
+
const timeoutMs = options.timeoutMs ?? HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT;
|
|
1135
|
+
return async ({ enrollmentId }) => {
|
|
1136
|
+
const target = new URL(url);
|
|
1137
|
+
target.searchParams.set("enrollmentId", enrollmentId);
|
|
1138
|
+
return withDeadlineSignal(timeoutMs, async (signal) => {
|
|
1139
|
+
const res = await fetcher(target, { headers: options.headers, signal });
|
|
1140
|
+
if (res.status === 404) {
|
|
1141
|
+
const body = await res.json().catch(() => void 0);
|
|
1142
|
+
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
1143
|
+
if (error?.code === HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE) return void 0;
|
|
1144
|
+
throw new Error(`relay host-generation resolver failed: 404 (no host-generation route at ${url})`);
|
|
1145
|
+
}
|
|
1146
|
+
if (!res.ok) throw new Error(`relay host-generation resolver failed: ${res.status} ${await res.text()}`);
|
|
1147
|
+
const result = parseHostGenerationResult(await res.json());
|
|
1148
|
+
if (!result) throw new Error("relay host-generation resolver returned a malformed result");
|
|
1149
|
+
return result;
|
|
1150
|
+
});
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
var RESOLVER_CACHE_MAX_ENTRIES = 8192;
|
|
1154
|
+
var REVOCATION_CACHE_TTL_MS_DEFAULT = 3e4;
|
|
1155
|
+
var TARGET_CACHE_TTL_MS_DEFAULT = 3e4;
|
|
1156
|
+
var DURABLE_OBJECT_TARGET_CACHE_TTL_MS_DEFAULT = 30 * 6e4;
|
|
1157
|
+
function pruneExpiringCache(cache, now, maxEntries) {
|
|
1158
|
+
for (const [key, entry] of cache) {
|
|
1159
|
+
if (entry.expiresAt <= now) cache.delete(key);
|
|
1160
|
+
}
|
|
1161
|
+
while (cache.size >= maxEntries) {
|
|
1162
|
+
const key = cache.keys().next().value;
|
|
1163
|
+
if (!key) return;
|
|
1164
|
+
cache.delete(key);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
var REVOCATION_RESOLVER_UNAVAILABLE = "relay_revocation_resolver_unavailable";
|
|
1168
|
+
function createCachedRevocationClient(inner, options = {}) {
|
|
1169
|
+
const ttlMs = options.ttlMs ?? REVOCATION_CACHE_TTL_MS_DEFAULT;
|
|
1170
|
+
const now = options.now ?? Date.now;
|
|
1171
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1172
|
+
const ask = (args, at) => {
|
|
1173
|
+
const prior = cache.get(args.jti)?.held;
|
|
1174
|
+
const settle = (standing) => {
|
|
1175
|
+
const settledAt = now();
|
|
1176
|
+
const kept = prior && settledAt - prior.checkedAt < ttlMs ? prior : void 0;
|
|
1177
|
+
const unreachable = !standing || !standing.result.active && standing.result.code === REVOCATION_RESOLVER_UNAVAILABLE;
|
|
1178
|
+
const held = unreachable && kept ? kept : standing;
|
|
1179
|
+
if (!held) {
|
|
1180
|
+
cache.delete(args.jti);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
pruneExpiringCache(cache, settledAt, RESOLVER_CACHE_MAX_ENTRIES);
|
|
1184
|
+
cache.set(args.jti, { held, expiresAt: held.checkedAt + ttlMs });
|
|
1185
|
+
};
|
|
1186
|
+
const asking = inner(args).then(
|
|
1187
|
+
(result) => {
|
|
1188
|
+
const standing = { result, checkedAt: now() };
|
|
1189
|
+
settle(standing);
|
|
1190
|
+
return standing;
|
|
1191
|
+
},
|
|
1192
|
+
(err) => {
|
|
1193
|
+
settle();
|
|
1194
|
+
throw err;
|
|
1195
|
+
}
|
|
1196
|
+
);
|
|
1197
|
+
cache.set(args.jti, { asking, ...prior ? { held: prior } : {}, expiresAt: at + ttlMs });
|
|
1198
|
+
return asking;
|
|
1199
|
+
};
|
|
1200
|
+
const within = async (args, maxAgeMs) => {
|
|
1201
|
+
const at = now();
|
|
1202
|
+
const entry = cache.get(args.jti);
|
|
1203
|
+
if (entry?.held && at - entry.held.checkedAt < Math.min(maxAgeMs, ttlMs)) return entry.held;
|
|
1204
|
+
return await (entry?.asking ?? ask(args, at));
|
|
1205
|
+
};
|
|
1206
|
+
return Object.assign(async (args) => (await within(args, ttlMs)).result, { within, ttlMs });
|
|
1207
|
+
}
|
|
1208
|
+
function runtimeAccessTokenRevocationDelayMs(input = {}) {
|
|
1209
|
+
const configuredTtl = input.revocationCacheTtlMs ?? REVOCATION_CACHE_TTL_MS_DEFAULT;
|
|
1210
|
+
const ttlMs = Number.isFinite(configuredTtl) && configuredTtl > 0 ? configuredTtl : 0;
|
|
1211
|
+
if (input.activeCheckIntervalMs === void 0) return ttlMs;
|
|
1212
|
+
if (!Number.isFinite(input.activeCheckIntervalMs) || input.activeCheckIntervalMs <= 0) {
|
|
1213
|
+
return Number.POSITIVE_INFINITY;
|
|
1214
|
+
}
|
|
1215
|
+
return ttlMs + input.activeCheckIntervalMs;
|
|
1216
|
+
}
|
|
1217
|
+
function createCachedHostGenerationClient(inner, options = {}) {
|
|
1218
|
+
const ttlMs = options.ttlMs ?? 1e4;
|
|
1219
|
+
const now = options.now ?? Date.now;
|
|
1220
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1221
|
+
return async (args) => {
|
|
1222
|
+
for (; ; ) {
|
|
1223
|
+
const at = now();
|
|
1224
|
+
const entry = cache.get(args.enrollmentId);
|
|
1225
|
+
if (entry && entry.expiresAt > at) {
|
|
1226
|
+
if (entry.result && entry.result.generation >= args.generation) return entry.result;
|
|
1227
|
+
if (!entry.result && entry.promise) {
|
|
1228
|
+
const shared = await entry.promise;
|
|
1229
|
+
if (!shared || shared.generation >= args.generation) return shared;
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const promise = inner(args).then((result) => {
|
|
1234
|
+
pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
|
|
1235
|
+
if (result) cache.set(args.enrollmentId, { result, expiresAt: now() + ttlMs });
|
|
1236
|
+
else cache.delete(args.enrollmentId);
|
|
1237
|
+
return result;
|
|
1238
|
+
}).catch((err) => {
|
|
1239
|
+
cache.delete(args.enrollmentId);
|
|
1240
|
+
throw err;
|
|
1241
|
+
});
|
|
1242
|
+
cache.set(args.enrollmentId, { promise, expiresAt: at + ttlMs });
|
|
1243
|
+
return await promise;
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
function createCachedTargetClient(inner, options = {}) {
|
|
1248
|
+
const ttlMs = options.ttlMs ?? TARGET_CACHE_TTL_MS_DEFAULT;
|
|
1249
|
+
const durableObjectTtlMs = options.durableObjectTtlMs ?? DURABLE_OBJECT_TARGET_CACHE_TTL_MS_DEFAULT;
|
|
1250
|
+
const now = options.now ?? Date.now;
|
|
1251
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1252
|
+
return async (args) => {
|
|
1253
|
+
const key = `${args.workspaceId}\0${args.hostId}\0${args.routingId ?? ""}`;
|
|
1254
|
+
const at = now();
|
|
1255
|
+
const entry = cache.get(key);
|
|
1256
|
+
if (entry && entry.expiresAt > at) {
|
|
1257
|
+
if (entry.target) return entry.target;
|
|
1258
|
+
if (entry.promise) return await entry.promise;
|
|
1259
|
+
}
|
|
1260
|
+
const promise = inner(args).then((target) => {
|
|
1261
|
+
pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
|
|
1262
|
+
if (target) cache.set(key, { target, expiresAt: now() + (target.backing === "durable-object" ? durableObjectTtlMs : ttlMs) });
|
|
1263
|
+
else cache.delete(key);
|
|
1264
|
+
return target;
|
|
1265
|
+
}).catch((err) => {
|
|
1266
|
+
cache.delete(key);
|
|
1267
|
+
throw err;
|
|
1268
|
+
});
|
|
1269
|
+
cache.set(key, { promise, expiresAt: at + ttlMs });
|
|
1270
|
+
return await promise;
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/stream-standing.ts
|
|
1275
|
+
var STANDING_REFRESH_LEAD_MS = 2e3;
|
|
1276
|
+
function guardStreamStanding(response, claims, options) {
|
|
1277
|
+
const source = response.body;
|
|
1278
|
+
const read = options.standing;
|
|
1279
|
+
if (!source || !read) return response;
|
|
1280
|
+
const now = options.now ?? Date.now;
|
|
1281
|
+
const reader = source.getReader();
|
|
1282
|
+
const refreshMs = Math.max(options.boundMs - (options.leadMs ?? STANDING_REFRESH_LEAD_MS), 1);
|
|
1283
|
+
let timer;
|
|
1284
|
+
let failures = 0;
|
|
1285
|
+
let done = false;
|
|
1286
|
+
const body = new ReadableStream({
|
|
1287
|
+
start(controller) {
|
|
1288
|
+
const end = (reason) => {
|
|
1289
|
+
if (done) return;
|
|
1290
|
+
done = true;
|
|
1291
|
+
clearTimeout(timer);
|
|
1292
|
+
controller.error(new Error(reason));
|
|
1293
|
+
void reader.cancel(reason).catch(() => {
|
|
1294
|
+
});
|
|
1295
|
+
};
|
|
1296
|
+
const wait = (checkedAt) => {
|
|
1297
|
+
if (done) return;
|
|
1298
|
+
const expiresAt = claims.exp * 1e3;
|
|
1299
|
+
const due = Math.min(checkedAt + refreshMs, expiresAt);
|
|
1300
|
+
timer = setTimeout(() => {
|
|
1301
|
+
if (now() >= expiresAt) return end("Runtime Access Token expired");
|
|
1302
|
+
void check(refreshMs);
|
|
1303
|
+
}, Math.max(due - now(), 0));
|
|
1304
|
+
};
|
|
1305
|
+
const check = async (maxAgeMs) => {
|
|
1306
|
+
const standing = await read(claims, maxAgeMs).catch(() => void 0);
|
|
1307
|
+
if (done) return;
|
|
1308
|
+
const unreachable = !standing || !standing.result.active && standing.result.code === REVOCATION_RESOLVER_UNAVAILABLE;
|
|
1309
|
+
if (unreachable) {
|
|
1310
|
+
failures += 1;
|
|
1311
|
+
return failures >= options.graceAttempts ? end("Runtime Access Token could not be checked") : wait(now());
|
|
1312
|
+
}
|
|
1313
|
+
if (!standing.result.active) return end(standing.result.reason);
|
|
1314
|
+
failures = 0;
|
|
1315
|
+
wait(standing.checkedAt);
|
|
1316
|
+
};
|
|
1317
|
+
void check(options.boundMs);
|
|
1318
|
+
},
|
|
1319
|
+
async pull(controller) {
|
|
1320
|
+
try {
|
|
1321
|
+
const chunk = await reader.read();
|
|
1322
|
+
if (done) return;
|
|
1323
|
+
if (!chunk.done) return controller.enqueue(chunk.value);
|
|
1324
|
+
done = true;
|
|
1325
|
+
clearTimeout(timer);
|
|
1326
|
+
controller.close();
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
if (done) return;
|
|
1329
|
+
done = true;
|
|
1330
|
+
clearTimeout(timer);
|
|
1331
|
+
controller.error(err);
|
|
1332
|
+
}
|
|
1333
|
+
},
|
|
1334
|
+
cancel(reason) {
|
|
1335
|
+
done = true;
|
|
1336
|
+
clearTimeout(timer);
|
|
1337
|
+
return reader.cancel(reason);
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1084
1343
|
// src/directory.ts
|
|
1085
1344
|
function presenceKey(hostId, workspaceId) {
|
|
1086
1345
|
return `${hostId}\0${workspaceId}`;
|
|
@@ -1323,18 +1582,17 @@ async function relayRoomPlacement(request, input) {
|
|
|
1323
1582
|
const configured = trimToUndefined2(input.configured);
|
|
1324
1583
|
const region = relayRequestRegion(request);
|
|
1325
1584
|
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
1585
|
const headers = new Headers(request.headers);
|
|
1333
1586
|
headers.delete(RELAY_REGION_HEADER);
|
|
1334
1587
|
const credential = await input.credential();
|
|
1335
1588
|
const sessionHost = credential !== void 0 && sessionHostRootOf(credential.hostId) !== void 0;
|
|
1589
|
+
const declared = relayLocationHint({
|
|
1590
|
+
...configured ? { configured } : {},
|
|
1591
|
+
...credential?.declaresRegion && region ? { region } : {},
|
|
1592
|
+
...credential && country ? { country } : {}
|
|
1593
|
+
});
|
|
1336
1594
|
return {
|
|
1337
|
-
locationHint: sessionHost ? SESSION_HOST_LOCATION_HINT :
|
|
1595
|
+
locationHint: sessionHost ? SESSION_HOST_LOCATION_HINT : declared,
|
|
1338
1596
|
url: withoutQueryParams(request.url, RELAY_REGION_QUERY_PARAMS),
|
|
1339
1597
|
headers
|
|
1340
1598
|
};
|
|
@@ -1410,12 +1668,12 @@ async function relayPlacementCredential(request, env, url, workspaceId) {
|
|
|
1410
1668
|
const token2 = bearerToken(request.headers.get("authorization"));
|
|
1411
1669
|
if (!hostId || !token2) return void 0;
|
|
1412
1670
|
await verifyHostTunnelToken(token2, key, { hostId, workspaceIds: workspaceIdsFromSearch(url) });
|
|
1413
|
-
return { hostId };
|
|
1671
|
+
return { hostId, declaresRegion: true };
|
|
1414
1672
|
}
|
|
1415
1673
|
const token = runtimeAccessTokenFrom(request);
|
|
1416
1674
|
if (!token) return void 0;
|
|
1417
1675
|
const claims = await verifyRuntimeAccessToken(token, key, { workspaceId });
|
|
1418
|
-
return { hostId: claims.host_id };
|
|
1676
|
+
return { hostId: claims.host_id, declaresRegion: claims.routing_id !== void 0 };
|
|
1419
1677
|
} catch {
|
|
1420
1678
|
return void 0;
|
|
1421
1679
|
}
|
|
@@ -1427,6 +1685,136 @@ function workspaceRelayRoomPlacement(request, env, workspaceId) {
|
|
|
1427
1685
|
});
|
|
1428
1686
|
}
|
|
1429
1687
|
|
|
1688
|
+
// src/relay-bytes.ts
|
|
1689
|
+
var BASE64_CHUNK_BYTES = 32768;
|
|
1690
|
+
function bytesToBase64(bytes) {
|
|
1691
|
+
let binary = "";
|
|
1692
|
+
for (let i = 0; i < bytes.length; i += BASE64_CHUNK_BYTES) {
|
|
1693
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_BYTES));
|
|
1694
|
+
}
|
|
1695
|
+
return btoa(binary);
|
|
1696
|
+
}
|
|
1697
|
+
function base64ToBytes(input) {
|
|
1698
|
+
return Uint8Array.from(atob(input), (char) => char.charCodeAt(0));
|
|
1699
|
+
}
|
|
1700
|
+
function concatBytes(chunks) {
|
|
1701
|
+
const length = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
1702
|
+
const result = new Uint8Array(length);
|
|
1703
|
+
let offset = 0;
|
|
1704
|
+
for (const chunk of chunks) {
|
|
1705
|
+
result.set(chunk, offset);
|
|
1706
|
+
offset += chunk.byteLength;
|
|
1707
|
+
}
|
|
1708
|
+
return result;
|
|
1709
|
+
}
|
|
1710
|
+
async function readRequestBodyCapped(request, maxBytes) {
|
|
1711
|
+
if (!request.body) return new Uint8Array(0);
|
|
1712
|
+
const reader = request.body.getReader();
|
|
1713
|
+
const chunks = [];
|
|
1714
|
+
let total = 0;
|
|
1715
|
+
while (true) {
|
|
1716
|
+
const { done, value } = await reader.read();
|
|
1717
|
+
if (done) break;
|
|
1718
|
+
total += value.byteLength;
|
|
1719
|
+
if (total > maxBytes) {
|
|
1720
|
+
await reader.cancel().catch(() => {
|
|
1721
|
+
});
|
|
1722
|
+
return "too_large";
|
|
1723
|
+
}
|
|
1724
|
+
chunks.push(value);
|
|
1725
|
+
}
|
|
1726
|
+
return concatBytes(chunks);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/relay-log.ts
|
|
1730
|
+
function relayLogOf(options) {
|
|
1731
|
+
return options.relayLog ?? ((event) => console.log(JSON.stringify(event)));
|
|
1732
|
+
}
|
|
1733
|
+
function frameDropWorthLogging(dropped) {
|
|
1734
|
+
return dropped <= 3 || dropped % 100 === 0;
|
|
1735
|
+
}
|
|
1736
|
+
var TIMED_OUT_REQUESTS_REMEMBERED = 32;
|
|
1737
|
+
function rememberTimedOut(timedOut, requestId, at) {
|
|
1738
|
+
timedOut.set(requestId, at);
|
|
1739
|
+
if (timedOut.size <= TIMED_OUT_REQUESTS_REMEMBERED) return;
|
|
1740
|
+
const oldest = timedOut.keys().next().value;
|
|
1741
|
+
if (oldest !== void 0) timedOut.delete(oldest);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// src/socket-frames.ts
|
|
1745
|
+
var UPSTREAM_WS_PRE_OPEN_QUEUE_FRAME_OVERHEAD_BYTES = 64;
|
|
1746
|
+
function preOpenFrameBytes(input) {
|
|
1747
|
+
const payload = typeof input === "string" ? new TextEncoder().encode(input).byteLength : input.byteLength;
|
|
1748
|
+
return payload + UPSTREAM_WS_PRE_OPEN_QUEUE_FRAME_OVERHEAD_BYTES;
|
|
1749
|
+
}
|
|
1750
|
+
function isBlobLike(input) {
|
|
1751
|
+
if (!input || typeof input !== "object") return false;
|
|
1752
|
+
return "arrayBuffer" in input && typeof input.arrayBuffer === "function";
|
|
1753
|
+
}
|
|
1754
|
+
function viewBytes(input) {
|
|
1755
|
+
return new Uint8Array(input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength));
|
|
1756
|
+
}
|
|
1757
|
+
function socketFrame(input) {
|
|
1758
|
+
if (typeof input === "string") {
|
|
1759
|
+
return {
|
|
1760
|
+
binary: false,
|
|
1761
|
+
data_base64: bytesToBase64(new TextEncoder().encode(input))
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
if (input instanceof ArrayBuffer) {
|
|
1765
|
+
return {
|
|
1766
|
+
binary: true,
|
|
1767
|
+
data_base64: bytesToBase64(new Uint8Array(input))
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
if (input instanceof Uint8Array) {
|
|
1771
|
+
return {
|
|
1772
|
+
binary: true,
|
|
1773
|
+
data_base64: bytesToBase64(input)
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
if (ArrayBuffer.isView(input)) {
|
|
1777
|
+
return {
|
|
1778
|
+
binary: true,
|
|
1779
|
+
data_base64: bytesToBase64(viewBytes(input))
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
if (!isBlobLike(input)) return void 0;
|
|
1783
|
+
return input.arrayBuffer().then((buffer) => ({
|
|
1784
|
+
binary: true,
|
|
1785
|
+
data_base64: bytesToBase64(new Uint8Array(buffer))
|
|
1786
|
+
}));
|
|
1787
|
+
}
|
|
1788
|
+
function socketPayload(input) {
|
|
1789
|
+
if (typeof input === "string") return input;
|
|
1790
|
+
if (input instanceof ArrayBuffer) return input;
|
|
1791
|
+
if (input instanceof Uint8Array) return input;
|
|
1792
|
+
if (ArrayBuffer.isView(input)) return viewBytes(input);
|
|
1793
|
+
if (!isBlobLike(input)) return void 0;
|
|
1794
|
+
return input.arrayBuffer();
|
|
1795
|
+
}
|
|
1796
|
+
function orderedFrameDelivery(deliver) {
|
|
1797
|
+
let tail;
|
|
1798
|
+
return (data) => {
|
|
1799
|
+
const payload = socketPayload(data);
|
|
1800
|
+
if (payload === void 0) return;
|
|
1801
|
+
if (tail === void 0 && !(payload instanceof Promise)) {
|
|
1802
|
+
deliver(payload);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
const queued = (tail ?? Promise.resolve()).then(async () => deliver(await payload)).catch(() => {
|
|
1806
|
+
});
|
|
1807
|
+
tail = queued;
|
|
1808
|
+
void queued.then(() => {
|
|
1809
|
+
if (tail === queued) tail = void 0;
|
|
1810
|
+
});
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
function tunnelFrame(input) {
|
|
1814
|
+
const bytes = base64ToBytes(input.data_base64);
|
|
1815
|
+
return input.binary ? bytes : new TextDecoder().decode(bytes);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1430
1818
|
// src/relay-trace.ts
|
|
1431
1819
|
import { trimToUndefined as trimToUndefined4 } from "@claxedo/helpers/string";
|
|
1432
1820
|
var TRACE_ID_HEADER = "x-claxedo-trace-id";
|
|
@@ -1443,12 +1831,15 @@ function traceRequest(request, routeKind, options) {
|
|
|
1443
1831
|
const forceSecret = trimToUndefined4(options.traceForceHeaderSecret);
|
|
1444
1832
|
const forced = !!forceSecret && request.headers.get(TRACE_FORCE_HEADER) === forceSecret;
|
|
1445
1833
|
const rate = clampRate(options.traceSampleRate);
|
|
1834
|
+
const sampled = forced || rate > 0 && (options.random ?? Math.random)() < rate;
|
|
1835
|
+
const roomColo = sampled ? options.roomColo?.() : void 0;
|
|
1446
1836
|
return {
|
|
1447
1837
|
id,
|
|
1448
|
-
sampled
|
|
1838
|
+
sampled,
|
|
1449
1839
|
startedAt: performance.now(),
|
|
1450
1840
|
routeKind,
|
|
1451
|
-
phases: []
|
|
1841
|
+
phases: [],
|
|
1842
|
+
...roomColo ? { roomColo } : {}
|
|
1452
1843
|
};
|
|
1453
1844
|
}
|
|
1454
1845
|
async function traceSpan(trace, name, fn) {
|
|
@@ -1489,7 +1880,8 @@ function webSocketTraceFrame(upstreamStartedAt, queueTimes, openedAt) {
|
|
|
1489
1880
|
function withTraceHeaders(response, trace) {
|
|
1490
1881
|
const headers = new Headers(response.headers);
|
|
1491
1882
|
headers.set(TRACE_ID_HEADER, trace.id);
|
|
1492
|
-
const
|
|
1883
|
+
const colo = trace.roomColo ? `relay-room-colo;desc=${trace.roomColo}` : "";
|
|
1884
|
+
const timing = trace.sampled ? [serverTiming(trace.phases), colo].filter(Boolean).join(", ") : "";
|
|
1493
1885
|
if (timing) {
|
|
1494
1886
|
const existing = headers.get("server-timing");
|
|
1495
1887
|
headers.set("server-timing", existing ? `${existing}, ${timing}` : timing);
|
|
@@ -1501,10 +1893,11 @@ function withTraceHeaders(response, trace) {
|
|
|
1501
1893
|
headers
|
|
1502
1894
|
});
|
|
1503
1895
|
}
|
|
1896
|
+
var SLOW_REQUEST_MS = 5e3;
|
|
1504
1897
|
function logTrace(options, request, workspaceId, trace, response) {
|
|
1505
|
-
if (!trace.sampled) return;
|
|
1506
|
-
const url = new URL(request.url);
|
|
1507
1898
|
const totalMs = performance.now() - trace.startedAt;
|
|
1899
|
+
if (!trace.sampled && response.status < 500 && totalMs <= SLOW_REQUEST_MS) return;
|
|
1900
|
+
const url = new URL(request.url);
|
|
1508
1901
|
const event = {
|
|
1509
1902
|
event: "workspace_relay_request",
|
|
1510
1903
|
traceId: trace.id,
|
|
@@ -1520,181 +1913,58 @@ function logTrace(options, request, workspaceId, trace, response) {
|
|
|
1520
1913
|
options.traceLog(event);
|
|
1521
1914
|
return;
|
|
1522
1915
|
}
|
|
1523
|
-
console.log(JSON.stringify(event));
|
|
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
|
-
}
|
|
1547
|
-
function workspaceIdFromRelayPath(pathname) {
|
|
1548
|
-
return /^\/workspaces\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
|
|
1549
|
-
}
|
|
1550
|
-
function websocketRequest(request) {
|
|
1551
|
-
return request.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
1552
|
-
}
|
|
1553
|
-
function relayWebSocketTraceEnabled(request) {
|
|
1554
|
-
return request.headers.get("x-claxedo-relay-ws-trace") === "1";
|
|
1555
|
-
}
|
|
1556
|
-
function randomToken() {
|
|
1557
|
-
return crypto.randomUUID();
|
|
1558
|
-
}
|
|
1559
|
-
function headerRecord(headers) {
|
|
1560
|
-
const result = /* @__PURE__ */ Object.create(null);
|
|
1561
|
-
headers.forEach((value, key) => {
|
|
1562
|
-
result[key] = value;
|
|
1563
|
-
});
|
|
1564
|
-
return result;
|
|
1565
|
-
}
|
|
1566
|
-
function headersFromRecord(input) {
|
|
1567
|
-
const headers = new Headers();
|
|
1568
|
-
for (const [key, value] of Object.entries(input)) headers.set(key, value);
|
|
1569
|
-
return headers;
|
|
1570
|
-
}
|
|
1571
|
-
var BASE64_CHUNK_BYTES = 32768;
|
|
1572
|
-
function bytesToBase64(bytes) {
|
|
1573
|
-
let binary = "";
|
|
1574
|
-
for (let i = 0; i < bytes.length; i += BASE64_CHUNK_BYTES) {
|
|
1575
|
-
binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_BYTES));
|
|
1576
|
-
}
|
|
1577
|
-
return btoa(binary);
|
|
1578
|
-
}
|
|
1579
|
-
function base64ToBytes(input) {
|
|
1580
|
-
return Uint8Array.from(atob(input), (char) => char.charCodeAt(0));
|
|
1581
|
-
}
|
|
1582
|
-
function preOpenFrameBytes(input) {
|
|
1583
|
-
const payload = typeof input === "string" ? new TextEncoder().encode(input).byteLength : input.byteLength;
|
|
1584
|
-
return payload + UPSTREAM_WS_PRE_OPEN_QUEUE_FRAME_OVERHEAD_BYTES;
|
|
1585
|
-
}
|
|
1586
|
-
function isBlobLike(input) {
|
|
1587
|
-
if (!input || typeof input !== "object") return false;
|
|
1588
|
-
return "arrayBuffer" in input && typeof input.arrayBuffer === "function";
|
|
1589
|
-
}
|
|
1590
|
-
function viewBytes(input) {
|
|
1591
|
-
return new Uint8Array(input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength));
|
|
1592
|
-
}
|
|
1593
|
-
function socketFrame(input) {
|
|
1594
|
-
if (typeof input === "string") {
|
|
1595
|
-
return {
|
|
1596
|
-
binary: false,
|
|
1597
|
-
data_base64: bytesToBase64(new TextEncoder().encode(input))
|
|
1598
|
-
};
|
|
1599
|
-
}
|
|
1600
|
-
if (input instanceof ArrayBuffer) {
|
|
1601
|
-
return {
|
|
1602
|
-
binary: true,
|
|
1603
|
-
data_base64: bytesToBase64(new Uint8Array(input))
|
|
1604
|
-
};
|
|
1605
|
-
}
|
|
1606
|
-
if (input instanceof Uint8Array) {
|
|
1607
|
-
return {
|
|
1608
|
-
binary: true,
|
|
1609
|
-
data_base64: bytesToBase64(input)
|
|
1610
|
-
};
|
|
1611
|
-
}
|
|
1612
|
-
if (ArrayBuffer.isView(input)) {
|
|
1613
|
-
return {
|
|
1614
|
-
binary: true,
|
|
1615
|
-
data_base64: bytesToBase64(viewBytes(input))
|
|
1616
|
-
};
|
|
1617
|
-
}
|
|
1618
|
-
if (!isBlobLike(input)) return void 0;
|
|
1619
|
-
return input.arrayBuffer().then((buffer) => ({
|
|
1620
|
-
binary: true,
|
|
1621
|
-
data_base64: bytesToBase64(new Uint8Array(buffer))
|
|
1622
|
-
}));
|
|
1916
|
+
console.log(JSON.stringify(event));
|
|
1623
1917
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1918
|
+
|
|
1919
|
+
// src/cloudflare.ts
|
|
1920
|
+
var TUNNEL_PENDING_HTTP_CAP_DEFAULT = 32;
|
|
1921
|
+
var TUNNEL_STARTED_STREAM_CAP_DEFAULT = 16;
|
|
1922
|
+
var TUNNEL_CHANNEL_CAP_DEFAULT = 16;
|
|
1923
|
+
var TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
|
|
1924
|
+
var TUNNEL_RESPONSE_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
|
|
1925
|
+
var SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT = 8 * 1024 * 1024;
|
|
1926
|
+
var SLOW_CONSUMER_TIMEOUT_MS_DEFAULT = 3e4;
|
|
1927
|
+
var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT = 8192;
|
|
1928
|
+
var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
|
|
1929
|
+
var WORKSPACE_TARGET_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
|
|
1930
|
+
var RESOLVER_OUTAGE_GRACE_ATTEMPTS_DEFAULT = 3;
|
|
1931
|
+
var HIBERNATED_REVOCATION_ALARM_INTERVAL_MS_DEFAULT = 3e4;
|
|
1932
|
+
var HOST_GENERATION_CHECK_INTERVAL_MS_DEFAULT = 3e4;
|
|
1933
|
+
function unrefTimer(timer) {
|
|
1934
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1631
1935
|
}
|
|
1632
|
-
function
|
|
1633
|
-
|
|
1634
|
-
return (data) => {
|
|
1635
|
-
const payload = socketPayload(data);
|
|
1636
|
-
if (payload === void 0) return;
|
|
1637
|
-
if (tail === void 0 && !(payload instanceof Promise)) {
|
|
1638
|
-
deliver(payload);
|
|
1639
|
-
return;
|
|
1640
|
-
}
|
|
1641
|
-
const queued = (tail ?? Promise.resolve()).then(async () => deliver(await payload)).catch(() => {
|
|
1642
|
-
});
|
|
1643
|
-
tail = queued;
|
|
1644
|
-
void queued.then(() => {
|
|
1645
|
-
if (tail === queued) tail = void 0;
|
|
1646
|
-
});
|
|
1647
|
-
};
|
|
1936
|
+
function isHostTunnelClient(client) {
|
|
1937
|
+
return "channelId" in client && typeof client.channelId === "string";
|
|
1648
1938
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
const payload = await socketPayload(data);
|
|
1652
|
-
return {
|
|
1653
|
-
// `undefined` here is exactly the silent drop: every caller treats it as
|
|
1654
|
-
// "nothing to forward" and returns without a throw or a log.
|
|
1655
|
-
frameDropped: frame === void 0,
|
|
1656
|
-
payloadDropped: payload === void 0,
|
|
1657
|
-
...frame ? { binary: frame.binary, dataBase64: frame.data_base64 } : {},
|
|
1658
|
-
payloadBase64: payload === void 0 ? void 0 : typeof payload === "string" ? bytesToBase64(new TextEncoder().encode(payload)) : bytesToBase64(payload instanceof ArrayBuffer ? new Uint8Array(payload) : payload)
|
|
1659
|
-
};
|
|
1939
|
+
function workspaceIdFromRelayPath(pathname) {
|
|
1940
|
+
return /^\/workspaces\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
|
|
1660
1941
|
}
|
|
1661
|
-
function
|
|
1662
|
-
|
|
1663
|
-
return input.binary ? bytes : new TextDecoder().decode(bytes);
|
|
1942
|
+
function websocketRequest(request) {
|
|
1943
|
+
return request.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
1664
1944
|
}
|
|
1665
|
-
function
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1945
|
+
function relayWebSocketTraceEnabled(request) {
|
|
1946
|
+
return request.headers.get("x-claxedo-relay-ws-trace") === "1";
|
|
1947
|
+
}
|
|
1948
|
+
function randomToken() {
|
|
1949
|
+
return crypto.randomUUID();
|
|
1950
|
+
}
|
|
1951
|
+
function headerRecord(headers) {
|
|
1952
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
1953
|
+
headers.forEach((value, key) => {
|
|
1954
|
+
result[key] = value;
|
|
1955
|
+
});
|
|
1673
1956
|
return result;
|
|
1674
1957
|
}
|
|
1958
|
+
function headersFromRecord(input) {
|
|
1959
|
+
const headers = new Headers();
|
|
1960
|
+
for (const [key, value] of Object.entries(input)) headers.set(key, value);
|
|
1961
|
+
return headers;
|
|
1962
|
+
}
|
|
1675
1963
|
function responseMustNotHaveBody(status) {
|
|
1676
1964
|
return status === 204 || status === 205 || status === 304;
|
|
1677
1965
|
}
|
|
1678
|
-
async function readRequestBodyCapped(request, maxBytes) {
|
|
1679
|
-
if (!request.body) return new Uint8Array(0);
|
|
1680
|
-
const reader = request.body.getReader();
|
|
1681
|
-
const chunks = [];
|
|
1682
|
-
let total = 0;
|
|
1683
|
-
while (true) {
|
|
1684
|
-
const { done, value } = await reader.read();
|
|
1685
|
-
if (done) break;
|
|
1686
|
-
total += value.byteLength;
|
|
1687
|
-
if (total > maxBytes) {
|
|
1688
|
-
await reader.cancel().catch(() => {
|
|
1689
|
-
});
|
|
1690
|
-
return "too_large";
|
|
1691
|
-
}
|
|
1692
|
-
chunks.push(value);
|
|
1693
|
-
}
|
|
1694
|
-
return concatBytes(chunks);
|
|
1695
|
-
}
|
|
1696
1966
|
function workspaceRelayDurableObjectRoomName(workspaceId) {
|
|
1697
|
-
return `
|
|
1967
|
+
return `relay-room:${workspaceId}`;
|
|
1698
1968
|
}
|
|
1699
1969
|
function workspaceRelayDurableObjectWorkspaceId(request) {
|
|
1700
1970
|
const url = new URL(request.url);
|
|
@@ -1865,7 +2135,22 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
1865
2135
|
return false;
|
|
1866
2136
|
}
|
|
1867
2137
|
};
|
|
1868
|
-
const
|
|
2138
|
+
const relayLog = relayLogOf(options);
|
|
2139
|
+
const clock = () => options.now?.() ?? Date.now();
|
|
2140
|
+
const cleanupTunnelResources = (tunnel, reason, detail = {}) => {
|
|
2141
|
+
if (!tunnel.ended) {
|
|
2142
|
+
relayLog({
|
|
2143
|
+
event: "tunnel_ended",
|
|
2144
|
+
hostId: tunnel.hostId,
|
|
2145
|
+
generation: tunnel.generation,
|
|
2146
|
+
reason,
|
|
2147
|
+
ageMs: clock() - tunnel.connectedAt,
|
|
2148
|
+
pendingFailed: tunnel.pending.size,
|
|
2149
|
+
channelsClosed: tunnel.channels.size,
|
|
2150
|
+
...detail
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
tunnel.ended = true;
|
|
1869
2154
|
if (tunnel.generationWatcher) clearInterval(tunnel.generationWatcher);
|
|
1870
2155
|
tunnel.generationWatcher = void 0;
|
|
1871
2156
|
for (const pending of tunnel.pending.values()) {
|
|
@@ -1911,6 +2196,8 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
1911
2196
|
generationCheckFailures: attachment.generationCheckFailures ?? 0,
|
|
1912
2197
|
socket,
|
|
1913
2198
|
pending: /* @__PURE__ */ new Map(),
|
|
2199
|
+
timedOut: /* @__PURE__ */ new Map(),
|
|
2200
|
+
droppedFrames: 0,
|
|
1914
2201
|
channels: /* @__PURE__ */ new Map()
|
|
1915
2202
|
});
|
|
1916
2203
|
directory.registerHostTunnel({
|
|
@@ -1956,7 +2243,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
1956
2243
|
}
|
|
1957
2244
|
};
|
|
1958
2245
|
const resolverGraceAttempts = options.resolverOutageGraceAttempts ?? RESOLVER_OUTAGE_GRACE_ATTEMPTS_DEFAULT;
|
|
1959
|
-
const resolverUnreachable = (code) => code ===
|
|
2246
|
+
const resolverUnreachable = (code) => code === REVOCATION_RESOLVER_UNAVAILABLE;
|
|
1960
2247
|
const watchRuntimeAccessToken = (client, onInactive) => {
|
|
1961
2248
|
const intervalMs = options.runtimeAccessTokenActiveCheckIntervalMs ?? RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT;
|
|
1962
2249
|
if (intervalMs <= 0) return void 0;
|
|
@@ -2248,8 +2535,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2248
2535
|
}));
|
|
2249
2536
|
};
|
|
2250
2537
|
const reportFrameHandlerFailure = (socket, err) => {
|
|
2251
|
-
|
|
2252
|
-
console.error(`[workspace-relay] frame handler failed: ${reason}`);
|
|
2538
|
+
options.report(err, "frame_handler");
|
|
2253
2539
|
closeSocket(socket, 1011, "Workspace relay failed to process a frame");
|
|
2254
2540
|
};
|
|
2255
2541
|
const hibernatedRevocationIntervalMs = () => options.hibernatedRevocationCheckIntervalMs ?? HIBERNATED_REVOCATION_ALARM_INTERVAL_MS_DEFAULT;
|
|
@@ -2363,10 +2649,23 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2363
2649
|
};
|
|
2364
2650
|
const handleTunnelMessage = async (hostId, socket, event) => {
|
|
2365
2651
|
const row = parseTunnelMessageData(event.data);
|
|
2366
|
-
|
|
2367
|
-
const parsed = validateTunnelMessage(row.value);
|
|
2652
|
+
const parsed = row.ok ? validateTunnelMessage(row.value) : { ok: false, reason: "not_json" };
|
|
2368
2653
|
const tunnel = hostTunnels.get(hostId);
|
|
2369
|
-
if (!
|
|
2654
|
+
if (!tunnel || tunnel.socket !== socket) return;
|
|
2655
|
+
if (!parsed.ok) {
|
|
2656
|
+
tunnel.droppedFrames++;
|
|
2657
|
+
if (frameDropWorthLogging(tunnel.droppedFrames)) {
|
|
2658
|
+
relayLog({
|
|
2659
|
+
event: "tunnel_frame_dropped",
|
|
2660
|
+
hostId,
|
|
2661
|
+
reason: parsed.reason,
|
|
2662
|
+
dropped: tunnel.droppedFrames,
|
|
2663
|
+
..."type" in parsed && parsed.type ? { type: parsed.type } : {},
|
|
2664
|
+
..."actual_protocol" in parsed ? { protocol: String(parsed.actual_protocol) } : {}
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2370
2669
|
directory.recordPong(tunnel.hostId);
|
|
2371
2670
|
if (parsed.message.type === "ping") {
|
|
2372
2671
|
tunnel.socket.send?.(JSON.stringify(makeTunnelPong(parsed.message)));
|
|
@@ -2385,7 +2684,13 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2385
2684
|
if (parsed.message.type === "http.response.start") {
|
|
2386
2685
|
const message = parsed.message;
|
|
2387
2686
|
const pending = tunnel.pending.get(message.request_id);
|
|
2388
|
-
if (!pending)
|
|
2687
|
+
if (!pending) {
|
|
2688
|
+
const timedOutAt = tunnel.timedOut.get(message.request_id);
|
|
2689
|
+
if (timedOutAt === void 0) return;
|
|
2690
|
+
tunnel.timedOut.delete(message.request_id);
|
|
2691
|
+
relayLog({ event: "tunnel_late_response", hostId: tunnel.hostId, requestId: message.request_id, lateMs: clock() - timedOutAt });
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2389
2694
|
pending.status = message.status;
|
|
2390
2695
|
pending.headers = headersFromRecord(message.headers);
|
|
2391
2696
|
if (responseMustNotHaveBody(pending.status)) return;
|
|
@@ -2475,10 +2780,10 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2475
2780
|
return { ok: false };
|
|
2476
2781
|
}
|
|
2477
2782
|
};
|
|
2478
|
-
const cleanupHostTunnel = (hostId, socket, reason) => {
|
|
2783
|
+
const cleanupHostTunnel = (hostId, socket, reason, detail) => {
|
|
2479
2784
|
const tunnel = hostTunnels.get(hostId);
|
|
2480
2785
|
if (tunnel?.socket !== socket) return;
|
|
2481
|
-
cleanupTunnelResources(tunnel, reason);
|
|
2786
|
+
cleanupTunnelResources(tunnel, reason, detail);
|
|
2482
2787
|
hostTunnels.delete(hostId);
|
|
2483
2788
|
directory.disconnectHost(hostId);
|
|
2484
2789
|
};
|
|
@@ -2499,29 +2804,33 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2499
2804
|
}
|
|
2500
2805
|
};
|
|
2501
2806
|
const admitHostTunnel = async (request, roomWorkspaceId, url) => {
|
|
2502
|
-
|
|
2807
|
+
const refused = (code, message, status) => {
|
|
2808
|
+
relayLog({ event: "tunnel_refused", code, hostId: trimToUndefined5(hostIdFromTunnelPath(url.pathname)) });
|
|
2809
|
+
return json(code, message, status);
|
|
2810
|
+
};
|
|
2811
|
+
if (!websocketRequest(request)) return refused("websocket_upgrade_required", "Workspace Relay requires a WebSocket upgrade", 426);
|
|
2503
2812
|
const hostId = trimToUndefined5(hostIdFromTunnelPath(url.pathname));
|
|
2504
|
-
if (!hostId) return
|
|
2813
|
+
if (!hostId) return refused("host_tunnel_host_required", "Host tunnel id is required", 400);
|
|
2505
2814
|
const workspaceIds = workspaceIdsFromSearch(url);
|
|
2506
2815
|
if (!workspaceIds.includes(roomWorkspaceId)) {
|
|
2507
|
-
return
|
|
2816
|
+
return refused("host_tunnel_workspace_mismatch", "Host tunnel must include the room workspace id", 403);
|
|
2508
2817
|
}
|
|
2509
2818
|
const token = bearerToken(request.headers.get("authorization"));
|
|
2510
|
-
if (!token) return
|
|
2819
|
+
if (!token) return refused("host_tunnel_token_required", "Host Tunnel Token is required", 401);
|
|
2511
2820
|
let claims;
|
|
2512
2821
|
try {
|
|
2513
2822
|
claims = await verifyHostTunnelToken(token, options.runtimeAccessKey, { hostId, workspaceIds });
|
|
2514
2823
|
} catch (err) {
|
|
2515
2824
|
if (err instanceof WorkspaceRelayAuthError) {
|
|
2516
|
-
return
|
|
2825
|
+
return refused(err.code, "Host Tunnel Token was denied", 403);
|
|
2517
2826
|
}
|
|
2518
2827
|
throw err;
|
|
2519
2828
|
}
|
|
2520
2829
|
const decision = await checkHostTunnelGeneration(options.resolveHostGeneration, claims);
|
|
2521
|
-
if (!decision.ok) return
|
|
2830
|
+
if (!decision.ok) return refused(decision.code, decision.reason, decision.retryable ? 503 : 403);
|
|
2522
2831
|
const previous = hostTunnels.get(hostId);
|
|
2523
2832
|
if (previous && hostTunnelIncumbentOutranks(previous.generation, claims.generation)) {
|
|
2524
|
-
return
|
|
2833
|
+
return refused("host_generation_superseded", "Host tunnel generation was superseded", 403);
|
|
2525
2834
|
}
|
|
2526
2835
|
const connectedAt = options.now?.() ?? Date.now();
|
|
2527
2836
|
const fence = {
|
|
@@ -2537,12 +2846,15 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2537
2846
|
generationCheckFailures: 0,
|
|
2538
2847
|
socket: pair.server,
|
|
2539
2848
|
pending: /* @__PURE__ */ new Map(),
|
|
2849
|
+
timedOut: /* @__PURE__ */ new Map(),
|
|
2850
|
+
droppedFrames: 0,
|
|
2540
2851
|
channels: /* @__PURE__ */ new Map()
|
|
2541
2852
|
};
|
|
2542
2853
|
if (previous) {
|
|
2543
2854
|
cleanupTunnelResources(previous, "Host tunnel replaced");
|
|
2544
2855
|
closeSocket(previous.socket, 1012, "Host tunnel replaced");
|
|
2545
2856
|
}
|
|
2857
|
+
relayLog({ event: "tunnel_admitted", hostId, workspaceIds, generation: claims.generation, ...previous ? { replacedAgeMs: connectedAt - previous.connectedAt } : {} });
|
|
2546
2858
|
hostTunnels.set(hostId, tunnel);
|
|
2547
2859
|
directory.registerHostTunnel({ hostId, workspaceIds });
|
|
2548
2860
|
watchHostGeneration(tunnel);
|
|
@@ -2552,7 +2864,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2552
2864
|
if (!event || event.data === void 0) return;
|
|
2553
2865
|
void handleTunnelMessage(hostId, pair.server, { data: event.data }).catch((err) => reportFrameHandlerFailure(pair.server, err));
|
|
2554
2866
|
});
|
|
2555
|
-
pair.server.addEventListener?.("close", () => cleanupHostTunnel(hostId, pair.server, "Host tunnel disconnected"));
|
|
2867
|
+
pair.server.addEventListener?.("close", (event) => cleanupHostTunnel(hostId, pair.server, "Host tunnel disconnected", { closeCode: event?.code, closeReason: event?.reason }));
|
|
2556
2868
|
pair.server.addEventListener?.("error", () => cleanupHostTunnel(hostId, pair.server, "Host tunnel disconnected"));
|
|
2557
2869
|
}
|
|
2558
2870
|
return upgradeResponse(pair.client, request);
|
|
@@ -2736,13 +3048,18 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2736
3048
|
})
|
|
2737
3049
|
);
|
|
2738
3050
|
if (!authorized.ok) return authorized.response;
|
|
2739
|
-
|
|
3051
|
+
const response = await forwardAuthorizedHttp(request, workspaceId, authorized.request, trace);
|
|
3052
|
+
if (!options.streamStanding) return response;
|
|
3053
|
+
return guardStreamStanding(response, authorized.request.claims, { ...options.streamStanding, graceAttempts: resolverGraceAttempts, ...options.now ? { now: options.now } : {} });
|
|
3054
|
+
};
|
|
3055
|
+
const forwardAuthorizedHttp = async (request, workspaceId, authorized, trace) => {
|
|
3056
|
+
if (isHostTunnelTarget(authorized.target)) {
|
|
2740
3057
|
trace && (trace.routeKind = "host-tunnel-http");
|
|
2741
|
-
return forwardHostTunnelHttp(request, authorized
|
|
3058
|
+
return forwardHostTunnelHttp(request, authorized, trace);
|
|
2742
3059
|
}
|
|
2743
|
-
if (isSessionHostTarget(authorized.
|
|
3060
|
+
if (isSessionHostTarget(authorized.target)) {
|
|
2744
3061
|
trace && (trace.routeKind = "session-host-http");
|
|
2745
|
-
return traceSpan(trace, "session-host-fetch", () => forwardSessionHostHttp(options.sessionHost, request, authorized
|
|
3062
|
+
return traceSpan(trace, "session-host-fetch", () => forwardSessionHostHttp(options.sessionHost, request, authorized));
|
|
2746
3063
|
}
|
|
2747
3064
|
trace && (trace.routeKind = "cloud-http");
|
|
2748
3065
|
const controller = new AbortController();
|
|
@@ -2750,11 +3067,11 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2750
3067
|
try {
|
|
2751
3068
|
const upstream = await traceSpan(trace, "upstream-fetch", () => (options.fetch ?? fetch)(
|
|
2752
3069
|
workspaceRelayTargetUrl(
|
|
2753
|
-
authorized.
|
|
2754
|
-
authorized.
|
|
3070
|
+
authorized.target,
|
|
3071
|
+
authorized.path,
|
|
2755
3072
|
new URL(request.url).search
|
|
2756
3073
|
),
|
|
2757
|
-
workspaceRelayForwardRequestInit(request, authorized.
|
|
3074
|
+
workspaceRelayForwardRequestInit(request, authorized.relayHostToken, workspaceId, {
|
|
2758
3075
|
signal: controller.signal
|
|
2759
3076
|
})
|
|
2760
3077
|
));
|
|
@@ -2799,9 +3116,23 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2799
3116
|
return json("request_body_too_large", "Host tunnel request body exceeds the relay limit", 413);
|
|
2800
3117
|
}
|
|
2801
3118
|
const body = bodyBytes ? bytesToBase64(bodyBytes) : void 0;
|
|
3119
|
+
const forwardTimeoutMs = options.forwardTimeoutMs ?? 3e4;
|
|
2802
3120
|
const pending = new Promise((resolve, reject) => {
|
|
2803
3121
|
const timeout = setTimeout(() => {
|
|
2804
3122
|
tunnel.pending.delete(requestId);
|
|
3123
|
+
rememberTimedOut(tunnel.timedOut, requestId, clock());
|
|
3124
|
+
relayLog({
|
|
3125
|
+
event: "tunnel_timeout",
|
|
3126
|
+
hostId: tunnel.hostId,
|
|
3127
|
+
...trace ? { traceId: trace.id } : {},
|
|
3128
|
+
requestId,
|
|
3129
|
+
method: request.method,
|
|
3130
|
+
path: authorized.path,
|
|
3131
|
+
bodyBytes: bodyBytes ? bodyBytes.byteLength : 0,
|
|
3132
|
+
timeoutMs: forwardTimeoutMs,
|
|
3133
|
+
tunnelAgeMs: clock() - tunnel.connectedAt,
|
|
3134
|
+
pending: tunnel.pending.size
|
|
3135
|
+
});
|
|
2805
3136
|
tunnel.socket.send?.(JSON.stringify({
|
|
2806
3137
|
type: "http.response.flow",
|
|
2807
3138
|
protocol: TUNNEL_PROTOCOL_VERSION,
|
|
@@ -2810,7 +3141,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2810
3141
|
reason: "closed"
|
|
2811
3142
|
}));
|
|
2812
3143
|
reject(hostTunnelFailure("host_tunnel_timeout"));
|
|
2813
|
-
},
|
|
3144
|
+
}, forwardTimeoutMs);
|
|
2814
3145
|
tunnel.pending.set(requestId, {
|
|
2815
3146
|
chunks: [],
|
|
2816
3147
|
bytes: 0,
|
|
@@ -2894,7 +3225,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2894
3225
|
try {
|
|
2895
3226
|
await runHibernatedRevocationCheck();
|
|
2896
3227
|
} catch (err) {
|
|
2897
|
-
|
|
3228
|
+
options.report(err, "revocation_alarm");
|
|
2898
3229
|
}
|
|
2899
3230
|
},
|
|
2900
3231
|
webSocketClose(socket, code, reason) {
|
|
@@ -2903,17 +3234,17 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2903
3234
|
const attachment = socketAttachment(socket);
|
|
2904
3235
|
if (!attachment) return;
|
|
2905
3236
|
if (attachment.kind === "host-tunnel") {
|
|
2906
|
-
cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected");
|
|
3237
|
+
cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected", { closeCode: code, closeReason: reason });
|
|
2907
3238
|
return;
|
|
2908
3239
|
}
|
|
2909
3240
|
cleanupHostTunnelClient(attachment.channelId, socket, { code, reason });
|
|
2910
3241
|
},
|
|
2911
|
-
webSocketError(socket) {
|
|
3242
|
+
webSocketError(socket, error) {
|
|
2912
3243
|
ensureHibernatedSocketsRebuilt();
|
|
2913
3244
|
const attachment = socketAttachment(socket);
|
|
2914
3245
|
if (!attachment) return;
|
|
2915
3246
|
if (attachment.kind === "host-tunnel") {
|
|
2916
|
-
cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected");
|
|
3247
|
+
cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected", { socketError: error instanceof Error ? error.message : typeof error === "string" ? error : "unknown" });
|
|
2917
3248
|
return;
|
|
2918
3249
|
}
|
|
2919
3250
|
cleanupHostTunnelClient(attachment.channelId, socket, { code: 1011, reason: "WebSocket error" });
|
|
@@ -2930,171 +3261,18 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2930
3261
|
drain: drainController
|
|
2931
3262
|
};
|
|
2932
3263
|
}
|
|
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
|
-
}
|
|
3086
3264
|
export {
|
|
3087
3265
|
DEFAULT_RELAY_APP_ORIGINS,
|
|
3088
3266
|
DEFAULT_RELAY_LOCATION_HINT,
|
|
3089
3267
|
RELAY_ALLOWED_REQUEST_HEADERS,
|
|
3090
3268
|
RELAY_LOCATION_HINTS,
|
|
3091
3269
|
RELAY_REGION_HEADER,
|
|
3270
|
+
REVOCATION_RESOLVER_UNAVAILABLE,
|
|
3092
3271
|
RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT,
|
|
3093
3272
|
RUNTIME_ACCESS_TOKEN_ISSUED_AT_FLOOR_SECONDS,
|
|
3094
3273
|
SESSION_HOST_LOCATION_HINT,
|
|
3095
3274
|
WorkspaceRelayAuthError,
|
|
3096
3275
|
authorizeWorkspaceRelayRequest,
|
|
3097
|
-
bytesToBase64,
|
|
3098
3276
|
checkHostTunnelGeneration,
|
|
3099
3277
|
corsPreflight,
|
|
3100
3278
|
createCachedHostGenerationClient,
|
|
@@ -3119,7 +3297,6 @@ export {
|
|
|
3119
3297
|
parseRuntimeAccessTokenActiveResult,
|
|
3120
3298
|
parseWorkspaceRelayTarget,
|
|
3121
3299
|
pruneExpiringCache,
|
|
3122
|
-
relayFrameDiagnostics,
|
|
3123
3300
|
relayHostInputFromRuntimeClaims,
|
|
3124
3301
|
relayHostTokenAudience,
|
|
3125
3302
|
relayHostTokenIssuer,
|