@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 +1 -1
- package/dist/index.js +372 -5
- package/package.json +4 -2
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
|
-
|
|
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({
|
|
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
|
-
|
|
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: "
|
|
492
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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"
|