@dev-anywhere/relay 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/web/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let o={};const c=e=>i(e,t),l={module:{uri:t},exports:o,require:c};s[t]=Promise.all(n.map(e=>l[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-10a3128e"],function(e){"use strict";importScripts("notification-sw.js"),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"notification-sw.js",revision:"ee0be62f4c1f36625b92680b3060becb"},{url:"index.html",revision:"4bc84aa98e8598d7707aac11006c5e6e"},{url:"assets/jmuxer.min-BMedIzGm.js",revision:null},{url:"assets/index-CTJCtPHc.css",revision:null},{url:"assets/index-Buv7a7PI.js",revision:null},{url:"assets/fvad-DreiJBJr.js",revision:null},{url:"assets/fvad-DYxbW2my.wasm",revision:null},{url:"assets/__vite-browser-external-D7Ct-6yo.js",revision:null},{url:"brand-icon.svg",revision:"efe8505a7b7ef12cbb3a4bcd286254bd"},{url:"maskable-icon-512x512.png",revision:"e762ff07cdedcd78f8650f59c6881727"},{url:"pwa-192x192.png",revision:"4e4603ce0f22528eded553ba7f2ffbc5"},{url:"pwa-512x512.png",revision:"e762ff07cdedcd78f8650f59c6881727"},{url:"pwa-64x64.png",revision:"d322160ad1dc08ba9e133cb3d296ae63"},{url:"manifest.webmanifest",revision:"4daa295c44052f4ce59a1fcfd6f06ab8"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/proxy(\/|$|\?)/,/^\/client(\/|$|\?)/,/^\/voice(\/|$|\?)/,/^\/fonts\//,/^\/health/,/^\/status/,/^\/api\//]})),e.registerRoute(/^\/fonts\/.*\.(woff2|woff|ttf|otf|css)$/,new e.CacheFirst({cacheName:"dev-anywhere-fonts",plugins:[new e.ExpirationPlugin({maxEntries:40,maxAgeSeconds:15552e3})]}),"GET")});
1
+ if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let t={};const l=e=>i(e,o),c={module:{uri:o},exports:t,require:l};s[o]=Promise.all(n.map(e=>c[e]||l(e))).then(e=>(r(...e),t))}}define(["./workbox-10a3128e"],function(e){"use strict";importScripts("notification-sw.js"),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"notification-sw.js",revision:"ee0be62f4c1f36625b92680b3060becb"},{url:"index.html",revision:"bb20b4ecbecb601d7f08075e68281105"},{url:"assets/jmuxer.min-CqkQ8MXd.js",revision:null},{url:"assets/index-BVDmlZok.css",revision:null},{url:"assets/index-BDBa51VR.js",revision:null},{url:"assets/fvad-okuu-1HD.js",revision:null},{url:"assets/fvad-DYxbW2my.wasm",revision:null},{url:"assets/__vite-browser-external-D7Ct-6yo.js",revision:null},{url:"brand-icon.svg",revision:"efe8505a7b7ef12cbb3a4bcd286254bd"},{url:"maskable-icon-512x512.png",revision:"e762ff07cdedcd78f8650f59c6881727"},{url:"pwa-192x192.png",revision:"4e4603ce0f22528eded553ba7f2ffbc5"},{url:"pwa-512x512.png",revision:"e762ff07cdedcd78f8650f59c6881727"},{url:"pwa-64x64.png",revision:"d322160ad1dc08ba9e133cb3d296ae63"},{url:"manifest.webmanifest",revision:"4daa295c44052f4ce59a1fcfd6f06ab8"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/proxy(\/|$|\?)/,/^\/client(\/|$|\?)/,/^\/voice(\/|$|\?)/,/^\/fonts\//,/^\/health/,/^\/status/,/^\/api\//]})),e.registerRoute(/^\/fonts\/.*\.(woff2|woff|ttf|otf|css)$/,new e.CacheFirst({cacheName:"dev-anywhere-fonts",plugins:[new e.ExpirationPlugin({maxEntries:40,maxAgeSeconds:15552e3})]}),"GET")});
@@ -130,7 +130,7 @@ function createSessionIdentitySchema(commonFields) {
130
130
  kind: z4.literal("terminal"),
131
131
  mode: z4.literal("pty"),
132
132
  provider: z4.literal("claude"),
133
- ptyOwner: z4.literal("local-terminal")
133
+ ptyOwner: z4.literal("proxy-hosted")
134
134
  }).strict()
135
135
  ]);
136
136
  }
@@ -489,7 +489,7 @@ var ProxyUpgradeBootstrapResponseSchema = z9.object({
489
489
  bootstrapVersion: z9.literal(PROXY_UPGRADE_BOOTSTRAP_VERSION),
490
490
  relayVersion: StableVersionSchema,
491
491
  controlProtocolVersion: z9.number().int().positive()
492
- }).strict();
492
+ });
493
493
 
494
494
  // ../../packages/shared/dist/schemas/web-preview.js
495
495
  import { z as z10 } from "zod";
@@ -1446,7 +1446,7 @@ var SessionCreateSuccessResponseSchema = z13.discriminatedUnion("mode", [
1446
1446
  kind: z13.literal("terminal"),
1447
1447
  mode: z13.literal("pty"),
1448
1448
  provider: z13.literal("claude"),
1449
- ptyOwner: z13.literal("local-terminal")
1449
+ ptyOwner: z13.literal("proxy-hosted")
1450
1450
  }).strict()
1451
1451
  ])
1452
1452
  ]);
@@ -2109,6 +2109,29 @@ var RelayCloseCode = {
2109
2109
  DEVICE_STREAM_BINDING_REJECTED: 4404,
2110
2110
  PROXY_PROTOCOL_REJECTED: 4405
2111
2111
  };
2112
+ var RelayProtocolRejectReason = {
2113
+ PAGE_OUTDATED: "page_outdated",
2114
+ SERVICE_OUTDATED: "service_outdated",
2115
+ PROTOCOL_MISMATCH: "protocol_mismatch"
2116
+ };
2117
+ var ProxyProtocolAdmissionDirection = {
2118
+ COMPATIBLE: "compatible",
2119
+ PROXY_OUTDATED: "proxy_outdated",
2120
+ RELAY_OUTDATED: "relay_outdated",
2121
+ PROTOCOL_MISMATCH: "protocol_mismatch"
2122
+ };
2123
+ function isPositiveSafeProtocolVersion(value) {
2124
+ return Number.isSafeInteger(value) && value > 0;
2125
+ }
2126
+ function compareProxyRelayProtocolVersions(proxyProtocolVersion, relayProtocolVersion) {
2127
+ if (!isPositiveSafeProtocolVersion(proxyProtocolVersion) || !isPositiveSafeProtocolVersion(relayProtocolVersion)) {
2128
+ return ProxyProtocolAdmissionDirection.PROTOCOL_MISMATCH;
2129
+ }
2130
+ if (proxyProtocolVersion === relayProtocolVersion) {
2131
+ return ProxyProtocolAdmissionDirection.COMPATIBLE;
2132
+ }
2133
+ return proxyProtocolVersion < relayProtocolVersion ? ProxyProtocolAdmissionDirection.PROXY_OUTDATED : ProxyProtocolAdmissionDirection.RELAY_OUTDATED;
2134
+ }
2112
2135
 
2113
2136
  // ../../packages/shared/dist/constants/wire-limits.js
2114
2137
  var RELAY_JSON_MESSAGE_MAX_BYTES = 8 * 1024 * 1024;
@@ -2628,8 +2651,8 @@ var RelayRegistry = class {
2628
2651
  }
2629
2652
  getAllClientWs() {
2630
2653
  const clients = [];
2631
- for (const ws of this.connectedClients.keys()) {
2632
- if (ws.readyState === WebSocket.OPEN) {
2654
+ for (const [ws, metadata] of this.connectedClients) {
2655
+ if (metadata.clientId && ws.readyState === WebSocket.OPEN) {
2633
2656
  clients.push(ws);
2634
2657
  }
2635
2658
  }
@@ -2718,9 +2741,11 @@ function healthRouter(registry, options = {}) {
2718
2741
  const proxyTokenRequired = options.proxyTokenRequired ?? false;
2719
2742
  const clientTokenRequired = options.clientTokenRequired ?? false;
2720
2743
  router.get("/health", (_req, res) => {
2744
+ res.setHeader("Cache-Control", "no-store");
2721
2745
  res.json({
2722
2746
  status: "ok",
2723
2747
  version: RELAY_VERSION,
2748
+ controlProtocolVersion: RELAY_CONTROL_PROTOCOL_VERSION,
2724
2749
  uptime: process.uptime(),
2725
2750
  auth: {
2726
2751
  proxyTokenRequired,
@@ -3223,6 +3248,8 @@ var PREVIEW_PUSH_TYPES = /* @__PURE__ */ new Set([
3223
3248
  "device_preview_state_push",
3224
3249
  "device_preview_removed_push"
3225
3250
  ]);
3251
+ var DEFAULT_PROXY_ADMISSION_TIMEOUT_MS = 1e4;
3252
+ var RETRYABLE_PROXY_ADMISSION_TIMEOUT_CLOSE_CODE = 1013;
3226
3253
  function isPreviewEventMessage(message) {
3227
3254
  return PREVIEW_EVENT_TYPES.has(message.type);
3228
3255
  }
@@ -3303,28 +3330,60 @@ function rejectNotRegistered(ws) {
3303
3330
  })
3304
3331
  );
3305
3332
  }
3306
- function closeRejectedProxyProtocol(ws) {
3307
- ws.close(RelayCloseCode.PROXY_PROTOCOL_REJECTED, "proxy protocol rejected");
3333
+ function closeRejectedProxyProtocol(ws, direction = ProxyProtocolAdmissionDirection.PROTOCOL_MISMATCH) {
3334
+ if (ws.admissionPhase === "rejected") return;
3335
+ ws.admissionPhase = "rejected";
3336
+ ws.close(RelayCloseCode.PROXY_PROTOCOL_REJECTED, direction);
3337
+ }
3338
+ function classifyInitialProxyProtocol(raw) {
3339
+ let value;
3340
+ try {
3341
+ value = JSON.parse(raw);
3342
+ } catch {
3343
+ return ProxyProtocolAdmissionDirection.PROTOCOL_MISMATCH;
3344
+ }
3345
+ if (!value || typeof value !== "object" || value.type !== "proxy_register") {
3346
+ return ProxyProtocolAdmissionDirection.PROTOCOL_MISMATCH;
3347
+ }
3348
+ const direction = compareProxyRelayProtocolVersions(
3349
+ value.protocolVersion,
3350
+ RELAY_CONTROL_PROTOCOL_VERSION
3351
+ );
3352
+ return direction === ProxyProtocolAdmissionDirection.COMPATIBLE ? null : direction;
3308
3353
  }
3309
3354
  function isCurrentProxySocket(ws, registry) {
3310
3355
  return ws.proxyId !== void 0 && registry.getProxy(ws.proxyId) === ws;
3311
3356
  }
3312
- function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, sessionHistoryRoutes, webPreviewRoutes, devicePreviewBridge, chaos, remoteFileBridge) {
3357
+ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, sessionHistoryRoutes, webPreviewRoutes, devicePreviewBridge, chaos, remoteFileBridge, proxyAdmissionTimeoutMs = DEFAULT_PROXY_ADMISSION_TIMEOUT_MS) {
3313
3358
  const proxyWs = ws;
3314
3359
  proxyWs.isAlive = true;
3360
+ proxyWs.admissionPhase = "awaiting";
3315
3361
  let registrationCompleted = false;
3316
- let upgradeBootstrapStarted = false;
3317
3362
  let initialFrameReceived = false;
3363
+ const effectiveAdmissionTimeoutMs = Number.isSafeInteger(proxyAdmissionTimeoutMs) && proxyAdmissionTimeoutMs > 0 ? proxyAdmissionTimeoutMs : DEFAULT_PROXY_ADMISSION_TIMEOUT_MS;
3364
+ let admissionTimeout = setTimeout(() => {
3365
+ admissionTimeout = null;
3366
+ if (proxyWs.admissionPhase !== "awaiting" || proxyWs.readyState !== WebSocket4.OPEN) return;
3367
+ proxyWs.admissionPhase = "rejected";
3368
+ logger.warn({ timeoutMs: effectiveAdmissionTimeoutMs }, "Proxy registration timed out");
3369
+ proxyWs.close(RETRYABLE_PROXY_ADMISSION_TIMEOUT_CLOSE_CODE, "proxy registration timeout");
3370
+ }, effectiveAdmissionTimeoutMs);
3371
+ admissionTimeout.unref?.();
3372
+ const clearAdmissionTimeout = () => {
3373
+ if (!admissionTimeout) return;
3374
+ clearTimeout(admissionTimeout);
3375
+ admissionTimeout = null;
3376
+ };
3318
3377
  proxyWs.on("pong", () => {
3319
3378
  proxyWs.isAlive = true;
3320
3379
  });
3321
3380
  proxyWs.on("message", (data, isBinary) => {
3322
- if (upgradeBootstrapStarted) return;
3381
+ if (proxyWs.admissionPhase === "quarantined" || proxyWs.admissionPhase === "rejected") return;
3323
3382
  const isInitialFrame = !initialFrameReceived;
3324
3383
  initialFrameReceived = true;
3325
3384
  if (isBinary) {
3326
3385
  if (!proxyWs.proxyId) {
3327
- closeRejectedProxyProtocol(proxyWs);
3386
+ closeRejectedProxyProtocol(proxyWs, ProxyProtocolAdmissionDirection.PROTOCOL_MISMATCH);
3328
3387
  return;
3329
3388
  }
3330
3389
  if (!isCurrentProxySocket(proxyWs, registry)) {
@@ -3366,13 +3425,16 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, sessionH
3366
3425
  { size: data.length, proxyId: proxyWs.proxyId },
3367
3426
  "JSON message rejected: exceeds max size"
3368
3427
  );
3369
- if (!proxyWs.proxyId) closeRejectedProxyProtocol(proxyWs);
3428
+ if (!proxyWs.proxyId) {
3429
+ closeRejectedProxyProtocol(proxyWs, ProxyProtocolAdmissionDirection.PROTOCOL_MISMATCH);
3430
+ }
3370
3431
  return;
3371
3432
  }
3372
3433
  const raw = data.toString();
3373
3434
  const upgradeBootstrap = isInitialFrame ? parseProxyUpgradeBootstrapRequest(raw, RELAY_VERSION) : null;
3374
3435
  if (upgradeBootstrap) {
3375
- upgradeBootstrapStarted = true;
3436
+ proxyWs.admissionPhase = "quarantined";
3437
+ clearAdmissionTimeout();
3376
3438
  logger.info(
3377
3439
  {
3378
3440
  proxyId: upgradeBootstrap.proxyId,
@@ -3384,6 +3446,14 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, sessionH
3384
3446
  sendProxyUpgradeBootstrapResponse(proxyWs, RELAY_VERSION);
3385
3447
  return;
3386
3448
  }
3449
+ if (isInitialFrame) {
3450
+ const rejection = classifyInitialProxyProtocol(raw);
3451
+ if (rejection) {
3452
+ logger.warn({ direction: rejection }, "Proxy control protocol rejected before admission");
3453
+ closeRejectedProxyProtocol(proxyWs, rejection);
3454
+ return;
3455
+ }
3456
+ }
3387
3457
  const result = parseMessage(raw);
3388
3458
  if (result.kind === "control" && result.message.type === "proxy_register") {
3389
3459
  if (registrationCompleted) {
@@ -3391,10 +3461,12 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, sessionH
3391
3461
  { proxyId: proxyWs.proxyId, requestedProxyId: result.message.proxyId },
3392
3462
  "Repeated Proxy registration on the same socket rejected"
3393
3463
  );
3394
- closeRejectedProxyProtocol(proxyWs);
3464
+ closeRejectedProxyProtocol(proxyWs, ProxyProtocolAdmissionDirection.PROTOCOL_MISMATCH);
3395
3465
  return;
3396
3466
  }
3397
3467
  const { proxyId, name, proxyVersion } = result.message;
3468
+ proxyWs.admissionPhase = "ready";
3469
+ clearAdmissionTimeout();
3398
3470
  const status = registry.registerProxy(proxyId, proxyWs, proxyVersion, name);
3399
3471
  proxyWs.proxyId = proxyId;
3400
3472
  registrationCompleted = true;
@@ -3421,8 +3493,7 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, sessionH
3421
3493
  return;
3422
3494
  }
3423
3495
  if (!proxyWs.proxyId) {
3424
- rejectNotRegistered(proxyWs);
3425
- closeRejectedProxyProtocol(proxyWs);
3496
+ closeRejectedProxyProtocol(proxyWs, ProxyProtocolAdmissionDirection.PROTOCOL_MISMATCH);
3426
3497
  return;
3427
3498
  }
3428
3499
  if (!isCurrentProxySocket(proxyWs, registry)) {
@@ -3722,6 +3793,7 @@ function handleProxyConnection(ws, registry, logger, ptySnapshotRoutes, sessionH
3722
3793
  }
3723
3794
  });
3724
3795
  proxyWs.on("close", (code, reason) => {
3796
+ clearAdmissionTimeout();
3725
3797
  if (!proxyWs.proxyId) return;
3726
3798
  const closeMeta = { code, reason: reason.toString() || void 0 };
3727
3799
  const current = registry.getProxy(proxyWs.proxyId);
@@ -4486,15 +4558,45 @@ var DevicePreviewRouteRegistry = class extends PreviewRouteRegistry {
4486
4558
  }
4487
4559
  };
4488
4560
 
4489
- // src/handlers/client.ts
4490
- function isMalformedClientRegister(raw) {
4561
+ // src/client-registration-admission.ts
4562
+ function inspectClientRegistrationAdmission(raw) {
4563
+ let value;
4491
4564
  try {
4492
- const parsed = JSON.parse(raw);
4493
- return parsed !== null && typeof parsed === "object" && parsed.type === "client_register";
4565
+ value = JSON.parse(raw);
4494
4566
  } catch {
4495
- return false;
4567
+ return { kind: "not_client_registration" };
4568
+ }
4569
+ if (value === null || typeof value !== "object") {
4570
+ return { kind: "not_client_registration" };
4571
+ }
4572
+ const candidate = value;
4573
+ if (candidate.type !== "client_register") {
4574
+ return { kind: "not_client_registration" };
4575
+ }
4576
+ const clientId = typeof candidate.clientId === "string" ? candidate.clientId : void 0;
4577
+ if (!("protocolVersion" in candidate)) {
4578
+ return {
4579
+ kind: "unversioned_client_registration",
4580
+ ...clientId !== void 0 ? { clientId } : {}
4581
+ };
4582
+ }
4583
+ return {
4584
+ kind: "versioned_client_registration",
4585
+ ...clientId !== void 0 ? { clientId } : {},
4586
+ protocolVersion: candidate.protocolVersion
4587
+ };
4588
+ }
4589
+ function classifyClientRegistrationProtocol(clientVersion, relayVersion = RELAY_CONTROL_PROTOCOL_VERSION) {
4590
+ if (typeof clientVersion !== "number" || !Number.isSafeInteger(clientVersion) || clientVersion <= 0) {
4591
+ return RelayProtocolRejectReason.PROTOCOL_MISMATCH;
4496
4592
  }
4593
+ if (clientVersion === relayVersion) return null;
4594
+ return clientVersion < relayVersion ? RelayProtocolRejectReason.PAGE_OUTDATED : RelayProtocolRejectReason.SERVICE_OUTDATED;
4497
4595
  }
4596
+
4597
+ // src/handlers/client.ts
4598
+ var DEFAULT_CLIENT_ADMISSION_TIMEOUT_MS = 1e4;
4599
+ var RETRYABLE_CLIENT_ADMISSION_TIMEOUT_CLOSE_CODE = 1013;
4498
4600
  function handleClientRegister(registration, clientWs, registry, logger, webPreviewRoutes, devicePreviewBridge) {
4499
4601
  const { clientId } = registration;
4500
4602
  if (clientWs.clientId && clientWs.clientId !== clientId) {
@@ -4849,8 +4951,36 @@ function validatePreviewScope(ws, registry, requestId, scope) {
4849
4951
  rejectStaleBinding(ws, requestId);
4850
4952
  return false;
4851
4953
  }
4852
- function closeRejectedClientProtocol(ws) {
4853
- ws.close(RelayCloseCode.CLIENT_PROTOCOL_REJECTED, "client protocol rejected");
4954
+ function closeRejectedClientProtocol(ws, reason = RelayProtocolRejectReason.PROTOCOL_MISMATCH) {
4955
+ if (!beginClientRejection(ws)) return;
4956
+ ws.close(RelayCloseCode.CLIENT_PROTOCOL_REJECTED, reason);
4957
+ }
4958
+ function beginClientRejection(ws) {
4959
+ if (ws.admissionPhase === "rejected") return false;
4960
+ ws.admissionPhase = "rejected";
4961
+ return ws.readyState !== WebSocket7.CLOSING && ws.readyState !== WebSocket7.CLOSED;
4962
+ }
4963
+ function closeUnversionedClientProtocol(ws) {
4964
+ if (!beginClientRejection(ws)) return;
4965
+ ws.send(
4966
+ JSON.stringify({
4967
+ type: "relay_client_kicked",
4968
+ reason: "\u9875\u9762\u7248\u672C\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0"
4969
+ })
4970
+ );
4971
+ ws.close(RelayCloseCode.CLIENT_KICKED, "client refresh required");
4972
+ }
4973
+ function rejectVersionedClientProtocol(ws, clientProtocolVersion, reason, clientId, logger) {
4974
+ logger.warn(
4975
+ {
4976
+ ...clientId !== void 0 ? { clientId } : {},
4977
+ clientProtocolVersion,
4978
+ relayProtocolVersion: RELAY_CONTROL_PROTOCOL_VERSION,
4979
+ reason
4980
+ },
4981
+ "Client control protocol rejected at admission"
4982
+ );
4983
+ closeRejectedClientProtocol(ws, reason);
4854
4984
  }
4855
4985
  function rejectProxySelect(ws, requestId, proxyId) {
4856
4986
  ws.send(
@@ -4903,14 +5033,33 @@ function sendRemoteFileUploadUrlFailure(ws, requestId, sessionId, error, errorCo
4903
5033
  })
4904
5034
  );
4905
5035
  }
4906
- function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, sessionHistoryRoutes, webPreviewRoutes, devicePreviewBridge, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}) {
5036
+ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, sessionHistoryRoutes, webPreviewRoutes, devicePreviewBridge, chaos, voiceConfigStore, voiceProviders, remoteFileBridge, connectionInfo = {}, clientAdmissionTimeoutMs = DEFAULT_CLIENT_ADMISSION_TIMEOUT_MS) {
4907
5037
  const clientWs = ws;
4908
5038
  clientWs.isAlive = true;
5039
+ clientWs.admissionPhase = "awaiting";
4909
5040
  registry.addClientWs(clientWs, connectionInfo);
5041
+ const effectiveAdmissionTimeoutMs = Number.isSafeInteger(clientAdmissionTimeoutMs) && clientAdmissionTimeoutMs > 0 ? clientAdmissionTimeoutMs : DEFAULT_CLIENT_ADMISSION_TIMEOUT_MS;
5042
+ let admissionTimeout = setTimeout(() => {
5043
+ admissionTimeout = null;
5044
+ if (clientWs.admissionPhase !== "awaiting" || clientWs.readyState !== WebSocket7.OPEN) return;
5045
+ if (!beginClientRejection(clientWs)) return;
5046
+ logger.warn(
5047
+ { timeoutMs: effectiveAdmissionTimeoutMs, remoteAddress: connectionInfo.remoteAddress },
5048
+ "Client registration timed out"
5049
+ );
5050
+ clientWs.close(RETRYABLE_CLIENT_ADMISSION_TIMEOUT_CLOSE_CODE, "client registration timeout");
5051
+ }, effectiveAdmissionTimeoutMs);
5052
+ admissionTimeout.unref?.();
5053
+ const clearAdmissionTimeout = () => {
5054
+ if (!admissionTimeout) return;
5055
+ clearTimeout(admissionTimeout);
5056
+ admissionTimeout = null;
5057
+ };
4910
5058
  clientWs.on("pong", () => {
4911
5059
  clientWs.isAlive = true;
4912
5060
  });
4913
5061
  clientWs.on("message", (data, isBinary) => {
5062
+ if (clientWs.admissionPhase === "rejected") return;
4914
5063
  if (isBinary) {
4915
5064
  if (!clientWs.clientId) closeRejectedClientProtocol(clientWs);
4916
5065
  return;
@@ -4924,6 +5073,32 @@ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, session
4924
5073
  return;
4925
5074
  }
4926
5075
  const raw = data.toString();
5076
+ const registrationAdmission = inspectClientRegistrationAdmission(raw);
5077
+ if (registrationAdmission.kind === "unversioned_client_registration") {
5078
+ logger.warn(
5079
+ {
5080
+ ...registrationAdmission.clientId !== void 0 ? { clientId: registrationAdmission.clientId } : {}
5081
+ },
5082
+ "Unversioned client registration rejected"
5083
+ );
5084
+ closeUnversionedClientProtocol(clientWs);
5085
+ return;
5086
+ }
5087
+ if (registrationAdmission.kind === "versioned_client_registration") {
5088
+ const registrationProtocolIssue = classifyClientRegistrationProtocol(
5089
+ registrationAdmission.protocolVersion
5090
+ );
5091
+ if (registrationProtocolIssue) {
5092
+ rejectVersionedClientProtocol(
5093
+ clientWs,
5094
+ registrationAdmission.protocolVersion,
5095
+ registrationProtocolIssue,
5096
+ registrationAdmission.clientId,
5097
+ logger
5098
+ );
5099
+ return;
5100
+ }
5101
+ }
4927
5102
  const result = parseMessage(raw);
4928
5103
  if (result.kind === "control") {
4929
5104
  const msg = result.message;
@@ -4940,6 +5115,8 @@ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, session
4940
5115
  webPreviewRoutes,
4941
5116
  devicePreviewBridge
4942
5117
  );
5118
+ clientWs.admissionPhase = "ready";
5119
+ clearAdmissionTimeout();
4943
5120
  return;
4944
5121
  }
4945
5122
  if (!clientWs.clientId) {
@@ -5457,7 +5634,13 @@ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, session
5457
5634
  routeClientMessage(raw, clientWs.boundProxyId, clientWs, registry, logger, chaos);
5458
5635
  return;
5459
5636
  }
5460
- logger.error({ error: result.error, raw: raw.slice(0, 200) }, "Invalid message from client");
5637
+ const isRegistrationAttempt = registrationAdmission.kind !== "not_client_registration";
5638
+ const logContext = {
5639
+ error: result.error,
5640
+ raw: raw.slice(0, 200),
5641
+ ...registrationAdmission.kind !== "not_client_registration" && registrationAdmission.clientId !== void 0 ? { clientId: registrationAdmission.clientId } : {}
5642
+ };
5643
+ logger.error(logContext, "Invalid message from client");
5461
5644
  clientWs.send(
5462
5645
  JSON.stringify({
5463
5646
  type: "relay_error",
@@ -5465,11 +5648,13 @@ function handleClientConnection(ws, registry, logger, ptySnapshotRoutes, session
5465
5648
  message: `${result.error} | raw: ${raw.slice(0, 200)}`
5466
5649
  })
5467
5650
  );
5468
- if (isMalformedClientRegister(raw) || !clientWs.clientId) {
5651
+ if (isRegistrationAttempt || !clientWs.clientId) {
5469
5652
  closeRejectedClientProtocol(clientWs);
5470
5653
  }
5471
5654
  });
5472
5655
  clientWs.on("close", (code, reason) => {
5656
+ clearAdmissionTimeout();
5657
+ clientWs.admissionPhase = "rejected";
5473
5658
  ptySnapshotRoutes.abandonSocket(clientWs);
5474
5659
  sessionHistoryRoutes.abandonSocket(clientWs);
5475
5660
  webPreviewRoutes.abandonSocket(clientWs);
@@ -8568,7 +8753,8 @@ function createRelayServer(options) {
8568
8753
  webPreviewRoutes,
8569
8754
  devicePreviewBridge,
8570
8755
  relayChaos,
8571
- remoteFileBridge
8756
+ remoteFileBridge,
8757
+ options.proxyAdmissionTimeoutMs
8572
8758
  );
8573
8759
  });
8574
8760
  devicePreviewStreamWss.on("connection", (ws) => {
@@ -8590,7 +8776,8 @@ function createRelayServer(options) {
8590
8776
  {
8591
8777
  userAgent: firstHeaderValue(request.headers["user-agent"]),
8592
8778
  remoteAddress: requestRemoteAddress(request)
8593
- }
8779
+ },
8780
+ options.clientAdmissionTimeoutMs
8594
8781
  );
8595
8782
  });
8596
8783
  voiceAsrWss.on("connection", (ws) => {
@@ -8697,4 +8884,4 @@ export {
8697
8884
  parseRelayChaosFromEnv,
8698
8885
  createRelayServer
8699
8886
  };
8700
- //# sourceMappingURL=chunk-JLLMBFAB.js.map
8887
+ //# sourceMappingURL=chunk-PMXR723R.js.map