@nggaigc/cli 0.1.9 → 0.1.12
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 +5 -3
- package/dist/archive.js +213 -0
- package/dist/bin.js +32 -11
- package/dist/bundle.js +60 -72
- package/dist/independent-skill.js +690 -0
- package/dist/install.js +18 -7
- package/dist/skill-command.js +115 -0
- package/package.json +7 -7
- package/vendor/manifest.json +28 -22
- package/vendor/nggaigc-plugin-1.1.14.zip +0 -0
- package/vendor/nggaigc-plugin-1.1.10.zip +0 -0
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# @nggaigc/cli
|
|
2
2
|
|
|
3
|
-
NGGAIGC Codex Skill
|
|
3
|
+
NGGAIGC Codex Skill 的一命令安装器。
|
|
4
4
|
|
|
5
5
|
```powershell
|
|
6
6
|
npx --yes @nggaigc/cli@latest install
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
安装器会先验证内置 Plugin 的 Ed25519 签名、归档哈希和逐文件哈希,再原子安装 Skill。首次运行会打开 NGGAIGC
|
|
9
|
+
安装器会先验证内置 Plugin 的 Ed25519 签名、归档哈希和逐文件哈希,再原子安装 Skill。首次运行会打开 NGGAIGC 浏览器授权页;浏览器仅接收短期连接挑战,安装器通过内存中的独立轮询凭据获取连接结果,设备凭据只通过标准输入进入操作系统受保护的凭据存储(Windows Credential Manager 或 macOS Keychain)。安装器不接受 API Key、授权码或设备凭据参数,也不从环境变量读取这些值。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
已发布的 0.1.10 安装器仍仅支持 Windows x64/arm64。此源码中的 0.1.12 / Plugin 1.1.14 是尚未签名、尚未发布的测试候选;候选计划保持 Windows x64/arm64,并增加 Apple 芯片 Mac arm64,使用本机 Node.js 22.x(22.13 起)或 24.x,不下载或内嵌 Node.js。
|
|
12
|
+
|
|
13
|
+
macOS 适配仍需在真实 M3 Mac 编译并核验 Keychain helper,再制作新签名 Plugin 与单一 CLI 包;旧 Windows-only 签名归档不会被重标为 Mac 兼容。Intel Mac 不在本候选声明范围内,也没有实机验收。不要将本候选版本替换为公开安装命令或 npm `latest`。
|
package/dist/archive.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { chmod, lstat, mkdir, open, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { inflateRawSync } from "node:zlib";
|
|
4
|
+
// The release builder emits ordinary single-disk ZIPs. Deliberately reject
|
|
5
|
+
// ZIP64, encryption, links and unsupported flags rather than delegating to an
|
|
6
|
+
// OS extractor with different traversal/overwrite semantics on each platform.
|
|
7
|
+
const MAX_ARCHIVE = 64 * 1024 * 1024;
|
|
8
|
+
const MAX_EXPANDED = 64 * 1024 * 1024;
|
|
9
|
+
const MAX_FILES = 1024;
|
|
10
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
11
|
+
export function safeArchivePath(value) {
|
|
12
|
+
return (value.length > 0 &&
|
|
13
|
+
value.length <= 240 &&
|
|
14
|
+
value === value.normalize("NFC") &&
|
|
15
|
+
!/[\\:<>"|?*]/.test(value) &&
|
|
16
|
+
!Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) &&
|
|
17
|
+
value
|
|
18
|
+
.split("/")
|
|
19
|
+
.every((part) => part !== "" &&
|
|
20
|
+
part !== "." &&
|
|
21
|
+
part !== ".." &&
|
|
22
|
+
!/[. ]$/.test(part) &&
|
|
23
|
+
!/^(con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\.|$)/i.test(part)));
|
|
24
|
+
}
|
|
25
|
+
export function crc32(bytes) {
|
|
26
|
+
let crc = 0xffffffff;
|
|
27
|
+
for (const byte of bytes) {
|
|
28
|
+
crc ^= byte;
|
|
29
|
+
for (let bit = 0; bit < 8; bit++)
|
|
30
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
31
|
+
}
|
|
32
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
33
|
+
}
|
|
34
|
+
function extraFields(bytes) {
|
|
35
|
+
for (let offset = 0; offset < bytes.length;) {
|
|
36
|
+
if (offset + 4 > bytes.length)
|
|
37
|
+
throw new Error("ZIP_EXTRA_INVALID");
|
|
38
|
+
const id = bytes.readUInt16LE(offset);
|
|
39
|
+
const length = bytes.readUInt16LE(offset + 2);
|
|
40
|
+
// ZIP64, alternate Unicode path, Unix link metadata and NTFS attributes
|
|
41
|
+
// are unnecessary for our package and can introduce conflicting names.
|
|
42
|
+
if ([0x0001, 0x000a, 0x000d, 0x5855, 0x756e, 0x7075].includes(id))
|
|
43
|
+
throw new Error("ZIP_EXTRA_UNSUPPORTED");
|
|
44
|
+
offset += 4 + length;
|
|
45
|
+
if (offset > bytes.length)
|
|
46
|
+
throw new Error("ZIP_EXTRA_INVALID");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function decodeArchive(archive) {
|
|
50
|
+
if (archive.length < 22 || archive.length > MAX_ARCHIVE)
|
|
51
|
+
throw new Error("ZIP_SIZE_INVALID");
|
|
52
|
+
let end = archive.length - 22;
|
|
53
|
+
const first = Math.max(0, end - 65535);
|
|
54
|
+
for (; end >= first; end--) {
|
|
55
|
+
if (archive.readUInt32LE(end) === 0x06054b50 &&
|
|
56
|
+
end + 22 + archive.readUInt16LE(end + 20) === archive.length)
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (end < first ||
|
|
60
|
+
archive.readUInt16LE(end + 4) !== 0 ||
|
|
61
|
+
archive.readUInt16LE(end + 6) !== 0)
|
|
62
|
+
throw new Error("ZIP_END_INVALID");
|
|
63
|
+
const count = archive.readUInt16LE(end + 10);
|
|
64
|
+
const centralSize = archive.readUInt32LE(end + 12);
|
|
65
|
+
const centralOffset = archive.readUInt32LE(end + 16);
|
|
66
|
+
if (count < 1 ||
|
|
67
|
+
count > MAX_FILES ||
|
|
68
|
+
archive.readUInt16LE(end + 8) !== count ||
|
|
69
|
+
centralOffset + centralSize !== end)
|
|
70
|
+
throw new Error("ZIP_DIRECTORY_INVALID");
|
|
71
|
+
const names = new Map();
|
|
72
|
+
const entries = [];
|
|
73
|
+
const ranges = [];
|
|
74
|
+
let expanded = 0;
|
|
75
|
+
let cursor = centralOffset;
|
|
76
|
+
for (let index = 0; index < count; index++) {
|
|
77
|
+
if (cursor + 46 > end || archive.readUInt32LE(cursor) !== 0x02014b50)
|
|
78
|
+
throw new Error("ZIP_ENTRY_INVALID");
|
|
79
|
+
const flags = archive.readUInt16LE(cursor + 8);
|
|
80
|
+
const method = archive.readUInt16LE(cursor + 10);
|
|
81
|
+
const crc = archive.readUInt32LE(cursor + 16);
|
|
82
|
+
const compressedSize = archive.readUInt32LE(cursor + 20);
|
|
83
|
+
const size = archive.readUInt32LE(cursor + 24);
|
|
84
|
+
const nameLength = archive.readUInt16LE(cursor + 28);
|
|
85
|
+
const extraLength = archive.readUInt16LE(cursor + 30);
|
|
86
|
+
const commentLength = archive.readUInt16LE(cursor + 32);
|
|
87
|
+
const attributes = archive.readUInt32LE(cursor + 38);
|
|
88
|
+
const unixType = (attributes >>> 16) & 0xf000;
|
|
89
|
+
const unixMode = (attributes >>> 16) & 0x0fff;
|
|
90
|
+
const local = archive.readUInt32LE(cursor + 42);
|
|
91
|
+
const next = cursor + 46 + nameLength + extraLength + commentLength;
|
|
92
|
+
expanded += size;
|
|
93
|
+
if (next > end ||
|
|
94
|
+
nameLength < 1 ||
|
|
95
|
+
(flags & ~0x808) !== 0 ||
|
|
96
|
+
(method !== 0 && method !== 8) ||
|
|
97
|
+
size > MAX_EXPANDED ||
|
|
98
|
+
expanded > MAX_EXPANDED ||
|
|
99
|
+
archive.readUInt16LE(cursor + 34) !== 0 ||
|
|
100
|
+
![0, 0x4000, 0x8000].includes(unixType) ||
|
|
101
|
+
(attributes & 0x400) !== 0)
|
|
102
|
+
throw new Error("ZIP_ENTRY_UNSUPPORTED");
|
|
103
|
+
const rawName = archive.subarray(cursor + 46, cursor + 46 + nameLength);
|
|
104
|
+
const fullName = decoder.decode(rawName);
|
|
105
|
+
const directory = fullName.endsWith("/");
|
|
106
|
+
const name = directory ? fullName.slice(0, -1) : fullName;
|
|
107
|
+
const executable = !directory && (unixMode & 0o111) !== 0;
|
|
108
|
+
const key = name.toLowerCase();
|
|
109
|
+
if (!safeArchivePath(name) ||
|
|
110
|
+
names.has(key) ||
|
|
111
|
+
(unixMode & 0o7000) !== 0 ||
|
|
112
|
+
(executable && name !== "skills/nggaigc/scripts/macos-keychain") ||
|
|
113
|
+
(directory && (size !== 0 || unixType === 0x8000)) ||
|
|
114
|
+
(!directory && (unixType === 0x4000 || (attributes & 0x10) !== 0)))
|
|
115
|
+
throw new Error("ZIP_PATH_INVALID");
|
|
116
|
+
names.set(key, directory);
|
|
117
|
+
extraFields(archive.subarray(cursor + 46 + nameLength, cursor + 46 + nameLength + extraLength));
|
|
118
|
+
if (local + 30 > centralOffset ||
|
|
119
|
+
archive.readUInt32LE(local) !== 0x04034b50 ||
|
|
120
|
+
archive.readUInt16LE(local + 6) !== flags ||
|
|
121
|
+
archive.readUInt16LE(local + 8) !== method ||
|
|
122
|
+
archive.readUInt16LE(local + 26) !== nameLength)
|
|
123
|
+
throw new Error("ZIP_LOCAL_INVALID");
|
|
124
|
+
const localExtra = archive.readUInt16LE(local + 28);
|
|
125
|
+
const start = local + 30 + nameLength + localExtra;
|
|
126
|
+
const dataEnd = start + compressedSize;
|
|
127
|
+
if (dataEnd > centralOffset ||
|
|
128
|
+
!archive.subarray(local + 30, local + 30 + nameLength).equals(rawName))
|
|
129
|
+
throw new Error("ZIP_LOCAL_INVALID");
|
|
130
|
+
extraFields(archive.subarray(local + 30 + nameLength, start));
|
|
131
|
+
let entryEnd = dataEnd;
|
|
132
|
+
if (flags & 8) {
|
|
133
|
+
const descriptor = dataEnd + (archive.readUInt32LE(dataEnd) === 0x08074b50 ? 4 : 0);
|
|
134
|
+
entryEnd = descriptor + 12;
|
|
135
|
+
if (entryEnd > centralOffset ||
|
|
136
|
+
archive.readUInt32LE(descriptor) !== crc ||
|
|
137
|
+
archive.readUInt32LE(descriptor + 4) !== compressedSize ||
|
|
138
|
+
archive.readUInt32LE(descriptor + 8) !== size)
|
|
139
|
+
throw new Error("ZIP_DESCRIPTOR_INVALID");
|
|
140
|
+
}
|
|
141
|
+
else if (archive.readUInt32LE(local + 14) !== crc ||
|
|
142
|
+
archive.readUInt32LE(local + 18) !== compressedSize ||
|
|
143
|
+
archive.readUInt32LE(local + 22) !== size)
|
|
144
|
+
throw new Error("ZIP_LOCAL_INVALID");
|
|
145
|
+
const compressed = archive.subarray(start, dataEnd);
|
|
146
|
+
const bytes = method === 0
|
|
147
|
+
? Buffer.from(compressed)
|
|
148
|
+
: inflateRawSync(compressed, { maxOutputLength: Math.max(1, size) });
|
|
149
|
+
if (bytes.length !== size || crc32(bytes) !== crc)
|
|
150
|
+
throw new Error("ZIP_CONTENT_INVALID");
|
|
151
|
+
entries.push({ name, bytes, directory, executable });
|
|
152
|
+
ranges.push([local, entryEnd]);
|
|
153
|
+
cursor = next;
|
|
154
|
+
}
|
|
155
|
+
if (cursor !== end)
|
|
156
|
+
throw new Error("ZIP_DIRECTORY_INVALID");
|
|
157
|
+
// Reject gaps, overlapping local records and unlisted hidden entries.
|
|
158
|
+
ranges.sort(([a], [b]) => a - b);
|
|
159
|
+
let offset = 0;
|
|
160
|
+
for (const [start, finish] of ranges) {
|
|
161
|
+
if (start !== offset)
|
|
162
|
+
throw new Error("ZIP_LOCAL_INVALID");
|
|
163
|
+
offset = finish;
|
|
164
|
+
}
|
|
165
|
+
if (offset !== centralOffset)
|
|
166
|
+
throw new Error("ZIP_LOCAL_INVALID");
|
|
167
|
+
for (const name of names.keys()) {
|
|
168
|
+
const segments = name.split("/");
|
|
169
|
+
segments.pop();
|
|
170
|
+
while (segments.length) {
|
|
171
|
+
if (names.get(segments.join("/")) === false)
|
|
172
|
+
throw new Error("ZIP_PATH_CONFLICT");
|
|
173
|
+
segments.pop();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return entries;
|
|
177
|
+
}
|
|
178
|
+
export async function writeArchive(entries, destination) {
|
|
179
|
+
const requested = path.resolve(destination);
|
|
180
|
+
// Require a new directory below an existing non-link directory. Never merge
|
|
181
|
+
// into caller-owned contents or follow an existing junction/symlink.
|
|
182
|
+
const parent = path.dirname(requested);
|
|
183
|
+
const facts = await lstat(parent);
|
|
184
|
+
if (!facts.isDirectory() || facts.isSymbolicLink())
|
|
185
|
+
throw new Error("ZIP_DESTINATION_UNSAFE");
|
|
186
|
+
// macOS /var is a system symlink to /private/var. Resolve parent aliases
|
|
187
|
+
// once while still rejecting a caller-supplied linked destination parent.
|
|
188
|
+
const root = path.join(await realpath(parent), path.basename(requested));
|
|
189
|
+
await mkdir(root, { mode: 0o700 });
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const file = path.resolve(root, ...entry.name.split("/"));
|
|
192
|
+
const relative = path.relative(root, file);
|
|
193
|
+
if (!relative ||
|
|
194
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
195
|
+
path.isAbsolute(relative))
|
|
196
|
+
throw new Error("ZIP_DESTINATION_UNSAFE");
|
|
197
|
+
await mkdir(entry.directory ? file : path.dirname(file), {
|
|
198
|
+
recursive: true,
|
|
199
|
+
mode: 0o700,
|
|
200
|
+
});
|
|
201
|
+
if (entry.directory)
|
|
202
|
+
continue;
|
|
203
|
+
const handle = await open(file, "wx", entry.executable ? 0o700 : 0o600);
|
|
204
|
+
try {
|
|
205
|
+
await handle.writeFile(entry.bytes);
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
await handle.close();
|
|
209
|
+
}
|
|
210
|
+
if (entry.executable)
|
|
211
|
+
await chmod(file, 0o755);
|
|
212
|
+
}
|
|
213
|
+
}
|
package/dist/bin.js
CHANGED
|
@@ -1,39 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { CliError, fixedErrorCode } from "./errors.js";
|
|
3
5
|
import { runInstall } from "./install.js";
|
|
6
|
+
import { runSkillStartCommand, } from "./skill-command.js";
|
|
4
7
|
const messages = {
|
|
5
8
|
authorizing: "浏览器已成功启动,请在 NGGAIGC 页面确认连接…",
|
|
6
9
|
connected: "NGGAIGC Skill 已安装,设备连接验证成功。",
|
|
7
10
|
installing: "正在安全安装已签名的 NGGAIGC Skill…",
|
|
8
|
-
launching: "
|
|
11
|
+
launching: "正在请求系统打开默认浏览器…",
|
|
9
12
|
verifying: "正在验证 NGGAIGC 签名安装包…",
|
|
10
13
|
};
|
|
11
14
|
const errorMessages = {
|
|
12
15
|
ARCHIVE_EXTRACTION_FAILED: "安装包无法安全解压,请稍后重试。",
|
|
13
16
|
BROWSER_AUTHORIZATION_FAILED: "浏览器授权未完成,请重新运行安装命令。",
|
|
14
|
-
BROWSER_OPEN_FAILED: "
|
|
17
|
+
BROWSER_OPEN_FAILED: "系统未能打开默认浏览器。请确认已设置可用的默认浏览器,然后重新运行安装命令;本次连接未完成。",
|
|
15
18
|
CONNECTION_VERIFICATION_FAILED: "设备连接验证失败,请重新运行安装命令。",
|
|
16
|
-
INSTALLATION_FAILED: "
|
|
17
|
-
|
|
19
|
+
INSTALLATION_FAILED: "本地安装未完成,请检查现有文件状态后重试。",
|
|
20
|
+
SKILL_PAIR_UPGRADE_REQUIRED: "检测到无法安全配套迁移的本地 Seedream Skill,已停止升级。请保留旧 Skill 和任务文件并联系支持。",
|
|
21
|
+
SKILL_PAIR_RECOVERY_REQUIRED: "检测到本地组合安装中断或目录变化,已停止自动操作。请保留相关目录和任务文件并联系支持。",
|
|
22
|
+
INVALID_INPUT: "命令格式无效。请使用 install,或 start-skill skill.<id> [version]。",
|
|
18
23
|
PACKAGE_VERIFICATION_FAILED: "安装包签名或完整性验证失败,已停止安装。",
|
|
19
24
|
RUNTIME_UNSUPPORTED: "请使用 Node.js 22.x(22.13 起)或 24.x 版本。",
|
|
20
|
-
UNSUPPORTED_PLATFORM: "
|
|
25
|
+
UNSUPPORTED_PLATFORM: "当前系统或架构尚未获得此签名安装包支持,请使用已支持的平台。",
|
|
21
26
|
};
|
|
22
|
-
export async function main(args) {
|
|
27
|
+
export async function main(args, runtime = {}) {
|
|
28
|
+
const output = runtime.output ?? ((line) => process.stdout.write(line));
|
|
29
|
+
const errorOutput = runtime.error ?? ((line) => process.stderr.write(line));
|
|
30
|
+
if (args[0] === "start-skill" && (args.length === 2 || args.length === 3)) {
|
|
31
|
+
try {
|
|
32
|
+
const result = await (runtime.startSkill ?? runSkillStartCommand)(args[1], args[2], runtime.skillOptions);
|
|
33
|
+
output(`${JSON.stringify({ status: "ready", ...result })}\n`);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const code = fixedErrorCode(error) ?? "SKILL_START_UNAVAILABLE";
|
|
38
|
+
errorOutput(`${JSON.stringify({ error: { code } })}\n`);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
23
42
|
if (args.length !== 1 || args[0] !== "install") {
|
|
24
|
-
|
|
43
|
+
errorOutput(`${errorMessages.INVALID_INPUT}\n`);
|
|
25
44
|
return 1;
|
|
26
45
|
}
|
|
27
46
|
try {
|
|
28
47
|
await runInstall({
|
|
29
|
-
reporter: (event) =>
|
|
48
|
+
reporter: (event) => output(`${messages[event]}\n`),
|
|
30
49
|
});
|
|
31
50
|
return 0;
|
|
32
51
|
}
|
|
33
52
|
catch (error) {
|
|
34
53
|
const code = error instanceof CliError ? error.code : "INSTALLATION_FAILED";
|
|
35
|
-
|
|
54
|
+
errorOutput(`${errorMessages[code] ?? errorMessages.INSTALLATION_FAILED}\n`);
|
|
36
55
|
return 1;
|
|
37
56
|
}
|
|
38
57
|
}
|
|
39
|
-
|
|
58
|
+
if (process.argv[1] &&
|
|
59
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
60
|
+
process.exitCode = await main(process.argv.slice(2));
|
package/dist/bundle.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { createHash, createPublicKey, verify as verifySignature, } from "node:crypto";
|
|
3
2
|
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { decodeArchive, safeArchivePath, writeArchive } from "./archive.js";
|
|
6
5
|
import { CliError } from "./errors.js";
|
|
7
6
|
const manifestKeys = [
|
|
8
7
|
"artifactId",
|
|
@@ -18,9 +17,10 @@ const manifestKeys = [
|
|
|
18
17
|
"version",
|
|
19
18
|
];
|
|
20
19
|
const signatureKeys = ["algorithm", "keyId", "scope", "value"];
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
20
|
+
const windowsPlatforms = ["win32-x64", "win32-arm64"];
|
|
21
|
+
const desktopPlatforms = [...windowsPlatforms, "darwin-arm64"];
|
|
22
|
+
export const trustedKeyId = "ngg-signing-skill-release-1";
|
|
23
|
+
export const trustedKeySpkiSha256 = "3d7450a1a26785d900be03e52b4b7d0b2e84fe4e4fcb6f2e903c1371b0b96cee";
|
|
24
24
|
function record(value) {
|
|
25
25
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
26
26
|
}
|
|
@@ -28,16 +28,6 @@ function exactKeys(value, expected) {
|
|
|
28
28
|
return (JSON.stringify(Object.keys(value).sort()) ===
|
|
29
29
|
JSON.stringify([...expected].sort()));
|
|
30
30
|
}
|
|
31
|
-
function safeArchivePath(value) {
|
|
32
|
-
return (value.length >= 1 &&
|
|
33
|
-
value.length <= 240 &&
|
|
34
|
-
!value.startsWith("/") &&
|
|
35
|
-
!value.includes("\\") &&
|
|
36
|
-
!value.includes("\0") &&
|
|
37
|
-
value
|
|
38
|
-
.split("/")
|
|
39
|
-
.every((part) => part !== "" && part !== "." && part !== ".."));
|
|
40
|
-
}
|
|
41
31
|
async function json(file) {
|
|
42
32
|
try {
|
|
43
33
|
return JSON.parse(await readFile(file, "utf8"));
|
|
@@ -57,8 +47,9 @@ export async function verifyBundle(input) {
|
|
|
57
47
|
!exactKeys(manifestValue, manifestKeys) ||
|
|
58
48
|
manifestValue.schemaVersion !== "nggaigc-plugin-package/v1" ||
|
|
59
49
|
manifestValue.artifactId !== "plugin.nggaigc" ||
|
|
60
|
-
manifestValue.version !== "1.1.
|
|
61
|
-
manifestValue.artifactPath !==
|
|
50
|
+
manifestValue.version !== "1.1.14" ||
|
|
51
|
+
manifestValue.artifactPath !==
|
|
52
|
+
`packages/plugins/nggaigc-plugin-${manifestValue.version}.zip` ||
|
|
62
53
|
typeof manifestValue.sourceCommit !== "string" ||
|
|
63
54
|
!/^[a-f0-9]{40}$/.test(manifestValue.sourceCommit) ||
|
|
64
55
|
typeof manifestValue.sha256 !== "string" ||
|
|
@@ -71,7 +62,7 @@ export async function verifyBundle(input) {
|
|
|
71
62
|
!Number.isSafeInteger(manifestValue.fileCount) ||
|
|
72
63
|
!Array.isArray(manifestValue.platforms) ||
|
|
73
64
|
JSON.stringify(manifestValue.platforms) !==
|
|
74
|
-
JSON.stringify(
|
|
65
|
+
JSON.stringify(desktopPlatforms) ||
|
|
75
66
|
!record(manifestValue.files) ||
|
|
76
67
|
Object.keys(manifestValue.files).length !== manifestValue.fileCount ||
|
|
77
68
|
Object.entries(manifestValue.files).some(([name, digest]) => !safeArchivePath(name) ||
|
|
@@ -104,16 +95,50 @@ export async function verifyBundle(input) {
|
|
|
104
95
|
const archiveFacts = await lstat(input.archive);
|
|
105
96
|
if (!archiveFacts.isFile() || archiveFacts.isSymbolicLink())
|
|
106
97
|
throw new Error();
|
|
98
|
+
if (archiveFacts.size !== manifestValue.sizeBytes)
|
|
99
|
+
throw new Error();
|
|
107
100
|
const archive = await readFile(input.archive);
|
|
108
101
|
if (archive.length !== manifestValue.sizeBytes ||
|
|
109
102
|
createHash("sha256").update(archive).digest("hex") !==
|
|
110
103
|
manifestValue.sha256 ||
|
|
111
104
|
!verifySignature(null, archive, publicKey, Buffer.from(signature.value, "base64")))
|
|
112
105
|
throw new Error();
|
|
106
|
+
// Metadata is not covered by the artifact-bytes signature. Bind version,
|
|
107
|
+
// file hashes and new platform support to the actual signed ZIP payload.
|
|
108
|
+
const contents = decodeArchive(archive).filter((entry) => !entry.directory);
|
|
109
|
+
const actualFiles = Object.fromEntries(contents.map((entry) => [
|
|
110
|
+
entry.name,
|
|
111
|
+
createHash("sha256").update(entry.bytes).digest("hex"),
|
|
112
|
+
]));
|
|
113
|
+
if (Object.keys(actualFiles).length !== manifestValue.fileCount ||
|
|
114
|
+
Object.entries(actualFiles).some(([name, hash]) => manifestValue.files[name] !== hash))
|
|
115
|
+
throw new Error();
|
|
116
|
+
const plugin = contents.find((entry) => entry.name === ".codex-plugin/plugin.json");
|
|
117
|
+
const pluginMetadata = plugin
|
|
118
|
+
? JSON.parse(plugin.bytes.toString("utf8"))
|
|
119
|
+
: undefined;
|
|
120
|
+
if (!record(pluginMetadata) ||
|
|
121
|
+
pluginMetadata.version !== manifestValue.version)
|
|
122
|
+
throw new Error();
|
|
123
|
+
const supported = contents.find((entry) => entry.name === "skills/nggaigc/scripts/supported-platforms.json");
|
|
124
|
+
if (!supported)
|
|
125
|
+
throw new Error();
|
|
126
|
+
const capabilities = JSON.parse(supported.bytes.toString("utf8"));
|
|
127
|
+
if (!record(capabilities) ||
|
|
128
|
+
!exactKeys(capabilities, ["schemaVersion", "platforms"]) ||
|
|
129
|
+
capabilities.schemaVersion !== "nggaigc-supported-platforms/v1" ||
|
|
130
|
+
JSON.stringify(capabilities.platforms) !==
|
|
131
|
+
JSON.stringify(desktopPlatforms))
|
|
132
|
+
throw new Error();
|
|
133
|
+
const helper = contents.find((entry) => entry.name === "skills/nggaigc/scripts/macos-keychain");
|
|
134
|
+
if (!helper?.executable || helper.bytes.length < 8)
|
|
135
|
+
throw new Error();
|
|
113
136
|
return {
|
|
114
137
|
archive: path.resolve(input.archive),
|
|
115
138
|
files: manifestValue.files,
|
|
116
139
|
version: manifestValue.version,
|
|
140
|
+
platforms: manifestValue.platforms,
|
|
141
|
+
sha256: manifestValue.sha256,
|
|
117
142
|
};
|
|
118
143
|
}
|
|
119
144
|
catch (error) {
|
|
@@ -122,60 +147,23 @@ export async function verifyBundle(input) {
|
|
|
122
147
|
throw new CliError("PACKAGE_VERIFICATION_FAILED");
|
|
123
148
|
}
|
|
124
149
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
windowsHide: true,
|
|
143
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
144
|
-
env: {
|
|
145
|
-
SystemRoot: process.env.SystemRoot,
|
|
146
|
-
LOCALAPPDATA: process.env.LOCALAPPDATA,
|
|
147
|
-
USERPROFILE: process.env.USERPROFILE,
|
|
148
|
-
PATH: process.env.PATH,
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
let bytes = 0;
|
|
152
|
-
let failed = false;
|
|
153
|
-
const limit = (chunk) => {
|
|
154
|
-
bytes += chunk.length;
|
|
155
|
-
if (bytes > 64 * 1024) {
|
|
156
|
-
failed = true;
|
|
157
|
-
child.kill();
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
child.stdout.on("data", limit);
|
|
161
|
-
child.stderr.on("data", limit);
|
|
162
|
-
const timer = setTimeout(() => {
|
|
163
|
-
failed = true;
|
|
164
|
-
child.kill();
|
|
165
|
-
}, 30_000);
|
|
166
|
-
timer.unref();
|
|
167
|
-
child.once("error", () => {
|
|
168
|
-
clearTimeout(timer);
|
|
169
|
-
reject(new CliError("ARCHIVE_EXTRACTION_FAILED"));
|
|
170
|
-
});
|
|
171
|
-
child.once("exit", (code) => {
|
|
172
|
-
clearTimeout(timer);
|
|
173
|
-
if (failed || code !== 0)
|
|
174
|
-
reject(new CliError("ARCHIVE_EXTRACTION_FAILED"));
|
|
175
|
-
else
|
|
176
|
-
resolve();
|
|
177
|
-
});
|
|
178
|
-
});
|
|
150
|
+
export async function extractBundle(archive, destination, expectedSha256) {
|
|
151
|
+
try {
|
|
152
|
+
const facts = await lstat(archive);
|
|
153
|
+
if (!facts.isFile() ||
|
|
154
|
+
facts.isSymbolicLink() ||
|
|
155
|
+
facts.size > 64 * 1024 * 1024)
|
|
156
|
+
throw new Error();
|
|
157
|
+
const bytes = await readFile(archive);
|
|
158
|
+
if (expectedSha256 &&
|
|
159
|
+
createHash("sha256").update(bytes).digest("hex") !== expectedSha256)
|
|
160
|
+
throw new Error();
|
|
161
|
+
const contents = decodeArchive(bytes);
|
|
162
|
+
await writeArchive(contents, destination);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new CliError("ARCHIVE_EXTRACTION_FAILED");
|
|
166
|
+
}
|
|
179
167
|
}
|
|
180
168
|
async function entries(directory, prefix = "") {
|
|
181
169
|
let children;
|