@nextclaw/remote 0.1.76 → 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.
- package/dist/index.d.ts +186 -177
- package/dist/index.js +1012 -1190
- package/package.json +4 -5
package/dist/index.js
CHANGED
|
@@ -1,1260 +1,1082 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/remote-runtime-actions.ts
|
|
12
17
|
var RemoteRuntimeActions = class {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
99
|
+
return readPlatformSessionTokenState(token).valid;
|
|
115
100
|
}
|
|
116
|
-
|
|
117
|
-
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/remote-platform-client.ts
|
|
118
103
|
function ensureDir(path) {
|
|
119
|
-
|
|
104
|
+
mkdirSync(path, { recursive: true });
|
|
120
105
|
}
|
|
121
106
|
function readJsonFile(path) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
285
|
-
import { ensureUiBridgeSecret } from "@nextclaw/server";
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/remote-relay-bridge.ts
|
|
286
242
|
function encodeBase64(bytes) {
|
|
287
|
-
|
|
243
|
+
return Buffer.from(bytes).toString("base64");
|
|
288
244
|
}
|
|
289
245
|
function decodeBase64(base64) {
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/remote-app.adapter.ts
|
|
489
425
|
function toWebSocketUrl(origin, path) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
729
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
769
|
-
|
|
650
|
+
const seconds = delayMs / 1e3;
|
|
651
|
+
return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s`;
|
|
770
652
|
}
|
|
771
|
-
|
|
772
|
-
|
|
653
|
+
//#endregion
|
|
654
|
+
//#region src/remote-connector-websocket-error.utils.ts
|
|
773
655
|
function readRemoteConnectorSocketErrorMessage(event) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region src/remote-service-module.ts
|
|
1150
993
|
var RemoteServiceModule = class {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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 };
|