@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.
Files changed (2) hide show
  1. package/dist/index.js +157 -122
  2. 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
- 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) {
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
- event,
450
- payload: data
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 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
- });
603
+ const response = await this.openStreamResponse(frame, controller);
604
+ if (!response) {
552
605
  return;
553
606
  }
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) {
607
+ const finalResult = await readRemoteAppStreamResult({
608
+ response,
609
+ onEvent: (event) => {
617
610
  this.send({
618
- type: "client.stream.error",
611
+ type: "client.stream.event",
619
612
  clientId: frame.clientId,
620
613
  streamId: frame.streamId,
621
- message: "stream ended without final event"
614
+ event: event.event,
615
+ payload: event.payload
622
616
  });
623
- return;
624
617
  }
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
- }
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, aborted: outcome === "aborted" };
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 { device: params.device, aborted: false };
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.aborted || !context.autoReconnect || opts.signal?.aborted) {
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.19",
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.23"
34
+ "@nextclaw/server": "0.10.25"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^20.17.6",