@claxedo/workspace-relay 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -9
- package/dist/auth.d.ts +54 -3
- package/dist/bun.d.ts +90 -35
- package/dist/bun.mjs +2492 -0
- package/dist/cloudflare.d.ts +14 -1
- package/dist/directory.d.ts +17 -6
- package/dist/http.d.ts +2 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +625 -1455
- package/dist/main.d.ts +36 -27
- package/dist/main.mjs +765 -271
- package/dist/server.d.ts +139 -17
- package/dist/upstream-websocket.d.ts +34 -0
- package/dist/user-hosted-forwarding.d.ts +40 -0
- package/dist/worker.d.ts +30 -2
- package/dist/workerd-fixture/boot.d.ts +18 -0
- package/package.json +15 -7
package/dist/main.mjs
CHANGED
|
@@ -8935,10 +8935,10 @@ var require_abort = __commonJS({
|
|
|
8935
8935
|
"../../node_modules/.bun/asynckit@0.4.0/node_modules/asynckit/lib/abort.js"(exports, module) {
|
|
8936
8936
|
module.exports = abort;
|
|
8937
8937
|
function abort(state) {
|
|
8938
|
-
Object.keys(state.jobs).forEach(
|
|
8938
|
+
Object.keys(state.jobs).forEach(clean.bind(state));
|
|
8939
8939
|
state.jobs = {};
|
|
8940
8940
|
}
|
|
8941
|
-
function
|
|
8941
|
+
function clean(key) {
|
|
8942
8942
|
if (typeof this.jobs[key] == "function") {
|
|
8943
8943
|
this.jobs[key]();
|
|
8944
8944
|
}
|
|
@@ -16548,9 +16548,6 @@ var require_axios = __commonJS({
|
|
|
16548
16548
|
}
|
|
16549
16549
|
});
|
|
16550
16550
|
|
|
16551
|
-
// src/main.ts
|
|
16552
|
-
import { createHash } from "node:crypto";
|
|
16553
|
-
|
|
16554
16551
|
// ../../node_modules/.bun/posthog-node@4.18.0+302f8eed98f6f908/node_modules/posthog-node/lib/node/index.mjs
|
|
16555
16552
|
import { posix, dirname, sep } from "path";
|
|
16556
16553
|
import { createReadStream } from "node:fs";
|
|
@@ -20331,12 +20328,12 @@ var PostHog = class extends PostHogBackendClient {
|
|
|
20331
20328
|
// src/main.ts
|
|
20332
20329
|
import {
|
|
20333
20330
|
createRemoteJWKSet,
|
|
20334
|
-
exportJWK as exportJWK2,
|
|
20335
20331
|
exportPKCS8,
|
|
20336
20332
|
generateKeyPair,
|
|
20337
20333
|
importPKCS8,
|
|
20338
20334
|
importSPKI
|
|
20339
20335
|
} from "jose";
|
|
20336
|
+
import { trimToUndefined } from "@claxedo/helpers/string";
|
|
20340
20337
|
|
|
20341
20338
|
// src/bun.ts
|
|
20342
20339
|
import {
|
|
@@ -20349,10 +20346,24 @@ import {
|
|
|
20349
20346
|
// src/server.ts
|
|
20350
20347
|
import { Hono } from "hono";
|
|
20351
20348
|
import { cors } from "hono/cors";
|
|
20352
|
-
import { exportJWK } from "jose";
|
|
20349
|
+
import { exportJWK as exportJWK2 } from "jose";
|
|
20350
|
+
|
|
20351
|
+
// ../claxedo-helpers/dist/guards.mjs
|
|
20352
|
+
function isRecord(value) {
|
|
20353
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20354
|
+
}
|
|
20355
|
+
function isFiniteNumber(value) {
|
|
20356
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
20357
|
+
}
|
|
20358
|
+
function asFiniteNumber(value) {
|
|
20359
|
+
return isFiniteNumber(value) ? value : void 0;
|
|
20360
|
+
}
|
|
20361
|
+
function numberClaim(payload, key) {
|
|
20362
|
+
return asFiniteNumber(payload[key]);
|
|
20363
|
+
}
|
|
20353
20364
|
|
|
20354
20365
|
// src/auth.ts
|
|
20355
|
-
import { SignJWT, errors, jwtVerify } from "jose";
|
|
20366
|
+
import { SignJWT, errors, exportJWK, importJWK, jwtVerify } from "jose";
|
|
20356
20367
|
var algorithms = ["EdDSA", "ES256", "RS256"];
|
|
20357
20368
|
var runtimeAccessTokenIssuer = "claxedo-control-plane";
|
|
20358
20369
|
var runtimeAccessTokenAudience = "workspace-relay";
|
|
@@ -20372,25 +20383,51 @@ function seconds(input = Date.now()) {
|
|
|
20372
20383
|
return Math.floor(input / 1e3);
|
|
20373
20384
|
}
|
|
20374
20385
|
function requireAlgorithm(input) {
|
|
20375
|
-
|
|
20386
|
+
const algorithm = algorithms.find((candidate) => candidate === input);
|
|
20387
|
+
if (algorithm) return algorithm;
|
|
20376
20388
|
throw new WorkspaceRelayAuthError("invalid_relay_token", "Unsupported relay token algorithm");
|
|
20377
20389
|
}
|
|
20378
20390
|
function stringClaim(payload, key) {
|
|
20379
20391
|
const value = payload[key];
|
|
20380
20392
|
return typeof value === "string" && value.trim() ? value : void 0;
|
|
20381
20393
|
}
|
|
20382
|
-
function numberClaim(payload, key) {
|
|
20383
|
-
const value = payload[key];
|
|
20384
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
20385
|
-
}
|
|
20386
20394
|
function stringArrayClaim(payload, key) {
|
|
20387
20395
|
const value = payload[key];
|
|
20388
20396
|
return Array.isArray(value) && value.every((item) => typeof item === "string" && item.trim()) ? value : void 0;
|
|
20389
20397
|
}
|
|
20398
|
+
function isHostGeneration(value) {
|
|
20399
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
20400
|
+
}
|
|
20401
|
+
function hostGenerationClaims(payload) {
|
|
20402
|
+
const enrollment_id = stringClaim(payload, "enrollment_id");
|
|
20403
|
+
const generation = payload.generation;
|
|
20404
|
+
if (generation === void 0) return enrollment_id ? { enrollment_id } : {};
|
|
20405
|
+
if (!isHostGeneration(generation) || !enrollment_id) return null;
|
|
20406
|
+
return { enrollment_id, generation };
|
|
20407
|
+
}
|
|
20390
20408
|
function roleClaim(payload) {
|
|
20391
20409
|
const value = stringClaim(payload, "role");
|
|
20392
20410
|
return value === "viewer" || value === "editor" || value === "admin" || value === "owner" ? value : void 0;
|
|
20393
20411
|
}
|
|
20412
|
+
function actorProfileClaims(payload) {
|
|
20413
|
+
const actor_public_id = stringClaim(payload, "actor_public_id");
|
|
20414
|
+
const actor_name = stringClaim(payload, "actor_name");
|
|
20415
|
+
const actor_avatar_url = stringClaim(payload, "actor_avatar_url");
|
|
20416
|
+
if (!actor_public_id && !actor_name && !actor_avatar_url) return {};
|
|
20417
|
+
if (!actor_public_id || !actor_name) return void 0;
|
|
20418
|
+
return { actor_public_id, actor_name, ...actor_avatar_url ? { actor_avatar_url } : {} };
|
|
20419
|
+
}
|
|
20420
|
+
function actorProfilePayload(input) {
|
|
20421
|
+
if (!input.actorPublicId && !input.actorName && !input.actorAvatarUrl) return {};
|
|
20422
|
+
if (!input.actorPublicId || !input.actorName) {
|
|
20423
|
+
throw new WorkspaceRelayAuthError("relay_token_claims_invalid", "Actor display profile claims are incomplete");
|
|
20424
|
+
}
|
|
20425
|
+
return {
|
|
20426
|
+
actor_public_id: input.actorPublicId,
|
|
20427
|
+
actor_name: input.actorName,
|
|
20428
|
+
...input.actorAvatarUrl ? { actor_avatar_url: input.actorAvatarUrl } : {}
|
|
20429
|
+
};
|
|
20430
|
+
}
|
|
20394
20431
|
function isRelayClaimPair(input) {
|
|
20395
20432
|
return input.access === "cloud" && input.backing === "cloud-vm" || input.access === "user-hosted" && input.backing === "local-worktree";
|
|
20396
20433
|
}
|
|
@@ -20411,6 +20448,21 @@ function checkTarget(payload, expected) {
|
|
|
20411
20448
|
throw new WorkspaceRelayAuthError("relay_token_host_mismatch", "Relay token host does not match request");
|
|
20412
20449
|
}
|
|
20413
20450
|
}
|
|
20451
|
+
async function deriveRelayHostPublicKey(privateKey) {
|
|
20452
|
+
const jwk = await exportJWK(privateKey);
|
|
20453
|
+
const imported = await importJWK({ kty: jwk.kty, crv: jwk.crv, x: jwk.x }, "EdDSA", { extractable: true });
|
|
20454
|
+
if (imported instanceof Uint8Array) {
|
|
20455
|
+
throw new Error("Relay host public key imported as raw bytes; expected an EdDSA public key");
|
|
20456
|
+
}
|
|
20457
|
+
return imported;
|
|
20458
|
+
}
|
|
20459
|
+
async function deriveRelayHostKid(publicKey) {
|
|
20460
|
+
const jwk = await exportJWK(publicKey);
|
|
20461
|
+
const material = jwk.x ?? jwk.n ?? "";
|
|
20462
|
+
if (!material) throw new Error("Cannot derive kid: public key has no public component");
|
|
20463
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(material));
|
|
20464
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 16);
|
|
20465
|
+
}
|
|
20414
20466
|
async function verifyJwt(token, key, input) {
|
|
20415
20467
|
try {
|
|
20416
20468
|
if (typeof key === "function") {
|
|
@@ -20477,28 +20529,40 @@ async function mintRelayHostToken(input, key, alg) {
|
|
|
20477
20529
|
const protectedHeader = { alg: requireAlgorithm(alg) };
|
|
20478
20530
|
if (input.kid) protectedHeader.kid = input.kid;
|
|
20479
20531
|
return await new SignJWT({
|
|
20532
|
+
principal_kind: input.principalKind,
|
|
20533
|
+
actor_id: input.actorId,
|
|
20534
|
+
actor_kind: input.actorKind,
|
|
20535
|
+
...actorProfilePayload(input),
|
|
20480
20536
|
org_id: input.orgId,
|
|
20481
20537
|
workspace_id: input.workspaceId,
|
|
20482
20538
|
host_id: input.hostId,
|
|
20483
20539
|
role: input.role,
|
|
20484
20540
|
access: input.access,
|
|
20485
|
-
backing: input.backing
|
|
20486
|
-
|
|
20541
|
+
backing: input.backing,
|
|
20542
|
+
parent_jti: input.parentJti
|
|
20543
|
+
}).setProtectedHeader(protectedHeader).setIssuer(relayHostTokenIssuer).setAudience(relayHostTokenAudience).setIssuedAt(now).setExpirationTime(now + (input.ttlSeconds ?? 60)).setJti(input.jti ?? jti()).sign(key);
|
|
20487
20544
|
}
|
|
20488
20545
|
function runtimeClaims(payload) {
|
|
20489
20546
|
const exp = numberClaim(payload, "exp");
|
|
20490
20547
|
const iat = numberClaim(payload, "iat");
|
|
20491
|
-
const
|
|
20548
|
+
const principal_kind = stringClaim(payload, "principal_kind");
|
|
20549
|
+
const actor_id = stringClaim(payload, "actor_id");
|
|
20550
|
+
const actor_kind = stringClaim(payload, "actor_kind");
|
|
20492
20551
|
const jti2 = stringClaim(payload, "jti");
|
|
20493
20552
|
const org_id = stringClaim(payload, "org_id");
|
|
20494
20553
|
const workspace_id = stringClaim(payload, "workspace_id");
|
|
20495
20554
|
const host_id = stringClaim(payload, "host_id");
|
|
20496
20555
|
const role = roleClaim(payload);
|
|
20497
|
-
if (!exp || !iat || !
|
|
20556
|
+
if (!exp || !iat || !jti2 || !org_id || !workspace_id || !host_id || !role || !actor_id || principal_kind !== "user" && principal_kind !== "service" || actor_kind !== "human" && actor_kind !== "agent" || principal_kind === "user" && actor_kind !== "human" || principal_kind === "service" && actor_kind !== "agent") return void 0;
|
|
20557
|
+
const actorProfile = actorProfileClaims(payload);
|
|
20558
|
+
if (!actorProfile) return void 0;
|
|
20498
20559
|
return {
|
|
20499
20560
|
iss: runtimeAccessTokenIssuer,
|
|
20500
20561
|
aud: runtimeAccessTokenAudience,
|
|
20501
|
-
|
|
20562
|
+
principal_kind,
|
|
20563
|
+
actor_id,
|
|
20564
|
+
actor_kind,
|
|
20565
|
+
...actorProfile,
|
|
20502
20566
|
org_id,
|
|
20503
20567
|
workspace_id,
|
|
20504
20568
|
host_id,
|
|
@@ -20515,13 +20579,15 @@ function hostTunnelClaims(payload) {
|
|
|
20515
20579
|
const jti2 = stringClaim(payload, "jti");
|
|
20516
20580
|
const host_id = stringClaim(payload, "host_id");
|
|
20517
20581
|
const workspace_ids = stringArrayClaim(payload, "workspace_ids");
|
|
20518
|
-
|
|
20582
|
+
const fence = hostGenerationClaims(payload);
|
|
20583
|
+
if (!exp || !iat || !sub || !jti2 || !host_id || !workspace_ids?.length || fence === null) return void 0;
|
|
20519
20584
|
return {
|
|
20520
20585
|
iss: runtimeAccessTokenIssuer,
|
|
20521
20586
|
aud: hostTunnelTokenAudience,
|
|
20522
20587
|
sub,
|
|
20523
20588
|
host_id,
|
|
20524
20589
|
workspace_ids,
|
|
20590
|
+
...fence,
|
|
20525
20591
|
exp,
|
|
20526
20592
|
iat,
|
|
20527
20593
|
jti: jti2
|
|
@@ -20569,11 +20635,7 @@ function createOriginMatcher(patterns) {
|
|
|
20569
20635
|
}
|
|
20570
20636
|
|
|
20571
20637
|
// src/http.ts
|
|
20572
|
-
|
|
20573
|
-
if (!header) return;
|
|
20574
|
-
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
20575
|
-
return match?.[1]?.trim() || void 0;
|
|
20576
|
-
}
|
|
20638
|
+
import { bearerToken } from "@claxedo/helpers/string";
|
|
20577
20639
|
function errorBody(code, message) {
|
|
20578
20640
|
return {
|
|
20579
20641
|
error: {
|
|
@@ -20583,7 +20645,76 @@ function errorBody(code, message) {
|
|
|
20583
20645
|
};
|
|
20584
20646
|
}
|
|
20585
20647
|
|
|
20648
|
+
// src/user-hosted-forwarding.ts
|
|
20649
|
+
function isUserHostedTarget(target) {
|
|
20650
|
+
return target.access === "user-hosted";
|
|
20651
|
+
}
|
|
20652
|
+
|
|
20586
20653
|
// src/server.ts
|
|
20654
|
+
function parseWorkspaceRelayTarget(input) {
|
|
20655
|
+
if (!isRecord(input)) return void 0;
|
|
20656
|
+
const row = input;
|
|
20657
|
+
const { workspaceId, hostId, baseUrl, upstreamHeaders } = row;
|
|
20658
|
+
if (typeof workspaceId !== "string" || typeof hostId !== "string" || typeof baseUrl !== "string") return void 0;
|
|
20659
|
+
if (upstreamHeaders !== void 0 && !isStringRecord(upstreamHeaders)) return void 0;
|
|
20660
|
+
const pair = relayClaimPair(row.access, row.backing);
|
|
20661
|
+
if (!pair) return void 0;
|
|
20662
|
+
return {
|
|
20663
|
+
workspaceId,
|
|
20664
|
+
hostId,
|
|
20665
|
+
baseUrl,
|
|
20666
|
+
...upstreamHeaders ? { upstreamHeaders } : {},
|
|
20667
|
+
...pair
|
|
20668
|
+
};
|
|
20669
|
+
}
|
|
20670
|
+
function parseRuntimeAccessTokenActiveResult(input) {
|
|
20671
|
+
if (!isRecord(input)) return void 0;
|
|
20672
|
+
const row = input;
|
|
20673
|
+
if (row.active === true) return { active: true };
|
|
20674
|
+
if (row.active !== false) return void 0;
|
|
20675
|
+
if (typeof row.code !== "string" || typeof row.reason !== "string") return void 0;
|
|
20676
|
+
return { active: false, code: row.code, reason: row.reason };
|
|
20677
|
+
}
|
|
20678
|
+
function parseHostGenerationResult(input) {
|
|
20679
|
+
if (!isRecord(input)) return void 0;
|
|
20680
|
+
const row = input;
|
|
20681
|
+
if (typeof row.enrollmentId !== "string" || !row.enrollmentId.trim()) return void 0;
|
|
20682
|
+
if (!isHostGeneration(row.generation) || typeof row.revoked !== "boolean") return void 0;
|
|
20683
|
+
return { enrollmentId: row.enrollmentId, generation: row.generation, revoked: row.revoked };
|
|
20684
|
+
}
|
|
20685
|
+
var HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT = 5e3;
|
|
20686
|
+
var HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE = "relay_resolver_enrollment_not_found";
|
|
20687
|
+
function createHostGenerationResolverLookup(url, options) {
|
|
20688
|
+
const fetcher = options.fetch ?? ((target, init) => fetch(target, init));
|
|
20689
|
+
const timeoutMs = options.timeoutMs ?? HOST_GENERATION_LOOKUP_TIMEOUT_MS_DEFAULT;
|
|
20690
|
+
return async ({ enrollmentId }) => {
|
|
20691
|
+
const target = new URL(url);
|
|
20692
|
+
target.searchParams.set("enrollmentId", enrollmentId);
|
|
20693
|
+
const res = await fetcher(target, { headers: options.headers, signal: AbortSignal.timeout(timeoutMs) });
|
|
20694
|
+
if (res.status === 404) {
|
|
20695
|
+
const body = await res.json().catch(() => void 0);
|
|
20696
|
+
const error = isRecord(body) && isRecord(body.error) ? body.error : void 0;
|
|
20697
|
+
if (error?.code === HOST_GENERATION_ENROLLMENT_NOT_FOUND_CODE) return void 0;
|
|
20698
|
+
throw new Error(`relay host-generation resolver failed: 404 (no host-generation route at ${url})`);
|
|
20699
|
+
}
|
|
20700
|
+
if (!res.ok) throw new Error(`relay host-generation resolver failed: ${res.status} ${await res.text()}`);
|
|
20701
|
+
const result = parseHostGenerationResult(await res.json());
|
|
20702
|
+
if (!result) throw new Error("relay host-generation resolver returned a malformed result");
|
|
20703
|
+
return result;
|
|
20704
|
+
};
|
|
20705
|
+
}
|
|
20706
|
+
function hostTunnelIncumbentOutranks(incumbentGeneration, candidateGeneration) {
|
|
20707
|
+
if (incumbentGeneration === void 0) return false;
|
|
20708
|
+
return candidateGeneration === void 0 || incumbentGeneration > candidateGeneration;
|
|
20709
|
+
}
|
|
20710
|
+
function relayClaimPair(access, backing) {
|
|
20711
|
+
if (access === "cloud" && backing === "cloud-vm") return { access, backing };
|
|
20712
|
+
if (access === "user-hosted" && backing === "local-worktree") return { access, backing };
|
|
20713
|
+
return void 0;
|
|
20714
|
+
}
|
|
20715
|
+
function isStringRecord(input) {
|
|
20716
|
+
return isRecord(input) && Object.values(input).every((value) => typeof value === "string");
|
|
20717
|
+
}
|
|
20587
20718
|
var RELAY_HOST_TOKEN_CACHE_MAX_ENTRIES = 4096;
|
|
20588
20719
|
var RUNTIME_ACCESS_TOKEN_CACHE_MAX_ENTRIES = 8192;
|
|
20589
20720
|
var RESOLVER_CACHE_MAX_ENTRIES = 8192;
|
|
@@ -20623,6 +20754,71 @@ function createCachedRevocationClient(inner, options = {}) {
|
|
|
20623
20754
|
return await promise;
|
|
20624
20755
|
};
|
|
20625
20756
|
}
|
|
20757
|
+
function createCachedHostGenerationClient(inner, options = {}) {
|
|
20758
|
+
const ttlMs = options.ttlMs ?? 1e4;
|
|
20759
|
+
const now = options.now ?? Date.now;
|
|
20760
|
+
const cache = /* @__PURE__ */ new Map();
|
|
20761
|
+
return async (args) => {
|
|
20762
|
+
for (; ; ) {
|
|
20763
|
+
const at = now();
|
|
20764
|
+
const entry = cache.get(args.enrollmentId);
|
|
20765
|
+
if (entry && entry.expiresAt > at) {
|
|
20766
|
+
if (entry.result && entry.result.generation >= args.generation) return entry.result;
|
|
20767
|
+
if (!entry.result && entry.promise) {
|
|
20768
|
+
const shared = await entry.promise;
|
|
20769
|
+
if (!shared || shared.generation >= args.generation) return shared;
|
|
20770
|
+
continue;
|
|
20771
|
+
}
|
|
20772
|
+
}
|
|
20773
|
+
const promise = inner(args).then((result) => {
|
|
20774
|
+
pruneExpiringCache(cache, now(), RESOLVER_CACHE_MAX_ENTRIES);
|
|
20775
|
+
if (result) cache.set(args.enrollmentId, { result, expiresAt: now() + ttlMs });
|
|
20776
|
+
else cache.delete(args.enrollmentId);
|
|
20777
|
+
return result;
|
|
20778
|
+
}).catch((err) => {
|
|
20779
|
+
cache.delete(args.enrollmentId);
|
|
20780
|
+
throw err;
|
|
20781
|
+
});
|
|
20782
|
+
cache.set(args.enrollmentId, { promise, expiresAt: at + ttlMs });
|
|
20783
|
+
return await promise;
|
|
20784
|
+
}
|
|
20785
|
+
};
|
|
20786
|
+
}
|
|
20787
|
+
async function checkHostTunnelGeneration(lookup, claims) {
|
|
20788
|
+
if (claims.generation === void 0 || !claims.enrollment_id) return { ok: true };
|
|
20789
|
+
if (!lookup) {
|
|
20790
|
+
return {
|
|
20791
|
+
ok: false,
|
|
20792
|
+
retryable: false,
|
|
20793
|
+
code: "host_generation_unverifiable",
|
|
20794
|
+
reason: "Host tunnel generation cannot be verified by a relay without a host-generation resolver"
|
|
20795
|
+
};
|
|
20796
|
+
}
|
|
20797
|
+
let result;
|
|
20798
|
+
try {
|
|
20799
|
+
result = await lookup({ enrollmentId: claims.enrollment_id, generation: claims.generation });
|
|
20800
|
+
} catch (err) {
|
|
20801
|
+
return {
|
|
20802
|
+
ok: false,
|
|
20803
|
+
retryable: true,
|
|
20804
|
+
code: "host_generation_lookup_unavailable",
|
|
20805
|
+
reason: `Host generation lookup is unavailable: ${err instanceof Error ? err.message : String(err)}`
|
|
20806
|
+
};
|
|
20807
|
+
}
|
|
20808
|
+
if (!result) {
|
|
20809
|
+
return { ok: false, retryable: false, code: "host_enrollment_unknown", reason: "Host enrollment is unknown" };
|
|
20810
|
+
}
|
|
20811
|
+
if (result.revoked) {
|
|
20812
|
+
return { ok: false, retryable: false, code: "host_enrollment_revoked", reason: "Host enrollment was revoked" };
|
|
20813
|
+
}
|
|
20814
|
+
if (result.generation > claims.generation) {
|
|
20815
|
+
return { ok: false, retryable: false, code: "host_generation_superseded", reason: "Host tunnel generation was superseded" };
|
|
20816
|
+
}
|
|
20817
|
+
if (result.generation < claims.generation) {
|
|
20818
|
+
return { ok: false, retryable: false, code: "host_generation_unknown", reason: "Host tunnel generation is ahead of the control plane" };
|
|
20819
|
+
}
|
|
20820
|
+
return { ok: true };
|
|
20821
|
+
}
|
|
20626
20822
|
function createCachedTargetClient(inner, options = {}) {
|
|
20627
20823
|
const ttlMs = options.ttlMs ?? 5e3;
|
|
20628
20824
|
const now = options.now ?? Date.now;
|
|
@@ -20713,11 +20909,9 @@ function targetUrl(target, path, search) {
|
|
|
20713
20909
|
}
|
|
20714
20910
|
var RELAY_VIEWER_DENIED_PATH = /^\/api\/wr\/pty(?:\/|$)/;
|
|
20715
20911
|
function relayRuntimePath(request) {
|
|
20716
|
-
return new URL(request.url).pathname.replace(/^\/workspaces\/[^/]+\/?/, "/");
|
|
20717
|
-
}
|
|
20718
|
-
function relayAuthorizationPath(path) {
|
|
20719
20912
|
try {
|
|
20720
|
-
|
|
20913
|
+
const decoded2 = decodeURIComponent(new URL(request.url).pathname.replace(/^\/workspaces\/[^/]+\/?/, "/")).replace(/^\/+/, "/");
|
|
20914
|
+
return new URL(decoded2.replace(/^\/+/, ""), "http://relay.invalid/").pathname;
|
|
20721
20915
|
} catch {
|
|
20722
20916
|
return void 0;
|
|
20723
20917
|
}
|
|
@@ -20725,8 +20919,7 @@ function relayAuthorizationPath(path) {
|
|
|
20725
20919
|
function roleAllowsRelayRequest(role, method, path) {
|
|
20726
20920
|
if (role === "owner" || role === "admin" || role === "editor") return true;
|
|
20727
20921
|
if (role === "viewer") {
|
|
20728
|
-
|
|
20729
|
-
if (!authorizationPath || RELAY_VIEWER_DENIED_PATH.test(authorizationPath)) return false;
|
|
20922
|
+
if (RELAY_VIEWER_DENIED_PATH.test(path)) return false;
|
|
20730
20923
|
return method === "GET" || method === "HEAD" || method === "OPTIONS";
|
|
20731
20924
|
}
|
|
20732
20925
|
return false;
|
|
@@ -20740,7 +20933,9 @@ function relayHostTokenCacheTtlMs(options, claims) {
|
|
|
20740
20933
|
function relayHostTokenCacheKey(options, claims, target) {
|
|
20741
20934
|
return [
|
|
20742
20935
|
claims.jti,
|
|
20743
|
-
claims.
|
|
20936
|
+
claims.principal_kind,
|
|
20937
|
+
claims.actor_id,
|
|
20938
|
+
claims.actor_kind,
|
|
20744
20939
|
claims.org_id,
|
|
20745
20940
|
claims.role,
|
|
20746
20941
|
claims.workspace_id,
|
|
@@ -20845,7 +21040,15 @@ async function relayHostTokenFor(options, claims, target, trace) {
|
|
|
20845
21040
|
try {
|
|
20846
21041
|
const token = await trace.span("rht-mint", async () => {
|
|
20847
21042
|
promise = mintRelayHostToken({
|
|
20848
|
-
|
|
21043
|
+
principalKind: claims.principal_kind,
|
|
21044
|
+
actorId: claims.actor_id,
|
|
21045
|
+
actorKind: claims.actor_kind,
|
|
21046
|
+
parentJti: claims.jti,
|
|
21047
|
+
...claims.actor_public_id && claims.actor_name ? {
|
|
21048
|
+
actorPublicId: claims.actor_public_id,
|
|
21049
|
+
actorName: claims.actor_name,
|
|
21050
|
+
...claims.actor_avatar_url ? { actorAvatarUrl: claims.actor_avatar_url } : {}
|
|
21051
|
+
} : {},
|
|
20849
21052
|
orgId: claims.org_id,
|
|
20850
21053
|
role: claims.role,
|
|
20851
21054
|
...target,
|
|
@@ -20869,7 +21072,15 @@ async function uncachedRelayHostTokenFor(options, claims, target, trace) {
|
|
|
20869
21072
|
return await trace.span(
|
|
20870
21073
|
"rht-mint",
|
|
20871
21074
|
async () => await mintRelayHostToken({
|
|
20872
|
-
|
|
21075
|
+
principalKind: claims.principal_kind,
|
|
21076
|
+
actorId: claims.actor_id,
|
|
21077
|
+
actorKind: claims.actor_kind,
|
|
21078
|
+
parentJti: claims.jti,
|
|
21079
|
+
...claims.actor_public_id && claims.actor_name ? {
|
|
21080
|
+
actorPublicId: claims.actor_public_id,
|
|
21081
|
+
actorName: claims.actor_name,
|
|
21082
|
+
...claims.actor_avatar_url ? { actorAvatarUrl: claims.actor_avatar_url } : {}
|
|
21083
|
+
} : {},
|
|
20873
21084
|
orgId: claims.org_id,
|
|
20874
21085
|
role: claims.role,
|
|
20875
21086
|
...target,
|
|
@@ -21008,13 +21219,30 @@ function originMatcherFor(options) {
|
|
|
21008
21219
|
}
|
|
21009
21220
|
return matcher;
|
|
21010
21221
|
}
|
|
21222
|
+
var RELAY_ALLOWED_REQUEST_HEADER_LIST = [
|
|
21223
|
+
"Accept",
|
|
21224
|
+
"Authorization",
|
|
21225
|
+
"Content-Type",
|
|
21226
|
+
"Last-Event-ID",
|
|
21227
|
+
"Traceparent",
|
|
21228
|
+
"Tracestate",
|
|
21229
|
+
"X-Fetch-Bypass-Throttle",
|
|
21230
|
+
"X-Daytona-Skip-Preview-Warning",
|
|
21231
|
+
"X-Workspace-Id",
|
|
21232
|
+
"X-OpenCode-Directory",
|
|
21233
|
+
"X-Claxedo-Runner",
|
|
21234
|
+
"X-Claxedo-Model",
|
|
21235
|
+
"X-Claxedo-Draft-Id",
|
|
21236
|
+
"X-Claxedo-Binary"
|
|
21237
|
+
];
|
|
21238
|
+
var RELAY_ALLOWED_REQUEST_HEADERS = RELAY_ALLOWED_REQUEST_HEADER_LIST.join(", ");
|
|
21011
21239
|
function denyCorsHeaders(request, originAllowed = defaultOriginMatcher) {
|
|
21012
21240
|
const origin = request.headers.get("origin");
|
|
21013
21241
|
if (!origin) return void 0;
|
|
21014
21242
|
if (!originAllowed(origin)) return void 0;
|
|
21015
21243
|
return {
|
|
21016
21244
|
"access-control-allow-origin": origin,
|
|
21017
|
-
"access-control-allow-headers":
|
|
21245
|
+
"access-control-allow-headers": RELAY_ALLOWED_REQUEST_HEADERS,
|
|
21018
21246
|
"access-control-allow-methods": "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS"
|
|
21019
21247
|
};
|
|
21020
21248
|
}
|
|
@@ -21024,7 +21252,9 @@ async function deny(options, input) {
|
|
|
21024
21252
|
action: "relay.request.denied",
|
|
21025
21253
|
result: "deny",
|
|
21026
21254
|
reason: input.code,
|
|
21027
|
-
|
|
21255
|
+
actorId: input.claims?.actor_id,
|
|
21256
|
+
actorKind: input.claims?.actor_kind,
|
|
21257
|
+
principalKind: input.claims?.principal_kind,
|
|
21028
21258
|
...input.claims ? { role: input.claims.role } : {},
|
|
21029
21259
|
workspaceId: input.claims?.workspace_id,
|
|
21030
21260
|
hostId: input.claims?.host_id,
|
|
@@ -21073,7 +21303,7 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
|
|
|
21073
21303
|
};
|
|
21074
21304
|
}
|
|
21075
21305
|
const path = relayRuntimePath(request);
|
|
21076
|
-
if (!roleAllowsRelayRequest(claims.role, request.method, path)) {
|
|
21306
|
+
if (!path || !roleAllowsRelayRequest(claims.role, request.method, path)) {
|
|
21077
21307
|
return {
|
|
21078
21308
|
ok: false,
|
|
21079
21309
|
code: "relay_role_denied",
|
|
@@ -21100,7 +21330,7 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
|
|
|
21100
21330
|
})
|
|
21101
21331
|
};
|
|
21102
21332
|
}
|
|
21103
|
-
if (target
|
|
21333
|
+
if (isUserHostedTarget(target) && !options.directory?.activeHost({ hostId: target.hostId, workspaceId: target.workspaceId })) {
|
|
21104
21334
|
return {
|
|
21105
21335
|
ok: false,
|
|
21106
21336
|
code: "user_hosted_app_offline",
|
|
@@ -21116,7 +21346,9 @@ async function authorizeWorkspaceRelayRequest(options, request, workspaceId, tra
|
|
|
21116
21346
|
await span("audit", async () => await audit(options, {
|
|
21117
21347
|
action: "relay.request.accepted",
|
|
21118
21348
|
result: "allow",
|
|
21119
|
-
|
|
21349
|
+
actorId: claims.actor_id,
|
|
21350
|
+
actorKind: claims.actor_kind,
|
|
21351
|
+
principalKind: claims.principal_kind,
|
|
21120
21352
|
role: claims.role,
|
|
21121
21353
|
workspaceId: claims.workspace_id,
|
|
21122
21354
|
hostId: claims.host_id,
|
|
@@ -21199,7 +21431,7 @@ function isLoopbackAddress(address) {
|
|
|
21199
21431
|
if (!address) return false;
|
|
21200
21432
|
const trimmed = address.trim();
|
|
21201
21433
|
if (LOOPBACK_REMOTES.has(trimmed)) return true;
|
|
21202
|
-
if (
|
|
21434
|
+
if (trimmed.startsWith("127.")) return true;
|
|
21203
21435
|
if (/^::ffff:127\./i.test(trimmed)) return true;
|
|
21204
21436
|
return false;
|
|
21205
21437
|
}
|
|
@@ -21255,25 +21487,26 @@ function buildMetricsBody(options) {
|
|
|
21255
21487
|
return body;
|
|
21256
21488
|
}
|
|
21257
21489
|
function createWorkspaceRelay(options) {
|
|
21258
|
-
const app = new Hono()
|
|
21490
|
+
const app = Object.assign(new Hono(), {
|
|
21491
|
+
disposeAuditSampler: () => disposeAuditSampler(options)
|
|
21492
|
+
});
|
|
21259
21493
|
const originAllowed = originMatcherFor(options);
|
|
21494
|
+
app.use("*", async (c, next) => {
|
|
21495
|
+
await next();
|
|
21496
|
+
const origin = c.res.headers.get("access-control-allow-origin");
|
|
21497
|
+
if (origin && origin !== "*" && !c.res.headers.has("timing-allow-origin")) {
|
|
21498
|
+
c.res.headers.set("timing-allow-origin", origin);
|
|
21499
|
+
}
|
|
21500
|
+
});
|
|
21260
21501
|
app.use("*", cors({
|
|
21261
21502
|
origin: (origin) => {
|
|
21262
21503
|
if (!origin) return void 0;
|
|
21263
21504
|
return originAllowed(origin) ? origin : void 0;
|
|
21264
21505
|
},
|
|
21265
|
-
|
|
21266
|
-
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
"X-Daytona-Skip-Preview-Warning",
|
|
21270
|
-
"X-Workspace-Id",
|
|
21271
|
-
"X-OpenCode-Directory",
|
|
21272
|
-
"X-Claxedo-Runner",
|
|
21273
|
-
"X-Claxedo-Model",
|
|
21274
|
-
"X-Claxedo-Draft-Id",
|
|
21275
|
-
"X-Claxedo-Binary"
|
|
21276
|
-
],
|
|
21506
|
+
// Spread from the shared list, not hand-maintained here: a duplicate
|
|
21507
|
+
// array drifts out of sync and silently drops headers like
|
|
21508
|
+
// `Last-Event-ID` or `X-Fetch-Bypass-Throttle`.
|
|
21509
|
+
allowHeaders: [...RELAY_ALLOWED_REQUEST_HEADER_LIST],
|
|
21277
21510
|
allowMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
|
21278
21511
|
}));
|
|
21279
21512
|
app.get("/health", (c) => {
|
|
@@ -21301,7 +21534,7 @@ function createWorkspaceRelay(options) {
|
|
|
21301
21534
|
let keys;
|
|
21302
21535
|
try {
|
|
21303
21536
|
keys = await Promise.all(sources.map(async (source) => {
|
|
21304
|
-
const jwk = await
|
|
21537
|
+
const jwk = await exportJWK2(source.publicKey);
|
|
21305
21538
|
return {
|
|
21306
21539
|
...jwk,
|
|
21307
21540
|
kid: source.kid,
|
|
@@ -21324,7 +21557,6 @@ function createWorkspaceRelay(options) {
|
|
|
21324
21557
|
c.header("cache-control", "public, max-age=300");
|
|
21325
21558
|
return c.json({ keys });
|
|
21326
21559
|
});
|
|
21327
|
-
app.disposeAuditSampler = () => disposeAuditSampler(options);
|
|
21328
21560
|
app.get("/metrics", (c) => {
|
|
21329
21561
|
const authResult = checkMetricsAuth(options, c.req.raw);
|
|
21330
21562
|
if (!authResult.ok) {
|
|
@@ -21362,7 +21594,16 @@ function createWorkspaceRelay(options) {
|
|
|
21362
21594
|
return app;
|
|
21363
21595
|
}
|
|
21364
21596
|
|
|
21597
|
+
// src/upstream-websocket.ts
|
|
21598
|
+
function resolveUpstreamWebSocket(override) {
|
|
21599
|
+
return override ?? WebSocket;
|
|
21600
|
+
}
|
|
21601
|
+
|
|
21365
21602
|
// src/bun.ts
|
|
21603
|
+
var WORKSPACE_RELAY_IDLE_TIMEOUT_SECONDS = 45;
|
|
21604
|
+
var isRelayClientSocket = (ws) => ws.data.kind === "client";
|
|
21605
|
+
var isHostTunnelSocket = (ws) => ws.data.kind === "host-tunnel";
|
|
21606
|
+
var isUserHostedClientSocket = (ws) => ws.data.kind === "user-hosted-client";
|
|
21366
21607
|
var TUNNEL_PENDING_HTTP_CAP = 32;
|
|
21367
21608
|
var TUNNEL_CHANNEL_CAP = 16;
|
|
21368
21609
|
var HOST_TUNNEL_REGISTRATION_RECONNECT_CAP = 5;
|
|
@@ -21376,12 +21617,12 @@ var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_FRAMES_DEFAULT = 64;
|
|
|
21376
21617
|
var UPSTREAM_WS_PRE_OPEN_QUEUE_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
|
|
21377
21618
|
var WS_BUFFERED_AMOUNT_MAX_BYTES_DEFAULT = 8 * 1024 * 1024;
|
|
21378
21619
|
var relayBufferedBytes = (socket) => {
|
|
21379
|
-
|
|
21380
|
-
if (typeof
|
|
21381
|
-
const measured =
|
|
21382
|
-
|
|
21383
|
-
return void 0;
|
|
21620
|
+
if (typeof socket !== "object" || socket === null) return void 0;
|
|
21621
|
+
if ("getBufferedAmount" in socket && typeof socket.getBufferedAmount === "function") {
|
|
21622
|
+
const measured = socket.getBufferedAmount();
|
|
21623
|
+
return typeof measured === "number" && Number.isFinite(measured) ? measured : void 0;
|
|
21384
21624
|
}
|
|
21625
|
+
if (!("bufferedAmount" in socket)) return void 0;
|
|
21385
21626
|
const property = socket.bufferedAmount;
|
|
21386
21627
|
return typeof property === "number" && Number.isFinite(property) ? property : void 0;
|
|
21387
21628
|
};
|
|
@@ -21390,6 +21631,8 @@ var relayOverBackpressureLimit = (socket, limitBytes) => {
|
|
|
21390
21631
|
return queued !== void 0 && queued > limitBytes;
|
|
21391
21632
|
};
|
|
21392
21633
|
var HOST_TUNNEL_MAX_MISSED_PONGS_DEFAULT = 2;
|
|
21634
|
+
var HOST_GENERATION_CHECK_INTERVAL_MS_DEFAULT = 3e4;
|
|
21635
|
+
var HOST_GENERATION_OUTAGE_GRACE_ATTEMPTS_DEFAULT = 3;
|
|
21393
21636
|
var TUNNEL_MESSAGE_BUFFER_CAP_BYTES = 4 * 1024 * 1024;
|
|
21394
21637
|
var SLOW_CONSUMER_HIGH_WATER_MARK_BYTES_DEFAULT = 8 * 1024 * 1024;
|
|
21395
21638
|
var SLOW_CONSUMER_TIMEOUT_MS_DEFAULT = 3e4;
|
|
@@ -21460,8 +21703,8 @@ function jsonError(code, message, status) {
|
|
|
21460
21703
|
headers: { "content-type": "application/json" }
|
|
21461
21704
|
});
|
|
21462
21705
|
}
|
|
21463
|
-
function corsJsonError(request, code, message, status) {
|
|
21464
|
-
const headers2 = relayCorsHeaders(request);
|
|
21706
|
+
function corsJsonError(request, originAllowed, code, message, status) {
|
|
21707
|
+
const headers2 = relayCorsHeaders(request, originAllowed);
|
|
21465
21708
|
headers2.set("content-type", "application/json");
|
|
21466
21709
|
return new Response(JSON.stringify({ error: { code, message } }), { status, headers: headers2 });
|
|
21467
21710
|
}
|
|
@@ -21469,7 +21712,7 @@ function preOpenFrameBytes(message) {
|
|
|
21469
21712
|
return typeof message === "string" ? Buffer.byteLength(message) : message.byteLength;
|
|
21470
21713
|
}
|
|
21471
21714
|
function safeCloseCode(input, fallback = 1011) {
|
|
21472
|
-
if (!Number.isInteger(input)) return fallback;
|
|
21715
|
+
if (input === void 0 || !Number.isInteger(input)) return fallback;
|
|
21473
21716
|
const code = input;
|
|
21474
21717
|
if (code === 1e3) return code;
|
|
21475
21718
|
if (code >= 1001 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006) return code;
|
|
@@ -21498,6 +21741,18 @@ function workspaceIdFromPath(pathname) {
|
|
|
21498
21741
|
function hostIdFromTunnelPath(pathname) {
|
|
21499
21742
|
return /^\/host-tunnels\/([^/]+)(?:\/|$)/.exec(pathname)?.[1];
|
|
21500
21743
|
}
|
|
21744
|
+
function tunnelKey(hostId, workspaceId) {
|
|
21745
|
+
return `${hostId}\0${workspaceId}`;
|
|
21746
|
+
}
|
|
21747
|
+
function tunnelIdentity(hostId, workspaceIds) {
|
|
21748
|
+
return `${hostId}\0${[...new Set(workspaceIds)].sort().join(",")}`;
|
|
21749
|
+
}
|
|
21750
|
+
function ownedWorkspaceIds(hostTunnels, ws) {
|
|
21751
|
+
return ws.data.workspaceIds.filter((workspaceId) => hostTunnels.get(tunnelKey(ws.data.hostId, workspaceId)) === ws);
|
|
21752
|
+
}
|
|
21753
|
+
function outranks(incumbent, candidateGeneration) {
|
|
21754
|
+
return hostTunnelIncumbentOutranks(incumbent.data.generation, candidateGeneration);
|
|
21755
|
+
}
|
|
21501
21756
|
function websocketRequest(request) {
|
|
21502
21757
|
return request.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
21503
21758
|
}
|
|
@@ -21526,6 +21781,7 @@ function relayWebSocketPayload(input) {
|
|
|
21526
21781
|
copy.set(new Uint8Array(input.buffer, input.byteOffset, input.byteLength));
|
|
21527
21782
|
return copy;
|
|
21528
21783
|
}
|
|
21784
|
+
return void 0;
|
|
21529
21785
|
}
|
|
21530
21786
|
function headersRecord(headers2) {
|
|
21531
21787
|
const result = {};
|
|
@@ -21547,20 +21803,20 @@ function isEventStream(input) {
|
|
|
21547
21803
|
);
|
|
21548
21804
|
}
|
|
21549
21805
|
var defaultRelayOriginMatcher = createOriginMatcher(DEFAULT_RELAY_APP_ORIGINS);
|
|
21550
|
-
function allowedCorsOrigin(origin) {
|
|
21551
|
-
if (!origin) return;
|
|
21552
|
-
|
|
21806
|
+
function allowedCorsOrigin(origin, matcher) {
|
|
21807
|
+
if (!origin) return void 0;
|
|
21808
|
+
return matcher(origin) ? origin : void 0;
|
|
21553
21809
|
}
|
|
21554
|
-
function requireAllowedOrigin(request) {
|
|
21810
|
+
function requireAllowedOrigin(request, matcher) {
|
|
21555
21811
|
const origin = request.headers.get("origin");
|
|
21556
|
-
if (allowedCorsOrigin(origin)) return null;
|
|
21812
|
+
if (allowedCorsOrigin(origin, matcher)) return null;
|
|
21557
21813
|
return jsonError(
|
|
21558
21814
|
"origin_not_allowed",
|
|
21559
21815
|
"Origin is not in the allowlist",
|
|
21560
21816
|
403
|
|
21561
21817
|
);
|
|
21562
21818
|
}
|
|
21563
|
-
function relayCorsHeaders(request, input = new Headers()) {
|
|
21819
|
+
function relayCorsHeaders(request, originAllowed, input = new Headers()) {
|
|
21564
21820
|
const result = new Headers(input);
|
|
21565
21821
|
result.delete("access-control-allow-origin");
|
|
21566
21822
|
result.delete("access-control-allow-credentials");
|
|
@@ -21568,10 +21824,10 @@ function relayCorsHeaders(request, input = new Headers()) {
|
|
|
21568
21824
|
result.delete("access-control-allow-methods");
|
|
21569
21825
|
result.delete("access-control-expose-headers");
|
|
21570
21826
|
result.delete("access-control-max-age");
|
|
21571
|
-
const origin = allowedCorsOrigin(request.headers.get("origin"));
|
|
21827
|
+
const origin = allowedCorsOrigin(request.headers.get("origin"), originAllowed);
|
|
21572
21828
|
if (origin) {
|
|
21573
21829
|
result.set("access-control-allow-origin", origin);
|
|
21574
|
-
result.set("access-control-allow-headers",
|
|
21830
|
+
result.set("access-control-allow-headers", RELAY_ALLOWED_REQUEST_HEADERS);
|
|
21575
21831
|
result.set("access-control-allow-methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS");
|
|
21576
21832
|
}
|
|
21577
21833
|
return result;
|
|
@@ -21599,7 +21855,7 @@ function decodedFrame(frame) {
|
|
|
21599
21855
|
return frame.binary ? new Uint8Array(body) : body.toString("utf8");
|
|
21600
21856
|
}
|
|
21601
21857
|
function tunnelMessage(ws, input, stats) {
|
|
21602
|
-
if (typeof input !== "string") return;
|
|
21858
|
+
if (typeof input !== "string") return void 0;
|
|
21603
21859
|
const combined = ws.data.messageBuffer.length > 0 ? ws.data.messageBuffer + input : input;
|
|
21604
21860
|
let parsed;
|
|
21605
21861
|
try {
|
|
@@ -21609,11 +21865,11 @@ function tunnelMessage(ws, input, stats) {
|
|
|
21609
21865
|
stats.oversizedClosed += 1;
|
|
21610
21866
|
ws.data.messageBuffer = "";
|
|
21611
21867
|
ws.close(1009, "Tunnel message buffer exceeded 4 MB");
|
|
21612
|
-
return;
|
|
21868
|
+
return void 0;
|
|
21613
21869
|
}
|
|
21614
21870
|
stats.fragmentsBuffered += 1;
|
|
21615
21871
|
ws.data.messageBuffer = combined;
|
|
21616
|
-
return;
|
|
21872
|
+
return void 0;
|
|
21617
21873
|
}
|
|
21618
21874
|
ws.data.messageBuffer = "";
|
|
21619
21875
|
const validated = validateTunnelMessage(parsed);
|
|
@@ -21621,7 +21877,7 @@ function tunnelMessage(ws, input, stats) {
|
|
|
21621
21877
|
if (validated.reason === "protocol_mismatch") {
|
|
21622
21878
|
ws.close(1002, `Tunnel protocol mismatch: expected ${validated.expected_protocol}`);
|
|
21623
21879
|
}
|
|
21624
|
-
return;
|
|
21880
|
+
return void 0;
|
|
21625
21881
|
}
|
|
21626
21882
|
function sendTunnelPing(ws) {
|
|
21627
21883
|
if (ws.readyState !== WebSocket.OPEN) return;
|
|
@@ -21659,7 +21915,7 @@ function cleanupHostTunnelSocket(input) {
|
|
|
21659
21915
|
for (const pending of input.ws.data.pending.values()) {
|
|
21660
21916
|
failPendingHttpResponse({
|
|
21661
21917
|
entry: pending,
|
|
21662
|
-
response: input.request ? corsJsonError(input.request, "user_hosted_app_offline", "User-hosted workspace is offline", 503) : jsonError("user_hosted_app_offline", "User-hosted workspace is offline", 503),
|
|
21918
|
+
response: input.request ? corsJsonError(input.request, input.originAllowed, "user_hosted_app_offline", "User-hosted workspace is offline", 503) : jsonError("user_hosted_app_offline", "User-hosted workspace is offline", 503),
|
|
21663
21919
|
error: new Error("User-hosted tunnel disconnected")
|
|
21664
21920
|
});
|
|
21665
21921
|
}
|
|
@@ -21669,17 +21925,17 @@ function cleanupHostTunnelSocket(input) {
|
|
|
21669
21925
|
}
|
|
21670
21926
|
input.ws.data.channels.clear();
|
|
21671
21927
|
if (!input.disconnectDirectory) return;
|
|
21672
|
-
|
|
21673
|
-
|
|
21674
|
-
|
|
21675
|
-
|
|
21676
|
-
|
|
21677
|
-
|
|
21678
|
-
|
|
21679
|
-
|
|
21680
|
-
|
|
21681
|
-
|
|
21682
|
-
}
|
|
21928
|
+
const owned = ownedWorkspaceIds(input.hostTunnels, input.ws);
|
|
21929
|
+
if (owned.length === 0) return;
|
|
21930
|
+
for (const workspaceId of owned) input.hostTunnels.delete(tunnelKey(input.ws.data.hostId, workspaceId));
|
|
21931
|
+
input.options.directory?.disconnectHost(input.ws.data.hostId, owned);
|
|
21932
|
+
scheduleHostTunnelStateChange(input.hostTunnelStateDebounce, input.options, {
|
|
21933
|
+
hostId: input.ws.data.hostId,
|
|
21934
|
+
workspaceIds: input.ws.data.workspaceIds,
|
|
21935
|
+
path: `/host-tunnels/${input.ws.data.hostId}`,
|
|
21936
|
+
state: "disconnected",
|
|
21937
|
+
debounceMs: input.bunOptions.hostTunnelStateDebounceMs ?? HOST_TUNNEL_STATE_DEBOUNCE_MS_DEFAULT
|
|
21938
|
+
});
|
|
21683
21939
|
}
|
|
21684
21940
|
async function audit2(options, input) {
|
|
21685
21941
|
await options.audit?.({
|
|
@@ -21691,22 +21947,43 @@ async function audit2(options, input) {
|
|
|
21691
21947
|
path: input.path
|
|
21692
21948
|
});
|
|
21693
21949
|
}
|
|
21950
|
+
async function auditHostTunnelDenial(options, input) {
|
|
21951
|
+
await options.audit?.({
|
|
21952
|
+
action: "host_tunnel.denied",
|
|
21953
|
+
result: "deny",
|
|
21954
|
+
reason: input.code,
|
|
21955
|
+
hostId: input.hostId,
|
|
21956
|
+
...input.workspaceId ? { workspaceId: input.workspaceId } : {},
|
|
21957
|
+
method: "WEBSOCKET",
|
|
21958
|
+
path: `/host-tunnels/${input.hostId}`
|
|
21959
|
+
});
|
|
21960
|
+
}
|
|
21961
|
+
async function denyHostTunnel(options, input) {
|
|
21962
|
+
await auditHostTunnelDenial(options, input);
|
|
21963
|
+
return jsonError(input.code, input.message, input.status);
|
|
21964
|
+
}
|
|
21965
|
+
async function refuseHostTunnelSocket(options, ws, input) {
|
|
21966
|
+
await auditHostTunnelDenial(options, { code: input.code, hostId: ws.data.hostId, ...input.workspaceId ? { workspaceId: input.workspaceId } : {} });
|
|
21967
|
+
closeWebSocket(ws, input.close, input.reason, input.close);
|
|
21968
|
+
}
|
|
21694
21969
|
function scheduleHostTunnelStateChange(state, options, input) {
|
|
21695
|
-
const
|
|
21970
|
+
const identity = tunnelIdentity(input.hostId, input.workspaceIds);
|
|
21971
|
+
const workspaceId = input.workspaceIds[0];
|
|
21972
|
+
const existing = state.get(identity);
|
|
21696
21973
|
const entry = existing ?? {
|
|
21697
21974
|
intendedState: "disconnected",
|
|
21698
21975
|
lastWritten: "disconnected",
|
|
21699
21976
|
flushTimer: void 0,
|
|
21700
|
-
pending: { workspaceId
|
|
21977
|
+
pending: { workspaceId, path: input.path }
|
|
21701
21978
|
};
|
|
21702
21979
|
entry.intendedState = input.state;
|
|
21703
|
-
entry.pending = { workspaceId
|
|
21980
|
+
entry.pending = { workspaceId, path: input.path };
|
|
21704
21981
|
if (entry.flushTimer) clearTimeout(entry.flushTimer);
|
|
21705
21982
|
entry.flushTimer = setTimeout(() => {
|
|
21706
21983
|
entry.flushTimer = void 0;
|
|
21707
21984
|
if (entry.intendedState === entry.lastWritten) {
|
|
21708
21985
|
if (entry.intendedState === "disconnected") {
|
|
21709
|
-
state.delete(
|
|
21986
|
+
state.delete(identity);
|
|
21710
21987
|
}
|
|
21711
21988
|
return;
|
|
21712
21989
|
}
|
|
@@ -21718,7 +21995,7 @@ function scheduleHostTunnelStateChange(state, options, input) {
|
|
|
21718
21995
|
path: entry.pending.path
|
|
21719
21996
|
});
|
|
21720
21997
|
}, input.debounceMs);
|
|
21721
|
-
state.set(
|
|
21998
|
+
state.set(identity, entry);
|
|
21722
21999
|
}
|
|
21723
22000
|
function drainPendingChunks(entry) {
|
|
21724
22001
|
while (entry.pendingChunks.length > 0) {
|
|
@@ -21830,7 +22107,7 @@ async function tunnelHttpRequest(input) {
|
|
|
21830
22107
|
const error = new Error("User-hosted tunnel response timed out");
|
|
21831
22108
|
failPendingHttpResponse({
|
|
21832
22109
|
entry,
|
|
21833
|
-
response: corsJsonError(input.request, "user_hosted_tunnel_timeout", "User-hosted tunnel response timed out", 504),
|
|
22110
|
+
response: corsJsonError(input.request, input.originAllowed, "user_hosted_tunnel_timeout", "User-hosted tunnel response timed out", 504),
|
|
21834
22111
|
error
|
|
21835
22112
|
});
|
|
21836
22113
|
}, input.responseTimeoutMs);
|
|
@@ -21842,10 +22119,11 @@ async function tunnelHttpRequest(input) {
|
|
|
21842
22119
|
timeout,
|
|
21843
22120
|
pendingChunks: [],
|
|
21844
22121
|
bytesQueued: 0,
|
|
21845
|
-
responseStarted: false
|
|
22122
|
+
responseStarted: false,
|
|
22123
|
+
corsHeaders: (upstream) => relayCorsHeaders(input.request, input.originAllowed, upstream)
|
|
21846
22124
|
});
|
|
21847
22125
|
});
|
|
21848
|
-
const body = input.request.method === "GET" || input.request.method === "HEAD" ? void 0 : await readBoundedBody(input.request, input.requestBodyMaxBytes);
|
|
22126
|
+
const body = input.request.method === "GET" || input.request.method === "HEAD" ? void 0 : await readBoundedBody(input.request, input.originAllowed, input.requestBodyMaxBytes);
|
|
21849
22127
|
if (body && "response" in body) {
|
|
21850
22128
|
const entry = input.ws.data.pending.get(requestId);
|
|
21851
22129
|
if (entry) clearPendingTimers(entry);
|
|
@@ -21863,8 +22141,8 @@ async function tunnelHttpRequest(input) {
|
|
|
21863
22141
|
input.request.headers,
|
|
21864
22142
|
input.relayHostToken,
|
|
21865
22143
|
input.workspaceId,
|
|
21866
|
-
//
|
|
21867
|
-
//
|
|
22144
|
+
// User-hosted tunnel: strip Cookie so browser cookies never reach the
|
|
22145
|
+
// host process on the user's laptop.
|
|
21868
22146
|
{ userHosted: true }
|
|
21869
22147
|
)),
|
|
21870
22148
|
...body?.bodyBase64 ? { body_base64: body.bodyBase64 } : {},
|
|
@@ -21872,7 +22150,7 @@ async function tunnelHttpRequest(input) {
|
|
|
21872
22150
|
}));
|
|
21873
22151
|
return await pending;
|
|
21874
22152
|
}
|
|
21875
|
-
async function readBoundedBody(request, maxBytes) {
|
|
22153
|
+
async function readBoundedBody(request, originAllowed, maxBytes) {
|
|
21876
22154
|
if (!request.body) return void 0;
|
|
21877
22155
|
const reader = request.body.getReader();
|
|
21878
22156
|
const chunks = [];
|
|
@@ -21887,7 +22165,7 @@ async function readBoundedBody(request, maxBytes) {
|
|
|
21887
22165
|
} catch {
|
|
21888
22166
|
}
|
|
21889
22167
|
return {
|
|
21890
|
-
response: corsJsonError(request, "request_body_too_large", "Tunnel request body exceeds the relay limit", 413)
|
|
22168
|
+
response: corsJsonError(request, originAllowed, "request_body_too_large", "Tunnel request body exceeds the relay limit", 413)
|
|
21891
22169
|
};
|
|
21892
22170
|
}
|
|
21893
22171
|
chunks.push(next.value);
|
|
@@ -21909,8 +22187,8 @@ async function directHttpRequest(input) {
|
|
|
21909
22187
|
input.relayHostToken,
|
|
21910
22188
|
input.workspaceId,
|
|
21911
22189
|
{
|
|
21912
|
-
//
|
|
21913
|
-
//
|
|
22190
|
+
// Cloud-vm direct path: cookies pass through — workspace dashboards in
|
|
22191
|
+
// the VM may legitimately need session cookies.
|
|
21914
22192
|
userHosted: false,
|
|
21915
22193
|
upstreamHeaders: input.upstreamHeaders,
|
|
21916
22194
|
signal: controller.signal
|
|
@@ -21923,10 +22201,21 @@ async function directHttpRequest(input) {
|
|
|
21923
22201
|
const release = limiter ? await span("direct-http-queue", () => limiter.acquire()) : void 0;
|
|
21924
22202
|
try {
|
|
21925
22203
|
const upstream = await span("upstream-fetch", async () => await fetch(input.targetUrl, init));
|
|
21926
|
-
|
|
22204
|
+
const headers2 = relayCorsHeaders(input.request, input.originAllowed, upstream.headers);
|
|
22205
|
+
const contentType = upstream.headers.get("content-type") ?? "";
|
|
22206
|
+
const streamResponse = contentType.includes("text/event-stream") || contentType.includes("application/octet-stream");
|
|
22207
|
+
if (streamResponse) {
|
|
22208
|
+
return new Response(upstream.body, {
|
|
22209
|
+
status: upstream.status,
|
|
22210
|
+
statusText: upstream.statusText,
|
|
22211
|
+
headers: headers2
|
|
22212
|
+
});
|
|
22213
|
+
}
|
|
22214
|
+
const body = await upstream.arrayBuffer();
|
|
22215
|
+
return new Response(body, {
|
|
21927
22216
|
status: upstream.status,
|
|
21928
22217
|
statusText: upstream.statusText,
|
|
21929
|
-
headers:
|
|
22218
|
+
headers: headers2
|
|
21930
22219
|
});
|
|
21931
22220
|
} finally {
|
|
21932
22221
|
release?.();
|
|
@@ -21935,6 +22224,7 @@ async function directHttpRequest(input) {
|
|
|
21935
22224
|
const aborted = err instanceof Error && err.name === "AbortError";
|
|
21936
22225
|
return corsJsonError(
|
|
21937
22226
|
input.request,
|
|
22227
|
+
input.originAllowed,
|
|
21938
22228
|
aborted ? "upstream_timeout" : "upstream_unavailable",
|
|
21939
22229
|
aborted ? "Workspace upstream timed out" : "Workspace upstream is unavailable",
|
|
21940
22230
|
aborted ? 504 : 503
|
|
@@ -21943,23 +22233,82 @@ async function directHttpRequest(input) {
|
|
|
21943
22233
|
clearTimeout(timer);
|
|
21944
22234
|
}
|
|
21945
22235
|
}
|
|
21946
|
-
function hostTunnel(hostTunnels, hostId) {
|
|
21947
|
-
const tunnel = hostTunnels.get(hostId);
|
|
22236
|
+
function hostTunnel(hostTunnels, hostId, workspaceId) {
|
|
22237
|
+
const tunnel = hostTunnels.get(tunnelKey(hostId, workspaceId));
|
|
21948
22238
|
if (tunnel?.readyState === WebSocket.OPEN) return tunnel;
|
|
21949
22239
|
return void 0;
|
|
21950
22240
|
}
|
|
21951
22241
|
async function authorizeHostTunnel(options, bunOptions, request, input) {
|
|
21952
|
-
if (bunOptions.authorizeHostTunnel)
|
|
22242
|
+
if (bunOptions.authorizeHostTunnel) {
|
|
22243
|
+
return { authorized: await bunOptions.authorizeHostTunnel(request, input) };
|
|
22244
|
+
}
|
|
21953
22245
|
const token = bearerToken(request.headers.get("authorization"));
|
|
21954
|
-
if (!token) return false;
|
|
22246
|
+
if (!token) return { authorized: false };
|
|
21955
22247
|
try {
|
|
21956
|
-
await verifyHostTunnelToken(token, options.runtimeAccessKey, input);
|
|
21957
|
-
return true;
|
|
22248
|
+
return { authorized: true, claims: await verifyHostTunnelToken(token, options.runtimeAccessKey, input) };
|
|
21958
22249
|
} catch (err) {
|
|
21959
|
-
if (err instanceof WorkspaceRelayAuthError) return false;
|
|
22250
|
+
if (err instanceof WorkspaceRelayAuthError) return { authorized: false };
|
|
21960
22251
|
throw err;
|
|
21961
22252
|
}
|
|
21962
22253
|
}
|
|
22254
|
+
function clearHostGenerationWatcher(data) {
|
|
22255
|
+
if (data.generationCheckTimer) clearInterval(data.generationCheckTimer);
|
|
22256
|
+
data.generationCheckTimer = void 0;
|
|
22257
|
+
}
|
|
22258
|
+
function watchHostGeneration(ws, options, bunOptions) {
|
|
22259
|
+
const intervalMs = bunOptions.hostGenerationCheckIntervalMs ?? HOST_GENERATION_CHECK_INTERVAL_MS_DEFAULT;
|
|
22260
|
+
if (ws.data.generationCheckTimer || !options.resolveHostGeneration || ws.data.generation === void 0 || intervalMs <= 0) return;
|
|
22261
|
+
const graceAttempts = bunOptions.hostGenerationOutageGraceAttempts ?? HOST_GENERATION_OUTAGE_GRACE_ATTEMPTS_DEFAULT;
|
|
22262
|
+
ws.data.generationCheckTimer = setInterval(() => {
|
|
22263
|
+
void checkHostTunnelGeneration(options.resolveHostGeneration, {
|
|
22264
|
+
enrollment_id: ws.data.enrollmentId,
|
|
22265
|
+
generation: ws.data.generation
|
|
22266
|
+
}).then((decision) => {
|
|
22267
|
+
if (decision.ok) {
|
|
22268
|
+
ws.data.generationCheckFailures = 0;
|
|
22269
|
+
return;
|
|
22270
|
+
}
|
|
22271
|
+
if (!decision.retryable) {
|
|
22272
|
+
clearHostGenerationWatcher(ws.data);
|
|
22273
|
+
closeWebSocket(ws, 1008, decision.reason, 1008);
|
|
22274
|
+
return;
|
|
22275
|
+
}
|
|
22276
|
+
ws.data.generationCheckFailures += 1;
|
|
22277
|
+
if (ws.data.generationCheckFailures < graceAttempts) return;
|
|
22278
|
+
clearHostGenerationWatcher(ws.data);
|
|
22279
|
+
closeWebSocket(ws, 1012, "Host generation check unavailable", 1012);
|
|
22280
|
+
});
|
|
22281
|
+
}, intervalMs);
|
|
22282
|
+
if (typeof ws.data.generationCheckTimer.unref === "function") ws.data.generationCheckTimer.unref();
|
|
22283
|
+
}
|
|
22284
|
+
var RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT = 3e4;
|
|
22285
|
+
function clearClientAccessWatchers(data) {
|
|
22286
|
+
if (data.accessCheckTimer) clearInterval(data.accessCheckTimer);
|
|
22287
|
+
if (data.expiryTimer) clearTimeout(data.expiryTimer);
|
|
22288
|
+
data.accessCheckTimer = void 0;
|
|
22289
|
+
data.expiryTimer = void 0;
|
|
22290
|
+
}
|
|
22291
|
+
function watchClientAccess(ws, options, bunOptions) {
|
|
22292
|
+
const now = bunOptions.now ?? Date.now;
|
|
22293
|
+
const close = (reason) => {
|
|
22294
|
+
clearClientAccessWatchers(ws.data);
|
|
22295
|
+
closeWebSocket(ws, 1008, reason, 1008);
|
|
22296
|
+
};
|
|
22297
|
+
ws.data.expiryTimer = setTimeout(
|
|
22298
|
+
() => close("Runtime Access Token expired"),
|
|
22299
|
+
Math.max(0, ws.data.claims.exp * 1e3 - now())
|
|
22300
|
+
);
|
|
22301
|
+
if (typeof ws.data.expiryTimer.unref === "function") ws.data.expiryTimer.unref();
|
|
22302
|
+
const intervalMs = bunOptions.runtimeAccessTokenActiveCheckIntervalMs ?? RUNTIME_ACCESS_TOKEN_ACTIVE_CHECK_INTERVAL_MS_DEFAULT;
|
|
22303
|
+
if (!options.isRuntimeAccessTokenActive || intervalMs <= 0) return;
|
|
22304
|
+
ws.data.accessCheckTimer = setInterval(() => {
|
|
22305
|
+
void Promise.resolve(options.isRuntimeAccessTokenActive(ws.data.claims)).then((active) => {
|
|
22306
|
+
if (!active.active) close(active.reason);
|
|
22307
|
+
}).catch(() => {
|
|
22308
|
+
});
|
|
22309
|
+
}, intervalMs);
|
|
22310
|
+
if (typeof ws.data.accessCheckTimer.unref === "function") ws.data.accessCheckTimer.unref();
|
|
22311
|
+
}
|
|
21963
22312
|
function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
21964
22313
|
const hostTunnels = /* @__PURE__ */ new Map();
|
|
21965
22314
|
const relayClients = /* @__PURE__ */ new Set();
|
|
@@ -21968,6 +22317,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
21968
22317
|
const hostTunnelStateDebounce = /* @__PURE__ */ new Map();
|
|
21969
22318
|
const fragmentationStats = createFragmentationStats();
|
|
21970
22319
|
const slowConsumerStats = createSlowConsumerStats();
|
|
22320
|
+
const relayOriginMatcher = options.allowedOrigins ? createOriginMatcher(options.allowedOrigins) : defaultRelayOriginMatcher;
|
|
21971
22321
|
const telemetry = {
|
|
21972
22322
|
getFragmentationStats: () => readFragmentationStats(fragmentationStats),
|
|
21973
22323
|
resetFragmentationStats: () => resetFragmentationStats(fragmentationStats),
|
|
@@ -21980,7 +22330,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
21980
22330
|
setDraining: (value) => {
|
|
21981
22331
|
draining = value;
|
|
21982
22332
|
if (!value) return;
|
|
21983
|
-
for (const tunnel of hostTunnels.values()) {
|
|
22333
|
+
for (const tunnel of new Set(hostTunnels.values())) {
|
|
21984
22334
|
closeWebSocket(tunnel, 1012, "Workspace relay is draining", 1012);
|
|
21985
22335
|
}
|
|
21986
22336
|
for (const client of relayClients) {
|
|
@@ -21989,7 +22339,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
21989
22339
|
},
|
|
21990
22340
|
pendingCount: () => {
|
|
21991
22341
|
let total = 0;
|
|
21992
|
-
for (const tunnel of hostTunnels.values()) {
|
|
22342
|
+
for (const tunnel of new Set(hostTunnels.values())) {
|
|
21993
22343
|
total += tunnel.data.pending.size;
|
|
21994
22344
|
}
|
|
21995
22345
|
return total;
|
|
@@ -22028,6 +22378,90 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22028
22378
|
}
|
|
22029
22379
|
return tracker;
|
|
22030
22380
|
}
|
|
22381
|
+
function claimTunnelIdentities(ws, workspaceIds) {
|
|
22382
|
+
const hostId = ws.data.hostId;
|
|
22383
|
+
const displaced = /* @__PURE__ */ new Set();
|
|
22384
|
+
for (const workspaceId of workspaceIds) {
|
|
22385
|
+
const previous = hostTunnels.get(tunnelKey(hostId, workspaceId));
|
|
22386
|
+
if (!previous || previous === ws) continue;
|
|
22387
|
+
if (outranks(previous, ws.data.generation)) return workspaceId;
|
|
22388
|
+
displaced.add(previous);
|
|
22389
|
+
}
|
|
22390
|
+
for (const workspaceId of workspaceIds) hostTunnels.set(tunnelKey(hostId, workspaceId), ws);
|
|
22391
|
+
for (const previous of displaced) {
|
|
22392
|
+
if (ownedWorkspaceIds(hostTunnels, previous).length > 0) continue;
|
|
22393
|
+
cleanupHostTunnelSocket({
|
|
22394
|
+
ws: previous,
|
|
22395
|
+
hostTunnels,
|
|
22396
|
+
hostTunnelStateDebounce,
|
|
22397
|
+
options,
|
|
22398
|
+
bunOptions,
|
|
22399
|
+
originAllowed: relayOriginMatcher,
|
|
22400
|
+
disconnectDirectory: false,
|
|
22401
|
+
closeChannels: true
|
|
22402
|
+
});
|
|
22403
|
+
clearHostGenerationWatcher(previous.data);
|
|
22404
|
+
closeWebSocket(previous, 1012, "Host tunnel replaced by a newer connection", 1012);
|
|
22405
|
+
}
|
|
22406
|
+
return void 0;
|
|
22407
|
+
}
|
|
22408
|
+
async function applyRegistrationUpdate(hostSocket, workspaceIds, token) {
|
|
22409
|
+
const hostId = hostSocket.data.hostId;
|
|
22410
|
+
const authorizationRequest = new Request(
|
|
22411
|
+
`http://relay.local/host-tunnels/${encodeURIComponent(hostId)}`,
|
|
22412
|
+
{ headers: { authorization: `Bearer ${token}` } }
|
|
22413
|
+
);
|
|
22414
|
+
const authorization = await authorizeHostTunnel(options, bunOptions, authorizationRequest, { hostId, workspaceIds });
|
|
22415
|
+
if (!authorization.authorized) {
|
|
22416
|
+
await refuseHostTunnelSocket(options, hostSocket, {
|
|
22417
|
+
code: "host_tunnel_token_invalid",
|
|
22418
|
+
close: 1008,
|
|
22419
|
+
reason: "Host tunnel registration update denied"
|
|
22420
|
+
});
|
|
22421
|
+
return;
|
|
22422
|
+
}
|
|
22423
|
+
const claims = authorization.claims;
|
|
22424
|
+
if (hostTunnelIncumbentOutranks(hostSocket.data.generation, claims?.generation)) {
|
|
22425
|
+
await refuseHostTunnelSocket(options, hostSocket, {
|
|
22426
|
+
code: "host_generation_superseded",
|
|
22427
|
+
close: 1008,
|
|
22428
|
+
reason: "Host tunnel registration update superseded"
|
|
22429
|
+
});
|
|
22430
|
+
return;
|
|
22431
|
+
}
|
|
22432
|
+
const generation = await checkHostTunnelGeneration(options.resolveHostGeneration, {
|
|
22433
|
+
enrollment_id: claims?.enrollment_id,
|
|
22434
|
+
generation: claims?.generation
|
|
22435
|
+
});
|
|
22436
|
+
if (!generation.ok) {
|
|
22437
|
+
await refuseHostTunnelSocket(options, hostSocket, {
|
|
22438
|
+
code: generation.code,
|
|
22439
|
+
close: generation.retryable ? 1012 : 1008,
|
|
22440
|
+
reason: generation.retryable ? "Host generation check unavailable" : generation.reason
|
|
22441
|
+
});
|
|
22442
|
+
return;
|
|
22443
|
+
}
|
|
22444
|
+
const owned = ownedWorkspaceIds(hostTunnels, hostSocket);
|
|
22445
|
+
if (owned.length === 0 || hostSocket.readyState !== WebSocket.OPEN) return;
|
|
22446
|
+
const released = owned.filter((workspaceId) => !workspaceIds.includes(workspaceId));
|
|
22447
|
+
for (const workspaceId of released) hostTunnels.delete(tunnelKey(hostId, workspaceId));
|
|
22448
|
+
if (released.length) options.directory?.disconnectHost(hostId, released);
|
|
22449
|
+
hostSocket.data.enrollmentId = claims?.enrollment_id;
|
|
22450
|
+
hostSocket.data.generation = claims?.generation;
|
|
22451
|
+
const outranked = claimTunnelIdentities(hostSocket, workspaceIds);
|
|
22452
|
+
if (outranked) {
|
|
22453
|
+
await refuseHostTunnelSocket(options, hostSocket, {
|
|
22454
|
+
code: "host_generation_superseded",
|
|
22455
|
+
close: 1008,
|
|
22456
|
+
reason: "Host tunnel generation was superseded",
|
|
22457
|
+
workspaceId: outranked
|
|
22458
|
+
});
|
|
22459
|
+
return;
|
|
22460
|
+
}
|
|
22461
|
+
hostSocket.data.workspaceIds = workspaceIds;
|
|
22462
|
+
options.directory?.registerHostTunnel({ hostId, workspaceIds });
|
|
22463
|
+
watchHostGeneration(hostSocket, options, bunOptions);
|
|
22464
|
+
}
|
|
22031
22465
|
function pruneReconnects(tracker, now) {
|
|
22032
22466
|
const cutoff = now - HOST_TUNNEL_REGISTRATION_RECONNECT_WINDOW_MS;
|
|
22033
22467
|
while (tracker.recent.length && tracker.recent[0] < cutoff) {
|
|
@@ -22035,7 +22469,14 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22035
22469
|
}
|
|
22036
22470
|
}
|
|
22037
22471
|
return {
|
|
22038
|
-
|
|
22472
|
+
// A function-valued property rather than a method: `main.ts` hands it
|
|
22473
|
+
// straight to `Bun.serve({ fetch: handler.fetch })`, detached from this
|
|
22474
|
+
// object, and it closes over the adapter's state rather than reading `this`.
|
|
22475
|
+
//
|
|
22476
|
+
// `undefined` is Bun's contract for "this request became a WebSocket":
|
|
22477
|
+
// `server.upgrade()` has already taken ownership of the socket, so there is
|
|
22478
|
+
// no HTTP response left to return.
|
|
22479
|
+
fetch: async (request, server) => {
|
|
22039
22480
|
const url = new URL(request.url);
|
|
22040
22481
|
const workspaceId = workspaceIdFromPath(url.pathname);
|
|
22041
22482
|
const hostId = hostIdFromTunnelPath(url.pathname);
|
|
@@ -22058,7 +22499,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22058
22499
|
);
|
|
22059
22500
|
}
|
|
22060
22501
|
if (draining && workspaceId) {
|
|
22061
|
-
const headers2 = relayCorsHeaders(request);
|
|
22502
|
+
const headers2 = relayCorsHeaders(request, relayOriginMatcher);
|
|
22062
22503
|
const body = JSON.stringify({
|
|
22063
22504
|
error: {
|
|
22064
22505
|
code: "relay_draining",
|
|
@@ -22073,32 +22514,63 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22073
22514
|
if (!workspaceIds.length || !options.directory) {
|
|
22074
22515
|
return new Response("Host tunnel registration is unavailable", { status: 503 });
|
|
22075
22516
|
}
|
|
22076
|
-
|
|
22517
|
+
const authorization = await authorizeHostTunnel(options, bunOptions, request, { hostId, workspaceIds });
|
|
22518
|
+
if (!authorization.authorized) {
|
|
22519
|
+
await denyHostTunnel(options, { code: "host_tunnel_token_invalid", message: "Host tunnel registration denied", status: 403, hostId });
|
|
22077
22520
|
return new Response("Host tunnel registration denied", { status: 403 });
|
|
22078
22521
|
}
|
|
22522
|
+
const claims = authorization.claims;
|
|
22523
|
+
const generation = await checkHostTunnelGeneration(options.resolveHostGeneration, {
|
|
22524
|
+
enrollment_id: claims?.enrollment_id,
|
|
22525
|
+
generation: claims?.generation
|
|
22526
|
+
});
|
|
22527
|
+
if (!generation.ok) {
|
|
22528
|
+
return denyHostTunnel(options, {
|
|
22529
|
+
code: generation.code,
|
|
22530
|
+
message: generation.reason,
|
|
22531
|
+
status: generation.retryable ? 503 : 403,
|
|
22532
|
+
hostId
|
|
22533
|
+
});
|
|
22534
|
+
}
|
|
22535
|
+
for (const workspaceId2 of workspaceIds) {
|
|
22536
|
+
const incumbent = hostTunnels.get(tunnelKey(hostId, workspaceId2));
|
|
22537
|
+
if (incumbent && outranks(incumbent, claims?.generation)) {
|
|
22538
|
+
return denyHostTunnel(options, {
|
|
22539
|
+
code: "host_generation_superseded",
|
|
22540
|
+
message: "Host tunnel generation was superseded",
|
|
22541
|
+
status: 403,
|
|
22542
|
+
hostId,
|
|
22543
|
+
workspaceId: workspaceId2
|
|
22544
|
+
});
|
|
22545
|
+
}
|
|
22546
|
+
}
|
|
22079
22547
|
const tracker = getRegistrationTracker(hostId);
|
|
22080
22548
|
const now = Date.now();
|
|
22081
22549
|
pruneReconnects(tracker, now);
|
|
22082
22550
|
if (tracker.recent.length >= HOST_TUNNEL_REGISTRATION_RECONNECT_CAP) {
|
|
22083
|
-
return
|
|
22084
|
-
"too_many_host_tunnel_reconnects",
|
|
22085
|
-
"Too many host-tunnel reconnects for this host within the last 60 seconds",
|
|
22086
|
-
429
|
|
22087
|
-
|
|
22551
|
+
return denyHostTunnel(options, {
|
|
22552
|
+
code: "too_many_host_tunnel_reconnects",
|
|
22553
|
+
message: "Too many host-tunnel reconnects for this host within the last 60 seconds",
|
|
22554
|
+
status: 429,
|
|
22555
|
+
hostId
|
|
22556
|
+
});
|
|
22088
22557
|
}
|
|
22089
22558
|
if (server.upgrade(request, {
|
|
22090
22559
|
data: {
|
|
22091
22560
|
kind: "host-tunnel",
|
|
22092
22561
|
hostId,
|
|
22093
22562
|
workspaceIds,
|
|
22563
|
+
...claims?.enrollment_id ? { enrollmentId: claims.enrollment_id } : {},
|
|
22564
|
+
...claims?.generation !== void 0 ? { generation: claims.generation } : {},
|
|
22094
22565
|
pending: /* @__PURE__ */ new Map(),
|
|
22095
22566
|
channels: /* @__PURE__ */ new Map(),
|
|
22096
22567
|
missedPongs: 0,
|
|
22097
|
-
messageBuffer: ""
|
|
22568
|
+
messageBuffer: "",
|
|
22569
|
+
generationCheckFailures: 0
|
|
22098
22570
|
}
|
|
22099
22571
|
})) {
|
|
22100
22572
|
tracker.recent.push(now);
|
|
22101
|
-
return;
|
|
22573
|
+
return void 0;
|
|
22102
22574
|
}
|
|
22103
22575
|
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
22104
22576
|
}
|
|
@@ -22106,7 +22578,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22106
22578
|
if (request.method === "OPTIONS") {
|
|
22107
22579
|
return new Response(null, {
|
|
22108
22580
|
status: 204,
|
|
22109
|
-
headers: relayCorsHeaders(request)
|
|
22581
|
+
headers: relayCorsHeaders(request, relayOriginMatcher)
|
|
22110
22582
|
});
|
|
22111
22583
|
}
|
|
22112
22584
|
if (!websocketRequest(request)) {
|
|
@@ -22114,11 +22586,12 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22114
22586
|
const response = await trace.span("relay-total", async () => {
|
|
22115
22587
|
const relay2 = await authorizeWorkspaceRelayRequest(options, request, workspaceId, trace);
|
|
22116
22588
|
if (!relay2.ok) return relay2.response;
|
|
22117
|
-
if (relay2.request.target
|
|
22118
|
-
const tunnel = hostTunnel(hostTunnels, relay2.request.target.hostId);
|
|
22589
|
+
if (isUserHostedTarget(relay2.request.target)) {
|
|
22590
|
+
const tunnel = hostTunnel(hostTunnels, relay2.request.target.hostId, relay2.request.target.workspaceId);
|
|
22119
22591
|
return tunnel ? await trace.span("tunnel-http", async () => await tunnelHttpRequest({
|
|
22120
22592
|
ws: tunnel,
|
|
22121
22593
|
request,
|
|
22594
|
+
originAllowed: relayOriginMatcher,
|
|
22122
22595
|
workspaceId: relay2.request.target.workspaceId,
|
|
22123
22596
|
path: `${relay2.request.path}${url.search}`,
|
|
22124
22597
|
relayHostToken: relay2.request.relayHostToken,
|
|
@@ -22131,6 +22604,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22131
22604
|
}
|
|
22132
22605
|
return await directHttpRequest({
|
|
22133
22606
|
request,
|
|
22607
|
+
originAllowed: relayOriginMatcher,
|
|
22134
22608
|
targetUrl: workspaceRelayTargetUrl(
|
|
22135
22609
|
relay2.request.target,
|
|
22136
22610
|
relay2.request.path,
|
|
@@ -22148,10 +22622,10 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22148
22622
|
}
|
|
22149
22623
|
const relay = await authorizeWorkspaceRelayRequest(options, request, workspaceId);
|
|
22150
22624
|
if (!relay.ok) return relay.response;
|
|
22151
|
-
const originDenied = requireAllowedOrigin(request);
|
|
22625
|
+
const originDenied = requireAllowedOrigin(request, relayOriginMatcher);
|
|
22152
22626
|
if (originDenied) return originDenied;
|
|
22153
|
-
if (relay.request.target
|
|
22154
|
-
const tunnel = hostTunnel(hostTunnels, relay.request.target.hostId);
|
|
22627
|
+
if (isUserHostedTarget(relay.request.target)) {
|
|
22628
|
+
const tunnel = hostTunnel(hostTunnels, relay.request.target.hostId, relay.request.target.workspaceId);
|
|
22155
22629
|
if (!tunnel) {
|
|
22156
22630
|
return new Response("User-hosted workspace is offline", { status: 503 });
|
|
22157
22631
|
}
|
|
@@ -22165,6 +22639,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22165
22639
|
if (server.upgrade(request, {
|
|
22166
22640
|
data: {
|
|
22167
22641
|
kind: "user-hosted-client",
|
|
22642
|
+
claims: relay.request.claims,
|
|
22168
22643
|
hostId: relay.request.target.hostId,
|
|
22169
22644
|
workspaceId: relay.request.target.workspaceId,
|
|
22170
22645
|
channelId: crypto.randomUUID(),
|
|
@@ -22172,13 +22647,14 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22172
22647
|
relayHostToken: relay.request.relayHostToken
|
|
22173
22648
|
}
|
|
22174
22649
|
})) {
|
|
22175
|
-
return;
|
|
22650
|
+
return void 0;
|
|
22176
22651
|
}
|
|
22177
22652
|
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
22178
22653
|
}
|
|
22179
22654
|
if (server.upgrade(request, {
|
|
22180
22655
|
data: {
|
|
22181
22656
|
kind: "client",
|
|
22657
|
+
claims: relay.request.claims,
|
|
22182
22658
|
upstreamUrl: workspaceRelayTargetUrl(
|
|
22183
22659
|
relay.request.target,
|
|
22184
22660
|
relay.request.path,
|
|
@@ -22188,7 +22664,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22188
22664
|
new Headers(),
|
|
22189
22665
|
relay.request.relayHostToken,
|
|
22190
22666
|
relay.request.target.workspaceId,
|
|
22191
|
-
//
|
|
22667
|
+
// Cloud-vm WS upgrade: cookies pass through.
|
|
22192
22668
|
{ userHosted: false, upstreamHeaders: relay.request.target.upstreamHeaders }
|
|
22193
22669
|
)),
|
|
22194
22670
|
queue: [],
|
|
@@ -22202,48 +22678,30 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22202
22678
|
} : {}
|
|
22203
22679
|
}
|
|
22204
22680
|
})) {
|
|
22205
|
-
return;
|
|
22681
|
+
return void 0;
|
|
22206
22682
|
}
|
|
22207
22683
|
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
22208
22684
|
},
|
|
22209
22685
|
websocket: {
|
|
22210
22686
|
maxPayloadLength: WS_MAX_PAYLOAD_LENGTH_BYTES,
|
|
22211
22687
|
message(ws, message) {
|
|
22212
|
-
if (ws
|
|
22688
|
+
if (isHostTunnelSocket(ws)) {
|
|
22213
22689
|
const parsed = tunnelMessage(
|
|
22214
22690
|
ws,
|
|
22215
22691
|
message,
|
|
22216
22692
|
fragmentationStats
|
|
22217
22693
|
);
|
|
22218
22694
|
if (parsed?.type === "ping") {
|
|
22219
|
-
options.directory?.recordPong(ws.data.hostId);
|
|
22695
|
+
options.directory?.recordPong(ws.data.hostId, ownedWorkspaceIds(hostTunnels, ws));
|
|
22220
22696
|
ws.send(JSON.stringify(makeTunnelPong(parsed)));
|
|
22221
22697
|
}
|
|
22222
22698
|
if (parsed?.type === "pong") {
|
|
22223
22699
|
ws.data.missedPongs = 0;
|
|
22224
|
-
options.directory?.recordPong(ws.data.hostId);
|
|
22700
|
+
options.directory?.recordPong(ws.data.hostId, ownedWorkspaceIds(hostTunnels, ws));
|
|
22225
22701
|
}
|
|
22226
22702
|
if (parsed?.type === "host.registration.update") {
|
|
22227
|
-
const update = parsed;
|
|
22228
|
-
const workspaceIds = [...new Set(update.workspace_ids)];
|
|
22229
22703
|
const hostSocket = ws;
|
|
22230
|
-
|
|
22231
|
-
const authorizationRequest = new Request(
|
|
22232
|
-
`http://relay.local/host-tunnels/${encodeURIComponent(hostId)}`,
|
|
22233
|
-
{ headers: { authorization: `Bearer ${update.token}` } }
|
|
22234
|
-
);
|
|
22235
|
-
void authorizeHostTunnel(options, bunOptions, authorizationRequest, {
|
|
22236
|
-
hostId,
|
|
22237
|
-
workspaceIds
|
|
22238
|
-
}).then((authorized) => {
|
|
22239
|
-
if (!authorized) {
|
|
22240
|
-
closeWebSocket(hostSocket, 1008, "Host tunnel registration update denied", 1008);
|
|
22241
|
-
return;
|
|
22242
|
-
}
|
|
22243
|
-
if (hostTunnels.get(hostId) !== hostSocket || hostSocket.readyState !== WebSocket.OPEN) return;
|
|
22244
|
-
hostSocket.data.workspaceIds = workspaceIds;
|
|
22245
|
-
options.directory?.registerHostTunnel({ hostId, workspaceIds });
|
|
22246
|
-
}).catch(() => closeWebSocket(hostSocket, 1008, "Host tunnel registration update denied", 1008));
|
|
22704
|
+
void applyRegistrationUpdate(hostSocket, [...new Set(parsed.workspace_ids)], parsed.token).catch(() => closeWebSocket(hostSocket, 1008, "Host tunnel registration update denied", 1008));
|
|
22247
22705
|
return;
|
|
22248
22706
|
}
|
|
22249
22707
|
if (parsed?.type === "error") {
|
|
@@ -22267,7 +22725,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22267
22725
|
if (isEventStream(response.headers)) clearTimeout(pending.timeout);
|
|
22268
22726
|
pending.resolve(new Response(pending.stream, {
|
|
22269
22727
|
status: response.status,
|
|
22270
|
-
headers: headers(response.headers)
|
|
22728
|
+
headers: pending.corsHeaders(headers(response.headers))
|
|
22271
22729
|
}));
|
|
22272
22730
|
}
|
|
22273
22731
|
}
|
|
@@ -22335,8 +22793,8 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22335
22793
|
}
|
|
22336
22794
|
return;
|
|
22337
22795
|
}
|
|
22338
|
-
if (ws
|
|
22339
|
-
const tunnel = hostTunnel(hostTunnels, ws.data.hostId);
|
|
22796
|
+
if (isUserHostedClientSocket(ws)) {
|
|
22797
|
+
const tunnel = hostTunnel(hostTunnels, ws.data.hostId, ws.data.workspaceId);
|
|
22340
22798
|
if (!tunnel) {
|
|
22341
22799
|
ws.close(1011, "User-hosted tunnel disconnected");
|
|
22342
22800
|
return;
|
|
@@ -22353,6 +22811,7 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22353
22811
|
}));
|
|
22354
22812
|
return;
|
|
22355
22813
|
}
|
|
22814
|
+
if (!isRelayClientSocket(ws)) return;
|
|
22356
22815
|
if (ws.data.upstream?.readyState === WebSocket.OPEN) {
|
|
22357
22816
|
ws.data.upstream.send(message);
|
|
22358
22817
|
return;
|
|
@@ -22367,21 +22826,17 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22367
22826
|
ws.data.queue.push({ payload: message, queuedAt: performance.now() });
|
|
22368
22827
|
},
|
|
22369
22828
|
open(ws) {
|
|
22370
|
-
if (ws
|
|
22371
|
-
const
|
|
22372
|
-
if (
|
|
22373
|
-
|
|
22374
|
-
|
|
22375
|
-
|
|
22376
|
-
|
|
22377
|
-
|
|
22378
|
-
bunOptions,
|
|
22379
|
-
disconnectDirectory: false,
|
|
22380
|
-
closeChannels: true
|
|
22829
|
+
if (isHostTunnelSocket(ws)) {
|
|
22830
|
+
const outranked = claimTunnelIdentities(ws, ws.data.workspaceIds);
|
|
22831
|
+
if (outranked) {
|
|
22832
|
+
void refuseHostTunnelSocket(options, ws, {
|
|
22833
|
+
code: "host_generation_superseded",
|
|
22834
|
+
close: 1008,
|
|
22835
|
+
reason: "Host tunnel generation was superseded",
|
|
22836
|
+
workspaceId: outranked
|
|
22381
22837
|
});
|
|
22382
|
-
|
|
22838
|
+
return;
|
|
22383
22839
|
}
|
|
22384
|
-
hostTunnels.set(ws.data.hostId, ws);
|
|
22385
22840
|
options.directory?.registerHostTunnel({
|
|
22386
22841
|
hostId: ws.data.hostId,
|
|
22387
22842
|
workspaceIds: ws.data.workspaceIds
|
|
@@ -22394,17 +22849,19 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22394
22849
|
}
|
|
22395
22850
|
sendTunnelPing(hostWs);
|
|
22396
22851
|
}, bunOptions.hostTunnelPingIntervalMs ?? 15e3);
|
|
22852
|
+
watchHostGeneration(ws, options, bunOptions);
|
|
22397
22853
|
scheduleHostTunnelStateChange(hostTunnelStateDebounce, options, {
|
|
22398
22854
|
hostId: ws.data.hostId,
|
|
22399
|
-
|
|
22855
|
+
workspaceIds: ws.data.workspaceIds,
|
|
22400
22856
|
path: `/host-tunnels/${ws.data.hostId}`,
|
|
22401
22857
|
state: "connected",
|
|
22402
22858
|
debounceMs: bunOptions.hostTunnelStateDebounceMs ?? HOST_TUNNEL_STATE_DEBOUNCE_MS_DEFAULT
|
|
22403
22859
|
});
|
|
22404
22860
|
return;
|
|
22405
22861
|
}
|
|
22406
|
-
if (ws
|
|
22407
|
-
|
|
22862
|
+
if (isUserHostedClientSocket(ws)) {
|
|
22863
|
+
watchClientAccess(ws, options, bunOptions);
|
|
22864
|
+
const tunnel = hostTunnel(hostTunnels, ws.data.hostId, ws.data.workspaceId);
|
|
22408
22865
|
if (!tunnel) {
|
|
22409
22866
|
ws.close(1011, "User-hosted tunnel disconnected");
|
|
22410
22867
|
return;
|
|
@@ -22424,17 +22881,18 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22424
22881
|
new Headers(),
|
|
22425
22882
|
ws.data.relayHostToken,
|
|
22426
22883
|
ws.data.workspaceId,
|
|
22427
|
-
//
|
|
22428
|
-
//
|
|
22429
|
-
// upstream client headers are forwarded in the future.
|
|
22884
|
+
// User-hosted tunnel WS upgrade: strip Cookie. Headers are empty
|
|
22885
|
+
// here today; the flag keeps the rule uniform.
|
|
22430
22886
|
{ userHosted: true }
|
|
22431
22887
|
))
|
|
22432
22888
|
}));
|
|
22433
22889
|
return;
|
|
22434
22890
|
}
|
|
22891
|
+
if (!isRelayClientSocket(ws)) return;
|
|
22435
22892
|
relayClients.add(ws);
|
|
22893
|
+
watchClientAccess(ws, options, bunOptions);
|
|
22436
22894
|
const data = ws.data;
|
|
22437
|
-
const UpstreamWebSocket = bunOptions.upstreamWebSocket
|
|
22895
|
+
const UpstreamWebSocket = resolveUpstreamWebSocket(bunOptions.upstreamWebSocket);
|
|
22438
22896
|
if (data.trace) data.trace.upstreamStartedAt = performance.now();
|
|
22439
22897
|
const upstream = new UpstreamWebSocket(data.upstreamUrl, {
|
|
22440
22898
|
headers: data.headers
|
|
@@ -22484,13 +22942,15 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22484
22942
|
};
|
|
22485
22943
|
},
|
|
22486
22944
|
close(ws, code, reason) {
|
|
22487
|
-
if (ws
|
|
22945
|
+
if (isHostTunnelSocket(ws)) {
|
|
22946
|
+
clearHostGenerationWatcher(ws.data);
|
|
22488
22947
|
cleanupHostTunnelSocket({
|
|
22489
22948
|
ws,
|
|
22490
22949
|
hostTunnels,
|
|
22491
22950
|
hostTunnelStateDebounce,
|
|
22492
22951
|
options,
|
|
22493
22952
|
bunOptions,
|
|
22953
|
+
originAllowed: relayOriginMatcher,
|
|
22494
22954
|
disconnectDirectory: true,
|
|
22495
22955
|
closeChannels: true
|
|
22496
22956
|
});
|
|
@@ -22501,8 +22961,9 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22501
22961
|
}
|
|
22502
22962
|
return;
|
|
22503
22963
|
}
|
|
22504
|
-
if (ws
|
|
22505
|
-
|
|
22964
|
+
if (isUserHostedClientSocket(ws)) {
|
|
22965
|
+
clearClientAccessWatchers(ws.data);
|
|
22966
|
+
const tunnel = hostTunnel(hostTunnels, ws.data.hostId, ws.data.workspaceId);
|
|
22506
22967
|
tunnel?.data.channels.delete(ws.data.channelId);
|
|
22507
22968
|
if (tunnel?.readyState === WebSocket.OPEN) {
|
|
22508
22969
|
tunnel.send(JSON.stringify({
|
|
@@ -22515,7 +22976,9 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22515
22976
|
}
|
|
22516
22977
|
return;
|
|
22517
22978
|
}
|
|
22979
|
+
if (!isRelayClientSocket(ws)) return;
|
|
22518
22980
|
relayClients.delete(ws);
|
|
22981
|
+
clearClientAccessWatchers(ws.data);
|
|
22519
22982
|
if (ws.data.upstreamOpenTimer) clearTimeout(ws.data.upstreamOpenTimer);
|
|
22520
22983
|
if (ws.data.upstream) closeWebSocket(ws.data.upstream, code, reason, 1e3);
|
|
22521
22984
|
}
|
|
@@ -22526,34 +22989,32 @@ function createWorkspaceRelayBun(options, bunOptions = {}) {
|
|
|
22526
22989
|
}
|
|
22527
22990
|
|
|
22528
22991
|
// src/directory.ts
|
|
22992
|
+
function presenceKey(hostId, workspaceId) {
|
|
22993
|
+
return `${hostId}\0${workspaceId}`;
|
|
22994
|
+
}
|
|
22529
22995
|
function createWorkspaceRelayDirectory(options = {}) {
|
|
22530
22996
|
const ttlMs = Math.max(1, options.ttlMs ?? 45e3);
|
|
22531
22997
|
const now = options.now ?? Date.now;
|
|
22532
22998
|
const sweepIntervalMs = options.sweepIntervalMs ?? 3e4;
|
|
22533
|
-
const
|
|
22534
|
-
const alive = (
|
|
22535
|
-
|
|
22999
|
+
const entries = /* @__PURE__ */ new Map();
|
|
23000
|
+
const alive = (key) => {
|
|
23001
|
+
const presence = entries.get(key);
|
|
23002
|
+
if (!presence) return void 0;
|
|
22536
23003
|
if (presence.expiresAt <= now()) {
|
|
22537
|
-
|
|
22538
|
-
return;
|
|
23004
|
+
entries.delete(key);
|
|
23005
|
+
return void 0;
|
|
22539
23006
|
}
|
|
22540
23007
|
return presence;
|
|
22541
23008
|
};
|
|
22542
|
-
const
|
|
22543
|
-
|
|
22544
|
-
|
|
22545
|
-
...presence,
|
|
22546
|
-
lastPongAt: timestamp,
|
|
22547
|
-
expiresAt: timestamp + ttlMs
|
|
22548
|
-
};
|
|
22549
|
-
hosts.set(next.hostId, next);
|
|
22550
|
-
return next;
|
|
23009
|
+
const keysOf = (hostId, workspaceIds) => {
|
|
23010
|
+
if (workspaceIds) return [...new Set(workspaceIds)].map((workspaceId) => presenceKey(hostId, workspaceId));
|
|
23011
|
+
return [...entries].filter(([, presence]) => presence.hostId === hostId).map(([key]) => key);
|
|
22551
23012
|
};
|
|
22552
23013
|
const sweep = () => {
|
|
22553
23014
|
const at = now();
|
|
22554
|
-
for (const [
|
|
23015
|
+
for (const [key, presence] of entries) {
|
|
22555
23016
|
if (presence.expiresAt <= at) {
|
|
22556
|
-
|
|
23017
|
+
entries.delete(key);
|
|
22557
23018
|
}
|
|
22558
23019
|
}
|
|
22559
23020
|
};
|
|
@@ -22571,29 +23032,35 @@ function createWorkspaceRelayDirectory(options = {}) {
|
|
|
22571
23032
|
lastPongAt: timestamp,
|
|
22572
23033
|
expiresAt: timestamp + ttlMs
|
|
22573
23034
|
};
|
|
22574
|
-
|
|
23035
|
+
for (const workspaceId of next.workspaceIds) entries.set(presenceKey(next.hostId, workspaceId), next);
|
|
22575
23036
|
return next;
|
|
22576
23037
|
},
|
|
22577
|
-
recordPong(hostId) {
|
|
22578
|
-
const
|
|
22579
|
-
|
|
23038
|
+
recordPong(hostId, workspaceIds) {
|
|
23039
|
+
const timestamp = now();
|
|
23040
|
+
let touched;
|
|
23041
|
+
for (const key of keysOf(hostId, workspaceIds)) {
|
|
23042
|
+
const presence = alive(key);
|
|
23043
|
+
if (!presence) continue;
|
|
23044
|
+
const next = { ...presence, lastPongAt: timestamp, expiresAt: timestamp + ttlMs };
|
|
23045
|
+
entries.set(key, next);
|
|
23046
|
+
touched ??= next;
|
|
23047
|
+
}
|
|
23048
|
+
return touched;
|
|
22580
23049
|
},
|
|
22581
|
-
disconnectHost(hostId) {
|
|
22582
|
-
|
|
23050
|
+
disconnectHost(hostId, workspaceIds) {
|
|
23051
|
+
for (const key of keysOf(hostId, workspaceIds)) entries.delete(key);
|
|
22583
23052
|
},
|
|
22584
23053
|
activeHost(input) {
|
|
22585
|
-
|
|
22586
|
-
if (!presence?.workspaceIds.includes(input.workspaceId)) return;
|
|
22587
|
-
return presence;
|
|
23054
|
+
return alive(presenceKey(input.hostId, input.workspaceId));
|
|
22588
23055
|
},
|
|
22589
23056
|
sweep,
|
|
22590
23057
|
size() {
|
|
22591
23058
|
const at = now();
|
|
22592
|
-
|
|
22593
|
-
for (const presence of
|
|
22594
|
-
if (presence.expiresAt > at)
|
|
23059
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
23060
|
+
for (const presence of entries.values()) {
|
|
23061
|
+
if (presence.expiresAt > at) hosts.add(presence.hostId);
|
|
22595
23062
|
}
|
|
22596
|
-
return
|
|
23063
|
+
return hosts.size;
|
|
22597
23064
|
},
|
|
22598
23065
|
dispose() {
|
|
22599
23066
|
if (intervalHandle !== void 0) {
|
|
@@ -22725,20 +23192,17 @@ function startSyntheticProbe(options) {
|
|
|
22725
23192
|
// src/main.ts
|
|
22726
23193
|
var BUN_TARGET_CACHE_TTL_MS_DEFAULT = 3e4;
|
|
22727
23194
|
var BUN_REVOCATION_CACHE_TTL_MS_DEFAULT = 1e4;
|
|
23195
|
+
var BUN_HOST_GENERATION_CACHE_TTL_MS_DEFAULT = 1e4;
|
|
22728
23196
|
var BUN_RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS_DEFAULT = 1e4;
|
|
22729
|
-
function clean(input) {
|
|
22730
|
-
const value = input?.trim();
|
|
22731
|
-
return value ? value : void 0;
|
|
22732
|
-
}
|
|
22733
23197
|
function positiveInteger(input) {
|
|
22734
|
-
const parsed = Number(
|
|
23198
|
+
const parsed = Number(trimToUndefined(input));
|
|
22735
23199
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
22736
23200
|
}
|
|
22737
23201
|
function pem(input) {
|
|
22738
|
-
return
|
|
23202
|
+
return trimToUndefined(input)?.replaceAll("\\n", "\n");
|
|
22739
23203
|
}
|
|
22740
23204
|
function requireEnv(name) {
|
|
22741
|
-
const value =
|
|
23205
|
+
const value = trimToUndefined(process.env[name]);
|
|
22742
23206
|
if (!value) {
|
|
22743
23207
|
console.error(`[workspace-relay] missing required env: ${name}`);
|
|
22744
23208
|
process.exit(2);
|
|
@@ -22746,17 +23210,17 @@ function requireEnv(name) {
|
|
|
22746
23210
|
return value;
|
|
22747
23211
|
}
|
|
22748
23212
|
function relayTelemetryOptions(env) {
|
|
22749
|
-
if (
|
|
22750
|
-
const key =
|
|
23213
|
+
if (trimToUndefined(env.CLAXEDO_TELEMETRY_MODE)?.toLowerCase() !== "on") return void 0;
|
|
23214
|
+
const key = trimToUndefined(env.CLAXEDO_POSTHOG_KEY) ?? trimToUndefined(env.POSTHOG_KEY);
|
|
22751
23215
|
if (!key) return void 0;
|
|
22752
|
-
const release =
|
|
23216
|
+
const release = trimToUndefined(env.CLAXEDO_RELEASE) ?? trimToUndefined(env.GIT_SHA);
|
|
22753
23217
|
return {
|
|
22754
23218
|
key,
|
|
22755
|
-
host:
|
|
23219
|
+
host: trimToUndefined(env.CLAXEDO_POSTHOG_HOST) ?? "https://us.i.posthog.com",
|
|
22756
23220
|
...release ? { release } : {},
|
|
22757
23221
|
tags: {
|
|
22758
23222
|
unit: "relay",
|
|
22759
|
-
deployment_mode:
|
|
23223
|
+
deployment_mode: trimToUndefined(env.CLAXEDO_DEPLOYMENT_MODE)?.toLowerCase() ?? "local"
|
|
22760
23224
|
}
|
|
22761
23225
|
};
|
|
22762
23226
|
}
|
|
@@ -22783,9 +23247,9 @@ async function reportFatal(error) {
|
|
|
22783
23247
|
}
|
|
22784
23248
|
}
|
|
22785
23249
|
function validateProductionEnv(env) {
|
|
22786
|
-
const isProduction =
|
|
23250
|
+
const isProduction = trimToUndefined(env.NODE_ENV) === "production";
|
|
22787
23251
|
if (!isProduction) return { ok: true };
|
|
22788
|
-
const resolverToken =
|
|
23252
|
+
const resolverToken = trimToUndefined(env.CLAXEDO_RELAY_RESOLVER_TOKEN);
|
|
22789
23253
|
if (!resolverToken) {
|
|
22790
23254
|
return {
|
|
22791
23255
|
ok: false,
|
|
@@ -22796,7 +23260,7 @@ function validateProductionEnv(env) {
|
|
|
22796
23260
|
return { ok: true };
|
|
22797
23261
|
}
|
|
22798
23262
|
async function loadRuntimeAccessKeyOrJwks(env) {
|
|
22799
|
-
const jwksUrl =
|
|
23263
|
+
const jwksUrl = trimToUndefined(env.CLAXEDO_CONTROL_PLANE_JWKS_URL);
|
|
22800
23264
|
if (jwksUrl) {
|
|
22801
23265
|
return createRemoteJWKSet(new URL(jwksUrl));
|
|
22802
23266
|
}
|
|
@@ -22808,14 +23272,6 @@ async function loadRuntimeAccessKeyOrJwks(env) {
|
|
|
22808
23272
|
}
|
|
22809
23273
|
return importSPKI(verifyPem, "EdDSA");
|
|
22810
23274
|
}
|
|
22811
|
-
async function deriveKidFromPublicKey(publicKey) {
|
|
22812
|
-
const jwk = await exportJWK2(publicKey);
|
|
22813
|
-
const material = String(jwk.x ?? jwk.n ?? "");
|
|
22814
|
-
if (!material) {
|
|
22815
|
-
throw new Error("Cannot derive kid: public key has no public component");
|
|
22816
|
-
}
|
|
22817
|
-
return createHash("sha256").update(material).digest("hex").slice(0, 16);
|
|
22818
|
-
}
|
|
22819
23275
|
async function loadRelayHostKeyMaterial(env) {
|
|
22820
23276
|
const privatePem = pem(env.CLAXEDO_RELAY_HOST_SIGNING_KEY_PEM);
|
|
22821
23277
|
let privateKey;
|
|
@@ -22823,7 +23279,7 @@ async function loadRelayHostKeyMaterial(env) {
|
|
|
22823
23279
|
if (privatePem) {
|
|
22824
23280
|
privateKey = await importPKCS8(privatePem, "EdDSA", { extractable: true });
|
|
22825
23281
|
} else {
|
|
22826
|
-
if (
|
|
23282
|
+
if (trimToUndefined(env.NODE_ENV) === "production") {
|
|
22827
23283
|
console.error(
|
|
22828
23284
|
"[workspace-relay] CLAXEDO_RELAY_HOST_SIGNING_KEY_PEM required in production; refusing to start with an ephemeral key"
|
|
22829
23285
|
);
|
|
@@ -22841,21 +23297,18 @@ async function loadRelayHostKeyMaterial(env) {
|
|
|
22841
23297
|
} else if (derivedPublicKey) {
|
|
22842
23298
|
publicKey = derivedPublicKey;
|
|
22843
23299
|
} else {
|
|
22844
|
-
|
|
22845
|
-
const publicJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x };
|
|
22846
|
-
const { importJWK } = await import("jose");
|
|
22847
|
-
publicKey = await importJWK(publicJwk, "EdDSA", { extractable: true });
|
|
23300
|
+
publicKey = await deriveRelayHostPublicKey(privateKey);
|
|
22848
23301
|
}
|
|
22849
|
-
const explicitCurrentKid =
|
|
22850
|
-
const currentKid = explicitCurrentKid ?? await
|
|
23302
|
+
const explicitCurrentKid = trimToUndefined(env.CLAXEDO_RELAY_HOST_KID);
|
|
23303
|
+
const currentKid = explicitCurrentKid ?? await deriveRelayHostKid(publicKey);
|
|
22851
23304
|
const nextPem = pem(env.CLAXEDO_RELAY_HOST_NEXT_PUBLIC_KEY_PEM);
|
|
22852
23305
|
let next;
|
|
22853
23306
|
if (nextPem) {
|
|
22854
23307
|
const nextPublicKey = await importSPKI(nextPem, "EdDSA", { extractable: true });
|
|
22855
|
-
const explicitNextKid =
|
|
23308
|
+
const explicitNextKid = trimToUndefined(env.CLAXEDO_RELAY_HOST_NEXT_KID);
|
|
22856
23309
|
next = {
|
|
22857
23310
|
publicKey: nextPublicKey,
|
|
22858
|
-
kid: explicitNextKid ?? await
|
|
23311
|
+
kid: explicitNextKid ?? await deriveRelayHostKid(nextPublicKey)
|
|
22859
23312
|
};
|
|
22860
23313
|
}
|
|
22861
23314
|
return {
|
|
@@ -22865,9 +23318,9 @@ async function loadRelayHostKeyMaterial(env) {
|
|
|
22865
23318
|
};
|
|
22866
23319
|
}
|
|
22867
23320
|
function parseAuditAcceptSampleRate(env) {
|
|
22868
|
-
const isProduction =
|
|
23321
|
+
const isProduction = trimToUndefined(env.NODE_ENV) === "production";
|
|
22869
23322
|
const fallback = isProduction ? 0.1 : 1;
|
|
22870
|
-
const raw =
|
|
23323
|
+
const raw = trimToUndefined(env.CLAXEDO_RELAY_AUDIT_ACCEPT_SAMPLE_RATE);
|
|
22871
23324
|
if (!raw) return fallback;
|
|
22872
23325
|
const value = Number(raw);
|
|
22873
23326
|
if (!Number.isFinite(value)) return fallback;
|
|
@@ -22875,7 +23328,7 @@ function parseAuditAcceptSampleRate(env) {
|
|
|
22875
23328
|
return value;
|
|
22876
23329
|
}
|
|
22877
23330
|
function parseMetricsToken(env) {
|
|
22878
|
-
return
|
|
23331
|
+
return trimToUndefined(env.CLAXEDO_RELAY_METRICS_TOKEN);
|
|
22879
23332
|
}
|
|
22880
23333
|
function runtimeAccessTokenCacheTtlMsFromEnv(env) {
|
|
22881
23334
|
return positiveInteger(env.CLAXEDO_RELAY_RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS) ?? BUN_RUNTIME_ACCESS_TOKEN_CACHE_TTL_MS_DEFAULT;
|
|
@@ -22884,9 +23337,12 @@ function directHttpConcurrencyFromEnv(env) {
|
|
|
22884
23337
|
return positiveInteger(env.CLAXEDO_RELAY_DIRECT_HTTP_CONCURRENCY);
|
|
22885
23338
|
}
|
|
22886
23339
|
function resolverClientCacheOptionsFromEnv(env) {
|
|
23340
|
+
const hostGenerationUrl = trimToUndefined(env.CLAXEDO_RELAY_HOST_GENERATION_URL);
|
|
22887
23341
|
return {
|
|
22888
23342
|
targetCacheTtlMs: positiveInteger(env.CLAXEDO_RELAY_TARGET_CACHE_TTL_MS) ?? BUN_TARGET_CACHE_TTL_MS_DEFAULT,
|
|
22889
|
-
revocationCacheTtlMs: positiveInteger(env.CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS) ?? BUN_REVOCATION_CACHE_TTL_MS_DEFAULT
|
|
23343
|
+
revocationCacheTtlMs: positiveInteger(env.CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS) ?? BUN_REVOCATION_CACHE_TTL_MS_DEFAULT,
|
|
23344
|
+
...hostGenerationUrl ? { hostGenerationUrl } : {},
|
|
23345
|
+
hostGenerationCacheTtlMs: positiveInteger(env.CLAXEDO_RELAY_HOST_GENERATION_CACHE_TTL_MS) ?? BUN_HOST_GENERATION_CACHE_TTL_MS_DEFAULT
|
|
22890
23346
|
};
|
|
22891
23347
|
}
|
|
22892
23348
|
function createResolverClient(baseUrl, token, options = {}) {
|
|
@@ -22900,7 +23356,9 @@ function createResolverClient(baseUrl, token, options = {}) {
|
|
|
22900
23356
|
const res = await fetch(url, { headers: headers2 });
|
|
22901
23357
|
if (res.status === 404) return void 0;
|
|
22902
23358
|
if (!res.ok) throw new Error(`relay target resolver failed: ${res.status} ${await res.text()}`);
|
|
22903
|
-
|
|
23359
|
+
const target2 = parseWorkspaceRelayTarget(await res.json());
|
|
23360
|
+
if (!target2) throw new Error("relay target resolver returned a malformed target");
|
|
23361
|
+
return target2;
|
|
22904
23362
|
};
|
|
22905
23363
|
const revocationUncached = async (args) => {
|
|
22906
23364
|
const url = new URL(`${root}/revocation`);
|
|
@@ -22915,7 +23373,9 @@ function createResolverClient(baseUrl, token, options = {}) {
|
|
|
22915
23373
|
reason: `revocation resolver returned ${res.status}`
|
|
22916
23374
|
};
|
|
22917
23375
|
}
|
|
22918
|
-
|
|
23376
|
+
const result = parseRuntimeAccessTokenActiveResult(await res.json());
|
|
23377
|
+
if (!result) throw new Error("relay revocation resolver returned a malformed result");
|
|
23378
|
+
return result;
|
|
22919
23379
|
};
|
|
22920
23380
|
const target = createCachedTargetClient(targetUncached, {
|
|
22921
23381
|
ttlMs: options.targetCacheTtlMs ?? BUN_TARGET_CACHE_TTL_MS_DEFAULT
|
|
@@ -22923,11 +23383,39 @@ function createResolverClient(baseUrl, token, options = {}) {
|
|
|
22923
23383
|
const revocation = createCachedRevocationClient(revocationUncached, {
|
|
22924
23384
|
ttlMs: options.revocationCacheTtlMs ?? BUN_REVOCATION_CACHE_TTL_MS_DEFAULT
|
|
22925
23385
|
});
|
|
23386
|
+
const hostGeneration = createCachedHostGenerationClient(
|
|
23387
|
+
createHostGenerationResolverLookup(options.hostGenerationUrl ?? `${root}/host-generation`, { headers: headers2 }),
|
|
23388
|
+
{ ttlMs: options.hostGenerationCacheTtlMs ?? BUN_HOST_GENERATION_CACHE_TTL_MS_DEFAULT }
|
|
23389
|
+
);
|
|
22926
23390
|
return {
|
|
22927
23391
|
target: (workspaceId, hostId) => target({ workspaceId, hostId }),
|
|
22928
|
-
revocation
|
|
23392
|
+
revocation,
|
|
23393
|
+
hostGeneration
|
|
22929
23394
|
};
|
|
22930
23395
|
}
|
|
23396
|
+
var STOP_SERVER_TIMEOUT_MS_DEFAULT = 5e3;
|
|
23397
|
+
async function runBoundedTeardown(step, label, timeoutMs, log) {
|
|
23398
|
+
if (!step) return;
|
|
23399
|
+
let timer;
|
|
23400
|
+
const attempt = (async () => {
|
|
23401
|
+
await step();
|
|
23402
|
+
})().catch((err) => {
|
|
23403
|
+
log(`[workspace-relay] ${label} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
23404
|
+
});
|
|
23405
|
+
try {
|
|
23406
|
+
await Promise.race([
|
|
23407
|
+
attempt,
|
|
23408
|
+
new Promise((resolve) => {
|
|
23409
|
+
timer = setTimeout(() => {
|
|
23410
|
+
log(`[workspace-relay] ${label} did not finish within ${timeoutMs}ms; exiting anyway`);
|
|
23411
|
+
resolve();
|
|
23412
|
+
}, timeoutMs);
|
|
23413
|
+
})
|
|
23414
|
+
]);
|
|
23415
|
+
} finally {
|
|
23416
|
+
if (timer) clearTimeout(timer);
|
|
23417
|
+
}
|
|
23418
|
+
}
|
|
22931
23419
|
function installShutdownDrainHandler(options) {
|
|
22932
23420
|
const exit = options.exit ?? ((code) => process.exit(code));
|
|
22933
23421
|
const log = options.log ?? (() => {
|
|
@@ -22950,11 +23438,12 @@ function installShutdownDrainHandler(options) {
|
|
|
22950
23438
|
`[workspace-relay] drain timeout: ${result.remaining} pending request(s) remaining after ${options.drainTimeoutMs}ms; force-closing`
|
|
22951
23439
|
);
|
|
22952
23440
|
}
|
|
22953
|
-
|
|
22954
|
-
|
|
22955
|
-
|
|
22956
|
-
|
|
22957
|
-
|
|
23441
|
+
await runBoundedTeardown(
|
|
23442
|
+
options.stopServer,
|
|
23443
|
+
"stopServer",
|
|
23444
|
+
options.stopTimeoutMs ?? STOP_SERVER_TIMEOUT_MS_DEFAULT,
|
|
23445
|
+
log
|
|
23446
|
+
);
|
|
22958
23447
|
try {
|
|
22959
23448
|
options.directory.dispose();
|
|
22960
23449
|
} catch (err) {
|
|
@@ -22995,11 +23484,12 @@ function installFatalProcessHandlers(options) {
|
|
|
22995
23484
|
log(`[workspace-relay] fatal report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
22996
23485
|
}
|
|
22997
23486
|
options.drain.setDraining(true);
|
|
22998
|
-
|
|
22999
|
-
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
|
|
23487
|
+
await runBoundedTeardown(
|
|
23488
|
+
options.stopServer,
|
|
23489
|
+
"fatal stopServer",
|
|
23490
|
+
options.stopTimeoutMs ?? STOP_SERVER_TIMEOUT_MS_DEFAULT,
|
|
23491
|
+
log
|
|
23492
|
+
);
|
|
23003
23493
|
try {
|
|
23004
23494
|
options.directory.dispose();
|
|
23005
23495
|
} catch (err) {
|
|
@@ -23035,14 +23525,14 @@ async function main() {
|
|
|
23035
23525
|
console.error(`[workspace-relay] ${validation.message}`);
|
|
23036
23526
|
process.exit(validation.exitCode);
|
|
23037
23527
|
}
|
|
23038
|
-
const port = Number(
|
|
23528
|
+
const port = Number(trimToUndefined(process.env.CLAXEDO_WORKSPACE_RELAY_PORT) ?? "7777");
|
|
23039
23529
|
if (!Number.isFinite(port) || port <= 0) {
|
|
23040
23530
|
console.error(`[workspace-relay] invalid CLAXEDO_WORKSPACE_RELAY_PORT: ${process.env.CLAXEDO_WORKSPACE_RELAY_PORT}`);
|
|
23041
23531
|
process.exit(2);
|
|
23042
23532
|
}
|
|
23043
|
-
const hostname =
|
|
23533
|
+
const hostname = trimToUndefined(process.env.CLAXEDO_WORKSPACE_RELAY_HOST) ?? "127.0.0.1";
|
|
23044
23534
|
const resolverUrl = requireEnv("CLAXEDO_RELAY_RESOLVER_URL");
|
|
23045
|
-
const resolverToken =
|
|
23535
|
+
const resolverToken = trimToUndefined(process.env.CLAXEDO_RELAY_RESOLVER_TOKEN);
|
|
23046
23536
|
let runtimeAccessKey;
|
|
23047
23537
|
try {
|
|
23048
23538
|
runtimeAccessKey = await loadRuntimeAccessKeyOrJwks({
|
|
@@ -23072,7 +23562,9 @@ async function main() {
|
|
|
23072
23562
|
if (relayHostMaterial.next) relayHostPublicKeys.push(relayHostMaterial.next);
|
|
23073
23563
|
const resolver = createResolverClient(resolverUrl, resolverToken, resolverClientCacheOptionsFromEnv({
|
|
23074
23564
|
CLAXEDO_RELAY_TARGET_CACHE_TTL_MS: process.env.CLAXEDO_RELAY_TARGET_CACHE_TTL_MS,
|
|
23075
|
-
CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS: process.env.CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS
|
|
23565
|
+
CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS: process.env.CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS,
|
|
23566
|
+
CLAXEDO_RELAY_HOST_GENERATION_URL: process.env.CLAXEDO_RELAY_HOST_GENERATION_URL,
|
|
23567
|
+
CLAXEDO_RELAY_HOST_GENERATION_CACHE_TTL_MS: process.env.CLAXEDO_RELAY_HOST_GENERATION_CACHE_TTL_MS
|
|
23076
23568
|
}));
|
|
23077
23569
|
const directory = createWorkspaceRelayDirectory();
|
|
23078
23570
|
const auditAcceptSampleRate = parseAuditAcceptSampleRate({
|
|
@@ -23088,7 +23580,7 @@ async function main() {
|
|
|
23088
23580
|
const directHttpConcurrency = directHttpConcurrencyFromEnv({
|
|
23089
23581
|
CLAXEDO_RELAY_DIRECT_HTTP_CONCURRENCY: process.env.CLAXEDO_RELAY_DIRECT_HTTP_CONCURRENCY
|
|
23090
23582
|
});
|
|
23091
|
-
const allowedOrigins = parseAllowedOrigins(
|
|
23583
|
+
const allowedOrigins = parseAllowedOrigins(trimToUndefined(process.env.CLAXEDO_RELAY_ALLOWED_ORIGINS));
|
|
23092
23584
|
const handler = createWorkspaceRelayBun({
|
|
23093
23585
|
runtimeAccessKey,
|
|
23094
23586
|
runtimeAccessTokenCacheTtlMs: runtimeAccessTokenCacheTtlMs2,
|
|
@@ -23106,6 +23598,7 @@ async function main() {
|
|
|
23106
23598
|
workspaceId: claims.workspace_id,
|
|
23107
23599
|
hostId: claims.host_id
|
|
23108
23600
|
}),
|
|
23601
|
+
resolveHostGeneration: resolver.hostGeneration,
|
|
23109
23602
|
audit: (event) => {
|
|
23110
23603
|
if (event.result === "deny") {
|
|
23111
23604
|
console.warn(
|
|
@@ -23113,23 +23606,25 @@ async function main() {
|
|
|
23113
23606
|
);
|
|
23114
23607
|
}
|
|
23115
23608
|
}
|
|
23116
|
-
}, {
|
|
23117
|
-
...directHttpConcurrency ? { directHttpConcurrency } : {}
|
|
23118
|
-
});
|
|
23609
|
+
}, directHttpConcurrency ? { directHttpConcurrency } : {});
|
|
23119
23610
|
const server = Bun.serve({
|
|
23120
23611
|
port,
|
|
23121
23612
|
hostname,
|
|
23613
|
+
// Runtime SSE heartbeats are intentionally 30s apart. Keep a bounded
|
|
23614
|
+
// timeout above that interval so quiet streams survive without leaving
|
|
23615
|
+
// unauthenticated or incomplete HTTP sockets open indefinitely.
|
|
23616
|
+
idleTimeout: WORKSPACE_RELAY_IDLE_TIMEOUT_SECONDS,
|
|
23122
23617
|
fetch: handler.fetch,
|
|
23123
23618
|
websocket: handler.websocket
|
|
23124
23619
|
});
|
|
23125
23620
|
console.log(`[workspace-relay] listening on http://${hostname}:${port} resolver=${resolverUrl}`);
|
|
23126
|
-
const rawDrainTimeout =
|
|
23621
|
+
const rawDrainTimeout = trimToUndefined(process.env.CLAXEDO_RELAY_DRAIN_TIMEOUT_MS);
|
|
23127
23622
|
const parsedDrainTimeout = rawDrainTimeout ? Number(rawDrainTimeout) : NaN;
|
|
23128
23623
|
const drainTimeoutMs = Number.isFinite(parsedDrainTimeout) && parsedDrainTimeout > 0 ? parsedDrainTimeout : 3e4;
|
|
23129
23624
|
let syntheticProbe;
|
|
23130
|
-
const syntheticDisabled =
|
|
23625
|
+
const syntheticDisabled = trimToUndefined(process.env.CLAXEDO_RELAY_SYNTHETIC_PROBE_DISABLED) === "1";
|
|
23131
23626
|
if (!syntheticDisabled) {
|
|
23132
|
-
const rawProbeInterval =
|
|
23627
|
+
const rawProbeInterval = trimToUndefined(process.env.CLAXEDO_RELAY_SYNTHETIC_PROBE_INTERVAL_MS);
|
|
23133
23628
|
const parsedProbeInterval = rawProbeInterval ? Number(rawProbeInterval) : NaN;
|
|
23134
23629
|
const probeIntervalMs = Number.isFinite(parsedProbeInterval) && parsedProbeInterval > 0 ? parsedProbeInterval : 6e4;
|
|
23135
23630
|
syntheticProbe = startSyntheticProbe({
|
|
@@ -23144,7 +23639,7 @@ async function main() {
|
|
|
23144
23639
|
drainTimeoutMs,
|
|
23145
23640
|
log: (message) => console.log(message),
|
|
23146
23641
|
stopServer: async () => {
|
|
23147
|
-
server.stop(true);
|
|
23642
|
+
await server.stop(true);
|
|
23148
23643
|
syntheticProbe?.stop();
|
|
23149
23644
|
}
|
|
23150
23645
|
});
|
|
@@ -23152,11 +23647,10 @@ async function main() {
|
|
|
23152
23647
|
drain: handler.drain,
|
|
23153
23648
|
directory,
|
|
23154
23649
|
log: (message) => console.error(message),
|
|
23155
|
-
//
|
|
23156
|
-
// a log buffer — flush them to PostHog (no-op when no key) before exiting.
|
|
23650
|
+
// Flush fatal crashes to PostHog (no-op without a key) before exiting.
|
|
23157
23651
|
report: (error) => reportFatal(error),
|
|
23158
23652
|
stopServer: async () => {
|
|
23159
|
-
server.stop(true);
|
|
23653
|
+
await server.stop(true);
|
|
23160
23654
|
syntheticProbe?.stop();
|
|
23161
23655
|
}
|
|
23162
23656
|
});
|