@claxedo/workspace-relay 0.10.2 → 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 +533 -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/package.json +3 -3
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" },
|
|
@@ -682,7 +693,7 @@ function hostTunnelIncumbentOutranks(incumbentGeneration, candidateGeneration) {
|
|
|
682
693
|
}
|
|
683
694
|
var RELAY_HOST_TOKEN_CACHE_MAX_ENTRIES = 4096;
|
|
684
695
|
var RUNTIME_ACCESS_TOKEN_CACHE_MAX_ENTRIES = 8192;
|
|
685
|
-
var RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS_DEFAULT =
|
|
696
|
+
var RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS_DEFAULT = 3e4;
|
|
686
697
|
var RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
|
|
687
698
|
var relayHostTokenCaches = /* @__PURE__ */ new WeakMap();
|
|
688
699
|
var runtimeAccessTokenCaches = /* @__PURE__ */ new WeakMap();
|
|
@@ -1089,6 +1100,246 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
|
|
|
1089
1100
|
}
|
|
1090
1101
|
}
|
|
1091
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
|
+
|
|
1092
1343
|
// src/directory.ts
|
|
1093
1344
|
function presenceKey(hostId, workspaceId) {
|
|
1094
1345
|
return `${hostId}\0${workspaceId}`;
|
|
@@ -1331,18 +1582,17 @@ async function relayRoomPlacement(request, input) {
|
|
|
1331
1582
|
const configured = trimToUndefined2(input.configured);
|
|
1332
1583
|
const region = relayRequestRegion(request);
|
|
1333
1584
|
const country = relayRequestCountry(request);
|
|
1334
|
-
const floor = relayLocationHint(configured ? { configured } : {});
|
|
1335
|
-
const declared = relayLocationHint({
|
|
1336
|
-
...configured ? { configured } : {},
|
|
1337
|
-
...region ? { region } : {},
|
|
1338
|
-
...country ? { country } : {}
|
|
1339
|
-
});
|
|
1340
1585
|
const headers = new Headers(request.headers);
|
|
1341
1586
|
headers.delete(RELAY_REGION_HEADER);
|
|
1342
1587
|
const credential = await input.credential();
|
|
1343
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
|
+
});
|
|
1344
1594
|
return {
|
|
1345
|
-
locationHint: sessionHost ? SESSION_HOST_LOCATION_HINT :
|
|
1595
|
+
locationHint: sessionHost ? SESSION_HOST_LOCATION_HINT : declared,
|
|
1346
1596
|
url: withoutQueryParams(request.url, RELAY_REGION_QUERY_PARAMS),
|
|
1347
1597
|
headers
|
|
1348
1598
|
};
|
|
@@ -1418,12 +1668,12 @@ async function relayPlacementCredential(request, env, url, workspaceId) {
|
|
|
1418
1668
|
const token2 = bearerToken(request.headers.get("authorization"));
|
|
1419
1669
|
if (!hostId || !token2) return void 0;
|
|
1420
1670
|
await verifyHostTunnelToken(token2, key, { hostId, workspaceIds: workspaceIdsFromSearch(url) });
|
|
1421
|
-
return { hostId };
|
|
1671
|
+
return { hostId, declaresRegion: true };
|
|
1422
1672
|
}
|
|
1423
1673
|
const token = runtimeAccessTokenFrom(request);
|
|
1424
1674
|
if (!token) return void 0;
|
|
1425
1675
|
const claims = await verifyRuntimeAccessToken(token, key, { workspaceId });
|
|
1426
|
-
return { hostId: claims.host_id };
|
|
1676
|
+
return { hostId: claims.host_id, declaresRegion: claims.routing_id !== void 0 };
|
|
1427
1677
|
} catch {
|
|
1428
1678
|
return void 0;
|
|
1429
1679
|
}
|
|
@@ -1435,6 +1685,136 @@ function workspaceRelayRoomPlacement(request, env, workspaceId) {
|
|
|
1435
1685
|
});
|
|
1436
1686
|
}
|
|
1437
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
|
+
|
|
1438
1818
|
// src/relay-trace.ts
|
|
1439
1819
|
import { trimToUndefined as trimToUndefined4 } from "@claxedo/helpers/string";
|
|
1440
1820
|
var TRACE_ID_HEADER = "x-claxedo-trace-id";
|
|
@@ -1451,12 +1831,15 @@ function traceRequest(request, routeKind, options) {
|
|
|
1451
1831
|
const forceSecret = trimToUndefined4(options.traceForceHeaderSecret);
|
|
1452
1832
|
const forced = !!forceSecret && request.headers.get(TRACE_FORCE_HEADER) === forceSecret;
|
|
1453
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;
|
|
1454
1836
|
return {
|
|
1455
1837
|
id,
|
|
1456
|
-
sampled
|
|
1838
|
+
sampled,
|
|
1457
1839
|
startedAt: performance.now(),
|
|
1458
1840
|
routeKind,
|
|
1459
|
-
phases: []
|
|
1841
|
+
phases: [],
|
|
1842
|
+
...roomColo ? { roomColo } : {}
|
|
1460
1843
|
};
|
|
1461
1844
|
}
|
|
1462
1845
|
async function traceSpan(trace, name, fn) {
|
|
@@ -1497,7 +1880,8 @@ function webSocketTraceFrame(upstreamStartedAt, queueTimes, openedAt) {
|
|
|
1497
1880
|
function withTraceHeaders(response, trace) {
|
|
1498
1881
|
const headers = new Headers(response.headers);
|
|
1499
1882
|
headers.set(TRACE_ID_HEADER, trace.id);
|
|
1500
|
-
const
|
|
1883
|
+
const colo = trace.roomColo ? `relay-room-colo;desc=${trace.roomColo}` : "";
|
|
1884
|
+
const timing = trace.sampled ? [serverTiming(trace.phases), colo].filter(Boolean).join(", ") : "";
|
|
1501
1885
|
if (timing) {
|
|
1502
1886
|
const existing = headers.get("server-timing");
|
|
1503
1887
|
headers.set("server-timing", existing ? `${existing}, ${timing}` : timing);
|
|
@@ -1509,10 +1893,11 @@ function withTraceHeaders(response, trace) {
|
|
|
1509
1893
|
headers
|
|
1510
1894
|
});
|
|
1511
1895
|
}
|
|
1896
|
+
var SLOW_REQUEST_MS = 5e3;
|
|
1512
1897
|
function logTrace(options, request, workspaceId, trace, response) {
|
|
1513
|
-
if (!trace.sampled) return;
|
|
1514
|
-
const url = new URL(request.url);
|
|
1515
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);
|
|
1516
1901
|
const event = {
|
|
1517
1902
|
event: "workspace_relay_request",
|
|
1518
1903
|
traceId: trace.id,
|
|
@@ -1528,181 +1913,58 @@ function logTrace(options, request, workspaceId, trace, response) {
|
|
|
1528
1913
|
options.traceLog(event);
|
|
1529
1914
|
return;
|
|
1530
1915
|
}
|
|
1531
|
-
console.log(JSON.stringify(event));
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// src/cloudflare.ts
|
|
1535
|
-
var TUNNEL_PENDING_HTTP_CAP_DEFAULT = 32;
|
|
1536
|
-
var TUNNEL_STARTED_STREAM_CAP_DEFAULT = 16;
|
|
1537
|
-
var TUNNEL_CHANNEL_CAP_DEFAULT = 16;
|
|
1538
|
-
var TUNNEL_REQUEST_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
|
|
1539
|
-
var TUNNEL_RESPONSE_BODY_MAX_BYTES_DEFAULT = 16 * 1024 * 1024;
|
|
1540
|
-
var SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT = 8 * 1024 * 1024;
|
|
1541
|
-
var SLOW_CONSUMER_TIMEOUT_MS_DEFAULT = 3e4;
|
|
1542
|
-
var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT = 8192;
|
|
1543
|
-
var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
|
|
1544
|
-
var UPSTREAM_WS_PRE_OPEN_QUEUE_FRAME_OVERHEAD_BYTES = 64;
|
|
1545
|
-
var WORKSPACE_TARGET_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
|
|
1546
|
-
var RESOLVER_OUTAGE_GRACE_ATTEMPTS_DEFAULT = 3;
|
|
1547
|
-
var HIBERNATED_REVOCATION_ALARM_INTERVAL_MS_DEFAULT = 3e4;
|
|
1548
|
-
var HOST_GENERATION_CHECK_INTERVAL_MS_DEFAULT = 3e4;
|
|
1549
|
-
function unrefTimer(timer) {
|
|
1550
|
-
if (typeof timer.unref === "function") timer.unref();
|
|
1551
|
-
}
|
|
1552
|
-
function isHostTunnelClient(client) {
|
|
1553
|
-
return "channelId" in client && typeof client.channelId === "string";
|
|
1554
|
-
}
|
|
1555
|
-
function workspaceIdFromRelayPath(pathname) {
|
|
1556
|
-
return /^\/workspaces\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
|
|
1557
|
-
}
|
|
1558
|
-
function websocketRequest(request) {
|
|
1559
|
-
return request.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
1560
|
-
}
|
|
1561
|
-
function relayWebSocketTraceEnabled(request) {
|
|
1562
|
-
return request.headers.get("x-claxedo-relay-ws-trace") === "1";
|
|
1563
|
-
}
|
|
1564
|
-
function randomToken() {
|
|
1565
|
-
return crypto.randomUUID();
|
|
1566
|
-
}
|
|
1567
|
-
function headerRecord(headers) {
|
|
1568
|
-
const result = /* @__PURE__ */ Object.create(null);
|
|
1569
|
-
headers.forEach((value, key) => {
|
|
1570
|
-
result[key] = value;
|
|
1571
|
-
});
|
|
1572
|
-
return result;
|
|
1573
|
-
}
|
|
1574
|
-
function headersFromRecord(input) {
|
|
1575
|
-
const headers = new Headers();
|
|
1576
|
-
for (const [key, value] of Object.entries(input)) headers.set(key, value);
|
|
1577
|
-
return headers;
|
|
1578
|
-
}
|
|
1579
|
-
var BASE64_CHUNK_BYTES = 32768;
|
|
1580
|
-
function bytesToBase64(bytes) {
|
|
1581
|
-
let binary = "";
|
|
1582
|
-
for (let i = 0; i < bytes.length; i += BASE64_CHUNK_BYTES) {
|
|
1583
|
-
binary += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK_BYTES));
|
|
1584
|
-
}
|
|
1585
|
-
return btoa(binary);
|
|
1586
|
-
}
|
|
1587
|
-
function base64ToBytes(input) {
|
|
1588
|
-
return Uint8Array.from(atob(input), (char) => char.charCodeAt(0));
|
|
1589
|
-
}
|
|
1590
|
-
function preOpenFrameBytes(input) {
|
|
1591
|
-
const payload = typeof input === "string" ? new TextEncoder().encode(input).byteLength : input.byteLength;
|
|
1592
|
-
return payload + UPSTREAM_WS_PRE_OPEN_QUEUE_FRAME_OVERHEAD_BYTES;
|
|
1593
|
-
}
|
|
1594
|
-
function isBlobLike(input) {
|
|
1595
|
-
if (!input || typeof input !== "object") return false;
|
|
1596
|
-
return "arrayBuffer" in input && typeof input.arrayBuffer === "function";
|
|
1597
|
-
}
|
|
1598
|
-
function viewBytes(input) {
|
|
1599
|
-
return new Uint8Array(input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength));
|
|
1600
|
-
}
|
|
1601
|
-
function socketFrame(input) {
|
|
1602
|
-
if (typeof input === "string") {
|
|
1603
|
-
return {
|
|
1604
|
-
binary: false,
|
|
1605
|
-
data_base64: bytesToBase64(new TextEncoder().encode(input))
|
|
1606
|
-
};
|
|
1607
|
-
}
|
|
1608
|
-
if (input instanceof ArrayBuffer) {
|
|
1609
|
-
return {
|
|
1610
|
-
binary: true,
|
|
1611
|
-
data_base64: bytesToBase64(new Uint8Array(input))
|
|
1612
|
-
};
|
|
1613
|
-
}
|
|
1614
|
-
if (input instanceof Uint8Array) {
|
|
1615
|
-
return {
|
|
1616
|
-
binary: true,
|
|
1617
|
-
data_base64: bytesToBase64(input)
|
|
1618
|
-
};
|
|
1619
|
-
}
|
|
1620
|
-
if (ArrayBuffer.isView(input)) {
|
|
1621
|
-
return {
|
|
1622
|
-
binary: true,
|
|
1623
|
-
data_base64: bytesToBase64(viewBytes(input))
|
|
1624
|
-
};
|
|
1625
|
-
}
|
|
1626
|
-
if (!isBlobLike(input)) return void 0;
|
|
1627
|
-
return input.arrayBuffer().then((buffer) => ({
|
|
1628
|
-
binary: true,
|
|
1629
|
-
data_base64: bytesToBase64(new Uint8Array(buffer))
|
|
1630
|
-
}));
|
|
1916
|
+
console.log(JSON.stringify(event));
|
|
1631
1917
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
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();
|
|
1639
1935
|
}
|
|
1640
|
-
function
|
|
1641
|
-
|
|
1642
|
-
return (data) => {
|
|
1643
|
-
const payload = socketPayload(data);
|
|
1644
|
-
if (payload === void 0) return;
|
|
1645
|
-
if (tail === void 0 && !(payload instanceof Promise)) {
|
|
1646
|
-
deliver(payload);
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
const queued = (tail ?? Promise.resolve()).then(async () => deliver(await payload)).catch(() => {
|
|
1650
|
-
});
|
|
1651
|
-
tail = queued;
|
|
1652
|
-
void queued.then(() => {
|
|
1653
|
-
if (tail === queued) tail = void 0;
|
|
1654
|
-
});
|
|
1655
|
-
};
|
|
1936
|
+
function isHostTunnelClient(client) {
|
|
1937
|
+
return "channelId" in client && typeof client.channelId === "string";
|
|
1656
1938
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
const payload = await socketPayload(data);
|
|
1660
|
-
return {
|
|
1661
|
-
// `undefined` here is exactly the silent drop: every caller treats it as
|
|
1662
|
-
// "nothing to forward" and returns without a throw or a log.
|
|
1663
|
-
frameDropped: frame === void 0,
|
|
1664
|
-
payloadDropped: payload === void 0,
|
|
1665
|
-
...frame ? { binary: frame.binary, dataBase64: frame.data_base64 } : {},
|
|
1666
|
-
payloadBase64: payload === void 0 ? void 0 : typeof payload === "string" ? bytesToBase64(new TextEncoder().encode(payload)) : bytesToBase64(payload instanceof ArrayBuffer ? new Uint8Array(payload) : payload)
|
|
1667
|
-
};
|
|
1939
|
+
function workspaceIdFromRelayPath(pathname) {
|
|
1940
|
+
return /^\/workspaces\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
|
|
1668
1941
|
}
|
|
1669
|
-
function
|
|
1670
|
-
|
|
1671
|
-
return input.binary ? bytes : new TextDecoder().decode(bytes);
|
|
1942
|
+
function websocketRequest(request) {
|
|
1943
|
+
return request.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
1672
1944
|
}
|
|
1673
|
-
function
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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
|
+
});
|
|
1681
1956
|
return result;
|
|
1682
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
|
+
}
|
|
1683
1963
|
function responseMustNotHaveBody(status) {
|
|
1684
1964
|
return status === 204 || status === 205 || status === 304;
|
|
1685
1965
|
}
|
|
1686
|
-
async function readRequestBodyCapped(request, maxBytes) {
|
|
1687
|
-
if (!request.body) return new Uint8Array(0);
|
|
1688
|
-
const reader = request.body.getReader();
|
|
1689
|
-
const chunks = [];
|
|
1690
|
-
let total = 0;
|
|
1691
|
-
while (true) {
|
|
1692
|
-
const { done, value } = await reader.read();
|
|
1693
|
-
if (done) break;
|
|
1694
|
-
total += value.byteLength;
|
|
1695
|
-
if (total > maxBytes) {
|
|
1696
|
-
await reader.cancel().catch(() => {
|
|
1697
|
-
});
|
|
1698
|
-
return "too_large";
|
|
1699
|
-
}
|
|
1700
|
-
chunks.push(value);
|
|
1701
|
-
}
|
|
1702
|
-
return concatBytes(chunks);
|
|
1703
|
-
}
|
|
1704
1966
|
function workspaceRelayDurableObjectRoomName(workspaceId) {
|
|
1705
|
-
return `
|
|
1967
|
+
return `relay-room:${workspaceId}`;
|
|
1706
1968
|
}
|
|
1707
1969
|
function workspaceRelayDurableObjectWorkspaceId(request) {
|
|
1708
1970
|
const url = new URL(request.url);
|
|
@@ -1873,7 +2135,22 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
1873
2135
|
return false;
|
|
1874
2136
|
}
|
|
1875
2137
|
};
|
|
1876
|
-
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;
|
|
1877
2154
|
if (tunnel.generationWatcher) clearInterval(tunnel.generationWatcher);
|
|
1878
2155
|
tunnel.generationWatcher = void 0;
|
|
1879
2156
|
for (const pending of tunnel.pending.values()) {
|
|
@@ -1919,6 +2196,8 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
1919
2196
|
generationCheckFailures: attachment.generationCheckFailures ?? 0,
|
|
1920
2197
|
socket,
|
|
1921
2198
|
pending: /* @__PURE__ */ new Map(),
|
|
2199
|
+
timedOut: /* @__PURE__ */ new Map(),
|
|
2200
|
+
droppedFrames: 0,
|
|
1922
2201
|
channels: /* @__PURE__ */ new Map()
|
|
1923
2202
|
});
|
|
1924
2203
|
directory.registerHostTunnel({
|
|
@@ -1964,7 +2243,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
1964
2243
|
}
|
|
1965
2244
|
};
|
|
1966
2245
|
const resolverGraceAttempts = options.resolverOutageGraceAttempts ?? RESOLVER_OUTAGE_GRACE_ATTEMPTS_DEFAULT;
|
|
1967
|
-
const resolverUnreachable = (code) => code ===
|
|
2246
|
+
const resolverUnreachable = (code) => code === REVOCATION_RESOLVER_UNAVAILABLE;
|
|
1968
2247
|
const watchRuntimeAccessToken = (client, onInactive) => {
|
|
1969
2248
|
const intervalMs = options.runtimeAccessTokenActiveCheckIntervalMs ?? RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT;
|
|
1970
2249
|
if (intervalMs <= 0) return void 0;
|
|
@@ -2256,8 +2535,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2256
2535
|
}));
|
|
2257
2536
|
};
|
|
2258
2537
|
const reportFrameHandlerFailure = (socket, err) => {
|
|
2259
|
-
|
|
2260
|
-
console.error(`[workspace-relay] frame handler failed: ${reason}`);
|
|
2538
|
+
options.report(err, "frame_handler");
|
|
2261
2539
|
closeSocket(socket, 1011, "Workspace relay failed to process a frame");
|
|
2262
2540
|
};
|
|
2263
2541
|
const hibernatedRevocationIntervalMs = () => options.hibernatedRevocationCheckIntervalMs ?? HIBERNATED_REVOCATION_ALARM_INTERVAL_MS_DEFAULT;
|
|
@@ -2371,10 +2649,23 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2371
2649
|
};
|
|
2372
2650
|
const handleTunnelMessage = async (hostId, socket, event) => {
|
|
2373
2651
|
const row = parseTunnelMessageData(event.data);
|
|
2374
|
-
|
|
2375
|
-
const parsed = validateTunnelMessage(row.value);
|
|
2652
|
+
const parsed = row.ok ? validateTunnelMessage(row.value) : { ok: false, reason: "not_json" };
|
|
2376
2653
|
const tunnel = hostTunnels.get(hostId);
|
|
2377
|
-
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
|
+
}
|
|
2378
2669
|
directory.recordPong(tunnel.hostId);
|
|
2379
2670
|
if (parsed.message.type === "ping") {
|
|
2380
2671
|
tunnel.socket.send?.(JSON.stringify(makeTunnelPong(parsed.message)));
|
|
@@ -2393,7 +2684,13 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2393
2684
|
if (parsed.message.type === "http.response.start") {
|
|
2394
2685
|
const message = parsed.message;
|
|
2395
2686
|
const pending = tunnel.pending.get(message.request_id);
|
|
2396
|
-
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
|
+
}
|
|
2397
2694
|
pending.status = message.status;
|
|
2398
2695
|
pending.headers = headersFromRecord(message.headers);
|
|
2399
2696
|
if (responseMustNotHaveBody(pending.status)) return;
|
|
@@ -2483,10 +2780,10 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2483
2780
|
return { ok: false };
|
|
2484
2781
|
}
|
|
2485
2782
|
};
|
|
2486
|
-
const cleanupHostTunnel = (hostId, socket, reason) => {
|
|
2783
|
+
const cleanupHostTunnel = (hostId, socket, reason, detail) => {
|
|
2487
2784
|
const tunnel = hostTunnels.get(hostId);
|
|
2488
2785
|
if (tunnel?.socket !== socket) return;
|
|
2489
|
-
cleanupTunnelResources(tunnel, reason);
|
|
2786
|
+
cleanupTunnelResources(tunnel, reason, detail);
|
|
2490
2787
|
hostTunnels.delete(hostId);
|
|
2491
2788
|
directory.disconnectHost(hostId);
|
|
2492
2789
|
};
|
|
@@ -2507,29 +2804,33 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2507
2804
|
}
|
|
2508
2805
|
};
|
|
2509
2806
|
const admitHostTunnel = async (request, roomWorkspaceId, url) => {
|
|
2510
|
-
|
|
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);
|
|
2511
2812
|
const hostId = trimToUndefined5(hostIdFromTunnelPath(url.pathname));
|
|
2512
|
-
if (!hostId) return
|
|
2813
|
+
if (!hostId) return refused("host_tunnel_host_required", "Host tunnel id is required", 400);
|
|
2513
2814
|
const workspaceIds = workspaceIdsFromSearch(url);
|
|
2514
2815
|
if (!workspaceIds.includes(roomWorkspaceId)) {
|
|
2515
|
-
return
|
|
2816
|
+
return refused("host_tunnel_workspace_mismatch", "Host tunnel must include the room workspace id", 403);
|
|
2516
2817
|
}
|
|
2517
2818
|
const token = bearerToken(request.headers.get("authorization"));
|
|
2518
|
-
if (!token) return
|
|
2819
|
+
if (!token) return refused("host_tunnel_token_required", "Host Tunnel Token is required", 401);
|
|
2519
2820
|
let claims;
|
|
2520
2821
|
try {
|
|
2521
2822
|
claims = await verifyHostTunnelToken(token, options.runtimeAccessKey, { hostId, workspaceIds });
|
|
2522
2823
|
} catch (err) {
|
|
2523
2824
|
if (err instanceof WorkspaceRelayAuthError) {
|
|
2524
|
-
return
|
|
2825
|
+
return refused(err.code, "Host Tunnel Token was denied", 403);
|
|
2525
2826
|
}
|
|
2526
2827
|
throw err;
|
|
2527
2828
|
}
|
|
2528
2829
|
const decision = await checkHostTunnelGeneration(options.resolveHostGeneration, claims);
|
|
2529
|
-
if (!decision.ok) return
|
|
2830
|
+
if (!decision.ok) return refused(decision.code, decision.reason, decision.retryable ? 503 : 403);
|
|
2530
2831
|
const previous = hostTunnels.get(hostId);
|
|
2531
2832
|
if (previous && hostTunnelIncumbentOutranks(previous.generation, claims.generation)) {
|
|
2532
|
-
return
|
|
2833
|
+
return refused("host_generation_superseded", "Host tunnel generation was superseded", 403);
|
|
2533
2834
|
}
|
|
2534
2835
|
const connectedAt = options.now?.() ?? Date.now();
|
|
2535
2836
|
const fence = {
|
|
@@ -2545,12 +2846,15 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2545
2846
|
generationCheckFailures: 0,
|
|
2546
2847
|
socket: pair.server,
|
|
2547
2848
|
pending: /* @__PURE__ */ new Map(),
|
|
2849
|
+
timedOut: /* @__PURE__ */ new Map(),
|
|
2850
|
+
droppedFrames: 0,
|
|
2548
2851
|
channels: /* @__PURE__ */ new Map()
|
|
2549
2852
|
};
|
|
2550
2853
|
if (previous) {
|
|
2551
2854
|
cleanupTunnelResources(previous, "Host tunnel replaced");
|
|
2552
2855
|
closeSocket(previous.socket, 1012, "Host tunnel replaced");
|
|
2553
2856
|
}
|
|
2857
|
+
relayLog({ event: "tunnel_admitted", hostId, workspaceIds, generation: claims.generation, ...previous ? { replacedAgeMs: connectedAt - previous.connectedAt } : {} });
|
|
2554
2858
|
hostTunnels.set(hostId, tunnel);
|
|
2555
2859
|
directory.registerHostTunnel({ hostId, workspaceIds });
|
|
2556
2860
|
watchHostGeneration(tunnel);
|
|
@@ -2560,7 +2864,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2560
2864
|
if (!event || event.data === void 0) return;
|
|
2561
2865
|
void handleTunnelMessage(hostId, pair.server, { data: event.data }).catch((err) => reportFrameHandlerFailure(pair.server, err));
|
|
2562
2866
|
});
|
|
2563
|
-
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 }));
|
|
2564
2868
|
pair.server.addEventListener?.("error", () => cleanupHostTunnel(hostId, pair.server, "Host tunnel disconnected"));
|
|
2565
2869
|
}
|
|
2566
2870
|
return upgradeResponse(pair.client, request);
|
|
@@ -2744,13 +3048,18 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2744
3048
|
})
|
|
2745
3049
|
);
|
|
2746
3050
|
if (!authorized.ok) return authorized.response;
|
|
2747
|
-
|
|
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)) {
|
|
2748
3057
|
trace && (trace.routeKind = "host-tunnel-http");
|
|
2749
|
-
return forwardHostTunnelHttp(request, authorized
|
|
3058
|
+
return forwardHostTunnelHttp(request, authorized, trace);
|
|
2750
3059
|
}
|
|
2751
|
-
if (isSessionHostTarget(authorized.
|
|
3060
|
+
if (isSessionHostTarget(authorized.target)) {
|
|
2752
3061
|
trace && (trace.routeKind = "session-host-http");
|
|
2753
|
-
return traceSpan(trace, "session-host-fetch", () => forwardSessionHostHttp(options.sessionHost, request, authorized
|
|
3062
|
+
return traceSpan(trace, "session-host-fetch", () => forwardSessionHostHttp(options.sessionHost, request, authorized));
|
|
2754
3063
|
}
|
|
2755
3064
|
trace && (trace.routeKind = "cloud-http");
|
|
2756
3065
|
const controller = new AbortController();
|
|
@@ -2758,11 +3067,11 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2758
3067
|
try {
|
|
2759
3068
|
const upstream = await traceSpan(trace, "upstream-fetch", () => (options.fetch ?? fetch)(
|
|
2760
3069
|
workspaceRelayTargetUrl(
|
|
2761
|
-
authorized.
|
|
2762
|
-
authorized.
|
|
3070
|
+
authorized.target,
|
|
3071
|
+
authorized.path,
|
|
2763
3072
|
new URL(request.url).search
|
|
2764
3073
|
),
|
|
2765
|
-
workspaceRelayForwardRequestInit(request, authorized.
|
|
3074
|
+
workspaceRelayForwardRequestInit(request, authorized.relayHostToken, workspaceId, {
|
|
2766
3075
|
signal: controller.signal
|
|
2767
3076
|
})
|
|
2768
3077
|
));
|
|
@@ -2807,9 +3116,23 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2807
3116
|
return json("request_body_too_large", "Host tunnel request body exceeds the relay limit", 413);
|
|
2808
3117
|
}
|
|
2809
3118
|
const body = bodyBytes ? bytesToBase64(bodyBytes) : void 0;
|
|
3119
|
+
const forwardTimeoutMs = options.forwardTimeoutMs ?? 3e4;
|
|
2810
3120
|
const pending = new Promise((resolve, reject) => {
|
|
2811
3121
|
const timeout = setTimeout(() => {
|
|
2812
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
|
+
});
|
|
2813
3136
|
tunnel.socket.send?.(JSON.stringify({
|
|
2814
3137
|
type: "http.response.flow",
|
|
2815
3138
|
protocol: TUNNEL_PROTOCOL_VERSION,
|
|
@@ -2818,7 +3141,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2818
3141
|
reason: "closed"
|
|
2819
3142
|
}));
|
|
2820
3143
|
reject(hostTunnelFailure("host_tunnel_timeout"));
|
|
2821
|
-
},
|
|
3144
|
+
}, forwardTimeoutMs);
|
|
2822
3145
|
tunnel.pending.set(requestId, {
|
|
2823
3146
|
chunks: [],
|
|
2824
3147
|
bytes: 0,
|
|
@@ -2902,7 +3225,7 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2902
3225
|
try {
|
|
2903
3226
|
await runHibernatedRevocationCheck();
|
|
2904
3227
|
} catch (err) {
|
|
2905
|
-
|
|
3228
|
+
options.report(err, "revocation_alarm");
|
|
2906
3229
|
}
|
|
2907
3230
|
},
|
|
2908
3231
|
webSocketClose(socket, code, reason) {
|
|
@@ -2911,17 +3234,17 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2911
3234
|
const attachment = socketAttachment(socket);
|
|
2912
3235
|
if (!attachment) return;
|
|
2913
3236
|
if (attachment.kind === "host-tunnel") {
|
|
2914
|
-
cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected");
|
|
3237
|
+
cleanupHostTunnel(attachment.hostId, socket, "Host tunnel disconnected", { closeCode: code, closeReason: reason });
|
|
2915
3238
|
return;
|
|
2916
3239
|
}
|
|
2917
3240
|
cleanupHostTunnelClient(attachment.channelId, socket, { code, reason });
|
|
2918
3241
|
},
|
|
2919
|
-
webSocketError(socket) {
|
|
3242
|
+
webSocketError(socket, error) {
|
|
2920
3243
|
ensureHibernatedSocketsRebuilt();
|
|
2921
3244
|
const attachment = socketAttachment(socket);
|
|
2922
3245
|
if (!attachment) return;
|
|
2923
3246
|
if (attachment.kind === "host-tunnel") {
|
|
2924
|
-
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" });
|
|
2925
3248
|
return;
|
|
2926
3249
|
}
|
|
2927
3250
|
cleanupHostTunnelClient(attachment.channelId, socket, { code: 1011, reason: "WebSocket error" });
|
|
@@ -2938,171 +3261,18 @@ function createWorkspaceRelayDurableObjectRoom(options) {
|
|
|
2938
3261
|
drain: drainController
|
|
2939
3262
|
};
|
|
2940
3263
|
}
|
|
2941
|
-
|
|
2942
|
-
// ../claxedo-helpers/dist/deadline.mjs
|
|
2943
|
-
async function withDeadlineSignal(timeoutMs, work, parent) {
|
|
2944
|
-
const deadline = new AbortController();
|
|
2945
|
-
const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
|
|
2946
|
-
try {
|
|
2947
|
-
return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
|
|
2948
|
-
} finally {
|
|
2949
|
-
clearTimeout(timer);
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
|
-
// src/resolver-clients.ts
|
|
2954
|
-
function parseRuntimeAccessTokenActiveResult(input) {
|
|
2955
|
-
if (!isRecord(input)) return void 0;
|
|
2956
|
-
const row = input;
|
|
2957
|
-
if (row.active === true) return { active: true };
|
|
2958
|
-
if (row.active !== false) return void 0;
|
|
2959
|
-
if (typeof row.code !== "string" || typeof row.reason !== "string") return void 0;
|
|
2960
|
-
return { active: false, code: row.code, reason: row.reason };
|
|
2961
|
-
}
|
|
2962
|
-
function parseHostGenerationResult(input) {
|
|
2963
|
-
if (!isRecord(input)) return void 0;
|
|
2964
|
-
const row = input;
|
|
2965
|
-
if (typeof row.enrollmentId !== "string" || !row.enrollmentId.trim()) return void 0;
|
|
2966
|
-
if (!isHostGeneration(row.generation) || typeof row.revoked !== "boolean") return void 0;
|
|
2967
|
-
return { enrollmentId: row.enrollmentId, generation: row.generation, revoked: row.revoked };
|
|
2968
|
-
}
|
|
2969
|
-
var HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT = 5e3;
|
|
2970
|
-
var HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE = "relay_resolver_enrollment_not_found";
|
|
2971
|
-
function createHostGenerationResolverLookup(url, options) {
|
|
2972
|
-
const fetcher = options.fetch ?? ((target, init) => fetch(target, init));
|
|
2973
|
-
const timeoutMs = options.timeoutMs ?? HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT;
|
|
2974
|
-
return async ({ enrollmentId }) => {
|
|
2975
|
-
const target = new URL(url);
|
|
2976
|
-
target.searchParams.set("enrollmentId", enrollmentId);
|
|
2977
|
-
return withDeadlineSignal(timeoutMs, async (signal) => {
|
|
2978
|
-
const res = await fetcher(target, { headers: options.headers, signal });
|
|
2979
|
-
if (res.status === 404) {
|
|
2980
|
-
const body = await res.json().catch(() => void 0);
|
|
2981
|
-
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
2982
|
-
if (error?.code === HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE) return void 0;
|
|
2983
|
-
throw new Error(`relay host-generation resolver failed: 404 (no host-generation route at ${url})`);
|
|
2984
|
-
}
|
|
2985
|
-
if (!res.ok) throw new Error(`relay host-generation resolver failed: ${res.status} ${await res.text()}`);
|
|
2986
|
-
const result = parseHostGenerationResult(await res.json());
|
|
2987
|
-
if (!result) throw new Error("relay host-generation resolver returned a malformed result");
|
|
2988
|
-
return result;
|
|
2989
|
-
});
|
|
2990
|
-
};
|
|
2991
|
-
}
|
|
2992
|
-
var RESOLVER_CACHE_MAX_ENTRIES = 8192;
|
|
2993
|
-
var REVOCATION_CACHE_TTL_MS_DEFAULT = 1e4;
|
|
2994
|
-
var TARGET_CACHE_TTL_MS_DEFAULT = 1e4;
|
|
2995
|
-
var DURABLE_OBJECT_TARGET_CACHE_TTL_MS_DEFAULT = 30 * 6e4;
|
|
2996
|
-
function pruneExpiringCache(cache, now, maxEntries) {
|
|
2997
|
-
for (const [key, entry] of cache) {
|
|
2998
|
-
if (entry.expiresAt <= now) cache.delete(key);
|
|
2999
|
-
}
|
|
3000
|
-
while (cache.size >= maxEntries) {
|
|
3001
|
-
const key = cache.keys().next().value;
|
|
3002
|
-
if (!key) return;
|
|
3003
|
-
cache.delete(key);
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
function createCachedRevocationClient(inner, options = {}) {
|
|
3007
|
-
const ttlMs = options.ttlMs ?? REVOCATION_CACHE_TTL_MS_DEFAULT;
|
|
3008
|
-
const now = options.now ?? Date.now;
|
|
3009
|
-
const cache = /* @__PURE__ */ new Map();
|
|
3010
|
-
return async (args) => {
|
|
3011
|
-
const at = now();
|
|
3012
|
-
const entry = cache.get(args.jti);
|
|
3013
|
-
if (entry && entry.expiresAt > at) {
|
|
3014
|
-
if (entry.result) return entry.result;
|
|
3015
|
-
if (entry.promise) return await entry.promise;
|
|
3016
|
-
}
|
|
3017
|
-
const promise = inner(args).then((result) => {
|
|
3018
|
-
pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
|
|
3019
|
-
cache.set(args.jti, { result, expiresAt: now() + ttlMs });
|
|
3020
|
-
return result;
|
|
3021
|
-
}).catch((err) => {
|
|
3022
|
-
cache.delete(args.jti);
|
|
3023
|
-
throw err;
|
|
3024
|
-
});
|
|
3025
|
-
cache.set(args.jti, { promise, expiresAt: at + ttlMs });
|
|
3026
|
-
return await promise;
|
|
3027
|
-
};
|
|
3028
|
-
}
|
|
3029
|
-
function runtimeAccessTokenRevocationDelayMs(input = {}) {
|
|
3030
|
-
const configuredTtl = input.revocationCacheTtlMs ?? REVOCATION_CACHE_TTL_MS_DEFAULT;
|
|
3031
|
-
const ttlMs = Number.isFinite(configuredTtl) && configuredTtl > 0 ? configuredTtl : 0;
|
|
3032
|
-
if (input.activeCheckIntervalMs === void 0) return ttlMs;
|
|
3033
|
-
if (!Number.isFinite(input.activeCheckIntervalMs) || input.activeCheckIntervalMs <= 0) {
|
|
3034
|
-
return Number.POSITIVE_INFINITY;
|
|
3035
|
-
}
|
|
3036
|
-
return ttlMs + input.activeCheckIntervalMs;
|
|
3037
|
-
}
|
|
3038
|
-
function createCachedHostGenerationClient(inner, options = {}) {
|
|
3039
|
-
const ttlMs = options.ttlMs ?? 1e4;
|
|
3040
|
-
const now = options.now ?? Date.now;
|
|
3041
|
-
const cache = /* @__PURE__ */ new Map();
|
|
3042
|
-
return async (args) => {
|
|
3043
|
-
for (; ; ) {
|
|
3044
|
-
const at = now();
|
|
3045
|
-
const entry = cache.get(args.enrollmentId);
|
|
3046
|
-
if (entry && entry.expiresAt > at) {
|
|
3047
|
-
if (entry.result && entry.result.generation >= args.generation) return entry.result;
|
|
3048
|
-
if (!entry.result && entry.promise) {
|
|
3049
|
-
const shared = await entry.promise;
|
|
3050
|
-
if (!shared || shared.generation >= args.generation) return shared;
|
|
3051
|
-
continue;
|
|
3052
|
-
}
|
|
3053
|
-
}
|
|
3054
|
-
const promise = inner(args).then((result) => {
|
|
3055
|
-
pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
|
|
3056
|
-
if (result) cache.set(args.enrollmentId, { result, expiresAt: now() + ttlMs });
|
|
3057
|
-
else cache.delete(args.enrollmentId);
|
|
3058
|
-
return result;
|
|
3059
|
-
}).catch((err) => {
|
|
3060
|
-
cache.delete(args.enrollmentId);
|
|
3061
|
-
throw err;
|
|
3062
|
-
});
|
|
3063
|
-
cache.set(args.enrollmentId, { promise, expiresAt: at + ttlMs });
|
|
3064
|
-
return await promise;
|
|
3065
|
-
}
|
|
3066
|
-
};
|
|
3067
|
-
}
|
|
3068
|
-
function createCachedTargetClient(inner, options = {}) {
|
|
3069
|
-
const ttlMs = options.ttlMs ?? TARGET_CACHE_TTL_MS_DEFAULT;
|
|
3070
|
-
const durableObjectTtlMs = options.durableObjectTtlMs ?? DURABLE_OBJECT_TARGET_CACHE_TTL_MS_DEFAULT;
|
|
3071
|
-
const now = options.now ?? Date.now;
|
|
3072
|
-
const cache = /* @__PURE__ */ new Map();
|
|
3073
|
-
return async (args) => {
|
|
3074
|
-
const key = `${args.workspaceId}\0${args.hostId}\0${args.routingId ?? ""}`;
|
|
3075
|
-
const at = now();
|
|
3076
|
-
const entry = cache.get(key);
|
|
3077
|
-
if (entry && entry.expiresAt > at) {
|
|
3078
|
-
if (entry.target) return entry.target;
|
|
3079
|
-
if (entry.promise) return await entry.promise;
|
|
3080
|
-
}
|
|
3081
|
-
const promise = inner(args).then((target) => {
|
|
3082
|
-
pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
|
|
3083
|
-
if (target) cache.set(key, { target, expiresAt: now() + (target.backing === "durable-object" ? durableObjectTtlMs : ttlMs) });
|
|
3084
|
-
else cache.delete(key);
|
|
3085
|
-
return target;
|
|
3086
|
-
}).catch((err) => {
|
|
3087
|
-
cache.delete(key);
|
|
3088
|
-
throw err;
|
|
3089
|
-
});
|
|
3090
|
-
cache.set(key, { promise, expiresAt: at + ttlMs });
|
|
3091
|
-
return await promise;
|
|
3092
|
-
};
|
|
3093
|
-
}
|
|
3094
3264
|
export {
|
|
3095
3265
|
DEFAULT_RELAY_APP_ORIGINS,
|
|
3096
3266
|
DEFAULT_RELAY_LOCATION_HINT,
|
|
3097
3267
|
RELAY_ALLOWED_REQUEST_HEADERS,
|
|
3098
3268
|
RELAY_LOCATION_HINTS,
|
|
3099
3269
|
RELAY_REGION_HEADER,
|
|
3270
|
+
REVOCATION_RESOLVER_UNAVAILABLE,
|
|
3100
3271
|
RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT,
|
|
3101
3272
|
RUNTIME_ACCESS_TOKEN_ISSUED_AT_FLOOR_SECONDS,
|
|
3102
3273
|
SESSION_HOST_LOCATION_HINT,
|
|
3103
3274
|
WorkspaceRelayAuthError,
|
|
3104
3275
|
authorizeWorkspaceRelayRequest,
|
|
3105
|
-
bytesToBase64,
|
|
3106
3276
|
checkHostTunnelGeneration,
|
|
3107
3277
|
corsPreflight,
|
|
3108
3278
|
createCachedHostGenerationClient,
|
|
@@ -3127,7 +3297,6 @@ export {
|
|
|
3127
3297
|
parseRuntimeAccessTokenActiveResult,
|
|
3128
3298
|
parseWorkspaceRelayTarget,
|
|
3129
3299
|
pruneExpiringCache,
|
|
3130
|
-
relayFrameDiagnostics,
|
|
3131
3300
|
relayHostInputFromRuntimeClaims,
|
|
3132
3301
|
relayHostTokenAudience,
|
|
3133
3302
|
relayHostTokenIssuer,
|