@skylandnpm/octopus-proxy 0.1.53 → 0.1.55
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/README.md +10 -0
- package/dist/{chunk-X7FBZI2P.js → chunk-NJNANA6A.js} +38 -15
- package/dist/cli.js +1 -1
- package/dist/main.js +1 -1
- package/native/desktop-macos.m +10 -2
- package/native/remote-macos.m +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,6 +119,16 @@ octopus-proxy status
|
|
|
119
119
|
|
|
120
120
|
`octopus-proxy doctor` 会分别显示当前命令和已安装服务的 Node.js 路径、版本及最低要求(Node.js >= 22)。服务检查读取已保存的 launchd/systemd 定义,再执行其中 Node 的 `--version`;版本过低、路径失效或版本输出无法识别都会标记为失败。配置无效时仍会检查 Node;尚未安装服务时提示未检查服务版本。`doctor --fix` 重写服务定义后会重新检查保存的 Node 路径和版本。
|
|
121
121
|
|
|
122
|
+
## 连接工作区时报 `ec_asn1.cc / p_ec.cc DECODE_ERROR`
|
|
123
|
+
|
|
124
|
+
如果服务在线,但 Claw 在 TLS 握手时显示上述错误,可能是主机身份由 macOS 自带的 LibreSSL 生成,证书使用了客户端不支持的显式椭圆曲线参数。这与服务是否在线、密码是否正确是不同的检查阶段;旧版本更新包也会保留已有证书。
|
|
125
|
+
|
|
126
|
+
这不是旧 macOS 独有的问题:在 macOS 26.6.2 的 `/usr/bin/openssl`(LibreSSL 3.3.6)上也已复现。Proxy 通过 PATH 查找 `openssl`,只用它生成身份文件;实际 TLS 连接使用 Node 自身的 OpenSSL。交互终端和 launchd 服务的 PATH 可能不同,因此终端安装新版 OpenSSL 不代表服务会使用它。升级 macOS 或替换命令不会自动转换已有身份文件。
|
|
127
|
+
|
|
128
|
+
包含此修复的版本会在启动或运行 `octopus-proxy identity` 时检查身份编码,把旧的显式参数转换为 `named_curve` 并重新签发证书,保留原来的私钥数值和公钥。正常的命名曲线证书不会重写,已有指纹保持不变;TLS 1.3、主机密码认证和指纹校验保持原有要求。转换旧证书后,编码变化会使其 SHA-256 指纹变化;首次连接应与主机上的 `octopus-proxy identity` 输出核对。使用自定义配置时带上原来的 `--config <path>`。
|
|
129
|
+
|
|
130
|
+
命名曲线编码遵循 [RFC 5480 §2.1.1](https://www.rfc-editor.org/rfc/rfc5480#section-2.1.1),继续使用 P-256 和 SHA-256。兼容处理不启用旧 TLS 协议、不放宽客户端证书解析,也不自动覆盖已固定的主机指纹。macOS 26 及后续版本保持这些安全要求;当前实机验证环境为 macOS 26.6.2,后续系统版本仍需实际回归验证。
|
|
131
|
+
|
|
122
132
|
## 发布
|
|
123
133
|
|
|
124
134
|
在 `apps/octopus-proxy` 目录执行 `npm run publish`:先递增 `package.json` 的补丁版本,再编译,最后发布。即使编译或发布失败,版本号也会保留递增结果,重试时继续递增。版本更新不会自动创建 Git 提交或标签。先登录有权发布 `@skylandnpm` 包的 npm 账号:
|
|
@@ -7600,8 +7600,8 @@ var proxyVersion = JSON.parse(readFileSync(new URL("../package.json", import.met
|
|
|
7600
7600
|
|
|
7601
7601
|
// src/host-identity.ts
|
|
7602
7602
|
import { execFile } from "node:child_process";
|
|
7603
|
-
import { createHash, randomUUID, X509Certificate } from "node:crypto";
|
|
7604
|
-
import { chmod, mkdir, open, readFile as readFile2, rename, stat as stat2, unlink } from "node:fs/promises";
|
|
7603
|
+
import { createHash, createPrivateKey, createPublicKey, randomUUID, X509Certificate } from "node:crypto";
|
|
7604
|
+
import { chmod, mkdir, open, readFile as readFile2, rename, stat as stat2, unlink, writeFile } from "node:fs/promises";
|
|
7605
7605
|
import { join as join2 } from "node:path";
|
|
7606
7606
|
import { promisify } from "node:util";
|
|
7607
7607
|
var run = promisify(execFile);
|
|
@@ -7613,20 +7613,43 @@ async function ensureHostIdentity(stateDirectory) {
|
|
|
7613
7613
|
const certPath = join2(stateDirectory, "host-identity-cert.pem");
|
|
7614
7614
|
const keyExists = await stat2(keyPath).then(() => true, () => false);
|
|
7615
7615
|
const certExists = await stat2(certPath).then(() => true, () => false);
|
|
7616
|
+
for (const path of [keyPath, certPath]) {
|
|
7617
|
+
const info = await stat2(path).catch((error) => {
|
|
7618
|
+
if (error.code === "ENOENT") return void 0;
|
|
7619
|
+
throw error;
|
|
7620
|
+
});
|
|
7621
|
+
if (info && (info.mode & 63) !== 0) throw new Error("Host identity permissions must be 0600");
|
|
7622
|
+
}
|
|
7623
|
+
if (certExists && !keyExists) throw new Error("Host identity private key is missing");
|
|
7616
7624
|
const suffix = randomUUID();
|
|
7617
7625
|
const temporaryKey = join2(stateDirectory, `.host-key-${suffix}`);
|
|
7618
7626
|
const temporaryCert = join2(stateDirectory, `.host-cert-${suffix}`);
|
|
7619
7627
|
try {
|
|
7620
7628
|
if (!keyExists) {
|
|
7621
7629
|
await (await open(temporaryKey, "wx", 384)).close();
|
|
7622
|
-
await run("openssl", ["genpkey", "-algorithm", "EC", "-pkeyopt", "ec_paramgen_curve:P-256", "-out", temporaryKey], { timeout: 15e3 });
|
|
7630
|
+
await run("openssl", ["genpkey", "-algorithm", "EC", "-pkeyopt", "ec_paramgen_curve:P-256", "-pkeyopt", "ec_param_enc:named_curve", "-out", temporaryKey], { timeout: 15e3 });
|
|
7623
7631
|
await chmod(temporaryKey, 384);
|
|
7624
7632
|
await rename(temporaryKey, keyPath);
|
|
7625
7633
|
}
|
|
7626
|
-
|
|
7634
|
+
const privateKey = createPrivateKey(await readFile2(keyPath));
|
|
7635
|
+
const existingCertificate = certExists ? new X509Certificate(await readFile2(certPath)) : void 0;
|
|
7636
|
+
if (existingCertificate && !existingCertificate.checkPrivateKey(privateKey)) throw new Error("Host identity certificate does not match private key");
|
|
7637
|
+
const normalizedKey = privateKey.asymmetricKeyType === "ec" ? createPrivateKey({ key: privateKey.export({ format: "jwk" }), format: "jwk" }) : privateKey;
|
|
7638
|
+
const publicKey = createPublicKey(normalizedKey).export({ type: "spki", format: "der" });
|
|
7639
|
+
const normalizeKey = !createPublicKey(privateKey).export({ type: "spki", format: "der" }).equals(publicKey);
|
|
7640
|
+
const renewCertificate = !existingCertificate || !existingCertificate.publicKey.export({ type: "spki", format: "der" }).equals(publicKey);
|
|
7641
|
+
if (normalizeKey || renewCertificate) {
|
|
7642
|
+
await writeFile(temporaryKey, normalizedKey.export({ type: "pkcs8", format: "pem" }), { mode: 384, flag: "wx" });
|
|
7643
|
+
}
|
|
7644
|
+
if (renewCertificate) {
|
|
7627
7645
|
await (await open(temporaryCert, "wx", 384)).close();
|
|
7628
|
-
await run("openssl", ["req", "-new", "-x509", "-sha256", "-days", "3650", "-key",
|
|
7646
|
+
await run("openssl", ["req", "-new", "-x509", "-sha256", "-days", "3650", "-key", temporaryKey, "-out", temporaryCert, "-subj", "/CN=octopus-proxy"], { timeout: 15e3 });
|
|
7647
|
+
const certificate = new X509Certificate(await readFile2(temporaryCert));
|
|
7648
|
+
if (!certificate.checkPrivateKey(normalizedKey) || !certificate.publicKey.export({ type: "spki", format: "der" }).equals(publicKey)) throw new Error("Host identity certificate normalization failed");
|
|
7629
7649
|
await chmod(temporaryCert, 384);
|
|
7650
|
+
}
|
|
7651
|
+
if (normalizeKey || renewCertificate) await rename(temporaryKey, keyPath);
|
|
7652
|
+
if (renewCertificate) {
|
|
7630
7653
|
await rename(temporaryCert, certPath);
|
|
7631
7654
|
}
|
|
7632
7655
|
} finally {
|
|
@@ -7883,13 +7906,13 @@ import { basename as basename2, dirname as dirname3, join as join3 } from "node:
|
|
|
7883
7906
|
import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
|
|
7884
7907
|
|
|
7885
7908
|
// src/storage.ts
|
|
7886
|
-
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile } from "node:fs/promises";
|
|
7909
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
7887
7910
|
import { dirname as dirname2 } from "node:path";
|
|
7888
7911
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
7889
7912
|
async function atomicJson(path, value) {
|
|
7890
7913
|
await mkdir3(dirname2(path), { recursive: true, mode: 448 });
|
|
7891
7914
|
const temp = `${path}.${randomUUID3()}.tmp`;
|
|
7892
|
-
await
|
|
7915
|
+
await writeFile2(temp, JSON.stringify(value), { mode: 384 });
|
|
7893
7916
|
await rename2(temp, path);
|
|
7894
7917
|
}
|
|
7895
7918
|
async function readJson(path) {
|
|
@@ -8344,7 +8367,7 @@ function pageHistory(input, payload) {
|
|
|
8344
8367
|
// src/artifacts.ts
|
|
8345
8368
|
import { createHash as createHash5 } from "node:crypto";
|
|
8346
8369
|
import { join as join5 } from "node:path";
|
|
8347
|
-
import { readFile as readFile4, writeFile as
|
|
8370
|
+
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir4, stat as stat5 } from "node:fs/promises";
|
|
8348
8371
|
var Artifacts = class {
|
|
8349
8372
|
constructor(workspace, directory, emit) {
|
|
8350
8373
|
this.workspace = workspace;
|
|
@@ -8400,7 +8423,7 @@ var Artifacts = class {
|
|
|
8400
8423
|
if (previous?.hash !== hash || previous.deleted) {
|
|
8401
8424
|
const dir = join5(this.directory, project, "blobs");
|
|
8402
8425
|
await mkdir4(dir, { recursive: true, mode: 448 });
|
|
8403
|
-
await
|
|
8426
|
+
await writeFile3(join5(dir, hash), data, { mode: 384 });
|
|
8404
8427
|
record2.revisions.push({ hash, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8405
8428
|
changed = true;
|
|
8406
8429
|
}
|
|
@@ -8453,7 +8476,7 @@ var Artifacts = class {
|
|
|
8453
8476
|
// src/secure-chat.ts
|
|
8454
8477
|
import WebSocket from "ws";
|
|
8455
8478
|
import { Duplex } from "node:stream";
|
|
8456
|
-
import { createPublicKey, randomUUID as randomUUID7, verify } from "node:crypto";
|
|
8479
|
+
import { createPublicKey as createPublicKey2, randomUUID as randomUUID7, verify } from "node:crypto";
|
|
8457
8480
|
import { userInfo as userInfo2 } from "node:os";
|
|
8458
8481
|
import { TLSSocket, createSecureContext } from "node:tls";
|
|
8459
8482
|
|
|
@@ -8741,7 +8764,7 @@ var SecureChat = class {
|
|
|
8741
8764
|
if (key.length !== 32 || signature.length !== 64) throw new Error("Invalid device proof");
|
|
8742
8765
|
const exporter = session.tls.exportKeyingMaterial(32, "EXPORTER-octopus-device-v1", Buffer.from(this.config.botId)).toString("base64");
|
|
8743
8766
|
const proof = Buffer.from(`${this.config.botId}\0${id}\0${exporter}`);
|
|
8744
|
-
if (!verify(null, proof,
|
|
8767
|
+
if (!verify(null, proof, createPublicKey2({ key: Buffer.concat([ED25519_SPKI, key]), format: "der", type: "spki" }), signature)) throw new Error("Invalid device proof");
|
|
8745
8768
|
const valid = await this.authenticate(this.config.stateDirectory, this.serviceAccount, String(message.password ?? ""));
|
|
8746
8769
|
if (!valid) throw new Error("Host authentication failed");
|
|
8747
8770
|
if (this.sessions.get(id) !== session) return;
|
|
@@ -10519,7 +10542,7 @@ function applyEdits(text, edits) {
|
|
|
10519
10542
|
}
|
|
10520
10543
|
|
|
10521
10544
|
// src/project-config.ts
|
|
10522
|
-
import { readFile as readFile5, writeFile as
|
|
10545
|
+
import { readFile as readFile5, writeFile as writeFile4, unlink as unlink6 } from "node:fs/promises";
|
|
10523
10546
|
import { createHash as createHash7, randomUUID as randomUUID10 } from "node:crypto";
|
|
10524
10547
|
var ProjectConfig = class {
|
|
10525
10548
|
constructor(workspace, request) {
|
|
@@ -10573,7 +10596,7 @@ var ProjectConfig = class {
|
|
|
10573
10596
|
else {
|
|
10574
10597
|
const temporary = await this.workspace.path(project, `.octopus-config-${randomUUID10()}.tmp`, true);
|
|
10575
10598
|
try {
|
|
10576
|
-
await
|
|
10599
|
+
await writeFile4(temporary, updated, { mode: 384, flag: "wx" });
|
|
10577
10600
|
await this.workspace.publish(project, temporary, name);
|
|
10578
10601
|
} finally {
|
|
10579
10602
|
await unlink6(temporary).catch(() => {
|
|
@@ -11034,7 +11057,7 @@ function redact(value) {
|
|
|
11034
11057
|
// src/runtime.ts
|
|
11035
11058
|
import { spawn as spawn2 } from "node:child_process";
|
|
11036
11059
|
import { createHash as createHash8 } from "node:crypto";
|
|
11037
|
-
import { mkdir as mkdir6, open as open5, readFile as readFile6, rm, writeFile as
|
|
11060
|
+
import { mkdir as mkdir6, open as open5, readFile as readFile6, rm, writeFile as writeFile5 } from "node:fs/promises";
|
|
11038
11061
|
import { createServer } from "node:net";
|
|
11039
11062
|
import { join as join7 } from "node:path";
|
|
11040
11063
|
var Runtime = class {
|
|
@@ -11138,7 +11161,7 @@ var Runtime = class {
|
|
|
11138
11161
|
throw new Error(`\u65E0\u6CD5\u6267\u884C OpenCode \u547D\u4EE4 ${this.config.tool.command}\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
11139
11162
|
}
|
|
11140
11163
|
try {
|
|
11141
|
-
await
|
|
11164
|
+
await writeFile5(this.endpointStatePath, JSON.stringify({
|
|
11142
11165
|
configuredBaseUrl: this.config.tool.baseUrl,
|
|
11143
11166
|
baseUrl: url.origin,
|
|
11144
11167
|
passwordHash: this.passwordHash,
|
package/dist/cli.js
CHANGED
package/dist/main.js
CHANGED
package/native/desktop-macos.m
CHANGED
|
@@ -138,8 +138,16 @@ int main(int argc, const char *argv[]) {
|
|
|
138
138
|
if (!display) { dispatch_async(dispatch_get_main_queue(), ^{ fail(@"所选显示器已断开,请刷新显示器列表后重试。" ); }); return; }
|
|
139
139
|
SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:display excludingWindows:@[]];
|
|
140
140
|
SCStreamConfiguration *configuration = [SCStreamConfiguration new];
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
if (@available(macOS 14.0, *)) {
|
|
142
|
+
configuration.width = CGDisplayPixelsWide(display.displayID);
|
|
143
|
+
configuration.height = CGDisplayPixelsHigh(display.displayID);
|
|
144
|
+
} else {
|
|
145
|
+
// On macOS 13, re-entering Quartz display queries from the
|
|
146
|
+
// SCShareableContent callback can block indefinitely. The returned
|
|
147
|
+
// SCDisplay already contains the dimensions needed for capture.
|
|
148
|
+
configuration.width = display.width;
|
|
149
|
+
configuration.height = display.height;
|
|
150
|
+
}
|
|
143
151
|
configuration.pixelFormat = kCVPixelFormatType_32BGRA;
|
|
144
152
|
configuration.showsCursor = YES;
|
|
145
153
|
configuration.queueDepth = 3;
|
package/native/remote-macos.m
CHANGED
|
@@ -271,6 +271,14 @@ int main(int argc,const char *argv[]) {
|
|
|
271
271
|
emit(@{@"capture":@(CGPreflightScreenCaptureAccess()),@"input":@((BOOL)(AXIsProcessTrusted() && CGPreflightPostEventAccess())),@"session":@(sessionPresent()),@"locked":@(screenLocked()),@"lockCapture":@(lockCapture)}); return 0;
|
|
272
272
|
}
|
|
273
273
|
if(argc!=6 || strcmp(argv[1],"serve")!=0 || !inputOK() || !CGPreflightScreenCaptureAccess()) return 1;
|
|
274
|
+
if (@available(macOS 14.0, *)) {
|
|
275
|
+
// Keep the current display initialization on modern macOS.
|
|
276
|
+
} else {
|
|
277
|
+
// Initialize AppKit's display connection on the main thread first.
|
|
278
|
+
// Otherwise Quartz display queries can hang on macOS 13 before the
|
|
279
|
+
// capture stream is even created. This does not request permissions.
|
|
280
|
+
(void)NSScreen.screens;
|
|
281
|
+
}
|
|
274
282
|
displayID=(CGDirectDisplayID)strtoul(argv[2],NULL,10);
|
|
275
283
|
int w=atoi(argv[3]),h=atoi(argv[4]),fps=atoi(argv[5]);
|
|
276
284
|
if(!CGDisplayIsOnline(displayID) || w<2 || h<2 || w>3840 || h>2160 || w%2 || h%2 || fps<1 || fps>30) return 1;
|