@cjhyy/code-shell-server 0.9.48 → 0.9.49

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 (34) hide show
  1. package/dist/desktop-web/http-api.d.ts +9 -0
  2. package/dist/desktop-web/http-api.js +17 -1
  3. package/dist/http-byte-range.d.ts +5 -0
  4. package/dist/http-byte-range.js +19 -0
  5. package/dist/hub/files.js +71 -9
  6. package/dist/index.panels.d.ts +1 -0
  7. package/dist/index.panels.js +1 -0
  8. package/dist/mobile-remote/access-passcode.d.ts +5 -0
  9. package/dist/mobile-remote/access-passcode.js +48 -9
  10. package/dist/mobile-remote/mobile-client-event-validator.js +30 -1
  11. package/dist/mobile-remote/remote-host-manager.d.ts +7 -1
  12. package/dist/mobile-remote/remote-host-manager.js +132 -56
  13. package/dist/mobile-remote/tunnel-manager.d.ts +1 -0
  14. package/dist/mobile-remote/tunnel-manager.js +45 -20
  15. package/dist/panels/background-service-process.d.ts +39 -0
  16. package/dist/panels/background-service-process.js +457 -0
  17. package/dist/panels/background-service-store.d.ts +67 -0
  18. package/dist/panels/background-service-store.js +539 -0
  19. package/dist/panels/background-services.d.ts +193 -0
  20. package/dist/panels/background-services.js +1159 -0
  21. package/dist/panels/bridge-contract.d.ts +1 -1
  22. package/dist/panels/execution-gate.d.ts +7 -1
  23. package/dist/panels/execution-gate.js +26 -0
  24. package/dist/panels/management.js +49 -13
  25. package/dist/panels/process-files.js +28 -3
  26. package/dist/panels/resources/library.d.ts +1 -4
  27. package/dist/panels/resources/library.js +2 -18
  28. package/dist/panels/resources/references.js +21 -8
  29. package/dist/panels/runtime.js +1 -1
  30. package/dist/panels/tool-jobs.d.ts +2 -0
  31. package/dist/panels/tool-jobs.js +1 -1
  32. package/dist/sessions-service.d.ts +4 -0
  33. package/dist/sessions-service.js +17 -0
  34. package/package.json +6 -6
@@ -34,8 +34,17 @@ export interface DesktopWebApiOptions {
34
34
  sessionTtlMs?: number;
35
35
  }
36
36
  export interface DesktopWebHttpApi {
37
+ /**
38
+ * Optional independent authorization for GET/HEAD Panel assets. A handler may
39
+ * serve only an existing short-lived capability after revalidating its owner
40
+ * and project grant. Return false without writing when no capability matches;
41
+ * the caller must then apply its ordinary access gate. Never accepts API calls.
42
+ */
43
+ handleCapabilityAsset?: (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
37
44
  handle: (request: IncomingMessage, response: ServerResponse, context: {
38
45
  baseUrl: string;
46
+ /** Recheck the entrance authorization captured after its access gate succeeded. */
47
+ isAccessAuthorized?: () => boolean;
39
48
  }) => Promise<boolean>;
40
49
  /** Reopen after RemoteHostManager.stop(), without reviving previous cookies. */
41
50
  start: () => void;
@@ -163,6 +163,8 @@ export function createDesktopWebApi(options) {
163
163
  if (!identity || closed || identity.generation !== generation)
164
164
  return false;
165
165
  try {
166
+ if (identity.isAccessAuthorized?.() === false)
167
+ return false;
166
168
  if (principal(identity.token)?.session !== identity.session)
167
169
  return false;
168
170
  const resolved = await options.resolveWorkspace(identity.input, identity.session.deviceId);
@@ -170,6 +172,7 @@ export function createDesktopWebApi(options) {
170
172
  return (resolved === identity.cwd &&
171
173
  !closed &&
172
174
  identity.generation === generation &&
175
+ identity.isAccessAuthorized?.() !== false &&
173
176
  principal(identity.token)?.session === identity.session);
174
177
  }
175
178
  catch {
@@ -302,6 +305,8 @@ export function createDesktopWebApi(options) {
302
305
  try {
303
306
  if (closed)
304
307
  throw new HubConfigurationError(503, "桌面远程服务已停止。");
308
+ if (context.isAccessAuthorized?.() === false)
309
+ throw new HubConfigurationError(401, "访问口令已更新,请重新连接桌面。");
305
310
  const requestGeneration = generation;
306
311
  const origin = sameOrigin(request, context.baseUrl);
307
312
  const secure = origin.protocol === "https:";
@@ -310,6 +315,8 @@ export function createDesktopWebApi(options) {
310
315
  const input = await credentials(request);
311
316
  if (closed || requestGeneration !== generation)
312
317
  throw new HubConfigurationError(503, "桌面远程服务已停止。");
318
+ if (context.isAccessAuthorized?.() === false)
319
+ throw new HubConfigurationError(401, "访问口令已更新,请重新连接桌面。");
313
320
  const device = options.devices.authenticate(input.deviceId, input.secretHash);
314
321
  if (!device)
315
322
  throw new HubConfigurationError(401, "设备未配对或已被撤销,请在桌面重新配对。");
@@ -360,6 +367,7 @@ export function createDesktopWebApi(options) {
360
367
  const id = await environmentIdentity(options.dataDir);
361
368
  if (closed ||
362
369
  generation !== requestGeneration ||
370
+ context.isAccessAuthorized?.() === false ||
363
371
  principal(token)?.session !== current.session)
364
372
  throw new HubConfigurationError(401, "设备连接已失效,请重新连接桌面。");
365
373
  json(response, 200, describeEnvironment(id, "desktop"));
@@ -369,11 +377,19 @@ export function createDesktopWebApi(options) {
369
377
  const cwd = await options.resolveWorkspace(input, current.device.id);
370
378
  if (closed ||
371
379
  generation !== requestGeneration ||
380
+ context.isAccessAuthorized?.() === false ||
372
381
  principal(token)?.session !== current.session)
373
382
  throw new HubConfigurationError(401, "设备连接已失效,请重新连接桌面。");
374
383
  if (!cwd || !isAbsolute(cwd))
375
384
  throw new HubConfigurationError(403, "只能访问桌面已知的项目或会话工作区。");
376
- identities.set(request, { token, session: current.session, input, cwd, generation });
385
+ identities.set(request, {
386
+ token,
387
+ session: current.session,
388
+ input,
389
+ cwd,
390
+ generation,
391
+ isAccessAuthorized: context.isAccessAuthorized,
392
+ });
377
393
  const services = servicesFor(cwd);
378
394
  services.active++;
379
395
  try {
@@ -0,0 +1,5 @@
1
+ /** A bounded single HTTP byte range. Multipart ranges are deliberately unsupported. */
2
+ export declare function rangeBounds(range: string, size: number): {
3
+ start: number;
4
+ end: number;
5
+ } | null;
@@ -0,0 +1,19 @@
1
+ /** A bounded single HTTP byte range. Multipart ranges are deliberately unsupported. */
2
+ export function rangeBounds(range, size) {
3
+ const match = /^bytes=(\d*)-(\d*)$/.exec(range);
4
+ if (!match || (!match[1] && !match[2]) || !Number.isSafeInteger(size) || size <= 0)
5
+ return null;
6
+ const first = match[1] ? Number(match[1]) : undefined;
7
+ const last = match[2] ? Number(match[2]) : undefined;
8
+ if ((first !== undefined && !Number.isSafeInteger(first)) ||
9
+ (last !== undefined && !Number.isSafeInteger(last)))
10
+ return null;
11
+ if (first === undefined) {
12
+ if (!last || last <= 0)
13
+ return null;
14
+ return { start: Math.max(0, size - last), end: size - 1 };
15
+ }
16
+ if (first >= size || (last !== undefined && last < first))
17
+ return null;
18
+ return { start: first, end: Math.min(last ?? size - 1, size - 1) };
19
+ }
package/dist/hub/files.js CHANGED
@@ -2,8 +2,10 @@ import { constants } from "node:fs";
2
2
  import { lstat, open, opendir, realpath } from "node:fs/promises";
3
3
  import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { pipeline } from "node:stream/promises";
5
+ import { Readable } from "node:stream";
6
+ import { rangeBounds } from "../http-byte-range.js";
5
7
  import { hubJson } from "./uploads.js";
6
- const MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
8
+ const MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024;
7
9
  const MAX_PREVIEW_BYTES = 512 * 1024;
8
10
  const IMAGE_TYPES = {
9
11
  ".png": "image/png",
@@ -121,7 +123,7 @@ export function createHubFiles(options) {
121
123
  try {
122
124
  if (closed)
123
125
  throw new FileRequestError(503, "服务正在关闭。");
124
- if (req.method !== "GET")
126
+ if (req.method !== "GET" && !(req.method === "HEAD" && url.pathname.endsWith("/content")))
125
127
  throw new FileRequestError(405, "文件页面只支持读取。");
126
128
  if (!(await options.isAuthorized(req)))
127
129
  throw new FileRequestError(401, "请重新登录。");
@@ -184,14 +186,16 @@ export function createHubFiles(options) {
184
186
  if (!(await options.isAuthorized(req)))
185
187
  throw new FileRequestError(401, "请重新登录。");
186
188
  await opened.verify();
189
+ if (!(await options.isAuthorized(req)))
190
+ throw new FileRequestError(401, "请重新登录。");
187
191
  hubJson(res, 200, { path: location.relativePath, files, truncated });
188
192
  }
189
193
  else {
190
- if (info.size > MAX_DOWNLOAD_BYTES)
191
- throw new FileRequestError(413, "文件超过 100 MB,请从服务器工作目录获取。");
192
194
  if (!(await options.isAuthorized(req)))
193
195
  throw new FileRequestError(401, "请重新登录。");
194
196
  await opened.verify();
197
+ if (!(await options.isAuthorized(req)))
198
+ throw new FileRequestError(401, "请重新登录。");
195
199
  const imageType = IMAGE_TYPES[extname(location.path).toLowerCase()];
196
200
  const preview = url.searchParams.get("preview") === "true";
197
201
  if (preview) {
@@ -199,6 +203,9 @@ export function createHubFiles(options) {
199
203
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
200
204
  const bytes = buffer.subarray(0, bytesRead);
201
205
  const binary = bytes.includes(0) || !!imageType;
206
+ await opened.verify();
207
+ if (!(await options.isAuthorized(req)))
208
+ throw new FileRequestError(401, "请重新登录。");
202
209
  hubJson(res, 200, {
203
210
  path: location.relativePath,
204
211
  name: basename(location.path),
@@ -211,21 +218,76 @@ export function createHubFiles(options) {
211
218
  });
212
219
  }
213
220
  else {
221
+ if (info.size > MAX_DOWNLOAD_BYTES)
222
+ throw new FileRequestError(413, "文件超过 2 GiB 下载上限。");
214
223
  const inline = url.searchParams.get("inline") === "true" && !!imageType;
215
224
  const filename = basename(location.path);
216
225
  const encoded = encodeURIComponent(filename).replace(/[!'()*]/g, (value) => `%${value.charCodeAt(0).toString(16).toUpperCase()}`);
217
- res.writeHead(200, {
226
+ // Workspace outputs are mutable; without a matching validator an
227
+ // If-Range request must receive the complete current representation.
228
+ const range = req.headers["if-range"] === undefined ? req.headers.range : undefined;
229
+ const bounds = range === undefined ? { start: 0, end: info.size - 1 } : rangeBounds(range, info.size);
230
+ const headers = {
218
231
  "content-type": inline ? imageType : "application/octet-stream",
219
- "content-length": info.size,
220
232
  "content-disposition": `${inline ? "inline" : "attachment"}; filename="download${extname(filename).replace(/[^.A-Za-z0-9]/g, "")}"; filename*=UTF-8''${encoded}`,
233
+ "accept-ranges": "bytes",
221
234
  "cache-control": "no-store",
222
235
  "x-content-type-options": "nosniff",
223
236
  "content-security-policy": "default-src 'none'; sandbox",
237
+ "referrer-policy": "no-referrer",
238
+ };
239
+ if (!bounds) {
240
+ res.writeHead(416, {
241
+ ...headers,
242
+ "content-range": `bytes */${info.size}`,
243
+ "content-length": 0,
244
+ });
245
+ res.end();
246
+ return true;
247
+ }
248
+ res.writeHead(range === undefined ? 200 : 206, {
249
+ ...headers,
250
+ "content-length": bounds.end - bounds.start + 1,
251
+ ...(range === undefined
252
+ ? {}
253
+ : { "content-range": `bytes ${bounds.start}-${bounds.end}/${info.size}` }),
224
254
  });
225
- if (info.size === 0)
255
+ if (req.method === "HEAD" || info.size === 0)
226
256
  res.end();
227
- else
228
- await pipeline(handle.createReadStream({ autoClose: false, end: Math.max(0, info.size - 1) }), res);
257
+ else {
258
+ const input = handle.createReadStream({ autoClose: false, ...bounds });
259
+ const authorize = async () => {
260
+ if (closed || res.destroyed || !(await options.isAuthorized(req)))
261
+ throw new FileRequestError(401, "下载授权已失效。");
262
+ };
263
+ let checking = false;
264
+ const timer = setInterval(() => {
265
+ if (checking)
266
+ return;
267
+ checking = true;
268
+ void authorize()
269
+ .catch(() => {
270
+ input.destroy();
271
+ res.destroy();
272
+ })
273
+ .finally(() => {
274
+ checking = false;
275
+ });
276
+ }, 250);
277
+ timer.unref();
278
+ try {
279
+ await pipeline(Readable.from((async function* () {
280
+ for await (const chunk of input) {
281
+ await authorize();
282
+ yield chunk;
283
+ }
284
+ })(), { objectMode: false }), res);
285
+ }
286
+ finally {
287
+ clearInterval(timer);
288
+ input.destroy();
289
+ }
290
+ }
229
291
  }
230
292
  }
231
293
  }
@@ -26,3 +26,4 @@ export * from "./panels/task-cookie-host.js";
26
26
  export * from "./panels/execution-gate.js";
27
27
  export * from "./panels/package-mutation.js";
28
28
  export * from "./panels/model-traces.js";
29
+ export * from "./panels/background-services.js";
@@ -25,3 +25,4 @@ export * from "./panels/task-cookie-host.js";
25
25
  export * from "./panels/execution-gate.js";
26
26
  export * from "./panels/package-mutation.js";
27
27
  export * from "./panels/model-traces.js";
28
+ export * from "./panels/background-services.js";
@@ -49,6 +49,11 @@ export declare class AccessPasscode {
49
49
  private lockedUntil;
50
50
  constructor(opts: AccessPasscodeOptions);
51
51
  isSet(): boolean;
52
+ /**
53
+ * After a successful gate, retain its passcode generation for derived grants.
54
+ * Rechecks never retain/retry the plaintext or spend the verification budget.
55
+ */
56
+ captureValidity(): () => boolean;
52
57
  /** Set (or rotate) the passcode. Rotating `secret` invalidates old tokens. */
53
58
  set(passcode: string): void;
54
59
  /**
@@ -11,6 +11,10 @@ const SCRYPT_KEYLEN = 32;
11
11
  export const MIN_ACCESS_PASSCODE_LENGTH = 4;
12
12
  export const MAX_ACCESS_PASSCODE_LENGTH = 256;
13
13
  const MAX_REMEMBER_TOKEN_LENGTH = 128;
14
+ /** Marks which gate decision produced a response: "granted", "denied" or
15
+ * "unconfigured". The challenge page reports a passcode outcome only from this
16
+ * marker, never from a status a tunnel edge or proxy could also produce. */
17
+ const ACCESS_OUTCOME_HEADER = "X-CodeShell-Access";
14
18
  const MAX_ACCESS_RECORD_BYTES = 4 * 1024;
15
19
  /**
16
20
  * The public-tunnel access gate. A single shared passcode (set by the desktop
@@ -49,6 +53,14 @@ export class AccessPasscode {
49
53
  isSet() {
50
54
  return this.read() !== undefined;
51
55
  }
56
+ /**
57
+ * After a successful gate, retain its passcode generation for derived grants.
58
+ * Rechecks never retain/retry the plaintext or spend the verification budget.
59
+ */
60
+ captureValidity() {
61
+ const secret = this.read()?.secret;
62
+ return () => secret !== undefined && this.read()?.secret === secret;
63
+ }
52
64
  /** Set (or rotate) the passcode. Rotating `secret` invalidates old tokens. */
53
65
  set(passcode) {
54
66
  if (!isValidPasscode(passcode)) {
@@ -132,13 +144,18 @@ export class AccessPasscode {
132
144
  gate(req, res) {
133
145
  if (!this.isSet()) {
134
146
  // No passcode configured → tunnel must not be reachable. Refuse.
135
- res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
147
+ res.writeHead(403, {
148
+ "content-type": "text/plain; charset=utf-8",
149
+ [ACCESS_OUTCOME_HEADER]: "unconfigured",
150
+ });
136
151
  res.end("access passcode not configured");
137
152
  return false;
138
153
  }
139
154
  const cookieToken = readCookie(req.headers.cookie, COOKIE_NAME);
140
- if (cookieToken && this.verifyToken(cookieToken))
155
+ if (cookieToken && this.verifyToken(cookieToken)) {
156
+ res.setHeader(ACCESS_OUTCOME_HEADER, "granted");
141
157
  return true;
158
+ }
142
159
  const supplied = readPasscodeParam(req);
143
160
  let wrongAttempt = false;
144
161
  if (supplied) {
@@ -151,6 +168,7 @@ export class AccessPasscode {
151
168
  // user every visit. Lax sends it on top-level navigations. Secure
152
169
  // because the tunnel terminates TLS (always https).
153
170
  `${COOKIE_NAME}=${token}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${Math.floor(this.tokenMaxAgeMs / 1000)}`);
171
+ res.setHeader(ACCESS_OUTCOME_HEADER, "granted");
154
172
  const cleanLocation = acceptsHtml(req) ? locationWithoutPasscode(req.url) : undefined;
155
173
  if (cleanLocation) {
156
174
  // The HTML challenge has a no-query JavaScript path below, but keep a
@@ -176,11 +194,17 @@ export class AccessPasscode {
176
194
  // to enter the passcode). Non-navigation requests (WS upgrade, fetch) keep
177
195
  // the lightweight text 401.
178
196
  if (acceptsHtml(req)) {
179
- res.writeHead(401, { "content-type": "text/html; charset=utf-8" });
197
+ res.writeHead(401, {
198
+ "content-type": "text/html; charset=utf-8",
199
+ [ACCESS_OUTCOME_HEADER]: "denied",
200
+ });
180
201
  res.end(challengePage(req.url, wrongAttempt, this.isLocked()));
181
202
  return false;
182
203
  }
183
- res.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
204
+ res.writeHead(401, {
205
+ "content-type": "text/plain; charset=utf-8",
206
+ [ACCESS_OUTCOME_HEADER]: "denied",
207
+ });
184
208
  res.end("访问口令无效或缺失");
185
209
  return false;
186
210
  }
@@ -457,17 +481,32 @@ function challengePage(url, wrong, locked) {
457
481
  const value = input?.value ?? "";
458
482
  if (!value) return;
459
483
  try {
484
+ // Request the page as the following navigation will (HTML), so SPA
485
+ // routes resolve the same way. A passcode outcome is reported only from
486
+ // the gate's own marker: a tunnel edge or proxy can answer 4xx/5xx
487
+ // without the request ever reaching the gate.
460
488
  const response = await fetch(window.location.href, {
461
- headers: { "X-Access-Passcode": value, Accept: "application/json" },
489
+ headers: { "X-Access-Passcode": value, Accept: "text/html" },
462
490
  credentials: "same-origin",
463
491
  });
464
- if (!response.ok) {
492
+ if (response.ok) {
493
+ window.location.replace(${cleanLocation});
494
+ return;
495
+ }
496
+ const access = response.headers.get("${ACCESS_OUTCOME_HEADER}");
497
+ const status = "(HTTP " + response.status + ")";
498
+ if (access === "denied") {
465
499
  if (note) note.textContent = "口令不正确或尝试过多,请稍后重试。";
466
- if (note) note.className = "err";
467
500
  if (input) input.value = "";
468
- return;
501
+ } else if (note) {
502
+ note.textContent =
503
+ access === "granted"
504
+ ? "访问验证已通过,但页面加载失败" + status + ",请稍后重试或检查桌面端。"
505
+ : access === "unconfigured"
506
+ ? "远程访问未就绪" + status + ",请在桌面端检查手机遥控设置。"
507
+ : "请求未完成" + status + ",口令尚未得到验证,请稍后重试。";
469
508
  }
470
- window.location.replace(${cleanLocation});
509
+ if (note) note.className = "err";
471
510
  } catch {
472
511
  if (note) note.textContent = "连接失败,请重试。";
473
512
  if (note) note.className = "err";
@@ -29,6 +29,9 @@ export function parseMobileClientEvent(value) {
29
29
  case "chat.send":
30
30
  valid =
31
31
  boundedOptionalEmptyString(event.text, MAX_MESSAGE_LENGTH) &&
32
+ (event.modelKey === undefined ||
33
+ (boundedString(event.modelKey, MAX_ID_LENGTH) &&
34
+ /^(codex|claude-code)\/[^\r\n\0]*$/.test(event.modelKey))) &&
32
35
  optionalBoundedString(event.sessionId, MAX_ID_LENGTH) &&
33
36
  (event.modelTraceSource === undefined ||
34
37
  (typeof event.modelTraceSource === "string" &&
@@ -52,6 +55,17 @@ export function parseMobileClientEvent(value) {
52
55
  case "session.recovery.cancel":
53
56
  valid = true;
54
57
  break;
58
+ case "session.outputRecovery.plan":
59
+ case "session.outputRecovery.apply":
60
+ valid =
61
+ boundedString(event.sessionId, 128) &&
62
+ boundedString(event.requestId, 128) &&
63
+ (event.type === "session.outputRecovery.plan" ||
64
+ (boundedString(event.planId, 128) && event.acceptHistoricalGap === true)) &&
65
+ Object.keys(event).every((key) => (event.type === "session.outputRecovery.plan"
66
+ ? ["type", "sessionId", "requestId"]
67
+ : ["type", "sessionId", "requestId", "planId", "acceptHistoricalGap"]).includes(key));
68
+ break;
55
69
  case "session.legacyBase":
56
70
  valid =
57
71
  boundedString(event.sessionId, 128) &&
@@ -97,6 +111,15 @@ export function parseMobileClientEvent(value) {
97
111
  optionalMember(event.pathScope, PATH_SCOPES);
98
112
  break;
99
113
  case "session.list":
114
+ valid =
115
+ optionalBoundedString(event.requestId, 128) &&
116
+ optionalBoundedString(event.cursor, 2048) &&
117
+ (event.cwds === undefined ||
118
+ (Array.isArray(event.cwds) &&
119
+ event.cwds.length > 0 &&
120
+ event.cwds.length <= 256 &&
121
+ event.cwds.every(boundedPath)));
122
+ break;
100
123
  case "room.list":
101
124
  case "room.projects":
102
125
  valid = true;
@@ -115,6 +138,9 @@ export function parseMobileClientEvent(value) {
115
138
  case "model.set":
116
139
  valid = boundedString(event.model, MAX_ID_LENGTH);
117
140
  break;
141
+ case "runtime.models":
142
+ valid = true;
143
+ break;
118
144
  case "goal.extend":
119
145
  valid =
120
146
  boundedString(event.sessionId, MAX_ID_LENGTH) &&
@@ -164,7 +190,10 @@ export function parseMobileClientEvent(value) {
164
190
  optionalMember(event.kind, KINDS);
165
191
  break;
166
192
  case "ccRoom.listSessions":
167
- valid = boundedPath(event.cwd) && optionalMember(event.kind, KINDS);
193
+ valid =
194
+ boundedPath(event.cwd) &&
195
+ optionalMember(event.kind, KINDS) &&
196
+ (event.all === undefined || typeof event.all === "boolean");
168
197
  break;
169
198
  case "ccRoom.openSession":
170
199
  valid =
@@ -21,7 +21,8 @@ export interface RemoteHostStartOptions {
21
21
  /**
22
22
  * "lan" (default) keeps the existing LAN behaviour unchanged. "tunnel" binds
23
23
  * 127.0.0.1 (cloudflared connects to loopback) and inserts a passcode gate in
24
- * front of every HTTP route and the WS upgrade. "relay" also requires an
24
+ * front of ordinary HTTP routes and the WS upgrade. Host-verified Panel asset
25
+ * capabilities use their own short-lived authorization. "relay" also requires an
25
26
  * initialized passcode and a stable HTTPS origin before pairing/WS upgrades.
26
27
  */
27
28
  mode?: "lan" | "tunnel" | "relay";
@@ -76,6 +77,8 @@ export declare class RemoteHostManager extends EventEmitter {
76
77
  private publicBaseUrl?;
77
78
  /** Passcode gate, present only in tunnel mode. */
78
79
  private passcode?;
80
+ private socketAccessValidity;
81
+ private accessSweep?;
79
82
  /** Where the built mobile app lives (out/mobile). */
80
83
  private readonly mobileRootDir;
81
84
  /** Dev proxy target for /mobile (HMR); undefined in prod. */
@@ -122,6 +125,8 @@ export declare class RemoteHostManager extends EventEmitter {
122
125
  /** Reply only to the currently authenticated tab; retired/revoked viewers receive nothing. */
123
126
  hasAuthenticatedViewer(viewerId: string, deviceId: string): boolean;
124
127
  sendToViewer(viewerId: string, event: MobileServerEvent): void;
128
+ /** Preserve the full worker envelope while retaining exact authenticated-tab routing. */
129
+ sendRawToViewer(viewerId: string, payload: string): void;
125
130
  /** Send a raw JSON-RPC worker line to ONLY one authenticated device. */
126
131
  sendRawToDevice(deviceId: string, payload: string): void;
127
132
  /** Broadcast a raw line (e.g. a mirrored worker→renderer JSON-RPC line) to
@@ -136,6 +141,7 @@ export declare class RemoteHostManager extends EventEmitter {
136
141
  * socket is still delivered. Returns false when retired.
137
142
  */
138
143
  private sendBounded;
144
+ private hasSocketAccess;
139
145
  private releaseSocket;
140
146
  private bindSocketDevice;
141
147
  /** Call after revoking/removing a trusted device to release both transports immediately. */