@nextclaw/remote 0.1.18 → 0.1.19

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