@nextclaw/remote 0.1.18 → 0.1.20

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/dist/index.d.ts CHANGED
@@ -153,7 +153,7 @@ declare class RemoteRelayBridge {
153
153
  ensureLocalUiHealthy(): Promise<void>;
154
154
  forward(frame: RelayRequestFrame, socket: WebSocket): Promise<void>;
155
155
  private createForwardHeaders;
156
- private requestBridgeCookie;
156
+ requestBridgeCookie(): Promise<string | null>;
157
157
  private sendStreamingResponse;
158
158
  }
159
159
 
package/dist/index.js CHANGED
@@ -403,6 +403,342 @@ var RemoteRelayBridge = class {
403
403
  }
404
404
  };
405
405
 
406
+ // src/remote-app.adapter.ts
407
+ import WebSocket2 from "ws";
408
+
409
+ // src/remote-app-stream.ts
410
+ function parseRemoteSseFrame(frame) {
411
+ const lines = frame.split("\n");
412
+ let event = "";
413
+ const dataLines = [];
414
+ for (const raw of lines) {
415
+ const line = raw.trimEnd();
416
+ if (!line || line.startsWith(":")) {
417
+ continue;
418
+ }
419
+ if (line.startsWith("event:")) {
420
+ event = line.slice(6).trim();
421
+ continue;
422
+ }
423
+ if (line.startsWith("data:")) {
424
+ dataLines.push(line.slice(5).trimStart());
425
+ }
426
+ }
427
+ if (!event) {
428
+ return null;
429
+ }
430
+ const data = dataLines.join("\n");
431
+ if (!data) {
432
+ return { event };
433
+ }
434
+ try {
435
+ return { event, payload: JSON.parse(data) };
436
+ } catch {
437
+ return { event, payload: data };
438
+ }
439
+ }
440
+ function readRemoteStreamError(payload, fallback) {
441
+ if (typeof payload === "object" && payload && "error" in payload) {
442
+ const typed = payload;
443
+ if (typed.error?.message) {
444
+ return typed.error.message;
445
+ }
446
+ }
447
+ if (typeof payload === "string" && payload.trim()) {
448
+ return payload.trim();
449
+ }
450
+ return fallback;
451
+ }
452
+ function processRemoteStreamFrame(params) {
453
+ const frame = parseRemoteSseFrame(params.rawFrame);
454
+ if (!frame) {
455
+ return;
456
+ }
457
+ if (frame.event === "final") {
458
+ params.setFinalResult(frame.payload);
459
+ return;
460
+ }
461
+ if (frame.event === "error") {
462
+ throw new Error(readRemoteStreamError(frame.payload, "stream failed"));
463
+ }
464
+ params.onEvent(frame);
465
+ }
466
+ function flushRemoteStreamFrames(params) {
467
+ let boundary = params.bufferState.value.indexOf("\n\n");
468
+ while (boundary !== -1) {
469
+ processRemoteStreamFrame({
470
+ rawFrame: params.bufferState.value.slice(0, boundary),
471
+ onEvent: params.onEvent,
472
+ setFinalResult: params.setFinalResult
473
+ });
474
+ params.bufferState.value = params.bufferState.value.slice(boundary + 2);
475
+ boundary = params.bufferState.value.indexOf("\n\n");
476
+ }
477
+ }
478
+ async function readRemoteAppStreamResult(params) {
479
+ const reader = params.response.body?.getReader();
480
+ if (!reader) {
481
+ throw new Error("SSE response body unavailable.");
482
+ }
483
+ const decoder = new TextDecoder();
484
+ const bufferState = { value: "" };
485
+ let finalResult = void 0;
486
+ try {
487
+ while (true) {
488
+ const { value, done } = await reader.read();
489
+ if (done) {
490
+ break;
491
+ }
492
+ bufferState.value += decoder.decode(value, { stream: true });
493
+ flushRemoteStreamFrames({
494
+ bufferState,
495
+ onEvent: params.onEvent,
496
+ setFinalResult: (nextValue) => {
497
+ finalResult = nextValue;
498
+ }
499
+ });
500
+ }
501
+ if (bufferState.value.trim()) {
502
+ processRemoteStreamFrame({
503
+ rawFrame: bufferState.value,
504
+ onEvent: params.onEvent,
505
+ setFinalResult: (nextValue) => {
506
+ finalResult = nextValue;
507
+ }
508
+ });
509
+ }
510
+ } finally {
511
+ reader.releaseLock();
512
+ }
513
+ if (finalResult === void 0) {
514
+ throw new Error("stream ended without final event");
515
+ }
516
+ return finalResult;
517
+ }
518
+
519
+ // src/remote-app.adapter.ts
520
+ function toWebSocketUrl(origin, path) {
521
+ const normalizedOrigin = origin.replace(/\/$/, "");
522
+ if (normalizedOrigin.startsWith("https://")) {
523
+ return `${normalizedOrigin.replace(/^https:/, "wss:")}${path}`;
524
+ }
525
+ if (normalizedOrigin.startsWith("http://")) {
526
+ return `${normalizedOrigin.replace(/^http:/, "ws:")}${path}`;
527
+ }
528
+ return `${normalizedOrigin}${path}`;
529
+ }
530
+ function readErrorMessage(body, fallback) {
531
+ if (typeof body === "object" && body && "error" in body) {
532
+ const typed = body;
533
+ if (typed.error?.message) {
534
+ return typed.error.message;
535
+ }
536
+ }
537
+ if (typeof body === "string" && body.trim()) {
538
+ return body.trim();
539
+ }
540
+ return fallback;
541
+ }
542
+ var RemoteAppAdapter = class {
543
+ constructor(localOrigin, platformSocket) {
544
+ this.localOrigin = localOrigin;
545
+ this.platformSocket = platformSocket;
546
+ this.relayBridge = new RemoteRelayBridge(localOrigin);
547
+ }
548
+ relayBridge;
549
+ activeStreams = /* @__PURE__ */ new Map();
550
+ localEventSocket = null;
551
+ eventReconnectTimer = null;
552
+ shuttingDown = false;
553
+ async start() {
554
+ await this.ensureEventSocket();
555
+ }
556
+ stop() {
557
+ this.shuttingDown = true;
558
+ if (this.eventReconnectTimer) {
559
+ clearTimeout(this.eventReconnectTimer);
560
+ this.eventReconnectTimer = null;
561
+ }
562
+ this.localEventSocket?.close();
563
+ this.localEventSocket = null;
564
+ for (const controller of this.activeStreams.values()) {
565
+ controller.abort();
566
+ }
567
+ this.activeStreams.clear();
568
+ }
569
+ async handle(frame) {
570
+ if (frame.type === "client.request") {
571
+ await this.handleRequest(frame);
572
+ return;
573
+ }
574
+ if (frame.type === "client.stream.open") {
575
+ void this.handleStream(frame);
576
+ return;
577
+ }
578
+ if (frame.type === "client.stream.cancel") {
579
+ this.activeStreams.get(frame.streamId)?.abort();
580
+ this.activeStreams.delete(frame.streamId);
581
+ }
582
+ }
583
+ async handleRequest(frame) {
584
+ const bridgeCookie = await this.relayBridge.requestBridgeCookie();
585
+ const response = await fetch(new URL(frame.target.path, this.localOrigin), {
586
+ method: frame.target.method,
587
+ headers: this.createJsonHeaders(bridgeCookie),
588
+ body: this.buildRequestBody(frame.target)
589
+ });
590
+ const body = await this.readResponseBody(response);
591
+ this.send({
592
+ type: "client.response",
593
+ clientId: frame.clientId,
594
+ id: frame.id,
595
+ status: response.status,
596
+ body
597
+ });
598
+ }
599
+ async handleStream(frame) {
600
+ const controller = new AbortController();
601
+ this.activeStreams.set(frame.streamId, controller);
602
+ try {
603
+ const response = await this.openStreamResponse(frame, controller);
604
+ if (!response) {
605
+ return;
606
+ }
607
+ const finalResult = await readRemoteAppStreamResult({
608
+ response,
609
+ onEvent: (event) => {
610
+ this.send({
611
+ type: "client.stream.event",
612
+ clientId: frame.clientId,
613
+ streamId: frame.streamId,
614
+ event: event.event,
615
+ payload: event.payload
616
+ });
617
+ }
618
+ });
619
+ this.send({
620
+ type: "client.stream.end",
621
+ clientId: frame.clientId,
622
+ streamId: frame.streamId,
623
+ result: finalResult
624
+ });
625
+ } catch (error) {
626
+ if (controller.signal.aborted) {
627
+ return;
628
+ }
629
+ this.send({
630
+ type: "client.stream.error",
631
+ clientId: frame.clientId,
632
+ streamId: frame.streamId,
633
+ message: error instanceof Error ? error.message : String(error)
634
+ });
635
+ } finally {
636
+ this.activeStreams.delete(frame.streamId);
637
+ }
638
+ }
639
+ async openStreamResponse(frame, controller) {
640
+ const bridgeCookie = await this.relayBridge.requestBridgeCookie();
641
+ const response = await fetch(new URL(frame.target.path, this.localOrigin), {
642
+ method: frame.target.method,
643
+ headers: this.createStreamHeaders(bridgeCookie),
644
+ body: this.buildRequestBody(frame.target),
645
+ signal: controller.signal
646
+ });
647
+ if (response.ok) {
648
+ return response;
649
+ }
650
+ const errorBody = await this.readResponseBody(response);
651
+ this.send({
652
+ type: "client.stream.error",
653
+ clientId: frame.clientId,
654
+ streamId: frame.streamId,
655
+ message: readErrorMessage(errorBody, `HTTP ${response.status}`)
656
+ });
657
+ return null;
658
+ }
659
+ async ensureEventSocket() {
660
+ if (this.localEventSocket && this.localEventSocket.readyState === WebSocket2.OPEN) {
661
+ return;
662
+ }
663
+ const bridgeCookie = await this.relayBridge.requestBridgeCookie();
664
+ await new Promise((resolve, reject) => {
665
+ const socket = new WebSocket2(toWebSocketUrl(this.localOrigin, "/ws"), {
666
+ headers: bridgeCookie ? { Cookie: bridgeCookie } : void 0
667
+ });
668
+ this.localEventSocket = socket;
669
+ socket.on("open", () => resolve());
670
+ socket.on("message", (data) => {
671
+ try {
672
+ const event = JSON.parse(String(data ?? ""));
673
+ this.send({
674
+ type: "client.event",
675
+ event
676
+ });
677
+ } catch (error) {
678
+ console.error("Failed to parse local ui event:", error);
679
+ }
680
+ });
681
+ socket.on("close", () => {
682
+ this.localEventSocket = null;
683
+ if (!this.shuttingDown) {
684
+ this.scheduleEventReconnect();
685
+ }
686
+ });
687
+ socket.on("error", (error) => {
688
+ if (!this.shuttingDown) {
689
+ reject(error instanceof Error ? error : new Error(String(error)));
690
+ }
691
+ });
692
+ });
693
+ }
694
+ scheduleEventReconnect() {
695
+ if (this.eventReconnectTimer) {
696
+ return;
697
+ }
698
+ this.eventReconnectTimer = setTimeout(() => {
699
+ this.eventReconnectTimer = null;
700
+ void this.ensureEventSocket().catch(() => void 0);
701
+ }, 3e3);
702
+ }
703
+ buildRequestBody(target) {
704
+ if (target.method === "GET" || target.method === "HEAD") {
705
+ return void 0;
706
+ }
707
+ if (target.body === void 0) {
708
+ return void 0;
709
+ }
710
+ return new TextEncoder().encode(JSON.stringify(target.body));
711
+ }
712
+ createJsonHeaders(bridgeCookie) {
713
+ const headers = new Headers({
714
+ "Content-Type": "application/json"
715
+ });
716
+ if (bridgeCookie) {
717
+ headers.set("cookie", bridgeCookie);
718
+ }
719
+ return headers;
720
+ }
721
+ createStreamHeaders(bridgeCookie) {
722
+ const headers = this.createJsonHeaders(bridgeCookie);
723
+ headers.set("Accept", "text/event-stream");
724
+ return headers;
725
+ }
726
+ async readResponseBody(response) {
727
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
728
+ if (contentType.includes("application/json")) {
729
+ return await response.json();
730
+ }
731
+ const text = await response.text();
732
+ return text;
733
+ }
734
+ send(frame) {
735
+ if (this.platformSocket.readyState !== WebSocket2.OPEN) {
736
+ return;
737
+ }
738
+ this.platformSocket.send(JSON.stringify(frame));
739
+ }
740
+ };
741
+
406
742
  // src/remote-connector.ts
407
743
  var RemoteConnector = class {
408
744
  constructor(deps) {
@@ -414,6 +750,7 @@ var RemoteConnector = class {
414
750
  async connectOnce(params) {
415
751
  return await new Promise((resolve, reject) => {
416
752
  const socket = new WebSocket(params.wsUrl);
753
+ const appAdapter = new RemoteAppAdapter(params.localOrigin, socket);
417
754
  let settled = false;
418
755
  let aborted = false;
419
756
  const cleanup = () => {
@@ -462,14 +799,24 @@ var RemoteConnector = class {
462
799
  lastError: null
463
800
  });
464
801
  this.logger.info(`\u2713 Remote connector connected: ${redactWsUrl(params.wsUrl)}`);
802
+ void appAdapter.start().catch((error) => {
803
+ this.logger.error(`Remote event bridge error: ${error instanceof Error ? error.message : String(error)}`);
804
+ });
465
805
  });
466
806
  socket.addEventListener("message", (event) => {
467
- this.handleSocketMessage({ data: event.data, relayBridge: params.relayBridge, socket });
807
+ this.handleSocketMessage({
808
+ data: event.data,
809
+ relayBridge: params.relayBridge,
810
+ appAdapter,
811
+ socket
812
+ });
468
813
  });
469
814
  socket.addEventListener("close", () => {
815
+ appAdapter.stop();
470
816
  finishResolve(aborted ? "aborted" : "closed");
471
817
  });
472
818
  socket.addEventListener("error", () => {
819
+ appAdapter.stop();
473
820
  if (aborted) {
474
821
  finishResolve("aborted");
475
822
  return;
@@ -485,11 +832,33 @@ var RemoteConnector = class {
485
832
  return;
486
833
  }
487
834
  try {
488
- await params.relayBridge.forward(frame, params.socket);
835
+ if (frame.type === "request") {
836
+ await params.relayBridge.forward(frame, params.socket);
837
+ return;
838
+ }
839
+ await params.appAdapter.handle(frame);
489
840
  } catch (error) {
841
+ if (frame.type === "request") {
842
+ params.socket.send(JSON.stringify({
843
+ type: "response.error",
844
+ requestId: frame.requestId,
845
+ message: error instanceof Error ? error.message : String(error)
846
+ }));
847
+ return;
848
+ }
849
+ if (frame.type === "client.request") {
850
+ params.socket.send(JSON.stringify({
851
+ type: "client.request.error",
852
+ clientId: frame.clientId,
853
+ id: frame.id,
854
+ message: error instanceof Error ? error.message : String(error)
855
+ }));
856
+ return;
857
+ }
490
858
  params.socket.send(JSON.stringify({
491
- type: "response.error",
492
- requestId: frame.requestId,
859
+ type: "client.stream.error",
860
+ clientId: frame.clientId,
861
+ streamId: frame.streamId,
493
862
  message: error instanceof Error ? error.message : String(error)
494
863
  }));
495
864
  }
@@ -498,7 +867,16 @@ var RemoteConnector = class {
498
867
  parseRelayFrame(data) {
499
868
  try {
500
869
  const frame = JSON.parse(String(data ?? ""));
501
- return frame.type === "request" ? frame : null;
870
+ if (typeof frame !== "object" || !frame || typeof frame.type !== "string") {
871
+ return null;
872
+ }
873
+ if (frame.type === "request") {
874
+ return frame;
875
+ }
876
+ if (frame.type === "client.request" || frame.type === "client.stream.open" || frame.type === "client.stream.cancel") {
877
+ return frame;
878
+ }
879
+ return null;
502
880
  } catch {
503
881
  return null;
504
882
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/remote",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "description": "Remote access runtime for NextClaw device registration, relay bridging, and service-managed connectivity.",
6
6
  "type": "module",
@@ -29,11 +29,13 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "commander": "^12.1.0",
32
- "@nextclaw/core": "0.9.8",
33
- "@nextclaw/server": "0.10.22"
32
+ "ws": "^8.18.0",
33
+ "@nextclaw/server": "0.10.24",
34
+ "@nextclaw/core": "0.9.8"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/node": "^20.17.6",
38
+ "@types/ws": "^8.5.14",
37
39
  "prettier": "^3.3.3",
38
40
  "tsup": "^8.3.5",
39
41
  "typescript": "^5.6.3"