@nextclaw/remote 0.1.17 → 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
@@ -21,7 +21,7 @@ var RemoteRuntimeActions = class {
21
21
  const result = this.deps.remoteCommands.enableConfig(opts);
22
22
  console.log("\u2713 Remote access enabled");
23
23
  if (result.config.remote.deviceName.trim()) {
24
- console.log(`Device: ${result.config.remote.deviceName.trim()}`);
24
+ console.log(`Instance: ${result.config.remote.deviceName.trim()}`);
25
25
  }
26
26
  if (result.config.remote.platformApiBase.trim()) {
27
27
  console.log(`Platform: ${result.config.remote.platformApiBase.trim()}`);
@@ -196,7 +196,7 @@ var RemotePlatformClient = class {
196
196
  };
197
197
  }
198
198
  async registerDevice(params) {
199
- const response = await fetch(`${params.platformBase}/platform/remote/devices/register`, {
199
+ const response = await fetch(`${params.platformBase}/platform/remote/instances/register`, {
200
200
  method: "POST",
201
201
  headers: {
202
202
  "content-type": "application/json",
@@ -211,8 +211,23 @@ var RemotePlatformClient = class {
211
211
  })
212
212
  });
213
213
  const payload = await response.json();
214
+ const instance = payload.data?.instance;
215
+ if (response.ok && payload.ok && instance) {
216
+ return {
217
+ id: instance.id,
218
+ deviceInstallId: instance.instanceInstallId,
219
+ displayName: instance.displayName,
220
+ platform: instance.platform,
221
+ appVersion: instance.appVersion,
222
+ localOrigin: instance.localOrigin,
223
+ status: instance.status,
224
+ lastSeenAt: instance.lastSeenAt,
225
+ createdAt: instance.createdAt,
226
+ updatedAt: instance.updatedAt
227
+ };
228
+ }
214
229
  if (!response.ok || !payload.ok || !payload.data?.device) {
215
- throw new Error(payload.error?.message ?? `Failed to register remote device (${response.status}).`);
230
+ throw new Error(payload.error?.message ?? `Failed to register remote instance (${response.status}).`);
216
231
  }
217
232
  return payload.data.device;
218
233
  }
@@ -388,6 +403,331 @@ var RemoteRelayBridge = class {
388
403
  }
389
404
  };
390
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
+
391
731
  // src/remote-connector.ts
392
732
  var RemoteConnector = class {
393
733
  constructor(deps) {
@@ -399,6 +739,7 @@ var RemoteConnector = class {
399
739
  async connectOnce(params) {
400
740
  return await new Promise((resolve, reject) => {
401
741
  const socket = new WebSocket(params.wsUrl);
742
+ const appAdapter = new RemoteAppAdapter(params.localOrigin, socket);
402
743
  let settled = false;
403
744
  let aborted = false;
404
745
  const cleanup = () => {
@@ -447,14 +788,24 @@ var RemoteConnector = class {
447
788
  lastError: null
448
789
  });
449
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
+ });
450
794
  });
451
795
  socket.addEventListener("message", (event) => {
452
- 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
+ });
453
802
  });
454
803
  socket.addEventListener("close", () => {
804
+ appAdapter.stop();
455
805
  finishResolve(aborted ? "aborted" : "closed");
456
806
  });
457
807
  socket.addEventListener("error", () => {
808
+ appAdapter.stop();
458
809
  if (aborted) {
459
810
  finishResolve("aborted");
460
811
  return;
@@ -470,11 +821,33 @@ var RemoteConnector = class {
470
821
  return;
471
822
  }
472
823
  try {
473
- 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);
474
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
+ }
475
847
  params.socket.send(JSON.stringify({
476
- type: "response.error",
477
- requestId: frame.requestId,
848
+ type: "client.stream.error",
849
+ clientId: frame.clientId,
850
+ streamId: frame.streamId,
478
851
  message: error instanceof Error ? error.message : String(error)
479
852
  }));
480
853
  }
@@ -483,7 +856,16 @@ var RemoteConnector = class {
483
856
  parseRelayFrame(data) {
484
857
  try {
485
858
  const frame = JSON.parse(String(data ?? ""));
486
- 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;
487
869
  } catch {
488
870
  return null;
489
871
  }
@@ -499,7 +881,7 @@ var RemoteConnector = class {
499
881
  displayName: params.context.displayName,
500
882
  localOrigin: params.context.localOrigin
501
883
  });
502
- this.logger.info(`\u2713 Remote device registered: ${device.displayName} (${device.id})`);
884
+ this.logger.info(`\u2713 Remote instance registered: ${device.displayName} (${device.id})`);
503
885
  this.logger.info(`\u2713 Local origin: ${params.context.localOrigin}`);
504
886
  this.logger.info(`\u2713 Platform: ${params.context.platformBase}`);
505
887
  return device;
@@ -519,7 +901,7 @@ var RemoteConnector = class {
519
901
  lastError: null
520
902
  });
521
903
  const device = await this.ensureDevice({ device: params.device, context: params.context });
522
- const wsUrl = `${params.context.platformBase.replace(/^http/i, "ws")}/platform/remote/connect?deviceId=${encodeURIComponent(device.id)}&token=${encodeURIComponent(params.context.token)}`;
904
+ const wsUrl = `${params.context.platformBase.replace(/^http/i, "ws")}/platform/remote/connect?instanceId=${encodeURIComponent(device.id)}&token=${encodeURIComponent(params.context.token)}`;
523
905
  const outcome = await this.connectOnce({
524
906
  wsUrl,
525
907
  relayBridge: params.relayBridge,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/remote",
3
- "version": "0.1.17",
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
- "@nextclaw/server": "0.10.21",
33
- "@nextclaw/core": "0.9.8"
32
+ "ws": "^8.18.0",
33
+ "@nextclaw/core": "0.9.8",
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"