@skylandnpm/octopus-proxy 0.1.50 → 0.1.52

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.
@@ -12,7 +12,7 @@ import {
12
12
  probeRemote,
13
13
  remoteSessionBudget,
14
14
  z
15
- } from "./chunk-PPH4OB7Y.js";
15
+ } from "./chunk-XIJKEY4L.js";
16
16
 
17
17
  // ../../node_modules/.pnpm/ajv@8.17.1/node_modules/ajv/dist/compile/codegen/code.js
18
18
  var require_code = __commonJS({
@@ -3321,8 +3321,8 @@ var require_utils = __commonJS({
3321
3321
  var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3322
3322
  var HOST_DELIM_RE = /[@/?#:]/g;
3323
3323
  var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
3324
- function reescapeHostDelimiters(host, isIP) {
3325
- const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3324
+ function reescapeHostDelimiters(host, isIP2) {
3325
+ const re = isIP2 ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3326
3326
  re.lastIndex = 0;
3327
3327
  return host.replace(re, (ch) => HOST_DELIMS[ch]);
3328
3328
  }
@@ -4011,8 +4011,8 @@ var require_fast_uri = __commonJS({
4011
4011
  const host = matches[4];
4012
4012
  return hasMalformedPercentEncoding(matches[3]) || host !== void 0 && !isIPLiteral(host) && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]);
4013
4013
  }
4014
- function canonicalizeHost(parsed, options, schemeHandler, isIP) {
4015
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
4014
+ function canonicalizeHost(parsed, options, schemeHandler, isIP2) {
4015
+ if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP2 === false && nonSimpleDomain(parsed.host)) {
4016
4016
  try {
4017
4017
  parsed.host = new URL("http://" + parsed.host).hostname;
4018
4018
  } catch (e) {
@@ -4039,7 +4039,7 @@ var require_fast_uri = __commonJS({
4039
4039
  let malformedHost = false;
4040
4040
  let malformedIPLiteral = false;
4041
4041
  let malformedScheme = false;
4042
- let isIP = false;
4042
+ let isIP2 = false;
4043
4043
  if (options.reference === "suffix") {
4044
4044
  if (options.scheme) {
4045
4045
  uri = options.scheme + ":" + uri;
@@ -4102,15 +4102,15 @@ var require_fast_uri = __commonJS({
4102
4102
  const bracketedIPLiteral = isIPLiteral(parsed.host);
4103
4103
  const hasIPLiteralBracket = parsed.host.indexOf("[") !== -1 || parsed.host.indexOf("]") !== -1;
4104
4104
  const ipv6result = normalizeIPv6(parsed.host);
4105
- isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true;
4105
+ isIP2 = ipv6result.isIPV6 || ipv6result.isIPVFuture === true;
4106
4106
  malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
4107
- parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
4107
+ parsed.host = isIP2 ? ipv6result.host : ipv6result.host.toLowerCase();
4108
4108
  if (malformedIPLiteral) {
4109
4109
  parsed.error = parsed.error || "URI host is malformed.";
4110
4110
  malformedAuthorityOrPort = true;
4111
4111
  }
4112
4112
  } else {
4113
- isIP = true;
4113
+ isIP2 = true;
4114
4114
  }
4115
4115
  }
4116
4116
  if (parsed.scheme === void 0 && parsed.userinfo === void 0 && parsed.host === void 0 && parsed.port === void 0 && parsed.query === void 0 && !parsed.path) {
@@ -4127,14 +4127,14 @@ var require_fast_uri = __commonJS({
4127
4127
  }
4128
4128
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
4129
4129
  if (!malformedIPLiteral) {
4130
- malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
4130
+ malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP2);
4131
4131
  }
4132
4132
  if (uri.indexOf("%") !== -1 && parsed.host !== void 0 && !malformedIPLiteral) {
4133
- let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4134
- if (!isIP) {
4133
+ let host = isIP2 ? parsed.host : normalizePercentEncoding(parsed.host, true);
4134
+ if (!isIP2) {
4135
4135
  host = normalizePercentEncoding(host.toLowerCase());
4136
4136
  }
4137
- parsed.host = reescapeHostDelimiters(host, isIP);
4137
+ parsed.host = reescapeHostDelimiters(host, isIP2);
4138
4138
  }
4139
4139
  if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4140
4140
  if (parsed.path) {
@@ -13335,6 +13335,71 @@ var PlatformDesktopSession = class {
13335
13335
  import { fork } from "node:child_process";
13336
13336
  import { randomBytes as randomBytes2, randomUUID as randomUUID16 } from "node:crypto";
13337
13337
  import { fileURLToPath as fileURLToPath4 } from "node:url";
13338
+
13339
+ // src/remote-stun.ts
13340
+ import { Resolver } from "node:dns/promises";
13341
+ import { isIP } from "node:net";
13342
+ var publicStunHost = "stun.cloudflare.com";
13343
+ var dnsJsonEndpoints = ["https://dns.alidns.com/resolve", "https://cloudflare-dns.com/dns-query"];
13344
+ var STUN_DNS_TIMEOUT = 1500;
13345
+ function defaultRemoteStunUrls(hubUrl) {
13346
+ return [.../* @__PURE__ */ new Set([`stun:${new URL(hubUrl).hostname}:3478`, `stun:${publicStunHost}:3478`])];
13347
+ }
13348
+ function publicAddress(address, family) {
13349
+ if (isIP(address) !== family) return false;
13350
+ if (family === 6) return /^[23][\da-f]{0,3}:/i.test(address);
13351
+ const [a, b] = address.split(".").map(Number);
13352
+ return a !== 0 && a !== 10 && a !== 127 && a < 224 && !(a === 100 && b >= 64 && b <= 127) && !(a === 169 && b === 254) && !(a === 172 && b >= 16 && b <= 31) && !(a === 192 && b === 168) && !(a === 198 && (b === 18 || b === 19));
13353
+ }
13354
+ async function systemLookup(family) {
13355
+ const resolver = new Resolver({ timeout: 500, tries: 1 });
13356
+ try {
13357
+ return await (family === 6 ? resolver.resolve6(publicStunHost) : resolver.resolve4(publicStunHost));
13358
+ } finally {
13359
+ resolver.cancel();
13360
+ }
13361
+ }
13362
+ async function queryDnsJson(endpoint, family, signal) {
13363
+ const type = family === 6 ? 28 : 1;
13364
+ const url = new URL(endpoint);
13365
+ url.searchParams.set("name", publicStunHost);
13366
+ url.searchParams.set("type", String(type));
13367
+ const response = await fetch(url, { headers: { accept: "application/dns-json" }, signal, redirect: "error" });
13368
+ if (!response.ok) throw new Error("STUN DNS request failed");
13369
+ const text = await response.text();
13370
+ if (text.length > 16384) throw new Error("STUN DNS response too large");
13371
+ const body = JSON.parse(text);
13372
+ if (body.Status !== 0 || !Array.isArray(body.Answer)) throw new Error("STUN DNS answer unavailable");
13373
+ return body.Answer.filter((record2) => record2.type === type && typeof record2.data === "string").map((record2) => record2.data);
13374
+ }
13375
+ async function resolvePublicStunAddresses(lookup = systemLookup, query = queryDnsJson) {
13376
+ const families = await Promise.all([6, 4].map(async (family) => {
13377
+ const valid = (addresses) => addresses.filter((address) => publicAddress(address, family)).slice(0, 1);
13378
+ const native = valid(await lookup(family).catch(() => []));
13379
+ if (native.length) return native;
13380
+ const controller = new AbortController();
13381
+ const timer = setTimeout(() => controller.abort(), STUN_DNS_TIMEOUT);
13382
+ try {
13383
+ return await Promise.any(dnsJsonEndpoints.map(async (endpoint) => {
13384
+ const addresses = valid(await query(endpoint, family, controller.signal));
13385
+ if (!addresses.length) throw new Error("No public STUN address");
13386
+ return addresses;
13387
+ })).catch(() => []);
13388
+ } finally {
13389
+ clearTimeout(timer);
13390
+ controller.abort();
13391
+ }
13392
+ }));
13393
+ return families.flat();
13394
+ }
13395
+ async function discoverRemoteStunUrls(hubUrl, configured, resolve5 = resolvePublicStunAddresses) {
13396
+ if (configured) return [...configured];
13397
+ const addresses = await resolve5().catch(() => []);
13398
+ const numeric = addresses.map((address) => `stun:${isIP(address) === 6 ? `[${address}]` : address}:3478`);
13399
+ return [.../* @__PURE__ */ new Set([...numeric, ...defaultRemoteStunUrls(hubUrl)])].slice(0, 4);
13400
+ }
13401
+
13402
+ // src/remote-manager.ts
13338
13403
  function spawnWorker(message) {
13339
13404
  const worker = fileURLToPath4(new URL(import.meta.url.endsWith(".ts") ? "./remote-worker.ts" : "./remote-worker.js", import.meta.url));
13340
13405
  const child = fork(worker, [], { stdio: ["ignore", "ignore", "ignore", "ipc"], execArgv: import.meta.url.endsWith(".ts") ? ["--import", "tsx"] : [] });
@@ -13342,7 +13407,7 @@ function spawnWorker(message) {
13342
13407
  return child;
13343
13408
  }
13344
13409
  var RemoteManager = class {
13345
- constructor(host, desktop, desktopSession, send, touch, workerFactory = spawnWorker, probe = probeRemote) {
13410
+ constructor(host, desktop, desktopSession, send, touch, workerFactory = spawnWorker, probe = probeRemote, discoverStun = discoverRemoteStunUrls) {
13346
13411
  this.host = host;
13347
13412
  this.desktop = desktop;
13348
13413
  this.desktopSession = desktopSession;
@@ -13350,13 +13415,14 @@ var RemoteManager = class {
13350
13415
  this.touch = touch;
13351
13416
  this.workerFactory = workerFactory;
13352
13417
  this.probe = probe;
13418
+ this.discoverStun = discoverStun;
13353
13419
  this.config = RemoteConfigSchema.parse(host.desktopRemote ?? {});
13354
13420
  }
13355
13421
  current;
13356
13422
  stopping = false;
13357
13423
  config;
13358
13424
  stunUrls() {
13359
- return this.config.stunUrls ?? [`stun:${new URL(this.host.hubUrl).hostname}:3478`];
13425
+ return this.config.stunUrls ?? defaultRemoteStunUrls(this.host.hubUrl);
13360
13426
  }
13361
13427
  async status() {
13362
13428
  const base = { qualityModes: RemoteQualitySchema.options, platform: process.platform, passwordInput: process.platform === "darwin", enabled: this.config.enabled, busy: !!this.current, stunUrls: this.stunUrls(), budget: { ...REMOTE_DEFAULTS, totalBitrate: this.config.totalBitrate, videoMaxBitrate: this.config.videoMaxBitrate } };
@@ -13404,7 +13470,10 @@ var RemoteManager = class {
13404
13470
  const session = { remoteId: randomUUID16(), owner, closed: false, signaling: Promise.resolve(), signals: 0, signalWindow: Date.now(), candidates: 0 };
13405
13471
  this.current = session;
13406
13472
  try {
13407
- const permissions = await this.probe(this.host.stateDirectory, this.config);
13473
+ const [permissions, stunUrls] = await Promise.all([
13474
+ this.probe(this.host.stateDirectory, this.config),
13475
+ this.discoverStun(this.host.hubUrl, this.config.stunUrls)
13476
+ ]);
13408
13477
  if (!permissions.session || !permissions.capture || !permissions.input) throw new Error("\u8FDC\u63A7\u9700\u8981\u4E3B\u673A\u56FE\u5F62\u4F1A\u8BDD\u3001\u5C4F\u5E55\u5F55\u5236\u53CA\u8F85\u52A9\u529F\u80FD\u6743\u9650");
13409
13478
  if (permissions.locked === true && permissions.lockCapture === false) throw new Error("\u9501\u5C4F\u8FDC\u63A7\u9700\u8981 macOS 14 \u53CA\u4EE5\u4E0A\u53CA\u65B0\u7248 Xcode Command Line Tools\uFF1B\u5F53\u524D\u4E3B\u673A\u8BF7\u5148\u672C\u5730\u89E3\u9501");
13410
13479
  const identity = await this.desktopSession.sessionIdentity();
@@ -13413,7 +13482,7 @@ var RemoteManager = class {
13413
13482
  const layout = await this.desktop.layoutIdentity?.(display.id);
13414
13483
  if (session.closed || this.current !== session) throw new Error("\u5DE5\u4F5C\u533A\u8FDE\u63A5\u5DF2\u5173\u95ED\uFF0C\u8FDC\u63A7\u5DF2\u53D6\u6D88");
13415
13484
  const bindToken = randomBytes2(32).toString("base64url"), expiresAt = Date.now() + 2e4;
13416
- const child = this.workerFactory({ remoteId: session.remoteId, bindToken, expiresAt, display, helper: permissions.helper, quality, legacy: payload.quality === void 0, config: this.config, stunUrls: this.stunUrls() });
13485
+ const child = this.workerFactory({ remoteId: session.remoteId, bindToken, expiresAt, display, helper: permissions.helper, quality, legacy: payload.quality === void 0, config: this.config, stunUrls });
13417
13486
  session.child = child;
13418
13487
  child.on("message", (message) => {
13419
13488
  if (this.current !== session || session.closed || !message || typeof message !== "object") return;
@@ -13447,7 +13516,7 @@ var RemoteManager = class {
13447
13516
  }
13448
13517
  }, 1e3);
13449
13518
  session.monitor.unref();
13450
- return { remoteId: session.remoteId, bindToken, expiresAt: new Date(expiresAt).toISOString(), display, revision: 1, stunUrls: this.stunUrls(), budget, quality, qualityModes: RemoteQualitySchema.options, protocolVersion: 1, heartbeatAck: true };
13519
+ return { remoteId: session.remoteId, bindToken, expiresAt: new Date(expiresAt).toISOString(), display, revision: 1, stunUrls, budget, quality, qualityModes: RemoteQualitySchema.options, protocolVersion: 1, heartbeatAck: true };
13451
13520
  } catch (error) {
13452
13521
  this.finish(session);
13453
13522
  throw error;
@@ -4187,7 +4187,7 @@ function assertLgplFFmpeg(output) {
4187
4187
  import { execFile } from "node:child_process";
4188
4188
  import { promisify } from "node:util";
4189
4189
  import { createHash, randomUUID } from "node:crypto";
4190
- import { chmod, mkdir, mkdtemp, readFile, rename, rm, stat } from "node:fs/promises";
4190
+ import { chmod, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
4191
4191
  import { join } from "node:path";
4192
4192
  import { fileURLToPath } from "node:url";
4193
4193
  var DESKTOP_IMAGE_LIMIT = 20 * 1024 * 1024;
@@ -4257,16 +4257,20 @@ var PlatformDesktop = class {
4257
4257
  await mkdir(this.stateDirectory, { recursive: true, mode: 448 });
4258
4258
  const target = join(this.stateDirectory, "octopus-desktop-capture");
4259
4259
  const source = fileURLToPath(new URL("../native/desktop-macos.m", import.meta.url));
4260
- const sourceInfo = await stat(source), existing = await stat(target).catch(() => null);
4261
- if (existing?.isFile() && existing.mtimeMs >= sourceInfo.mtimeMs && (existing.mode & 63) === 0) return target;
4262
- const temporary = `${target}.${randomUUID()}`;
4260
+ const flags = ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ScreenCaptureKit", "-framework", "CoreMedia", "-framework", "CoreVideo", "-framework", "VideoToolbox", "-framework", "ImageIO"];
4261
+ const fingerprint = createHash("sha256").update(await readFile(source)).update(JSON.stringify([process.arch, flags])).digest("hex");
4262
+ const stamp = `${target}.sha256`, existing = await stat(target).catch(() => null);
4263
+ if (existing?.isFile() && (existing.mode & 63) === 0 && await readFile(stamp, "utf8").catch(() => "") === fingerprint) return target;
4264
+ const temporary = `${target}.${randomUUID()}`, temporaryStamp = `${temporary}.sha256`;
4263
4265
  try {
4264
- await this.execute("/usr/bin/clang", ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ScreenCaptureKit", "-framework", "CoreMedia", "-framework", "CoreVideo", "-framework", "VideoToolbox", "-framework", "ImageIO", source, "-o", temporary]);
4266
+ await this.execute("/usr/bin/clang", [...flags, source, "-o", temporary]);
4265
4267
  await chmod(temporary, 448);
4268
+ await writeFile(temporaryStamp, fingerprint, { mode: 384 });
4266
4269
  await rename(temporary, target);
4270
+ await rename(temporaryStamp, stamp);
4267
4271
  return target;
4268
4272
  } finally {
4269
- await rm(temporary, { force: true });
4273
+ await Promise.all([rm(temporary, { force: true }), rm(temporaryStamp, { force: true })]);
4270
4274
  }
4271
4275
  })();
4272
4276
  this.helper.catch(() => {
@@ -4447,7 +4451,7 @@ var DesktopManager = class {
4447
4451
  // src/remote-native.ts
4448
4452
  import { execFile as execFile2, spawn } from "node:child_process";
4449
4453
  import { promisify as promisify2 } from "node:util";
4450
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, stat as stat2, writeFile } from "node:fs/promises";
4454
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
4451
4455
  import { join as join2 } from "node:path";
4452
4456
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4453
4457
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
@@ -4461,7 +4465,7 @@ async function ensureRemoteHelper(stateDirectory) {
4461
4465
  const source = fileURLToPath2(new URL(mac ? "../native/remote-macos.m" : "../native/remote-x11.c", import.meta.url));
4462
4466
  const target = join2(stateDirectory, "octopus-remote-desktop");
4463
4467
  const stamp = `${target}.sha256`;
4464
- const flags = mac ? ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ScreenCaptureKit", "-framework", "CoreMedia", "-framework", "CoreVideo", "-framework", "ApplicationServices", "-framework", "Carbon"] : ["-O2", "-lX11", "-lXtst"];
4468
+ const flags = mac ? ["-O2", "-fobjc-arc", "-mmacosx-version-min=12.3", "-framework", "Foundation", "-framework", "AppKit", "-framework", "ScreenCaptureKit", "-framework", "CoreMedia", "-framework", "CoreVideo", "-framework", "ApplicationServices", "-framework", "Carbon", "-framework", "IOKit"] : ["-O2", "-lX11", "-lXtst"];
4465
4469
  const fingerprint = createHash2("sha256").update(await readFile2(source)).update(JSON.stringify([process.platform, process.arch, flags])).digest("hex");
4466
4470
  const existing = await stat2(target).catch(() => void 0);
4467
4471
  if (existing?.isFile() && (existing.mode & 63) === 0 && await readFile2(stamp, "utf8").catch(() => "") === fingerprint) return target;
@@ -4470,7 +4474,7 @@ async function ensureRemoteHelper(stateDirectory) {
4470
4474
  try {
4471
4475
  await exec(mac ? "/usr/bin/clang" : "cc", args, { timeout: 6e4, maxBuffer: 16384 });
4472
4476
  await chmod2(temporary, 448);
4473
- await writeFile(temporaryStamp, fingerprint, { mode: 384 });
4477
+ await writeFile2(temporaryStamp, fingerprint, { mode: 384 });
4474
4478
  await rename2(temporary, target);
4475
4479
  await rename2(temporaryStamp, stamp);
4476
4480
  return target;
package/dist/cli.js CHANGED
@@ -31,13 +31,13 @@ import {
31
31
  supportedPlatform,
32
32
  systemdUnitPath,
33
33
  toolSearchDirs
34
- } from "./chunk-6NV726RL.js";
34
+ } from "./chunk-3KIA6HF6.js";
35
35
  import {
36
36
  DESKTOP_PERMISSION_GUIDANCE,
37
37
  PlatformDesktop,
38
38
  checkDesktop,
39
39
  requestDesktopPermission
40
- } from "./chunk-PPH4OB7Y.js";
40
+ } from "./chunk-XIJKEY4L.js";
41
41
 
42
42
  // src/cli.ts
43
43
  import { spawn as spawn2 } from "node:child_process";
package/dist/main.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  ConfigError,
4
4
  runDaemon
5
- } from "./chunk-6NV726RL.js";
6
- import "./chunk-PPH4OB7Y.js";
5
+ } from "./chunk-3KIA6HF6.js";
6
+ import "./chunk-XIJKEY4L.js";
7
7
 
8
8
  // src/main.ts
9
9
  runDaemon(process.env.OCTOPUS_PROXY_CONFIG ?? "proxy.json").catch((error) => {
@@ -14,7 +14,7 @@ import {
14
14
  remoteResolutionTiers,
15
15
  remoteSessionBudget,
16
16
  terminateNative
17
- } from "./chunk-PPH4OB7Y.js";
17
+ } from "./chunk-XIJKEY4L.js";
18
18
 
19
19
  // src/remote-worker.ts
20
20
  import rtc from "@roamhq/wrtc";
@@ -106,7 +106,9 @@ int main(int argc, const char *argv[]) {
106
106
  if (argc == 2 && strcmp(argv[1], "list") == 0) {
107
107
  CGDirectDisplayID ids[64];
108
108
  uint32_t count = 0;
109
- if (CGGetActiveDisplayList(64, ids, &count) != kCGErrorSuccess) {
109
+ // Sleeping displays are still connected and must remain selectable so
110
+ // an authenticated remote connection can wake them with user input.
111
+ if (CGGetOnlineDisplayList(64, ids, &count) != kCGErrorSuccess) {
110
112
  fail(@"无法读取活动显示器,请检查 Proxy 是否运行在已登录的图形桌面会话中。" );
111
113
  return 1;
112
114
  }
@@ -3,6 +3,7 @@
3
3
  #import <ApplicationServices/ApplicationServices.h>
4
4
  #import <ScreenCaptureKit/ScreenCaptureKit.h>
5
5
  #import <Carbon/Carbon.h>
6
+ #import <IOKit/pwr_mgt/IOPMLib.h>
6
7
  #include <arpa/inet.h>
7
8
  #include <fcntl.h>
8
9
  #include <signal.h>
@@ -17,6 +18,8 @@ static NSMutableSet<NSNumber *> *heldKeys;
17
18
  static BOOL heldButtons[3];
18
19
  static double lastPing;
19
20
  static pid_t parentPID;
21
+ static IOPMAssertionID activityAssertion = kIOPMNullAssertionID;
22
+ static double lastActivity;
20
23
  static void signalStop(int signal) { stopping = 1; }
21
24
  static BOOL sessionPresent(void) {
22
25
  NSDictionary *s = CFBridgingRelease(CGSessionCopyCurrentDictionary());
@@ -29,6 +32,19 @@ static BOOL screenLocked(void) {
29
32
  return [s[@"CGSSessionScreenIsLocked"] boolValue];
30
33
  }
31
34
  static BOOL inputOK(void) { return AXIsProcessTrusted() && CGPreflightPostEventAccess() && sessionPresent(); }
35
+ // Quartz events can fill the password field without marking the console user
36
+ // active. loginwindow then times out its activity check and reports a bad
37
+ // password. Declare activity on connection and real console input, never on
38
+ // capture/pings. This also wakes a sleeping connected display.
39
+ static void userActivity(void) {
40
+ double now=NSProcessInfo.processInfo.systemUptime;
41
+ if(activityAssertion!=kIOPMNullAssertionID && now-lastActivity<1) return;
42
+ if(IOPMAssertionDeclareUserActivity(CFSTR("Octopus remote console input"),kIOPMUserActiveLocal,&activityAssertion)==kIOReturnSuccess) lastActivity=now;
43
+ }
44
+ static void releaseActivity(void) {
45
+ if(activityAssertion!=kIOPMNullAssertionID) IOPMAssertionRelease(activityAssertion);
46
+ activityAssertion=kIOPMNullAssertionID; lastActivity=0;
47
+ }
32
48
  static void emit(NSDictionary *value) {
33
49
  NSData *data = [NSJSONSerialization dataWithJSONObject:value options:0 error:NULL];
34
50
  if (!data) return;
@@ -205,15 +221,18 @@ static void command(NSString *line,RemoteStream *capture) {
205
221
  }
206
222
  if (([op isEqual:@"pointer"] && parts.count==3) || ([op isEqual:@"button"] && parts.count==5) || ([op isEqual:@"wheel"] && parts.count==5)) {
207
223
  position(parts[1].doubleValue,parts[2].doubleValue);
208
- if ([op isEqual:@"button"]) { int button=parts[3].intValue; if(button<0 || button>2) { stopping=1; return; } heldButtons[button]=parts[4].boolValue; mouseEvent(mouseType(button,heldButtons[button]),mouseButton(button)); }
224
+ if ([op isEqual:@"button"] && (parts[3].intValue<0 || parts[3].intValue>2)) { stopping=1; return; }
225
+ userActivity();
226
+ if ([op isEqual:@"button"]) { int button=parts[3].intValue; heldButtons[button]=parts[4].boolValue; mouseEvent(mouseType(button,heldButtons[button]),mouseButton(button)); }
209
227
  else if ([op isEqual:@"wheel"]) { mouseEvent(kCGEventMouseMoved,kCGMouseButtonLeft); CGEventRef e=CGEventCreateScrollWheelEvent(NULL,kCGScrollEventUnitPixel,2,-parts[4].intValue,-parts[3].intValue); if(e) { CGEventSetFlags(e,flags()); CGEventPost(kCGHIDEventTap,e); CFRelease(e); } }
210
228
  else { CGEventType type=heldButtons[0]?kCGEventLeftMouseDragged:heldButtons[2]?kCGEventRightMouseDragged:heldButtons[1]?kCGEventOtherMouseDragged:kCGEventMouseMoved; mouseEvent(type,heldButtons[2]?kCGMouseButtonRight:heldButtons[1]?kCGMouseButtonCenter:kCGMouseButtonLeft); }
211
- } else if ([op isEqual:@"key"] && parts.count==3) { NSNumber *key=keyCodes()[parts[1]]; if(key) keyEvent(key.unsignedShortValue,parts[2].boolValue); }
229
+ } else if ([op isEqual:@"key"] && parts.count==3) { NSNumber *key=keyCodes()[parts[1]]; if(key) { userActivity(); keyEvent(key.unsignedShortValue,parts[2].boolValue); } }
212
230
  else if (([op isEqual:@"text"] && parts.count==3) || ([op isEqual:@"password"] && parts.count==2)) {
213
231
  BOOL password=[op isEqual:@"password"];
214
232
  NSData *data=[[NSData alloc] initWithBase64EncodedString:parts[password ? 1 : 2] options:0]; NSString *text=[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
215
233
  if(!text || data.length>(password ? 1024 : 16384) || (password && (!data.length || [text rangeOfCharacterFromSet:NSCharacterSet.controlCharacterSet].location!=NSNotFound))) { stopping=1; return; }
216
234
  if(password && !screenLocked()) { emit(@{@"kind":@"input-error"}); return; }
235
+ if(text.length) userActivity();
217
236
  releaseAll();
218
237
  for(NSUInteger i=0;i<text.length;) {
219
238
  if(!inputOK()) { stopping=1; return; }
@@ -252,9 +271,15 @@ int main(int argc,const char *argv[]) {
252
271
  emit(@{@"capture":@(CGPreflightScreenCaptureAccess()),@"input":@((BOOL)(AXIsProcessTrusted() && CGPreflightPostEventAccess())),@"session":@(sessionPresent()),@"locked":@(screenLocked()),@"lockCapture":@(lockCapture)}); return 0;
253
272
  }
254
273
  if(argc!=6 || strcmp(argv[1],"serve")!=0 || !inputOK() || !CGPreflightScreenCaptureAccess()) return 1;
255
- displayID=(CGDirectDisplayID)strtoul(argv[2],NULL,10); bounds=CGDisplayBounds(displayID);
274
+ displayID=(CGDirectDisplayID)strtoul(argv[2],NULL,10);
256
275
  int w=atoi(argv[3]),h=atoi(argv[4]),fps=atoi(argv[5]);
257
- if(!CGDisplayIsActive(displayID) || w<2 || h<2 || w>3840 || h>2160 || w%2 || h%2 || fps<1 || fps>30) return 1;
276
+ if(!CGDisplayIsOnline(displayID) || w<2 || h<2 || w>3840 || h>2160 || w%2 || h%2 || fps<1 || fps>30) return 1;
277
+ userActivity();
278
+ NSDate *wakeDeadline=[NSDate dateWithTimeIntervalSinceNow:2];
279
+ while(!CGDisplayIsActive(displayID) && CGDisplayIsOnline(displayID) && inputOK() && wakeDeadline.timeIntervalSinceNow>0)
280
+ [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.02]];
281
+ if(!CGDisplayIsActive(displayID) || !inputOK()) { releaseActivity(); return 1; }
282
+ bounds=CGDisplayBounds(displayID);
258
283
  heldKeys=[NSMutableSet set]; cursor=bounds.origin; parentPID=getppid(); lastPing=NSDate.date.timeIntervalSince1970;
259
284
  signal(SIGTERM,signalStop); signal(SIGINT,signalStop); signal(SIGPIPE,signalStop);
260
285
  fcntl(STDIN_FILENO,F_SETFL,O_NONBLOCK);
@@ -279,7 +304,7 @@ int main(int argc,const char *argv[]) {
279
304
  NSMutableData *incoming=[NSMutableData data];
280
305
  while(!stopping) {
281
306
  @autoreleasepool {
282
- if(getppid()!=parentPID || NSDate.date.timeIntervalSince1970-lastPing>15 || !inputOK() || !CGPreflightScreenCaptureAccess() || !CGDisplayIsActive(displayID) || !CGRectEqualToRect(bounds,CGDisplayBounds(displayID))) break;
307
+ if(getppid()!=parentPID || NSDate.date.timeIntervalSince1970-lastPing>15 || !inputOK() || !CGPreflightScreenCaptureAccess() || !CGDisplayIsOnline(displayID) || !CGRectEqualToRect(bounds,CGDisplayBounds(displayID))) break;
283
308
  if(screenLocked()) [capture captureLockedFrame];
284
309
  else if(capture.streamStopped && capture.stream) {
285
310
  capture.streamStopped=NO;
@@ -297,7 +322,7 @@ int main(int argc,const char *argv[]) {
297
322
  [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.005]];
298
323
  }
299
324
  }
300
- stopping=1; releaseAll(); [capture.stream stopCaptureWithCompletionHandler:nil]; return 0;
325
+ stopping=1; releaseAll(); releaseActivity(); [capture.stream stopCaptureWithCompletionHandler:nil]; return 0;
301
326
  }
302
327
  return 1;
303
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylandnpm/octopus-proxy",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "type": "module",
5
5
  "description": "Octopus host proxy: OpenCode, Codex, ZCode and Pi adapters with service installer CLI",
6
6
  "bin": {
@@ -7,7 +7,6 @@
7
7
  "tool": { "kind": "opencode", "command": "/Users/luhuili/.opencode/bin/opencode", "baseUrl": "http://127.0.0.1:4096", "mode": "external", "password": "replace-with-a-random-secret-at-least-20-characters" },
8
8
  "desktopRemote": {
9
9
  "enabled": true,
10
- "stunUrls": ["stun:hub.example.com:3478"],
11
10
  "totalBitrate": 22000000,
12
11
  "videoMaxBitrate": 16000000
13
12
  }