@dev-anywhere/relay 0.4.80 → 0.4.82

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.
@@ -617,6 +617,21 @@ var relayControlDefinitions = [
617
617
  url: z8.string().optional(),
618
618
  expiresAt: z8.number().int().nonnegative().optional()
619
619
  }),
620
+ control("remote_file_metadata_request", {
621
+ ...RequiredRequestIdShape,
622
+ sessionId: IdSchema,
623
+ path: z8.string().min(1)
624
+ }),
625
+ control("remote_file_metadata_response", {
626
+ ...RequiredRequestIdShape,
627
+ ...RequestErrorShape,
628
+ sessionId: IdSchema,
629
+ path: z8.string().optional(),
630
+ success: z8.boolean(),
631
+ mimeType: z8.string().optional(),
632
+ size: z8.number().int().nonnegative().optional(),
633
+ fileName: z8.string().optional()
634
+ }),
620
635
  control("remote_file_stream_request", {
621
636
  streamId: IdSchema,
622
637
  sessionId: IdSchema,
@@ -1768,6 +1783,10 @@ function handleProxyConnection(ws, registry, logger, chaos, remoteFileBridge) {
1768
1783
  remoteFileBridge?.handleProxyControl(proxyWs.proxyId, result.message);
1769
1784
  return;
1770
1785
  }
1786
+ if (proxyWs.proxyId && result.message.type === "remote_file_metadata_response") {
1787
+ remoteFileBridge?.handleProxyControl(proxyWs.proxyId, result.message);
1788
+ return;
1789
+ }
1771
1790
  if (proxyWs.proxyId && result.message.type === "remote_file_stream_complete") {
1772
1791
  remoteFileBridge?.handleProxyControl(proxyWs.proxyId, result.message);
1773
1792
  return;
@@ -2773,24 +2792,44 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
2773
2792
  );
2774
2793
  return;
2775
2794
  }
2776
- const { url, expiresAt } = remoteFileBridge.createUrl({
2795
+ void remoteFileBridge.createUrl({
2777
2796
  clientId: clientWs.clientId,
2778
2797
  proxyId: targetProxyId,
2779
2798
  sessionId: msg.sessionId,
2780
2799
  path: msg.path,
2781
2800
  disposition: msg.disposition
2801
+ }).then((result2) => {
2802
+ if (clientWs.readyState !== WebSocket7.OPEN) return;
2803
+ if (!result2.success) {
2804
+ sendRemoteFileUrlFailure(
2805
+ clientWs,
2806
+ msg.requestId,
2807
+ msg.sessionId,
2808
+ result2.error,
2809
+ result2.errorCode
2810
+ );
2811
+ return;
2812
+ }
2813
+ clientWs.send(
2814
+ serializeControl({
2815
+ type: "remote_file_url_response",
2816
+ requestId: msg.requestId,
2817
+ sessionId: msg.sessionId,
2818
+ path: result2.path,
2819
+ success: true,
2820
+ url: result2.url,
2821
+ expiresAt: result2.expiresAt
2822
+ })
2823
+ );
2824
+ }).catch((err) => {
2825
+ if (clientWs.readyState !== WebSocket7.OPEN) return;
2826
+ sendRemoteFileUrlFailure(
2827
+ clientWs,
2828
+ msg.requestId,
2829
+ msg.sessionId,
2830
+ err instanceof Error ? err.message : String(err)
2831
+ );
2782
2832
  });
2783
- clientWs.send(
2784
- serializeControl({
2785
- type: "remote_file_url_response",
2786
- requestId: msg.requestId,
2787
- sessionId: msg.sessionId,
2788
- path: msg.path,
2789
- success: true,
2790
- url,
2791
- expiresAt
2792
- })
2793
- );
2794
2833
  return;
2795
2834
  }
2796
2835
  if (msg.type === "remote_file_upload_url_request") {
@@ -2993,12 +3032,20 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
2993
3032
  closeRejectedClientProtocol(clientWs);
2994
3033
  }
2995
3034
  });
2996
- clientWs.on("close", () => {
3035
+ clientWs.on("close", (code, reason) => {
2997
3036
  registry.removeClientWs(clientWs);
2998
3037
  if (clientWs.clientId) {
2999
3038
  registry.unbindClientById(clientWs.clientId);
3000
3039
  }
3001
- logger.info({ clientId: clientWs.clientId }, "Client disconnected");
3040
+ logger.info(
3041
+ {
3042
+ clientId: clientWs.clientId,
3043
+ boundProxyId: clientWs.boundProxyId,
3044
+ code,
3045
+ reason: reason.toString() || void 0
3046
+ },
3047
+ "Client disconnected"
3048
+ );
3002
3049
  });
3003
3050
  clientWs.on("error", (err) => {
3004
3051
  logger.error({ err }, "Client WebSocket error");
@@ -3009,7 +3056,7 @@ function handleClientConnection(ws, registry, logger, chaos, voiceConfigStore, v
3009
3056
  function markAlive(ws) {
3010
3057
  ws.isAlive = true;
3011
3058
  }
3012
- function setupHeartbeat(wss, interval = 3e4) {
3059
+ function setupHeartbeat(wss, interval = 3e4, options = {}) {
3013
3060
  wss.on("connection", (ws) => {
3014
3061
  markAlive(ws);
3015
3062
  ws.on("pong", () => {
@@ -3020,6 +3067,13 @@ function setupHeartbeat(wss, interval = 3e4) {
3020
3067
  for (const ws of wss.clients) {
3021
3068
  const sock = ws;
3022
3069
  if (sock.isAlive === false) {
3070
+ options.logger?.warn(
3071
+ {
3072
+ peerType: options.peerType,
3073
+ ...options.describePeer?.(ws) ?? {}
3074
+ },
3075
+ "WebSocket heartbeat timeout"
3076
+ );
3023
3077
  sock.terminate();
3024
3078
  continue;
3025
3079
  }
@@ -3578,8 +3632,10 @@ function encodeContentDisposition(disposition, fileName) {
3578
3632
  function statusForErrorCode(errorCode) {
3579
3633
  switch (errorCode) {
3580
3634
  case ControlErrorCode.SESSION_NOT_FOUND:
3635
+ case ControlErrorCode.PATH_NOT_FOUND:
3581
3636
  return 404;
3582
3637
  case ControlErrorCode.INVALID_PATH:
3638
+ case ControlErrorCode.PATH_NOT_DIRECTORY:
3583
3639
  return 400;
3584
3640
  case ControlErrorCode.PATH_ACCESS_DENIED:
3585
3641
  return 403;
@@ -3594,22 +3650,54 @@ var RemoteFileBridge = class {
3594
3650
  deps;
3595
3651
  tokens = /* @__PURE__ */ new Map();
3596
3652
  uploadTokens = /* @__PURE__ */ new Map();
3653
+ pendingUrlMetadata = /* @__PURE__ */ new Map();
3597
3654
  pendingStreams = /* @__PURE__ */ new Map();
3598
3655
  pendingUploads = /* @__PURE__ */ new Map();
3599
3656
  createUrl(input) {
3600
3657
  this.cleanupExpiredTokens();
3601
- const token = nanoid3(32);
3602
- const expiresAt = Date.now() + REMOTE_FILE_TOKEN_TTL_MS;
3603
- this.tokens.set(token, {
3604
- token,
3605
- clientId: input.clientId,
3606
- proxyId: input.proxyId,
3607
- sessionId: input.sessionId,
3608
- path: input.path,
3609
- disposition: input.disposition,
3610
- expiresAt
3658
+ const proxyWs = this.deps.registry.getProxy(input.proxyId);
3659
+ if (!proxyWs || proxyWs.readyState !== WebSocket11.OPEN) {
3660
+ return Promise.resolve({
3661
+ success: false,
3662
+ path: input.path,
3663
+ error: "\u5F53\u524D\u672A\u8FDE\u63A5\u5F00\u53D1\u673A",
3664
+ errorCode: ControlErrorCode.PROXY_OFFLINE
3665
+ });
3666
+ }
3667
+ const requestId = nanoid3(21);
3668
+ return new Promise((resolve2) => {
3669
+ const timer = setTimeout(() => {
3670
+ this.pendingUrlMetadata.delete(requestId);
3671
+ resolve2({
3672
+ success: false,
3673
+ path: input.path,
3674
+ error: "\u8BFB\u53D6\u6587\u4EF6\u5143\u6570\u636E\u8D85\u65F6",
3675
+ errorCode: ControlErrorCode.UNKNOWN
3676
+ });
3677
+ }, REMOTE_FILE_METADATA_TIMEOUT_MS);
3678
+ this.pendingUrlMetadata.set(requestId, {
3679
+ requestId,
3680
+ clientId: input.clientId,
3681
+ proxyId: input.proxyId,
3682
+ sessionId: input.sessionId,
3683
+ path: input.path,
3684
+ disposition: input.disposition,
3685
+ timer,
3686
+ resolve: resolve2
3687
+ });
3688
+ proxyWs.send(
3689
+ serializeControl({
3690
+ type: "remote_file_metadata_request",
3691
+ requestId,
3692
+ sessionId: input.sessionId,
3693
+ path: input.path
3694
+ })
3695
+ );
3696
+ this.deps.logger.info(
3697
+ { requestId, proxyId: input.proxyId, sessionId: input.sessionId, path: input.path },
3698
+ "Remote file URL metadata requested"
3699
+ );
3611
3700
  });
3612
- return { url: `/api/remote-files/${token}`, expiresAt };
3613
3701
  }
3614
3702
  createUploadUrl(input) {
3615
3703
  this.cleanupExpiredTokens();
@@ -3748,6 +3836,9 @@ var RemoteFileBridge = class {
3748
3836
  if (msg.type === "remote_file_upload_stream_response") {
3749
3837
  return this.handleUploadResponse(proxyId, msg);
3750
3838
  }
3839
+ if (msg.type === "remote_file_metadata_response") {
3840
+ return this.handleUrlMetadataResponse(proxyId, msg);
3841
+ }
3751
3842
  const pending = this.pendingStreams.get(msg.streamId);
3752
3843
  if (!pending) return true;
3753
3844
  if (pending.token.proxyId !== proxyId) {
@@ -3764,6 +3855,50 @@ var RemoteFileBridge = class {
3764
3855
  this.handleStreamComplete(pending, msg);
3765
3856
  return true;
3766
3857
  }
3858
+ issueUrlToken(input) {
3859
+ const token = nanoid3(32);
3860
+ const expiresAt = Date.now() + REMOTE_FILE_TOKEN_TTL_MS;
3861
+ this.tokens.set(token, {
3862
+ token,
3863
+ clientId: input.clientId,
3864
+ proxyId: input.proxyId,
3865
+ sessionId: input.sessionId,
3866
+ path: input.path,
3867
+ disposition: input.disposition,
3868
+ expiresAt
3869
+ });
3870
+ return { url: `/api/remote-files/${token}`, expiresAt };
3871
+ }
3872
+ handleUrlMetadataResponse(proxyId, msg) {
3873
+ const pending = this.pendingUrlMetadata.get(msg.requestId);
3874
+ if (!pending) return true;
3875
+ if (pending.proxyId !== proxyId) {
3876
+ this.deps.logger.warn(
3877
+ { requestId: msg.requestId, expectedProxyId: pending.proxyId, proxyId },
3878
+ "Remote file metadata ignored: proxy mismatch"
3879
+ );
3880
+ return true;
3881
+ }
3882
+ this.pendingUrlMetadata.delete(msg.requestId);
3883
+ clearTimeout(pending.timer);
3884
+ if (!msg.success) {
3885
+ pending.resolve({
3886
+ success: false,
3887
+ path: pending.path,
3888
+ error: msg.error ?? "\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25",
3889
+ ...msg.errorCode ? { errorCode: msg.errorCode } : {}
3890
+ });
3891
+ return true;
3892
+ }
3893
+ const { url, expiresAt } = this.issueUrlToken(pending);
3894
+ pending.resolve({
3895
+ success: true,
3896
+ path: pending.path,
3897
+ url,
3898
+ expiresAt
3899
+ });
3900
+ return true;
3901
+ }
3767
3902
  handleProxyBinary(proxyId, data) {
3768
3903
  const decoded = decodeFileStreamFrame(data);
3769
3904
  if (!decoded) return false;
@@ -4091,10 +4226,27 @@ function createRelayServer(options) {
4091
4226
  voiceTtsWss.on("connection", (ws) => {
4092
4227
  handleVoiceTtsConnection(ws, voiceConfigStore, logger, voiceProviders);
4093
4228
  });
4094
- const proxyHeartbeat = setupHeartbeat(proxyWss, heartbeatInterval);
4095
- const clientHeartbeat = setupHeartbeat(clientWss, heartbeatInterval);
4096
- const voiceAsrHeartbeat = setupHeartbeat(voiceAsrWss, heartbeatInterval);
4097
- const voiceTtsHeartbeat = setupHeartbeat(voiceTtsWss, heartbeatInterval);
4229
+ const proxyHeartbeat = setupHeartbeat(proxyWss, heartbeatInterval, {
4230
+ logger,
4231
+ peerType: "proxy",
4232
+ describePeer: (ws) => ({ proxyId: ws.proxyId })
4233
+ });
4234
+ const clientHeartbeat = setupHeartbeat(clientWss, heartbeatInterval, {
4235
+ logger,
4236
+ peerType: "client",
4237
+ describePeer: (ws) => ({
4238
+ clientId: ws.clientId,
4239
+ boundProxyId: ws.boundProxyId
4240
+ })
4241
+ });
4242
+ const voiceAsrHeartbeat = setupHeartbeat(voiceAsrWss, heartbeatInterval, {
4243
+ logger,
4244
+ peerType: "voice-asr"
4245
+ });
4246
+ const voiceTtsHeartbeat = setupHeartbeat(voiceTtsWss, heartbeatInterval, {
4247
+ logger,
4248
+ peerType: "voice-tts"
4249
+ });
4098
4250
  async function close() {
4099
4251
  clearInterval(proxyHeartbeat);
4100
4252
  clearInterval(clientHeartbeat);
@@ -4153,4 +4305,4 @@ export {
4153
4305
  parseRelayChaosFromEnv,
4154
4306
  createRelayServer
4155
4307
  };
4156
- //# sourceMappingURL=chunk-L2KEGMDB.js.map
4308
+ //# sourceMappingURL=chunk-5B2OTBDL.js.map