@nextclaw/remote 0.1.19 → 0.1.21
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.js +157 -122
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -405,17 +405,9 @@ var RemoteRelayBridge = class {
|
|
|
405
405
|
|
|
406
406
|
// src/remote-app.adapter.ts
|
|
407
407
|
import WebSocket2 from "ws";
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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) {
|
|
408
|
+
|
|
409
|
+
// src/remote-app-stream.ts
|
|
410
|
+
function parseRemoteSseFrame(frame) {
|
|
419
411
|
const lines = frame.split("\n");
|
|
420
412
|
let event = "";
|
|
421
413
|
const dataLines = [];
|
|
@@ -440,16 +432,100 @@ function parseSseFrame(frame) {
|
|
|
440
432
|
return { event };
|
|
441
433
|
}
|
|
442
434
|
try {
|
|
443
|
-
return {
|
|
444
|
-
event,
|
|
445
|
-
payload: JSON.parse(data)
|
|
446
|
-
};
|
|
435
|
+
return { event, payload: JSON.parse(data) };
|
|
447
436
|
} catch {
|
|
448
|
-
return {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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;
|
|
452
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}`;
|
|
453
529
|
}
|
|
454
530
|
function readErrorMessage(body, fallback) {
|
|
455
531
|
if (typeof body === "object" && body && "error" in body) {
|
|
@@ -524,113 +600,28 @@ var RemoteAppAdapter = class {
|
|
|
524
600
|
const controller = new AbortController();
|
|
525
601
|
this.activeStreams.set(frame.streamId, controller);
|
|
526
602
|
try {
|
|
527
|
-
const
|
|
528
|
-
|
|
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
|
-
});
|
|
603
|
+
const response = await this.openStreamResponse(frame, controller);
|
|
604
|
+
if (!response) {
|
|
552
605
|
return;
|
|
553
606
|
}
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
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) {
|
|
607
|
+
const finalResult = await readRemoteAppStreamResult({
|
|
608
|
+
response,
|
|
609
|
+
onEvent: (event) => {
|
|
617
610
|
this.send({
|
|
618
|
-
type: "client.stream.
|
|
611
|
+
type: "client.stream.event",
|
|
619
612
|
clientId: frame.clientId,
|
|
620
613
|
streamId: frame.streamId,
|
|
621
|
-
|
|
614
|
+
event: event.event,
|
|
615
|
+
payload: event.payload
|
|
622
616
|
});
|
|
623
|
-
return;
|
|
624
617
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
632
|
-
reader.releaseLock();
|
|
633
|
-
}
|
|
618
|
+
});
|
|
619
|
+
this.send({
|
|
620
|
+
type: "client.stream.end",
|
|
621
|
+
clientId: frame.clientId,
|
|
622
|
+
streamId: frame.streamId,
|
|
623
|
+
result: finalResult
|
|
624
|
+
});
|
|
634
625
|
} catch (error) {
|
|
635
626
|
if (controller.signal.aborted) {
|
|
636
627
|
return;
|
|
@@ -645,6 +636,26 @@ var RemoteAppAdapter = class {
|
|
|
645
636
|
this.activeStreams.delete(frame.streamId);
|
|
646
637
|
}
|
|
647
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
|
+
}
|
|
648
659
|
async ensureEventSocket() {
|
|
649
660
|
if (this.localEventSocket && this.localEventSocket.readyState === WebSocket2.OPEN) {
|
|
650
661
|
return;
|
|
@@ -728,6 +739,19 @@ var RemoteAppAdapter = class {
|
|
|
728
739
|
}
|
|
729
740
|
};
|
|
730
741
|
|
|
742
|
+
// src/remote-connector-error.ts
|
|
743
|
+
var TERMINAL_REMOTE_ERROR_PATTERNS = [
|
|
744
|
+
/invalid or expired token/i,
|
|
745
|
+
/missing bearer token/i,
|
|
746
|
+
/token expired/i,
|
|
747
|
+
/token is invalid/i,
|
|
748
|
+
/run "nextclaw login"/i
|
|
749
|
+
];
|
|
750
|
+
function isTerminalRemoteConnectorError(error) {
|
|
751
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
752
|
+
return TERMINAL_REMOTE_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
753
|
+
}
|
|
754
|
+
|
|
731
755
|
// src/remote-connector.ts
|
|
732
756
|
var RemoteConnector = class {
|
|
733
757
|
constructor(deps) {
|
|
@@ -923,7 +947,7 @@ var RemoteConnector = class {
|
|
|
923
947
|
lastError: null
|
|
924
948
|
});
|
|
925
949
|
}
|
|
926
|
-
return { device,
|
|
950
|
+
return { device, outcome: outcome === "aborted" ? "aborted" : "retry" };
|
|
927
951
|
} catch (error) {
|
|
928
952
|
const message = error instanceof Error ? error.message : String(error);
|
|
929
953
|
this.writeRemoteState(params.opts.statusStore, {
|
|
@@ -936,7 +960,10 @@ var RemoteConnector = class {
|
|
|
936
960
|
lastError: message
|
|
937
961
|
});
|
|
938
962
|
this.logger.error(`Remote connector error: ${message}`);
|
|
939
|
-
return {
|
|
963
|
+
return {
|
|
964
|
+
device: params.device,
|
|
965
|
+
outcome: isTerminalRemoteConnectorError(error) ? "stop" : "retry"
|
|
966
|
+
};
|
|
940
967
|
}
|
|
941
968
|
}
|
|
942
969
|
async run(opts = {}) {
|
|
@@ -946,10 +973,15 @@ var RemoteConnector = class {
|
|
|
946
973
|
);
|
|
947
974
|
await relayBridge.ensureLocalUiHealthy();
|
|
948
975
|
let device = null;
|
|
976
|
+
let preserveRuntimeError = false;
|
|
949
977
|
while (!opts.signal?.aborted) {
|
|
950
978
|
const cycle = await this.runCycle({ device, context, relayBridge, opts });
|
|
951
979
|
device = cycle.device;
|
|
952
|
-
if (cycle.
|
|
980
|
+
if (cycle.outcome === "stop") {
|
|
981
|
+
preserveRuntimeError = true;
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
if (cycle.outcome === "aborted" || !context.autoReconnect || opts.signal?.aborted) {
|
|
953
985
|
break;
|
|
954
986
|
}
|
|
955
987
|
this.logger.warn("Remote connector disconnected. Reconnecting in 3s...");
|
|
@@ -959,6 +991,9 @@ var RemoteConnector = class {
|
|
|
959
991
|
break;
|
|
960
992
|
}
|
|
961
993
|
}
|
|
994
|
+
if (preserveRuntimeError) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
962
997
|
this.writeRemoteState(opts.statusStore, {
|
|
963
998
|
enabled: opts.mode === "service" ? true : Boolean(context.config.remote.enabled),
|
|
964
999
|
state: opts.signal?.aborted ? "disconnected" : "disabled",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/remote",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Remote access runtime for NextClaw device registration, relay bridging, and service-managed connectivity.",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"commander": "^12.1.0",
|
|
32
32
|
"ws": "^8.18.0",
|
|
33
33
|
"@nextclaw/core": "0.9.8",
|
|
34
|
-
"@nextclaw/server": "0.10.
|
|
34
|
+
"@nextclaw/server": "0.10.25"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^20.17.6",
|