@nextclaw/remote 0.1.77 → 0.1.78

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 (3) hide show
  1. package/dist/index.d.ts +186 -177
  2. package/dist/index.js +1012 -1190
  3. package/package.json +4 -5
package/dist/index.js CHANGED
@@ -1,1260 +1,1082 @@
1
- // src/register-remote-commands.ts
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { hostname, platform } from "node:os";
4
+ import { ensureUiBridgeSecret } from "@nextclaw/server";
5
+ import WebSocket$1 from "ws";
6
+ //#region src/register-remote-commands.ts
2
7
  function registerRemoteCommands(program, runtime) {
3
- const remote = program.command("remote").description("Manage remote access");
4
- remote.command("enable").description("Enable service-managed remote access").option("--api-base <url>", "Platform API base (supports /v1 suffix)").option("--name <name>", "Device display name").action(async (opts) => runtime.enable(opts));
5
- remote.command("disable").description("Disable service-managed remote access").action(async () => runtime.disable());
6
- remote.command("status").description("Show remote access status").option("--json", "Print JSON").action(async (opts) => runtime.status(opts));
7
- remote.command("doctor").description("Run remote access diagnostics").option("--json", "Print JSON").action(async (opts) => runtime.doctor(opts));
8
- remote.command("connect").description("Foreground debug mode: register this machine and keep the connector online").option("--api-base <url>", "Platform API base (supports /v1 suffix)").option("--local-origin <url>", "Local NextClaw UI origin (default: active service or http://127.0.0.1:55667)").option("--name <name>", "Device display name").option("--once", "Connect once without auto-reconnect", false).action(async (opts) => runtime.connect(opts));
8
+ const remote = program.command("remote").description("Manage remote access");
9
+ remote.command("enable").description("Enable service-managed remote access").option("--api-base <url>", "Platform API base (supports /v1 suffix)").option("--name <name>", "Device display name").action(async (opts) => runtime.enable(opts));
10
+ remote.command("disable").description("Disable service-managed remote access").action(async () => runtime.disable());
11
+ remote.command("status").description("Show remote access status").option("--json", "Print JSON").action(async (opts) => runtime.status(opts));
12
+ remote.command("doctor").description("Run remote access diagnostics").option("--json", "Print JSON").action(async (opts) => runtime.doctor(opts));
13
+ remote.command("connect").description("Foreground debug mode: register this machine and keep the connector online").option("--api-base <url>", "Platform API base (supports /v1 suffix)").option("--local-origin <url>", "Local NextClaw UI origin (default: active service or http://127.0.0.1:55667)").option("--name <name>", "Device display name").option("--once", "Connect once without auto-reconnect", false).action(async (opts) => runtime.connect(opts));
9
14
  }
10
-
11
- // src/remote-runtime-actions.ts
15
+ //#endregion
16
+ //#region src/remote-runtime-actions.ts
12
17
  var RemoteRuntimeActions = class {
13
- constructor(deps) {
14
- this.deps = deps;
15
- }
16
- async connect(opts = {}) {
17
- await this.deps.remoteCommands.connect(opts);
18
- }
19
- async enable(opts = {}) {
20
- await this.deps.initAuto("remote enable");
21
- const result = this.deps.remoteCommands.enableConfig(opts);
22
- console.log("\u2713 Remote access enabled");
23
- if (result.config.remote.deviceName.trim()) {
24
- console.log(`Instance: ${result.config.remote.deviceName.trim()}`);
25
- }
26
- if (result.config.remote.platformApiBase.trim()) {
27
- console.log(`Platform: ${result.config.remote.platformApiBase.trim()}`);
28
- }
29
- if (this.deps.hasRunningManagedService()) {
30
- await this.deps.restartBackgroundService("remote configuration updated");
31
- console.log("\u2713 Applied remote settings to running background service");
32
- return;
33
- }
34
- console.log(`Tip: Run "${this.deps.appName} start" to bring the managed remote connector online.`);
35
- }
36
- async disable() {
37
- const result = this.deps.remoteCommands.disableConfig();
38
- console.log(result.changed ? "\u2713 Remote access disabled" : "Remote access was already disabled");
39
- if (this.deps.hasRunningManagedService()) {
40
- await this.deps.restartBackgroundService("remote access disabled");
41
- console.log("\u2713 Running background service restarted without remote access");
42
- }
43
- }
44
- async status(opts = {}) {
45
- await this.deps.remoteCommands.status(opts);
46
- }
47
- async doctor(opts = {}) {
48
- await this.deps.remoteCommands.doctor(opts);
49
- }
18
+ constructor(deps) {
19
+ this.deps = deps;
20
+ }
21
+ async connect(opts = {}) {
22
+ await this.deps.remoteCommands.connect(opts);
23
+ }
24
+ async enable(opts = {}) {
25
+ await this.deps.initAuto("remote enable");
26
+ const result = this.deps.remoteCommands.enableConfig(opts);
27
+ console.log(" Remote access enabled");
28
+ if (result.config.remote.deviceName.trim()) console.log(`Instance: ${result.config.remote.deviceName.trim()}`);
29
+ if (result.config.remote.platformApiBase.trim()) console.log(`Platform: ${result.config.remote.platformApiBase.trim()}`);
30
+ if (this.deps.hasRunningManagedService()) {
31
+ await this.deps.restartBackgroundService("remote configuration updated");
32
+ console.log("✓ Applied remote settings to running background service");
33
+ return;
34
+ }
35
+ console.log(`Tip: Run "${this.deps.appName} start" to bring the managed remote connector online.`);
36
+ }
37
+ async disable() {
38
+ const result = this.deps.remoteCommands.disableConfig();
39
+ console.log(result.changed ? " Remote access disabled" : "Remote access was already disabled");
40
+ if (this.deps.hasRunningManagedService()) {
41
+ await this.deps.restartBackgroundService("remote access disabled");
42
+ console.log("✓ Running background service restarted without remote access");
43
+ }
44
+ }
45
+ async status(opts = {}) {
46
+ await this.deps.remoteCommands.status(opts);
47
+ }
48
+ async doctor(opts = {}) {
49
+ await this.deps.remoteCommands.doctor(opts);
50
+ }
50
51
  };
51
-
52
- // src/remote-platform-client.ts
53
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
54
- import { dirname, join } from "path";
55
- import { hostname, platform as readPlatform } from "os";
56
-
57
- // src/platform-session-token.ts
52
+ //#endregion
53
+ //#region src/platform-session-token.ts
58
54
  function decodeBase64UrlSegment(segment) {
59
- try {
60
- return Buffer.from(segment, "base64url").toString("utf-8");
61
- } catch {
62
- return null;
63
- }
55
+ try {
56
+ return Buffer.from(segment, "base64url").toString("utf-8");
57
+ } catch {
58
+ return null;
59
+ }
64
60
  }
65
61
  function decodePlatformSessionTokenPayload(token) {
66
- const segments = token.split(".");
67
- if (segments.length !== 3 || segments[0] !== "nca" || !segments[1]) {
68
- return null;
69
- }
70
- const raw = decodeBase64UrlSegment(segments[1]);
71
- if (!raw) {
72
- return null;
73
- }
74
- try {
75
- const parsed = JSON.parse(raw);
76
- return typeof parsed === "object" && parsed !== null ? parsed : null;
77
- } catch {
78
- return null;
79
- }
62
+ const segments = token.split(".");
63
+ if (segments.length !== 3 || segments[0] !== "nca" || !segments[1]) return null;
64
+ const raw = decodeBase64UrlSegment(segments[1]);
65
+ if (!raw) return null;
66
+ try {
67
+ const parsed = JSON.parse(raw);
68
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
69
+ } catch {
70
+ return null;
71
+ }
80
72
  }
81
73
  function readPlatformSessionTokenState(token) {
82
- if (typeof token !== "string" || token.trim().length === 0) {
83
- return {
84
- valid: false,
85
- reason: "missing",
86
- payload: null
87
- };
88
- }
89
- const trimmed = token.trim();
90
- const payload = decodePlatformSessionTokenPayload(trimmed);
91
- if (!payload) {
92
- return {
93
- valid: false,
94
- reason: "malformed",
95
- payload: null
96
- };
97
- }
98
- const exp = typeof payload.exp === "number" && Number.isFinite(payload.exp) ? payload.exp : Number.NaN;
99
- const now = Math.floor(Date.now() / 1e3);
100
- if (!Number.isFinite(exp) || exp <= now) {
101
- return {
102
- valid: false,
103
- reason: "expired",
104
- payload
105
- };
106
- }
107
- return {
108
- valid: true,
109
- reason: "valid",
110
- payload
111
- };
74
+ if (typeof token !== "string" || token.trim().length === 0) return {
75
+ valid: false,
76
+ reason: "missing",
77
+ payload: null
78
+ };
79
+ const payload = decodePlatformSessionTokenPayload(token.trim());
80
+ if (!payload) return {
81
+ valid: false,
82
+ reason: "malformed",
83
+ payload: null
84
+ };
85
+ const exp = typeof payload.exp === "number" && Number.isFinite(payload.exp) ? payload.exp : NaN;
86
+ const now = Math.floor(Date.now() / 1e3);
87
+ if (!Number.isFinite(exp) || exp <= now) return {
88
+ valid: false,
89
+ reason: "expired",
90
+ payload
91
+ };
92
+ return {
93
+ valid: true,
94
+ reason: "valid",
95
+ payload
96
+ };
112
97
  }
113
98
  function isValidPlatformSessionToken(token) {
114
- return readPlatformSessionTokenState(token).valid;
99
+ return readPlatformSessionTokenState(token).valid;
115
100
  }
116
-
117
- // src/remote-platform-client.ts
101
+ //#endregion
102
+ //#region src/remote-platform-client.ts
118
103
  function ensureDir(path) {
119
- mkdirSync(path, { recursive: true });
104
+ mkdirSync(path, { recursive: true });
120
105
  }
121
106
  function readJsonFile(path) {
122
- if (!existsSync(path)) {
123
- return null;
124
- }
125
- try {
126
- return JSON.parse(readFileSync(path, "utf-8"));
127
- } catch {
128
- return null;
129
- }
107
+ if (!existsSync(path)) return null;
108
+ try {
109
+ return JSON.parse(readFileSync(path, "utf-8"));
110
+ } catch {
111
+ return null;
112
+ }
130
113
  }
131
114
  function writeJsonFile(path, value) {
132
- ensureDir(dirname(path));
133
- writeFileSync(path, `${JSON.stringify(value, null, 2)}
134
- `, "utf-8");
115
+ ensureDir(dirname(path));
116
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
135
117
  }
136
118
  function maskToken(value) {
137
- if (value.length <= 12) {
138
- return "<redacted>";
139
- }
140
- return `${value.slice(0, 6)}...${value.slice(-4)}`;
119
+ if (value.length <= 12) return "<redacted>";
120
+ return `${value.slice(0, 6)}...${value.slice(-4)}`;
141
121
  }
142
122
  function normalizeOptionalString(value) {
143
- if (typeof value !== "string") {
144
- return void 0;
145
- }
146
- const trimmed = value.trim();
147
- return trimmed.length > 0 ? trimmed : void 0;
123
+ if (typeof value !== "string") return;
124
+ const trimmed = value.trim();
125
+ return trimmed.length > 0 ? trimmed : void 0;
148
126
  }
149
127
  function delay(ms, signal) {
150
- return new Promise((resolveDelay, rejectDelay) => {
151
- const timer = setTimeout(() => {
152
- signal?.removeEventListener("abort", onAbort);
153
- resolveDelay();
154
- }, ms);
155
- const onAbort = () => {
156
- clearTimeout(timer);
157
- rejectDelay(new Error("Remote connector aborted."));
158
- };
159
- if (signal) {
160
- signal.addEventListener("abort", onAbort, { once: true });
161
- }
162
- });
128
+ return new Promise((resolveDelay, rejectDelay) => {
129
+ const timer = setTimeout(() => {
130
+ signal?.removeEventListener("abort", onAbort);
131
+ resolveDelay();
132
+ }, ms);
133
+ const onAbort = () => {
134
+ clearTimeout(timer);
135
+ rejectDelay(/* @__PURE__ */ new Error("Remote connector aborted."));
136
+ };
137
+ if (signal) signal.addEventListener("abort", onAbort, { once: true });
138
+ });
163
139
  }
164
140
  function redactWsUrl(url) {
165
- try {
166
- const parsed = new URL(url);
167
- const token = parsed.searchParams.get("token");
168
- if (token) {
169
- parsed.searchParams.set("token", maskToken(token));
170
- }
171
- return parsed.toString();
172
- } catch {
173
- return url;
174
- }
141
+ try {
142
+ const parsed = new URL(url);
143
+ const token = parsed.searchParams.get("token");
144
+ if (token) parsed.searchParams.set("token", maskToken(token));
145
+ return parsed.toString();
146
+ } catch {
147
+ return url;
148
+ }
175
149
  }
176
150
  var RemotePlatformClient = class {
177
- constructor(deps) {
178
- this.deps = deps;
179
- }
180
- get remoteDir() {
181
- return join(this.deps.getDataDir(), "remote");
182
- }
183
- get devicePath() {
184
- return join(this.remoteDir, "device.json");
185
- }
186
- resolveRunContext(opts) {
187
- const { platformBase, token, config } = this.resolvePlatformAccess(opts);
188
- return {
189
- config,
190
- platformBase,
191
- token,
192
- localOrigin: this.resolveLocalOrigin(config, opts),
193
- displayName: this.resolveDisplayName(config, opts),
194
- deviceInstallId: this.ensureDeviceInstallId(),
195
- autoReconnect: opts.once ? false : opts.autoReconnect ?? config.remote.autoReconnect
196
- };
197
- }
198
- async registerDevice(params) {
199
- const response = await fetch(`${params.platformBase}/platform/remote/instances/register`, {
200
- method: "POST",
201
- headers: {
202
- "content-type": "application/json",
203
- authorization: `Bearer ${params.token}`
204
- },
205
- body: JSON.stringify({
206
- deviceInstallId: params.deviceInstallId,
207
- displayName: params.displayName,
208
- platform: readPlatform(),
209
- appVersion: this.deps.getPackageVersion(),
210
- localOrigin: params.localOrigin
211
- })
212
- });
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
- }
229
- if (!response.ok || !payload.ok || !payload.data?.device) {
230
- throw new Error(payload.error?.message ?? `Failed to register remote instance (${response.status}).`);
231
- }
232
- return payload.data.device;
233
- }
234
- ensureDeviceInstallId() {
235
- const existing = readJsonFile(this.devicePath);
236
- if (existing?.deviceInstallId?.trim()) {
237
- return existing.deviceInstallId.trim();
238
- }
239
- const deviceInstallId = crypto.randomUUID();
240
- ensureDir(this.remoteDir);
241
- writeJsonFile(this.devicePath, { deviceInstallId });
242
- return deviceInstallId;
243
- }
244
- resolvePlatformAccess(opts) {
245
- const config = this.deps.loadConfig();
246
- const providers = config.providers;
247
- const nextclawProvider = providers.nextclaw;
248
- const token = typeof nextclawProvider?.apiKey === "string" ? nextclawProvider.apiKey.trim() : "";
249
- const tokenState = readPlatformSessionTokenState(token);
250
- if (tokenState.reason === "missing") {
251
- throw new Error('NextClaw platform token is missing. Run "nextclaw login" first.');
252
- }
253
- if (tokenState.reason === "expired") {
254
- throw new Error('NextClaw platform token expired. Run "nextclaw login" or browser sign-in again.');
255
- }
256
- if (tokenState.reason === "malformed") {
257
- throw new Error('NextClaw platform token is invalid. Run "nextclaw login" again.');
258
- }
259
- const configuredApiBase = normalizeOptionalString(config.remote.platformApiBase) ?? (typeof nextclawProvider?.apiBase === "string" ? nextclawProvider.apiBase.trim() : "");
260
- const rawApiBase = normalizeOptionalString(opts.apiBase) ?? configuredApiBase;
261
- if (!rawApiBase) {
262
- throw new Error("Platform API base is missing. Pass --api-base, run nextclaw login, or set remote.platformApiBase.");
263
- }
264
- const platformBase = this.deps.resolvePlatformBase(rawApiBase);
265
- return { platformBase, token: token.trim(), config };
266
- }
267
- resolveLocalOrigin(config, opts) {
268
- const explicitOrigin = normalizeOptionalString(opts.localOrigin);
269
- if (explicitOrigin) {
270
- return explicitOrigin.replace(/\/$/, "");
271
- }
272
- const state = this.deps.readManagedServiceState?.();
273
- if (state && this.deps.isProcessRunning?.(state.pid) && Number.isFinite(state.uiPort)) {
274
- return `http://127.0.0.1:${state.uiPort}`;
275
- }
276
- const configuredPort = typeof config.ui?.port === "number" && Number.isFinite(config.ui.port) ? config.ui.port : 55667;
277
- return `http://127.0.0.1:${configuredPort}`;
278
- }
279
- resolveDisplayName(config, opts) {
280
- return normalizeOptionalString(opts.name) ?? normalizeOptionalString(config.remote.deviceName) ?? hostname();
281
- }
151
+ constructor(deps) {
152
+ this.deps = deps;
153
+ }
154
+ get remoteDir() {
155
+ return join(this.deps.getDataDir(), "remote");
156
+ }
157
+ get devicePath() {
158
+ return join(this.remoteDir, "device.json");
159
+ }
160
+ resolveRunContext(opts) {
161
+ const { platformBase, token, config } = this.resolvePlatformAccess(opts);
162
+ return {
163
+ config,
164
+ platformBase,
165
+ token,
166
+ localOrigin: this.resolveLocalOrigin(config, opts),
167
+ displayName: this.resolveDisplayName(config, opts),
168
+ deviceInstallId: this.ensureDeviceInstallId(),
169
+ autoReconnect: opts.once ? false : opts.autoReconnect ?? config.remote.autoReconnect
170
+ };
171
+ }
172
+ async registerDevice(params) {
173
+ const response = await fetch(`${params.platformBase}/platform/remote/instances/register`, {
174
+ method: "POST",
175
+ headers: {
176
+ "content-type": "application/json",
177
+ authorization: `Bearer ${params.token}`
178
+ },
179
+ body: JSON.stringify({
180
+ deviceInstallId: params.deviceInstallId,
181
+ displayName: params.displayName,
182
+ platform: platform(),
183
+ appVersion: this.deps.getPackageVersion(),
184
+ localOrigin: params.localOrigin
185
+ })
186
+ });
187
+ const payload = await response.json();
188
+ const instance = payload.data?.instance;
189
+ if (response.ok && payload.ok && instance) return {
190
+ id: instance.id,
191
+ deviceInstallId: instance.instanceInstallId,
192
+ displayName: instance.displayName,
193
+ platform: instance.platform,
194
+ appVersion: instance.appVersion,
195
+ localOrigin: instance.localOrigin,
196
+ status: instance.status,
197
+ lastSeenAt: instance.lastSeenAt,
198
+ createdAt: instance.createdAt,
199
+ updatedAt: instance.updatedAt
200
+ };
201
+ if (!response.ok || !payload.ok || !payload.data?.device) throw new Error(payload.error?.message ?? `Failed to register remote instance (${response.status}).`);
202
+ return payload.data.device;
203
+ }
204
+ ensureDeviceInstallId() {
205
+ const existing = readJsonFile(this.devicePath);
206
+ if (existing?.deviceInstallId?.trim()) return existing.deviceInstallId.trim();
207
+ const deviceInstallId = crypto.randomUUID();
208
+ ensureDir(this.remoteDir);
209
+ writeJsonFile(this.devicePath, { deviceInstallId });
210
+ return deviceInstallId;
211
+ }
212
+ resolvePlatformAccess(opts) {
213
+ const config = this.deps.loadConfig();
214
+ const nextclawProvider = config.providers.nextclaw;
215
+ const token = typeof nextclawProvider?.apiKey === "string" ? nextclawProvider.apiKey.trim() : "";
216
+ const tokenState = readPlatformSessionTokenState(token);
217
+ if (tokenState.reason === "missing") throw new Error("NextClaw platform token is missing. Run \"nextclaw login\" first.");
218
+ if (tokenState.reason === "expired") throw new Error("NextClaw platform token expired. Run \"nextclaw login\" or browser sign-in again.");
219
+ if (tokenState.reason === "malformed") throw new Error("NextClaw platform token is invalid. Run \"nextclaw login\" again.");
220
+ const configuredApiBase = normalizeOptionalString(config.remote.platformApiBase) ?? (typeof nextclawProvider?.apiBase === "string" ? nextclawProvider.apiBase.trim() : "");
221
+ const rawApiBase = normalizeOptionalString(opts.apiBase) ?? configuredApiBase;
222
+ if (!rawApiBase) throw new Error("Platform API base is missing. Pass --api-base, run nextclaw login, or set remote.platformApiBase.");
223
+ return {
224
+ platformBase: this.deps.resolvePlatformBase(rawApiBase),
225
+ token: token.trim(),
226
+ config
227
+ };
228
+ }
229
+ resolveLocalOrigin(config, opts) {
230
+ const explicitOrigin = normalizeOptionalString(opts.localOrigin);
231
+ if (explicitOrigin) return explicitOrigin.replace(/\/$/, "");
232
+ const state = this.deps.readManagedServiceState?.();
233
+ if (state && this.deps.isProcessRunning?.(state.pid) && Number.isFinite(state.uiPort)) return `http://127.0.0.1:${state.uiPort}`;
234
+ return `http://127.0.0.1:${typeof config.ui?.port === "number" && Number.isFinite(config.ui.port) ? config.ui.port : 55667}`;
235
+ }
236
+ resolveDisplayName(config, opts) {
237
+ return normalizeOptionalString(opts.name) ?? normalizeOptionalString(config.remote.deviceName) ?? hostname();
238
+ }
282
239
  };
283
-
284
- // src/remote-relay-bridge.ts
285
- import { ensureUiBridgeSecret } from "@nextclaw/server";
240
+ //#endregion
241
+ //#region src/remote-relay-bridge.ts
286
242
  function encodeBase64(bytes) {
287
- return Buffer.from(bytes).toString("base64");
243
+ return Buffer.from(bytes).toString("base64");
288
244
  }
289
245
  function decodeBase64(base64) {
290
- if (!base64) {
291
- return new Uint8Array();
292
- }
293
- return new Uint8Array(Buffer.from(base64, "base64"));
246
+ if (!base64) return new Uint8Array();
247
+ return new Uint8Array(Buffer.from(base64, "base64"));
294
248
  }
295
249
  var RemoteRelayBridge = class {
296
- constructor(localOrigin) {
297
- this.localOrigin = localOrigin;
298
- }
299
- async ensureLocalUiHealthy() {
300
- const response = await fetch(`${this.localOrigin}/api/health`);
301
- if (!response.ok) {
302
- throw new Error(`Local UI is not healthy at ${this.localOrigin}. Start NextClaw first.`);
303
- }
304
- }
305
- async forward(frame, socket) {
306
- const bridgeCookie = await this.requestBridgeCookie();
307
- const url = new URL(frame.path, this.localOrigin);
308
- const headers = this.createForwardHeaders(frame.headers, bridgeCookie);
309
- const response = await fetch(url, {
310
- method: frame.method,
311
- headers,
312
- body: frame.method === "GET" || frame.method === "HEAD" ? void 0 : decodeBase64(frame.bodyBase64)
313
- });
314
- const responseHeaders = Array.from(response.headers.entries()).filter(([key]) => {
315
- const lower = key.toLowerCase();
316
- return !["content-length", "connection", "transfer-encoding", "set-cookie"].includes(lower);
317
- });
318
- const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
319
- if (response.body && contentType.startsWith("text/event-stream")) {
320
- await this.sendStreamingResponse({ frame, response, responseHeaders, socket });
321
- return;
322
- }
323
- const responseBody = response.body ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array();
324
- socket.send(JSON.stringify({
325
- type: "response",
326
- requestId: frame.requestId,
327
- status: response.status,
328
- headers: responseHeaders,
329
- bodyBase64: encodeBase64(responseBody)
330
- }));
331
- }
332
- createForwardHeaders(headersList, bridgeCookie) {
333
- const headers = new Headers();
334
- for (const [key, value] of headersList) {
335
- const lower = key.toLowerCase();
336
- if ([
337
- "host",
338
- "connection",
339
- "content-length",
340
- "cookie",
341
- "x-forwarded-for",
342
- "x-forwarded-proto",
343
- "cf-connecting-ip"
344
- ].includes(lower)) {
345
- continue;
346
- }
347
- headers.set(key, value);
348
- }
349
- if (bridgeCookie) {
350
- headers.set("cookie", bridgeCookie);
351
- }
352
- return headers;
353
- }
354
- async requestBridgeCookie() {
355
- const response = await fetch(`${this.localOrigin}/api/auth/bridge`, {
356
- method: "POST",
357
- headers: {
358
- "x-nextclaw-ui-bridge-secret": ensureUiBridgeSecret()
359
- }
360
- });
361
- const payload = await response.json();
362
- if (!response.ok || !payload.ok) {
363
- throw new Error(payload.error?.message ?? `Failed to request local auth bridge (${response.status}).`);
364
- }
365
- return typeof payload.data?.cookie === "string" && payload.data.cookie.trim().length > 0 ? payload.data.cookie.trim() : null;
366
- }
367
- async sendStreamingResponse(params) {
368
- params.socket.send(JSON.stringify({
369
- type: "response.start",
370
- requestId: params.frame.requestId,
371
- status: params.response.status,
372
- headers: params.responseHeaders
373
- }));
374
- const reader = params.response.body?.getReader();
375
- if (!reader) {
376
- params.socket.send(JSON.stringify({
377
- type: "response.end",
378
- requestId: params.frame.requestId
379
- }));
380
- return;
381
- }
382
- try {
383
- while (true) {
384
- const { value, done } = await reader.read();
385
- if (done) {
386
- break;
387
- }
388
- if (value && value.length > 0) {
389
- params.socket.send(JSON.stringify({
390
- type: "response.chunk",
391
- requestId: params.frame.requestId,
392
- bodyBase64: encodeBase64(value)
393
- }));
394
- }
395
- }
396
- } finally {
397
- reader.releaseLock();
398
- }
399
- params.socket.send(JSON.stringify({
400
- type: "response.end",
401
- requestId: params.frame.requestId
402
- }));
403
- }
250
+ constructor(localOrigin) {
251
+ this.localOrigin = localOrigin;
252
+ }
253
+ async ensureLocalUiHealthy() {
254
+ if (!(await fetch(`${this.localOrigin}/api/health`)).ok) throw new Error(`Local UI is not healthy at ${this.localOrigin}. Start NextClaw first.`);
255
+ }
256
+ async forward(frame, socket) {
257
+ const bridgeCookie = await this.requestBridgeCookie();
258
+ const url = new URL(frame.path, this.localOrigin);
259
+ const headers = this.createForwardHeaders(frame.headers, bridgeCookie);
260
+ const response = await fetch(url, {
261
+ method: frame.method,
262
+ headers,
263
+ body: frame.method === "GET" || frame.method === "HEAD" ? void 0 : decodeBase64(frame.bodyBase64)
264
+ });
265
+ const responseHeaders = Array.from(response.headers.entries()).filter(([key]) => {
266
+ const lower = key.toLowerCase();
267
+ return ![
268
+ "content-length",
269
+ "connection",
270
+ "transfer-encoding",
271
+ "set-cookie"
272
+ ].includes(lower);
273
+ });
274
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
275
+ if (response.body && contentType.startsWith("text/event-stream")) {
276
+ await this.sendStreamingResponse({
277
+ frame,
278
+ response,
279
+ responseHeaders,
280
+ socket
281
+ });
282
+ return;
283
+ }
284
+ const responseBody = response.body ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array();
285
+ socket.send(JSON.stringify({
286
+ type: "response",
287
+ requestId: frame.requestId,
288
+ status: response.status,
289
+ headers: responseHeaders,
290
+ bodyBase64: encodeBase64(responseBody)
291
+ }));
292
+ }
293
+ createForwardHeaders(headersList, bridgeCookie) {
294
+ const headers = new Headers();
295
+ for (const [key, value] of headersList) {
296
+ const lower = key.toLowerCase();
297
+ if ([
298
+ "host",
299
+ "connection",
300
+ "content-length",
301
+ "cookie",
302
+ "x-forwarded-for",
303
+ "x-forwarded-proto",
304
+ "cf-connecting-ip"
305
+ ].includes(lower)) continue;
306
+ headers.set(key, value);
307
+ }
308
+ if (bridgeCookie) headers.set("cookie", bridgeCookie);
309
+ return headers;
310
+ }
311
+ async requestBridgeCookie() {
312
+ const response = await fetch(`${this.localOrigin}/api/auth/bridge`, {
313
+ method: "POST",
314
+ headers: { "x-nextclaw-ui-bridge-secret": ensureUiBridgeSecret() }
315
+ });
316
+ const payload = await response.json();
317
+ if (!response.ok || !payload.ok) throw new Error(payload.error?.message ?? `Failed to request local auth bridge (${response.status}).`);
318
+ return typeof payload.data?.cookie === "string" && payload.data.cookie.trim().length > 0 ? payload.data.cookie.trim() : null;
319
+ }
320
+ async sendStreamingResponse(params) {
321
+ params.socket.send(JSON.stringify({
322
+ type: "response.start",
323
+ requestId: params.frame.requestId,
324
+ status: params.response.status,
325
+ headers: params.responseHeaders
326
+ }));
327
+ const reader = params.response.body?.getReader();
328
+ if (!reader) {
329
+ params.socket.send(JSON.stringify({
330
+ type: "response.end",
331
+ requestId: params.frame.requestId
332
+ }));
333
+ return;
334
+ }
335
+ try {
336
+ while (true) {
337
+ const { value, done } = await reader.read();
338
+ if (done) break;
339
+ if (value && value.length > 0) params.socket.send(JSON.stringify({
340
+ type: "response.chunk",
341
+ requestId: params.frame.requestId,
342
+ bodyBase64: encodeBase64(value)
343
+ }));
344
+ }
345
+ } finally {
346
+ reader.releaseLock();
347
+ }
348
+ params.socket.send(JSON.stringify({
349
+ type: "response.end",
350
+ requestId: params.frame.requestId
351
+ }));
352
+ }
404
353
  };
405
-
406
- // src/remote-app.adapter.ts
407
- import WebSocket2 from "ws";
408
-
409
- // src/remote-app-stream.ts
354
+ //#endregion
355
+ //#region src/remote-app-stream.ts
410
356
  function parseRemoteSseFrame(frame) {
411
- const lines = frame.split("\n");
412
- let event = "";
413
- const dataLines = [];
414
- for (const raw of lines) {
415
- const line = raw.trimEnd();
416
- if (!line || line.startsWith(":")) {
417
- continue;
418
- }
419
- if (line.startsWith("event:")) {
420
- event = line.slice(6).trim();
421
- continue;
422
- }
423
- if (line.startsWith("data:")) {
424
- dataLines.push(line.slice(5).trimStart());
425
- }
426
- }
427
- if (!event) {
428
- return null;
429
- }
430
- const data = dataLines.join("\n");
431
- if (!data) {
432
- return { event };
433
- }
434
- try {
435
- return { event, payload: JSON.parse(data) };
436
- } catch {
437
- return { event, payload: data };
438
- }
357
+ const lines = frame.split("\n");
358
+ let event = "";
359
+ const dataLines = [];
360
+ for (const raw of lines) {
361
+ const line = raw.trimEnd();
362
+ if (!line || line.startsWith(":")) continue;
363
+ if (line.startsWith("event:")) {
364
+ event = line.slice(6).trim();
365
+ continue;
366
+ }
367
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
368
+ }
369
+ if (!event) return null;
370
+ const data = dataLines.join("\n");
371
+ if (!data) return { event };
372
+ try {
373
+ return {
374
+ event,
375
+ payload: JSON.parse(data)
376
+ };
377
+ } catch {
378
+ return {
379
+ event,
380
+ payload: data
381
+ };
382
+ }
439
383
  }
440
384
  function processRemoteStreamFrame(params) {
441
- const frame = parseRemoteSseFrame(params.rawFrame);
442
- if (!frame) {
443
- return;
444
- }
445
- params.onEvent(frame);
385
+ const frame = parseRemoteSseFrame(params.rawFrame);
386
+ if (!frame) return;
387
+ params.onEvent(frame);
446
388
  }
447
389
  function flushRemoteStreamFrames(params) {
448
- let boundary = params.bufferState.value.indexOf("\n\n");
449
- while (boundary !== -1) {
450
- processRemoteStreamFrame({
451
- rawFrame: params.bufferState.value.slice(0, boundary),
452
- onEvent: params.onEvent
453
- });
454
- params.bufferState.value = params.bufferState.value.slice(boundary + 2);
455
- boundary = params.bufferState.value.indexOf("\n\n");
456
- }
390
+ let boundary = params.bufferState.value.indexOf("\n\n");
391
+ while (boundary !== -1) {
392
+ processRemoteStreamFrame({
393
+ rawFrame: params.bufferState.value.slice(0, boundary),
394
+ onEvent: params.onEvent
395
+ });
396
+ params.bufferState.value = params.bufferState.value.slice(boundary + 2);
397
+ boundary = params.bufferState.value.indexOf("\n\n");
398
+ }
457
399
  }
458
400
  async function readRemoteAppStreamResult(params) {
459
- const reader = params.response.body?.getReader();
460
- if (!reader) {
461
- throw new Error("SSE response body unavailable.");
462
- }
463
- const decoder = new TextDecoder();
464
- const bufferState = { value: "" };
465
- try {
466
- while (true) {
467
- const { value, done } = await reader.read();
468
- if (done) {
469
- break;
470
- }
471
- bufferState.value += decoder.decode(value, { stream: true });
472
- flushRemoteStreamFrames({
473
- bufferState,
474
- onEvent: params.onEvent
475
- });
476
- }
477
- if (bufferState.value.trim()) {
478
- processRemoteStreamFrame({
479
- rawFrame: bufferState.value,
480
- onEvent: params.onEvent
481
- });
482
- }
483
- } finally {
484
- reader.releaseLock();
485
- }
401
+ const reader = params.response.body?.getReader();
402
+ if (!reader) throw new Error("SSE response body unavailable.");
403
+ const decoder = new TextDecoder();
404
+ const bufferState = { value: "" };
405
+ try {
406
+ while (true) {
407
+ const { value, done } = await reader.read();
408
+ if (done) break;
409
+ bufferState.value += decoder.decode(value, { stream: true });
410
+ flushRemoteStreamFrames({
411
+ bufferState,
412
+ onEvent: params.onEvent
413
+ });
414
+ }
415
+ if (bufferState.value.trim()) processRemoteStreamFrame({
416
+ rawFrame: bufferState.value,
417
+ onEvent: params.onEvent
418
+ });
419
+ } finally {
420
+ reader.releaseLock();
421
+ }
486
422
  }
487
-
488
- // src/remote-app.adapter.ts
423
+ //#endregion
424
+ //#region src/remote-app.adapter.ts
489
425
  function toWebSocketUrl(origin, path) {
490
- const normalizedOrigin = origin.replace(/\/$/, "");
491
- if (normalizedOrigin.startsWith("https://")) {
492
- return `${normalizedOrigin.replace(/^https:/, "wss:")}${path}`;
493
- }
494
- if (normalizedOrigin.startsWith("http://")) {
495
- return `${normalizedOrigin.replace(/^http:/, "ws:")}${path}`;
496
- }
497
- return `${normalizedOrigin}${path}`;
426
+ const normalizedOrigin = origin.replace(/\/$/, "");
427
+ if (normalizedOrigin.startsWith("https://")) return `${normalizedOrigin.replace(/^https:/, "wss:")}${path}`;
428
+ if (normalizedOrigin.startsWith("http://")) return `${normalizedOrigin.replace(/^http:/, "ws:")}${path}`;
429
+ return `${normalizedOrigin}${path}`;
498
430
  }
499
431
  function readErrorMessage(body, fallback) {
500
- if (typeof body === "object" && body && "error" in body) {
501
- const typed = body;
502
- if (typed.error?.message) {
503
- return typed.error.message;
504
- }
505
- }
506
- if (typeof body === "string" && body.trim()) {
507
- return body.trim();
508
- }
509
- return fallback;
432
+ if (typeof body === "object" && body && "error" in body) {
433
+ const typed = body;
434
+ if (typed.error?.message) return typed.error.message;
435
+ }
436
+ if (typeof body === "string" && body.trim()) return body.trim();
437
+ return fallback;
510
438
  }
511
439
  var RemoteAppAdapter = class {
512
- constructor(localOrigin, platformSocket) {
513
- this.localOrigin = localOrigin;
514
- this.platformSocket = platformSocket;
515
- this.relayBridge = new RemoteRelayBridge(localOrigin);
516
- }
517
- relayBridge;
518
- activeStreams = /* @__PURE__ */ new Map();
519
- localEventSocket = null;
520
- eventReconnectTimer = null;
521
- shuttingDown = false;
522
- async start() {
523
- await this.ensureEventSocket();
524
- }
525
- stop() {
526
- this.shuttingDown = true;
527
- if (this.eventReconnectTimer) {
528
- clearTimeout(this.eventReconnectTimer);
529
- this.eventReconnectTimer = null;
530
- }
531
- this.localEventSocket?.close();
532
- this.localEventSocket = null;
533
- for (const controller of this.activeStreams.values()) {
534
- controller.abort();
535
- }
536
- this.activeStreams.clear();
537
- }
538
- async handle(frame) {
539
- if (frame.type === "client.request") {
540
- await this.handleRequest(frame);
541
- return;
542
- }
543
- if (frame.type === "client.stream.open") {
544
- void this.handleStream(frame);
545
- return;
546
- }
547
- if (frame.type === "client.stream.cancel") {
548
- this.activeStreams.get(frame.streamId)?.abort();
549
- this.activeStreams.delete(frame.streamId);
550
- }
551
- }
552
- async handleRequest(frame) {
553
- const bridgeCookie = await this.relayBridge.requestBridgeCookie();
554
- const response = await fetch(new URL(frame.target.path, this.localOrigin), {
555
- method: frame.target.method,
556
- headers: this.createJsonHeaders(bridgeCookie),
557
- body: this.buildRequestBody(frame.target)
558
- });
559
- const body = await this.readResponseBody(response);
560
- this.send({
561
- type: "client.response",
562
- clientId: frame.clientId,
563
- id: frame.id,
564
- status: response.status,
565
- body
566
- });
567
- }
568
- async handleStream(frame) {
569
- const controller = new AbortController();
570
- this.activeStreams.set(frame.streamId, controller);
571
- try {
572
- const response = await this.openStreamResponse(frame, controller);
573
- if (!response) {
574
- return;
575
- }
576
- await readRemoteAppStreamResult({
577
- response,
578
- onEvent: (event) => {
579
- this.send({
580
- type: "client.stream.event",
581
- clientId: frame.clientId,
582
- streamId: frame.streamId,
583
- event: event.event,
584
- payload: event.payload
585
- });
586
- }
587
- });
588
- this.send({
589
- type: "client.stream.end",
590
- clientId: frame.clientId,
591
- streamId: frame.streamId
592
- });
593
- } catch (error) {
594
- if (controller.signal.aborted) {
595
- return;
596
- }
597
- this.send({
598
- type: "client.stream.error",
599
- clientId: frame.clientId,
600
- streamId: frame.streamId,
601
- message: error instanceof Error ? error.message : String(error)
602
- });
603
- } finally {
604
- this.activeStreams.delete(frame.streamId);
605
- }
606
- }
607
- async openStreamResponse(frame, controller) {
608
- const bridgeCookie = await this.relayBridge.requestBridgeCookie();
609
- const response = await fetch(new URL(frame.target.path, this.localOrigin), {
610
- method: frame.target.method,
611
- headers: this.createStreamHeaders(bridgeCookie),
612
- body: this.buildRequestBody(frame.target),
613
- signal: controller.signal
614
- });
615
- if (response.ok) {
616
- return response;
617
- }
618
- const errorBody = await this.readResponseBody(response);
619
- this.send({
620
- type: "client.stream.error",
621
- clientId: frame.clientId,
622
- streamId: frame.streamId,
623
- message: readErrorMessage(errorBody, `HTTP ${response.status}`)
624
- });
625
- return null;
626
- }
627
- async ensureEventSocket() {
628
- if (this.localEventSocket && this.localEventSocket.readyState === WebSocket2.OPEN) {
629
- return;
630
- }
631
- const bridgeCookie = await this.relayBridge.requestBridgeCookie();
632
- await new Promise((resolve, reject) => {
633
- const socket = new WebSocket2(toWebSocketUrl(this.localOrigin, "/ws"), {
634
- headers: bridgeCookie ? { Cookie: bridgeCookie } : void 0
635
- });
636
- this.localEventSocket = socket;
637
- socket.on("open", () => resolve());
638
- socket.on("message", (data) => {
639
- try {
640
- const event = JSON.parse(String(data ?? ""));
641
- this.send({
642
- type: "client.event",
643
- event
644
- });
645
- } catch (error) {
646
- console.error("Failed to parse local ui event:", error);
647
- }
648
- });
649
- socket.on("close", () => {
650
- this.localEventSocket = null;
651
- if (!this.shuttingDown) {
652
- this.scheduleEventReconnect();
653
- }
654
- });
655
- socket.on("error", (error) => {
656
- if (!this.shuttingDown) {
657
- reject(error instanceof Error ? error : new Error(String(error)));
658
- }
659
- });
660
- });
661
- }
662
- scheduleEventReconnect() {
663
- if (this.eventReconnectTimer) {
664
- return;
665
- }
666
- this.eventReconnectTimer = setTimeout(() => {
667
- this.eventReconnectTimer = null;
668
- void this.ensureEventSocket().catch(() => void 0);
669
- }, 3e3);
670
- }
671
- buildRequestBody(target) {
672
- if (target.method === "GET" || target.method === "HEAD") {
673
- return void 0;
674
- }
675
- if (target.body === void 0) {
676
- return void 0;
677
- }
678
- return new TextEncoder().encode(JSON.stringify(target.body));
679
- }
680
- createJsonHeaders(bridgeCookie) {
681
- const headers = new Headers({
682
- "Content-Type": "application/json"
683
- });
684
- if (bridgeCookie) {
685
- headers.set("cookie", bridgeCookie);
686
- }
687
- return headers;
688
- }
689
- createStreamHeaders(bridgeCookie) {
690
- const headers = this.createJsonHeaders(bridgeCookie);
691
- headers.set("Accept", "text/event-stream");
692
- return headers;
693
- }
694
- async readResponseBody(response) {
695
- const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
696
- if (contentType.includes("application/json")) {
697
- return await response.json();
698
- }
699
- const text = await response.text();
700
- return text;
701
- }
702
- send(frame) {
703
- if (this.platformSocket.readyState !== WebSocket2.OPEN) {
704
- return;
705
- }
706
- this.platformSocket.send(JSON.stringify(frame));
707
- }
440
+ relayBridge;
441
+ activeStreams = /* @__PURE__ */ new Map();
442
+ localEventSocket = null;
443
+ eventReconnectTimer = null;
444
+ shuttingDown = false;
445
+ constructor(localOrigin, platformSocket) {
446
+ this.localOrigin = localOrigin;
447
+ this.platformSocket = platformSocket;
448
+ this.relayBridge = new RemoteRelayBridge(localOrigin);
449
+ }
450
+ async start() {
451
+ await this.ensureEventSocket();
452
+ }
453
+ stop() {
454
+ this.shuttingDown = true;
455
+ if (this.eventReconnectTimer) {
456
+ clearTimeout(this.eventReconnectTimer);
457
+ this.eventReconnectTimer = null;
458
+ }
459
+ this.localEventSocket?.close();
460
+ this.localEventSocket = null;
461
+ for (const controller of this.activeStreams.values()) controller.abort();
462
+ this.activeStreams.clear();
463
+ }
464
+ async handle(frame) {
465
+ if (frame.type === "client.request") {
466
+ await this.handleRequest(frame);
467
+ return;
468
+ }
469
+ if (frame.type === "client.stream.open") {
470
+ this.handleStream(frame);
471
+ return;
472
+ }
473
+ if (frame.type === "client.stream.cancel") {
474
+ this.activeStreams.get(frame.streamId)?.abort();
475
+ this.activeStreams.delete(frame.streamId);
476
+ }
477
+ }
478
+ async handleRequest(frame) {
479
+ const bridgeCookie = await this.relayBridge.requestBridgeCookie();
480
+ const response = await fetch(new URL(frame.target.path, this.localOrigin), {
481
+ method: frame.target.method,
482
+ headers: this.createJsonHeaders(bridgeCookie),
483
+ body: this.buildRequestBody(frame.target)
484
+ });
485
+ const body = await this.readResponseBody(response);
486
+ this.send({
487
+ type: "client.response",
488
+ clientId: frame.clientId,
489
+ id: frame.id,
490
+ status: response.status,
491
+ body
492
+ });
493
+ }
494
+ async handleStream(frame) {
495
+ const controller = new AbortController();
496
+ this.activeStreams.set(frame.streamId, controller);
497
+ try {
498
+ const response = await this.openStreamResponse(frame, controller);
499
+ if (!response) return;
500
+ await readRemoteAppStreamResult({
501
+ response,
502
+ onEvent: (event) => {
503
+ this.send({
504
+ type: "client.stream.event",
505
+ clientId: frame.clientId,
506
+ streamId: frame.streamId,
507
+ event: event.event,
508
+ payload: event.payload
509
+ });
510
+ }
511
+ });
512
+ this.send({
513
+ type: "client.stream.end",
514
+ clientId: frame.clientId,
515
+ streamId: frame.streamId
516
+ });
517
+ } catch (error) {
518
+ if (controller.signal.aborted) return;
519
+ this.send({
520
+ type: "client.stream.error",
521
+ clientId: frame.clientId,
522
+ streamId: frame.streamId,
523
+ message: error instanceof Error ? error.message : String(error)
524
+ });
525
+ } finally {
526
+ this.activeStreams.delete(frame.streamId);
527
+ }
528
+ }
529
+ async openStreamResponse(frame, controller) {
530
+ const bridgeCookie = await this.relayBridge.requestBridgeCookie();
531
+ const response = await fetch(new URL(frame.target.path, this.localOrigin), {
532
+ method: frame.target.method,
533
+ headers: this.createStreamHeaders(bridgeCookie),
534
+ body: this.buildRequestBody(frame.target),
535
+ signal: controller.signal
536
+ });
537
+ if (response.ok) return response;
538
+ const errorBody = await this.readResponseBody(response);
539
+ this.send({
540
+ type: "client.stream.error",
541
+ clientId: frame.clientId,
542
+ streamId: frame.streamId,
543
+ message: readErrorMessage(errorBody, `HTTP ${response.status}`)
544
+ });
545
+ return null;
546
+ }
547
+ async ensureEventSocket() {
548
+ if (this.localEventSocket && this.localEventSocket.readyState === WebSocket$1.OPEN) return;
549
+ const bridgeCookie = await this.relayBridge.requestBridgeCookie();
550
+ await new Promise((resolve, reject) => {
551
+ const socket = new WebSocket$1(toWebSocketUrl(this.localOrigin, "/ws"), { headers: bridgeCookie ? { Cookie: bridgeCookie } : void 0 });
552
+ this.localEventSocket = socket;
553
+ socket.on("open", () => resolve());
554
+ socket.on("message", (data) => {
555
+ try {
556
+ const event = JSON.parse(String(data ?? ""));
557
+ this.send({
558
+ type: "client.event",
559
+ event
560
+ });
561
+ } catch (error) {
562
+ console.error("Failed to parse local ui event:", error);
563
+ }
564
+ });
565
+ socket.on("close", () => {
566
+ this.localEventSocket = null;
567
+ if (!this.shuttingDown) this.scheduleEventReconnect();
568
+ });
569
+ socket.on("error", (error) => {
570
+ if (!this.shuttingDown) reject(error instanceof Error ? error : new Error(String(error)));
571
+ });
572
+ });
573
+ }
574
+ scheduleEventReconnect() {
575
+ if (this.eventReconnectTimer) return;
576
+ this.eventReconnectTimer = setTimeout(() => {
577
+ this.eventReconnectTimer = null;
578
+ this.ensureEventSocket().catch(() => void 0);
579
+ }, 3e3);
580
+ }
581
+ buildRequestBody(target) {
582
+ if (target.method === "GET" || target.method === "HEAD") return;
583
+ if (target.body === void 0) return;
584
+ return new TextEncoder().encode(JSON.stringify(target.body));
585
+ }
586
+ createJsonHeaders(bridgeCookie) {
587
+ const headers = new Headers({ "Content-Type": "application/json" });
588
+ if (bridgeCookie) headers.set("cookie", bridgeCookie);
589
+ return headers;
590
+ }
591
+ createStreamHeaders(bridgeCookie) {
592
+ const headers = this.createJsonHeaders(bridgeCookie);
593
+ headers.set("Accept", "text/event-stream");
594
+ return headers;
595
+ }
596
+ async readResponseBody(response) {
597
+ if ((response.headers.get("content-type")?.toLowerCase() ?? "").includes("application/json")) return await response.json();
598
+ return await response.text();
599
+ }
600
+ send(frame) {
601
+ if (this.platformSocket.readyState !== WebSocket$1.OPEN) return;
602
+ this.platformSocket.send(JSON.stringify(frame));
603
+ }
708
604
  };
709
-
710
- // src/remote-connector-error.ts
711
- var TERMINAL_REMOTE_ERROR_PATTERNS = [
712
- /invalid or expired token/i,
713
- /missing bearer token/i,
714
- /token expired/i,
715
- /token is invalid/i,
716
- /run "nextclaw login"/i,
717
- /replaced by a newer connector session/i,
718
- /already owned by (?:running )?nextclaw service/i,
719
- /already owned by local nextclaw process/i,
720
- /unexpected server response:\s*400/i,
721
- /unexpected server response:\s*401/i,
722
- /unexpected server response:\s*403/i,
723
- /unexpected server response:\s*404/i,
724
- /invalid url/i,
725
- /unsupported protocol/i
605
+ //#endregion
606
+ //#region src/remote-connector-error.ts
607
+ const TERMINAL_REMOTE_ERROR_PATTERNS = [
608
+ /invalid or expired token/i,
609
+ /missing bearer token/i,
610
+ /token expired/i,
611
+ /token is invalid/i,
612
+ /run "nextclaw login"/i,
613
+ /replaced by a newer connector session/i,
614
+ /already owned by (?:running )?nextclaw service/i,
615
+ /already owned by local nextclaw process/i,
616
+ /unexpected server response:\s*400/i,
617
+ /unexpected server response:\s*401/i,
618
+ /unexpected server response:\s*403/i,
619
+ /unexpected server response:\s*404/i,
620
+ /invalid url/i,
621
+ /unsupported protocol/i
726
622
  ];
727
623
  function isTerminalRemoteConnectorError(error) {
728
- const message = error instanceof Error ? error.message : String(error);
729
- return TERMINAL_REMOTE_ERROR_PATTERNS.some((pattern) => pattern.test(message));
624
+ const message = error instanceof Error ? error.message : String(error);
625
+ return TERMINAL_REMOTE_ERROR_PATTERNS.some((pattern) => pattern.test(message));
730
626
  }
731
627
  function describeUnexpectedRemoteConnectorClose(event) {
732
- const code = typeof event.code === "number" && Number.isFinite(event.code) ? event.code : null;
733
- const reason = typeof event.reason === "string" ? event.reason.trim() : "";
734
- const wasClean = typeof event.wasClean === "boolean" ? event.wasClean : null;
735
- if ((code === null || code === 1e3) && !reason) {
736
- return null;
737
- }
738
- const detailParts = [];
739
- if (code !== null) {
740
- detailParts.push(`code ${code}`);
741
- }
742
- if (wasClean !== null) {
743
- detailParts.push(wasClean ? "clean" : "unclean");
744
- }
745
- const detail = detailParts.length > 0 ? ` (${detailParts.join(", ")})` : "";
746
- if (reason) {
747
- return `Remote connector websocket closed${detail}: ${reason}`;
748
- }
749
- return `Remote connector websocket closed${detail}.`;
628
+ const code = typeof event.code === "number" && Number.isFinite(event.code) ? event.code : null;
629
+ const reason = typeof event.reason === "string" ? event.reason.trim() : "";
630
+ const wasClean = typeof event.wasClean === "boolean" ? event.wasClean : null;
631
+ if ((code === null || code === 1e3) && !reason) return null;
632
+ const detailParts = [];
633
+ if (code !== null) detailParts.push(`code ${code}`);
634
+ if (wasClean !== null) detailParts.push(wasClean ? "clean" : "unclean");
635
+ const detail = detailParts.length > 0 ? ` (${detailParts.join(", ")})` : "";
636
+ if (reason) return `Remote connector websocket closed${detail}: ${reason}`;
637
+ return `Remote connector websocket closed${detail}.`;
750
638
  }
751
-
752
- // src/remote-connector-retry.utils.ts
753
- var BASE_RECONNECT_DELAY_MS = 3e3;
754
- var MAX_RECONNECT_DELAY_MS = 30 * 6e4;
755
- var RECONNECT_JITTER_RATIO = 0.2;
639
+ //#endregion
640
+ //#region src/remote-connector-retry.utils.ts
641
+ const BASE_RECONNECT_DELAY_MS = 3e3;
642
+ const MAX_RECONNECT_DELAY_MS = 30 * 6e4;
643
+ const RECONNECT_JITTER_RATIO = .2;
756
644
  function resolveReconnectDelayMs(attempt, random) {
757
- const exponentialDelayMs = Math.min(
758
- BASE_RECONNECT_DELAY_MS * 2 ** Math.max(0, attempt - 1),
759
- MAX_RECONNECT_DELAY_MS
760
- );
761
- const jitterRatio = (random() * 2 - 1) * RECONNECT_JITTER_RATIO;
762
- return Math.max(
763
- BASE_RECONNECT_DELAY_MS,
764
- Math.round(exponentialDelayMs * (1 + jitterRatio))
765
- );
645
+ const exponentialDelayMs = Math.min(BASE_RECONNECT_DELAY_MS * 2 ** Math.max(0, attempt - 1), MAX_RECONNECT_DELAY_MS);
646
+ const jitterRatio = (random() * 2 - 1) * RECONNECT_JITTER_RATIO;
647
+ return Math.max(BASE_RECONNECT_DELAY_MS, Math.round(exponentialDelayMs * (1 + jitterRatio)));
766
648
  }
767
649
  function formatReconnectDelay(delayMs) {
768
- const seconds = delayMs / 1e3;
769
- return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
650
+ const seconds = delayMs / 1e3;
651
+ return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
770
652
  }
771
-
772
- // src/remote-connector-websocket-error.utils.ts
653
+ //#endregion
654
+ //#region src/remote-connector-websocket-error.utils.ts
773
655
  function readRemoteConnectorSocketErrorMessage(event) {
774
- const typedEvent = event;
775
- if (typeof typedEvent.message === "string") {
776
- const directMessage = typedEvent.message.trim();
777
- if (directMessage.length > 0) {
778
- return directMessage;
779
- }
780
- }
781
- const nestedError = typedEvent.error;
782
- if (nestedError instanceof Error && nestedError.message.trim().length > 0) {
783
- return nestedError.message.trim();
784
- }
785
- if (typeof nestedError === "string" && nestedError.trim().length > 0) {
786
- return nestedError.trim();
787
- }
788
- if (typeof nestedError === "object" && nestedError && typeof nestedError.message === "string") {
789
- const nestedMessage = nestedError.message.trim();
790
- if (nestedMessage.length > 0) {
791
- return nestedMessage;
792
- }
793
- }
794
- return "Remote connector websocket failed.";
656
+ const typedEvent = event;
657
+ if (typeof typedEvent.message === "string") {
658
+ const directMessage = typedEvent.message.trim();
659
+ if (directMessage.length > 0) return directMessage;
660
+ }
661
+ const nestedError = typedEvent.error;
662
+ if (nestedError instanceof Error && nestedError.message.trim().length > 0) return nestedError.message.trim();
663
+ if (typeof nestedError === "string" && nestedError.trim().length > 0) return nestedError.trim();
664
+ if (typeof nestedError === "object" && nestedError && typeof nestedError.message === "string") {
665
+ const nestedMessage = nestedError.message.trim();
666
+ if (nestedMessage.length > 0) return nestedMessage;
667
+ }
668
+ return "Remote connector websocket failed.";
795
669
  }
796
-
797
- // src/remote-connector.ts
798
- var CONNECTOR_KEEPALIVE_INTERVAL_MS = 25e3;
799
- var CONNECTOR_KEEPALIVE_FRAME = JSON.stringify({ type: "connector.ping" });
670
+ //#endregion
671
+ //#region src/remote-connector.ts
672
+ const CONNECTOR_KEEPALIVE_INTERVAL_MS = 25e3;
673
+ const CONNECTOR_KEEPALIVE_FRAME = JSON.stringify({ type: "connector.ping" });
800
674
  var RemoteConnector = class {
801
- constructor(deps) {
802
- this.deps = deps;
803
- }
804
- get logger() {
805
- return this.deps.logger ?? console;
806
- }
807
- get delayFn() {
808
- return this.deps.delayFn ?? delay;
809
- }
810
- get random() {
811
- return this.deps.random ?? Math.random;
812
- }
813
- createSocket(wsUrl) {
814
- return this.deps.createSocket?.(wsUrl) ?? new WebSocket(wsUrl);
815
- }
816
- async connectOnce(params) {
817
- return await new Promise((resolve, reject) => {
818
- const socket = this.createSocket(params.wsUrl);
819
- const appAdapter = new RemoteAppAdapter(params.localOrigin, socket);
820
- let keepaliveTimer = null;
821
- let settled = false;
822
- let aborted = false;
823
- const cleanup = () => {
824
- if (keepaliveTimer) {
825
- clearInterval(keepaliveTimer);
826
- keepaliveTimer = null;
827
- }
828
- params.signal?.removeEventListener("abort", onAbort);
829
- };
830
- const finishResolve = (value) => {
831
- if (settled) {
832
- return;
833
- }
834
- settled = true;
835
- cleanup();
836
- resolve(value);
837
- };
838
- const finishReject = (error) => {
839
- if (settled) {
840
- return;
841
- }
842
- settled = true;
843
- cleanup();
844
- reject(error);
845
- };
846
- const onAbort = () => {
847
- aborted = true;
848
- try {
849
- socket.close(1e3, "Remote connector aborted");
850
- } catch {
851
- finishResolve("aborted");
852
- }
853
- };
854
- if (params.signal) {
855
- if (params.signal.aborted) {
856
- onAbort();
857
- } else {
858
- params.signal.addEventListener("abort", onAbort, { once: true });
859
- }
860
- }
861
- socket.addEventListener("open", () => {
862
- keepaliveTimer = setInterval(() => {
863
- if (socket.readyState !== 1) {
864
- return;
865
- }
866
- try {
867
- socket.send(CONNECTOR_KEEPALIVE_FRAME);
868
- } catch {
869
- }
870
- }, CONNECTOR_KEEPALIVE_INTERVAL_MS);
871
- params.statusStore?.write({
872
- enabled: true,
873
- state: "connected",
874
- deviceId: params.deviceId,
875
- deviceName: params.displayName,
876
- platformBase: params.platformBase,
877
- localOrigin: params.localOrigin,
878
- lastConnectedAt: (/* @__PURE__ */ new Date()).toISOString(),
879
- lastError: null
880
- });
881
- this.logger.info(`\u2713 Remote connector connected: ${redactWsUrl(params.wsUrl)}`);
882
- void appAdapter.start().catch((error) => {
883
- this.logger.error(`Remote event bridge error: ${error instanceof Error ? error.message : String(error)}`);
884
- });
885
- });
886
- socket.addEventListener("message", (event) => {
887
- this.handleSocketMessage({
888
- data: event.data,
889
- relayBridge: params.relayBridge,
890
- appAdapter,
891
- socket
892
- });
893
- });
894
- socket.addEventListener("close", (event) => {
895
- appAdapter.stop();
896
- const closeMessage = describeUnexpectedRemoteConnectorClose(event ?? {});
897
- if (!aborted && closeMessage) {
898
- finishReject(new Error(closeMessage));
899
- return;
900
- }
901
- finishResolve(aborted ? "aborted" : "closed");
902
- });
903
- socket.addEventListener("error", (event) => {
904
- appAdapter.stop();
905
- if (aborted) {
906
- finishResolve("aborted");
907
- return;
908
- }
909
- finishReject(new Error(readRemoteConnectorSocketErrorMessage(event)));
910
- });
911
- });
912
- }
913
- handleSocketMessage(params) {
914
- void (async () => {
915
- const frame = this.parseRelayFrame(params.data);
916
- if (!frame) {
917
- return;
918
- }
919
- try {
920
- if (frame.type === "request") {
921
- await params.relayBridge.forward(frame, params.socket);
922
- return;
923
- }
924
- await params.appAdapter.handle(frame);
925
- } catch (error) {
926
- if (frame.type === "request") {
927
- params.socket.send(JSON.stringify({
928
- type: "response.error",
929
- requestId: frame.requestId,
930
- message: error instanceof Error ? error.message : String(error)
931
- }));
932
- return;
933
- }
934
- if (frame.type === "client.request") {
935
- params.socket.send(JSON.stringify({
936
- type: "client.request.error",
937
- clientId: frame.clientId,
938
- id: frame.id,
939
- message: error instanceof Error ? error.message : String(error)
940
- }));
941
- return;
942
- }
943
- params.socket.send(JSON.stringify({
944
- type: "client.stream.error",
945
- clientId: frame.clientId,
946
- streamId: frame.streamId,
947
- message: error instanceof Error ? error.message : String(error)
948
- }));
949
- }
950
- })();
951
- }
952
- parseRelayFrame(data) {
953
- try {
954
- const frame = JSON.parse(String(data ?? ""));
955
- if (typeof frame !== "object" || !frame || typeof frame.type !== "string") {
956
- return null;
957
- }
958
- if (frame.type === "request") {
959
- return frame;
960
- }
961
- if (frame.type === "client.request" || frame.type === "client.stream.open" || frame.type === "client.stream.cancel") {
962
- return frame;
963
- }
964
- return null;
965
- } catch {
966
- return null;
967
- }
968
- }
969
- async ensureDevice(params) {
970
- if (params.device) {
971
- return params.device;
972
- }
973
- const device = await this.deps.platformClient.registerDevice({
974
- platformBase: params.context.platformBase,
975
- token: params.context.token,
976
- deviceInstallId: params.context.deviceInstallId,
977
- displayName: params.context.displayName,
978
- localOrigin: params.context.localOrigin
979
- });
980
- this.logger.info(`\u2713 Remote instance registered: ${device.displayName} (${device.id})`);
981
- this.logger.info(`\u2713 Local origin: ${params.context.localOrigin}`);
982
- this.logger.info(`\u2713 Platform: ${params.context.platformBase}`);
983
- return device;
984
- }
985
- writeRemoteState(statusStore, next) {
986
- statusStore?.write(next);
987
- }
988
- async runCycle(params) {
989
- let device = params.device;
990
- try {
991
- this.writeRemoteState(params.opts.statusStore, {
992
- enabled: true,
993
- state: "connecting",
994
- deviceId: params.device?.id,
995
- deviceName: params.context.displayName,
996
- platformBase: params.context.platformBase,
997
- localOrigin: params.context.localOrigin,
998
- lastError: null
999
- });
1000
- device = await this.ensureDevice({ device, context: params.context });
1001
- const wsUrl = `${params.context.platformBase.replace(/^http/i, "ws")}/platform/remote/connect?instanceId=${encodeURIComponent(device.id)}&token=${encodeURIComponent(params.context.token)}`;
1002
- const outcome = await this.connectOnce({
1003
- wsUrl,
1004
- relayBridge: params.relayBridge,
1005
- signal: params.opts.signal,
1006
- statusStore: params.opts.statusStore,
1007
- displayName: params.context.displayName,
1008
- deviceId: device.id,
1009
- platformBase: params.context.platformBase,
1010
- localOrigin: params.context.localOrigin
1011
- });
1012
- if (outcome !== "aborted") {
1013
- this.writeRemoteState(params.opts.statusStore, {
1014
- enabled: true,
1015
- state: "disconnected",
1016
- deviceId: device.id,
1017
- deviceName: params.context.displayName,
1018
- platformBase: params.context.platformBase,
1019
- localOrigin: params.context.localOrigin,
1020
- lastError: null
1021
- });
1022
- }
1023
- return {
1024
- device,
1025
- outcome: outcome === "aborted" ? "aborted" : "retry",
1026
- retryFailure: false,
1027
- lastError: null
1028
- };
1029
- } catch (error) {
1030
- const message = error instanceof Error ? error.message : String(error);
1031
- this.writeRemoteState(params.opts.statusStore, {
1032
- enabled: true,
1033
- state: "error",
1034
- deviceId: params.device?.id,
1035
- deviceName: params.context.displayName,
1036
- platformBase: params.context.platformBase,
1037
- localOrigin: params.context.localOrigin,
1038
- lastError: message
1039
- });
1040
- this.logger.error(`Remote connector error: ${message}`);
1041
- return {
1042
- device,
1043
- outcome: isTerminalRemoteConnectorError(error) ? "stop" : "retry",
1044
- retryFailure: true,
1045
- lastError: message
1046
- };
1047
- }
1048
- }
1049
- async run(opts = {}) {
1050
- const context = this.deps.platformClient.resolveRunContext(opts);
1051
- const relayBridge = (this.deps.relayBridgeFactory ?? ((localOrigin) => new RemoteRelayBridge(localOrigin)))(
1052
- context.localOrigin
1053
- );
1054
- await relayBridge.ensureLocalUiHealthy();
1055
- let device = null;
1056
- let preserveRuntimeError = false;
1057
- let consecutiveReconnectFailures = 0;
1058
- while (!opts.signal?.aborted) {
1059
- const cycle = await this.runCycle({ device, context, relayBridge, opts });
1060
- device = cycle.device;
1061
- consecutiveReconnectFailures = cycle.retryFailure ? consecutiveReconnectFailures + 1 : 0;
1062
- if (cycle.outcome === "stop") {
1063
- preserveRuntimeError = true;
1064
- break;
1065
- }
1066
- if (cycle.outcome === "aborted" || !context.autoReconnect || opts.signal?.aborted) {
1067
- break;
1068
- }
1069
- const reconnectDelayMs = resolveReconnectDelayMs(cycle.retryFailure ? consecutiveReconnectFailures : 1, this.random);
1070
- this.logger.warn(
1071
- `Remote connector disconnected. Reconnecting in ${formatReconnectDelay(reconnectDelayMs)}...`
1072
- );
1073
- try {
1074
- await this.delayFn(reconnectDelayMs, opts.signal);
1075
- } catch {
1076
- break;
1077
- }
1078
- }
1079
- if (preserveRuntimeError) {
1080
- return;
1081
- }
1082
- this.writeRemoteState(opts.statusStore, {
1083
- enabled: opts.mode === "service" ? true : Boolean(context.config.remote.enabled),
1084
- state: opts.signal?.aborted ? "disconnected" : "disabled",
1085
- deviceId: device?.id,
1086
- deviceName: context.displayName,
1087
- platformBase: context.platformBase,
1088
- localOrigin: context.localOrigin,
1089
- lastError: null
1090
- });
1091
- }
675
+ constructor(deps) {
676
+ this.deps = deps;
677
+ }
678
+ get logger() {
679
+ return this.deps.logger ?? console;
680
+ }
681
+ get delayFn() {
682
+ return this.deps.delayFn ?? delay;
683
+ }
684
+ get random() {
685
+ return this.deps.random ?? Math.random;
686
+ }
687
+ createSocket(wsUrl) {
688
+ return this.deps.createSocket?.(wsUrl) ?? new WebSocket(wsUrl);
689
+ }
690
+ async connectOnce(params) {
691
+ return await new Promise((resolve, reject) => {
692
+ const socket = this.createSocket(params.wsUrl);
693
+ const appAdapter = new RemoteAppAdapter(params.localOrigin, socket);
694
+ let keepaliveTimer = null;
695
+ let settled = false;
696
+ let aborted = false;
697
+ const cleanup = () => {
698
+ if (keepaliveTimer) {
699
+ clearInterval(keepaliveTimer);
700
+ keepaliveTimer = null;
701
+ }
702
+ params.signal?.removeEventListener("abort", onAbort);
703
+ };
704
+ const finishResolve = (value) => {
705
+ if (settled) return;
706
+ settled = true;
707
+ cleanup();
708
+ resolve(value);
709
+ };
710
+ const finishReject = (error) => {
711
+ if (settled) return;
712
+ settled = true;
713
+ cleanup();
714
+ reject(error);
715
+ };
716
+ const onAbort = () => {
717
+ aborted = true;
718
+ try {
719
+ socket.close(1e3, "Remote connector aborted");
720
+ } catch {
721
+ finishResolve("aborted");
722
+ }
723
+ };
724
+ if (params.signal) if (params.signal.aborted) onAbort();
725
+ else params.signal.addEventListener("abort", onAbort, { once: true });
726
+ socket.addEventListener("open", () => {
727
+ keepaliveTimer = setInterval(() => {
728
+ if (socket.readyState !== 1) return;
729
+ try {
730
+ socket.send(CONNECTOR_KEEPALIVE_FRAME);
731
+ } catch {}
732
+ }, CONNECTOR_KEEPALIVE_INTERVAL_MS);
733
+ params.statusStore?.write({
734
+ enabled: true,
735
+ state: "connected",
736
+ deviceId: params.deviceId,
737
+ deviceName: params.displayName,
738
+ platformBase: params.platformBase,
739
+ localOrigin: params.localOrigin,
740
+ lastConnectedAt: (/* @__PURE__ */ new Date()).toISOString(),
741
+ lastError: null
742
+ });
743
+ this.logger.info(`✓ Remote connector connected: ${redactWsUrl(params.wsUrl)}`);
744
+ appAdapter.start().catch((error) => {
745
+ this.logger.error(`Remote event bridge error: ${error instanceof Error ? error.message : String(error)}`);
746
+ });
747
+ });
748
+ socket.addEventListener("message", (event) => {
749
+ this.handleSocketMessage({
750
+ data: event.data,
751
+ relayBridge: params.relayBridge,
752
+ appAdapter,
753
+ socket
754
+ });
755
+ });
756
+ socket.addEventListener("close", (event) => {
757
+ appAdapter.stop();
758
+ const closeMessage = describeUnexpectedRemoteConnectorClose(event ?? {});
759
+ if (!aborted && closeMessage) {
760
+ finishReject(new Error(closeMessage));
761
+ return;
762
+ }
763
+ finishResolve(aborted ? "aborted" : "closed");
764
+ });
765
+ socket.addEventListener("error", (event) => {
766
+ appAdapter.stop();
767
+ if (aborted) {
768
+ finishResolve("aborted");
769
+ return;
770
+ }
771
+ finishReject(new Error(readRemoteConnectorSocketErrorMessage(event)));
772
+ });
773
+ });
774
+ }
775
+ handleSocketMessage(params) {
776
+ (async () => {
777
+ const frame = this.parseRelayFrame(params.data);
778
+ if (!frame) return;
779
+ try {
780
+ if (frame.type === "request") {
781
+ await params.relayBridge.forward(frame, params.socket);
782
+ return;
783
+ }
784
+ await params.appAdapter.handle(frame);
785
+ } catch (error) {
786
+ if (frame.type === "request") {
787
+ params.socket.send(JSON.stringify({
788
+ type: "response.error",
789
+ requestId: frame.requestId,
790
+ message: error instanceof Error ? error.message : String(error)
791
+ }));
792
+ return;
793
+ }
794
+ if (frame.type === "client.request") {
795
+ params.socket.send(JSON.stringify({
796
+ type: "client.request.error",
797
+ clientId: frame.clientId,
798
+ id: frame.id,
799
+ message: error instanceof Error ? error.message : String(error)
800
+ }));
801
+ return;
802
+ }
803
+ params.socket.send(JSON.stringify({
804
+ type: "client.stream.error",
805
+ clientId: frame.clientId,
806
+ streamId: frame.streamId,
807
+ message: error instanceof Error ? error.message : String(error)
808
+ }));
809
+ }
810
+ })();
811
+ }
812
+ parseRelayFrame(data) {
813
+ try {
814
+ const frame = JSON.parse(String(data ?? ""));
815
+ if (typeof frame !== "object" || !frame || typeof frame.type !== "string") return null;
816
+ if (frame.type === "request") return frame;
817
+ if (frame.type === "client.request" || frame.type === "client.stream.open" || frame.type === "client.stream.cancel") return frame;
818
+ return null;
819
+ } catch {
820
+ return null;
821
+ }
822
+ }
823
+ async ensureDevice(params) {
824
+ if (params.device) return params.device;
825
+ const device = await this.deps.platformClient.registerDevice({
826
+ platformBase: params.context.platformBase,
827
+ token: params.context.token,
828
+ deviceInstallId: params.context.deviceInstallId,
829
+ displayName: params.context.displayName,
830
+ localOrigin: params.context.localOrigin
831
+ });
832
+ this.logger.info(`✓ Remote instance registered: ${device.displayName} (${device.id})`);
833
+ this.logger.info(`✓ Local origin: ${params.context.localOrigin}`);
834
+ this.logger.info(`✓ Platform: ${params.context.platformBase}`);
835
+ return device;
836
+ }
837
+ writeRemoteState(statusStore, next) {
838
+ statusStore?.write(next);
839
+ }
840
+ async runCycle(params) {
841
+ let device = params.device;
842
+ try {
843
+ this.writeRemoteState(params.opts.statusStore, {
844
+ enabled: true,
845
+ state: "connecting",
846
+ deviceId: params.device?.id,
847
+ deviceName: params.context.displayName,
848
+ platformBase: params.context.platformBase,
849
+ localOrigin: params.context.localOrigin,
850
+ lastError: null
851
+ });
852
+ device = await this.ensureDevice({
853
+ device,
854
+ context: params.context
855
+ });
856
+ const wsUrl = `${params.context.platformBase.replace(/^http/i, "ws")}/platform/remote/connect?instanceId=${encodeURIComponent(device.id)}&token=${encodeURIComponent(params.context.token)}`;
857
+ const outcome = await this.connectOnce({
858
+ wsUrl,
859
+ relayBridge: params.relayBridge,
860
+ signal: params.opts.signal,
861
+ statusStore: params.opts.statusStore,
862
+ displayName: params.context.displayName,
863
+ deviceId: device.id,
864
+ platformBase: params.context.platformBase,
865
+ localOrigin: params.context.localOrigin
866
+ });
867
+ if (outcome !== "aborted") this.writeRemoteState(params.opts.statusStore, {
868
+ enabled: true,
869
+ state: "disconnected",
870
+ deviceId: device.id,
871
+ deviceName: params.context.displayName,
872
+ platformBase: params.context.platformBase,
873
+ localOrigin: params.context.localOrigin,
874
+ lastError: null
875
+ });
876
+ return {
877
+ device,
878
+ outcome: outcome === "aborted" ? "aborted" : "retry",
879
+ retryFailure: false,
880
+ lastError: null
881
+ };
882
+ } catch (error) {
883
+ const message = error instanceof Error ? error.message : String(error);
884
+ this.writeRemoteState(params.opts.statusStore, {
885
+ enabled: true,
886
+ state: "error",
887
+ deviceId: params.device?.id,
888
+ deviceName: params.context.displayName,
889
+ platformBase: params.context.platformBase,
890
+ localOrigin: params.context.localOrigin,
891
+ lastError: message
892
+ });
893
+ this.logger.error(`Remote connector error: ${message}`);
894
+ return {
895
+ device,
896
+ outcome: isTerminalRemoteConnectorError(error) ? "stop" : "retry",
897
+ retryFailure: true,
898
+ lastError: message
899
+ };
900
+ }
901
+ }
902
+ async run(opts = {}) {
903
+ const context = this.deps.platformClient.resolveRunContext(opts);
904
+ const relayBridge = (this.deps.relayBridgeFactory ?? ((localOrigin) => new RemoteRelayBridge(localOrigin)))(context.localOrigin);
905
+ await relayBridge.ensureLocalUiHealthy();
906
+ let device = null;
907
+ let preserveRuntimeError = false;
908
+ let consecutiveReconnectFailures = 0;
909
+ while (!opts.signal?.aborted) {
910
+ const cycle = await this.runCycle({
911
+ device,
912
+ context,
913
+ relayBridge,
914
+ opts
915
+ });
916
+ device = cycle.device;
917
+ consecutiveReconnectFailures = cycle.retryFailure ? consecutiveReconnectFailures + 1 : 0;
918
+ if (cycle.outcome === "stop") {
919
+ preserveRuntimeError = true;
920
+ break;
921
+ }
922
+ if (cycle.outcome === "aborted" || !context.autoReconnect || opts.signal?.aborted) break;
923
+ const reconnectDelayMs = resolveReconnectDelayMs(cycle.retryFailure ? consecutiveReconnectFailures : 1, this.random);
924
+ this.logger.warn(`Remote connector disconnected. Reconnecting in ${formatReconnectDelay(reconnectDelayMs)}...`);
925
+ try {
926
+ await this.delayFn(reconnectDelayMs, opts.signal);
927
+ } catch {
928
+ break;
929
+ }
930
+ }
931
+ if (preserveRuntimeError) return;
932
+ this.writeRemoteState(opts.statusStore, {
933
+ enabled: opts.mode === "service" ? true : Boolean(context.config.remote.enabled),
934
+ state: opts.signal?.aborted ? "disconnected" : "disabled",
935
+ deviceId: device?.id,
936
+ deviceName: context.displayName,
937
+ platformBase: context.platformBase,
938
+ localOrigin: context.localOrigin,
939
+ lastError: null
940
+ });
941
+ }
1092
942
  };
1093
-
1094
- // src/remote-status-store.ts
1095
- import { hostname as hostname2 } from "os";
1096
- function normalizeOptionalString2(value) {
1097
- if (typeof value !== "string") {
1098
- return void 0;
1099
- }
1100
- const trimmed = value.trim();
1101
- return trimmed.length > 0 ? trimmed : void 0;
943
+ //#endregion
944
+ //#region src/remote-status-store.ts
945
+ function normalizeOptionalString$1(value) {
946
+ if (typeof value !== "string") return;
947
+ const trimmed = value.trim();
948
+ return trimmed.length > 0 ? trimmed : void 0;
1102
949
  }
1103
950
  function buildConfiguredRemoteState(config) {
1104
- const remote = config.remote;
1105
- return {
1106
- enabled: Boolean(remote.enabled),
1107
- mode: "service",
1108
- state: remote.enabled ? "disconnected" : "disabled",
1109
- ...normalizeOptionalString2(remote.deviceName) ? { deviceName: normalizeOptionalString2(remote.deviceName) } : {},
1110
- ...normalizeOptionalString2(remote.platformApiBase) ? { platformBase: normalizeOptionalString2(remote.platformApiBase) } : {},
1111
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1112
- };
951
+ const remote = config.remote;
952
+ return {
953
+ enabled: Boolean(remote.enabled),
954
+ mode: "service",
955
+ state: remote.enabled ? "disconnected" : "disabled",
956
+ ...normalizeOptionalString$1(remote.deviceName) ? { deviceName: normalizeOptionalString$1(remote.deviceName) } : {},
957
+ ...normalizeOptionalString$1(remote.platformApiBase) ? { platformBase: normalizeOptionalString$1(remote.platformApiBase) } : {},
958
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
959
+ };
1113
960
  }
1114
961
  function resolveRemoteStatusSnapshot(params) {
1115
- if (params.currentRemoteState) {
1116
- return {
1117
- configuredEnabled: Boolean(params.config.remote.enabled),
1118
- runtime: params.currentRemoteState
1119
- };
1120
- }
1121
- if (params.config.remote.enabled) {
1122
- return {
1123
- configuredEnabled: true,
1124
- runtime: {
1125
- ...buildConfiguredRemoteState(params.config),
1126
- deviceName: normalizeOptionalString2(params.config.remote.deviceName) ?? normalizeOptionalString2(params.fallbackDeviceName) ?? hostname2()
1127
- }
1128
- };
1129
- }
1130
- return {
1131
- configuredEnabled: false,
1132
- runtime: null
1133
- };
962
+ if (params.currentRemoteState) return {
963
+ configuredEnabled: Boolean(params.config.remote.enabled),
964
+ runtime: params.currentRemoteState
965
+ };
966
+ if (params.config.remote.enabled) return {
967
+ configuredEnabled: true,
968
+ runtime: {
969
+ ...buildConfiguredRemoteState(params.config),
970
+ deviceName: normalizeOptionalString$1(params.config.remote.deviceName) ?? normalizeOptionalString$1(params.fallbackDeviceName) ?? hostname()
971
+ }
972
+ };
973
+ return {
974
+ configuredEnabled: false,
975
+ runtime: null
976
+ };
1134
977
  }
1135
978
  var RemoteStatusStore = class {
1136
- constructor(mode, deps) {
1137
- this.mode = mode;
1138
- this.deps = deps;
1139
- }
1140
- write(next) {
1141
- this.deps.writeRemoteState({
1142
- ...next,
1143
- mode: this.mode,
1144
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1145
- });
1146
- }
979
+ constructor(mode, deps) {
980
+ this.mode = mode;
981
+ this.deps = deps;
982
+ }
983
+ write(next) {
984
+ this.deps.writeRemoteState({
985
+ ...next,
986
+ mode: this.mode,
987
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
988
+ });
989
+ }
1147
990
  };
1148
-
1149
- // src/remote-service-module.ts
991
+ //#endregion
992
+ //#region src/remote-service-module.ts
1150
993
  var RemoteServiceModule = class {
1151
- constructor(deps) {
1152
- this.deps = deps;
1153
- }
1154
- abortController = null;
1155
- runTask = null;
1156
- releaseOwnership = null;
1157
- start() {
1158
- if (this.runTask) {
1159
- return this.runTask;
1160
- }
1161
- if (!this.deps.uiEnabled) {
1162
- return null;
1163
- }
1164
- const config = this.deps.loadConfig();
1165
- if (!config.remote.enabled) {
1166
- this.deps.statusStore.write({
1167
- enabled: false,
1168
- state: "disabled",
1169
- deviceName: void 0,
1170
- deviceId: void 0,
1171
- platformBase: void 0,
1172
- localOrigin: this.deps.localOrigin,
1173
- lastError: null,
1174
- lastConnectedAt: null
1175
- });
1176
- return null;
1177
- }
1178
- const logger = this.deps.logger ?? {
1179
- info: (message) => console.log(`[remote] ${message}`),
1180
- warn: (message) => console.warn(`[remote] ${message}`),
1181
- error: (message) => console.error(`[remote] ${message}`)
1182
- };
1183
- const ownership = this.deps.claimOwnership?.();
1184
- if (ownership && !ownership.ok) {
1185
- this.deps.statusStore.write({
1186
- enabled: true,
1187
- state: "error",
1188
- deviceName: config.remote.deviceName || void 0,
1189
- deviceId: void 0,
1190
- platformBase: config.remote.platformApiBase || void 0,
1191
- localOrigin: this.deps.localOrigin,
1192
- lastError: ownership.error,
1193
- lastConnectedAt: null
1194
- });
1195
- logger.error(ownership.error);
1196
- return null;
1197
- }
1198
- this.releaseOwnership = ownership?.release ?? null;
1199
- this.abortController = new AbortController();
1200
- const connector = this.deps.createConnector(logger);
1201
- this.runTask = connector.run({
1202
- mode: "service",
1203
- signal: this.abortController.signal,
1204
- autoReconnect: config.remote.autoReconnect,
1205
- localOrigin: this.deps.localOrigin,
1206
- statusStore: this.deps.statusStore
1207
- });
1208
- void this.runTask.catch((error) => {
1209
- const message = error instanceof Error ? error.message : String(error);
1210
- const latestConfig = this.deps.loadConfig();
1211
- this.deps.statusStore.write({
1212
- enabled: true,
1213
- state: "error",
1214
- deviceName: latestConfig.remote.deviceName || void 0,
1215
- deviceId: void 0,
1216
- platformBase: latestConfig.remote.platformApiBase || void 0,
1217
- localOrigin: this.deps.localOrigin,
1218
- lastError: message
1219
- });
1220
- logger.error(message);
1221
- }).finally(() => {
1222
- this.releaseOwnership?.();
1223
- this.releaseOwnership = null;
1224
- });
1225
- return this.runTask;
1226
- }
1227
- async restart() {
1228
- await this.stop();
1229
- this.start();
1230
- }
1231
- async stop() {
1232
- this.abortController?.abort();
1233
- try {
1234
- await this.runTask;
1235
- } catch {
1236
- } finally {
1237
- this.abortController = null;
1238
- this.runTask = null;
1239
- this.releaseOwnership?.();
1240
- this.releaseOwnership = null;
1241
- }
1242
- }
1243
- };
1244
- export {
1245
- RemoteConnector,
1246
- RemotePlatformClient,
1247
- RemoteRelayBridge,
1248
- RemoteRuntimeActions,
1249
- RemoteServiceModule,
1250
- RemoteStatusStore,
1251
- buildConfiguredRemoteState,
1252
- decodePlatformSessionTokenPayload,
1253
- delay,
1254
- isValidPlatformSessionToken,
1255
- normalizeOptionalString,
1256
- readPlatformSessionTokenState,
1257
- redactWsUrl,
1258
- registerRemoteCommands,
1259
- resolveRemoteStatusSnapshot
994
+ abortController = null;
995
+ runTask = null;
996
+ releaseOwnership = null;
997
+ constructor(deps) {
998
+ this.deps = deps;
999
+ }
1000
+ start() {
1001
+ if (this.runTask) return this.runTask;
1002
+ if (!this.deps.uiEnabled) return null;
1003
+ const config = this.deps.loadConfig();
1004
+ if (!config.remote.enabled) {
1005
+ this.deps.statusStore.write({
1006
+ enabled: false,
1007
+ state: "disabled",
1008
+ deviceName: void 0,
1009
+ deviceId: void 0,
1010
+ platformBase: void 0,
1011
+ localOrigin: this.deps.localOrigin,
1012
+ lastError: null,
1013
+ lastConnectedAt: null
1014
+ });
1015
+ return null;
1016
+ }
1017
+ const logger = this.deps.logger ?? {
1018
+ info: (message) => console.log(`[remote] ${message}`),
1019
+ warn: (message) => console.warn(`[remote] ${message}`),
1020
+ error: (message) => console.error(`[remote] ${message}`)
1021
+ };
1022
+ const ownership = this.deps.claimOwnership?.();
1023
+ if (ownership && !ownership.ok) {
1024
+ this.deps.statusStore.write({
1025
+ enabled: true,
1026
+ state: "error",
1027
+ deviceName: config.remote.deviceName || void 0,
1028
+ deviceId: void 0,
1029
+ platformBase: config.remote.platformApiBase || void 0,
1030
+ localOrigin: this.deps.localOrigin,
1031
+ lastError: ownership.error,
1032
+ lastConnectedAt: null
1033
+ });
1034
+ logger.error(ownership.error);
1035
+ return null;
1036
+ }
1037
+ this.releaseOwnership = ownership?.release ?? null;
1038
+ this.abortController = new AbortController();
1039
+ this.runTask = this.deps.createConnector(logger).run({
1040
+ mode: "service",
1041
+ signal: this.abortController.signal,
1042
+ autoReconnect: config.remote.autoReconnect,
1043
+ localOrigin: this.deps.localOrigin,
1044
+ statusStore: this.deps.statusStore
1045
+ });
1046
+ this.runTask.catch((error) => {
1047
+ const message = error instanceof Error ? error.message : String(error);
1048
+ const latestConfig = this.deps.loadConfig();
1049
+ this.deps.statusStore.write({
1050
+ enabled: true,
1051
+ state: "error",
1052
+ deviceName: latestConfig.remote.deviceName || void 0,
1053
+ deviceId: void 0,
1054
+ platformBase: latestConfig.remote.platformApiBase || void 0,
1055
+ localOrigin: this.deps.localOrigin,
1056
+ lastError: message
1057
+ });
1058
+ logger.error(message);
1059
+ }).finally(() => {
1060
+ this.releaseOwnership?.();
1061
+ this.releaseOwnership = null;
1062
+ });
1063
+ return this.runTask;
1064
+ }
1065
+ async restart() {
1066
+ await this.stop();
1067
+ this.start();
1068
+ }
1069
+ async stop() {
1070
+ this.abortController?.abort();
1071
+ try {
1072
+ await this.runTask;
1073
+ } catch {} finally {
1074
+ this.abortController = null;
1075
+ this.runTask = null;
1076
+ this.releaseOwnership?.();
1077
+ this.releaseOwnership = null;
1078
+ }
1079
+ }
1260
1080
  };
1081
+ //#endregion
1082
+ export { RemoteConnector, RemotePlatformClient, RemoteRelayBridge, RemoteRuntimeActions, RemoteServiceModule, RemoteStatusStore, buildConfiguredRemoteState, decodePlatformSessionTokenPayload, delay, isValidPlatformSessionToken, normalizeOptionalString, readPlatformSessionTokenState, redactWsUrl, registerRemoteCommands, resolveRemoteStatusSnapshot };