@eggplanty/mycli 1.0.1 → 1.0.4
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/checksums.txt +6 -0
- package/package.json +2 -1
- package/scripts/install.js +224 -38
package/checksums.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
753b0b2b16c207f1c27aa718ca3f53f0039046ecf09af4249407dc7a0701a17c mycli-1.0.4-darwin-amd64.tar.gz
|
|
2
|
+
a3fde5128295e6159ce2bf82c2bc4992c9b04b184cd5c39a1d40577ef90a94a0 mycli-1.0.4-darwin-arm64.tar.gz
|
|
3
|
+
05bec28c5a9e3daf77c674c946a066c0c5bbe49c17bac434315e7cdbba0b3c1a mycli-1.0.4-linux-amd64.tar.gz
|
|
4
|
+
fd42d731ad61935b13dc4a5d2c192eb9f6594ba24acd2cbc8ae5b68fbe312d68 mycli-1.0.4-linux-arm64.tar.gz
|
|
5
|
+
cadf31682286d254c4b1730b6feba805bd7009263f364ed2eeba80292cf0a90a mycli-1.0.4-windows-amd64.zip
|
|
6
|
+
2b4245371a21439c56cc8120a12723364fbe7e4e3553c597514554daac7dd5b5 mycli-1.0.4-windows-arm64.zip
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eggplanty/mycli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A simple CLI demo built with Go",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mycli": "scripts/run.js"
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"files": [
|
|
29
29
|
"scripts/install.js",
|
|
30
30
|
"scripts/run.js",
|
|
31
|
+
"checksums.txt",
|
|
31
32
|
"CHANGELOG.md"
|
|
32
33
|
]
|
|
33
34
|
}
|
package/scripts/install.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const {
|
|
3
|
+
const { execFileSync } = require("child_process");
|
|
4
4
|
const os = require("os");
|
|
5
|
+
const crypto = require("crypto");
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
class ChecksumError extends Error {}
|
|
8
|
+
class NetworkError extends Error {}
|
|
9
|
+
|
|
10
|
+
const VERSION = require("../package.json").version;
|
|
7
11
|
const REPO = "larksuite/cli";
|
|
8
12
|
const NAME = "lark-cli";
|
|
9
13
|
|
|
@@ -23,7 +27,7 @@ const arch = ARCH_MAP[process.arch];
|
|
|
23
27
|
|
|
24
28
|
if (!platform || !arch) {
|
|
25
29
|
console.error(
|
|
26
|
-
|
|
30
|
+
`Unsupported platform: ${process.platform}-${process.arch}`
|
|
27
31
|
);
|
|
28
32
|
process.exit(1);
|
|
29
33
|
}
|
|
@@ -31,62 +35,244 @@ if (!platform || !arch) {
|
|
|
31
35
|
const isWindows = process.platform === "win32";
|
|
32
36
|
const ext = isWindows ? ".zip" : ".tar.gz";
|
|
33
37
|
const archiveName = `${NAME}-${VERSION}-${platform}-${arch}${ext}`;
|
|
34
|
-
const
|
|
35
|
-
|
|
38
|
+
const SOURCES = [
|
|
39
|
+
`https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`,
|
|
40
|
+
`https://registry.npmmirror.com/-/binary/lark-cli/v${VERSION}/${archiveName}`,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const ALLOWED_INITIAL_HOSTS = new Set([
|
|
44
|
+
"github.com",
|
|
45
|
+
"registry.npmmirror.com",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const CURL_CONNECT_TIMEOUT_SEC = 10;
|
|
49
|
+
const CURL_MAX_TIME_SEC = 120;
|
|
50
|
+
const CURL_MAX_REDIRS = 5;
|
|
51
|
+
|
|
52
|
+
const DEFAULT_CHECKSUM_PATH = path.join(__dirname, "..", "checksums.txt");
|
|
53
|
+
|
|
54
|
+
// Defensive: escape single quotes for PowerShell literal-string embedding.
|
|
55
|
+
// tmpDir comes from mkdtempSync so is controlled, but this hardens against
|
|
56
|
+
// future refactors that route external input into the script.
|
|
57
|
+
function escapeSingleQuotes(s) {
|
|
58
|
+
return s.replace(/'/g, "''");
|
|
59
|
+
}
|
|
36
60
|
|
|
37
61
|
const binDir = path.join(__dirname, "..", "bin");
|
|
38
62
|
const dest = path.join(binDir, NAME + (isWindows ? ".exe" : ""));
|
|
39
63
|
|
|
40
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
41
|
-
|
|
42
64
|
function download(url, destPath) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
47
|
-
|
|
65
|
+
// JS-layer pre-check: initial URL must be https and in allowlist.
|
|
66
|
+
// Redirect targets are NOT host-checked; we rely on curl's
|
|
67
|
+
// --proto-redir =https + --max-redirs + SHA256 verify for safety.
|
|
68
|
+
const parsed = new URL(url);
|
|
69
|
+
if (parsed.protocol !== "https:") {
|
|
70
|
+
throw new NetworkError(`Non-HTTPS URL rejected: ${url}`);
|
|
71
|
+
}
|
|
72
|
+
if (!ALLOWED_INITIAL_HOSTS.has(parsed.hostname)) {
|
|
73
|
+
throw new NetworkError(`Untrusted initial host: ${parsed.hostname}`);
|
|
74
|
+
}
|
|
48
75
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
76
|
+
const args = [
|
|
77
|
+
"--fail", // HTTP 4xx/5xx -> non-zero exit
|
|
78
|
+
"--location", // follow redirects
|
|
79
|
+
"--proto", "=https", // initial URL: https only
|
|
80
|
+
"--proto-redir", "=https", // redirect targets: https only
|
|
81
|
+
"--max-redirs", String(CURL_MAX_REDIRS),
|
|
82
|
+
"--tlsv1.2", // minimum TLS 1.2
|
|
83
|
+
"--connect-timeout", String(CURL_CONNECT_TIMEOUT_SEC),
|
|
84
|
+
"--max-time", String(CURL_MAX_TIME_SEC),
|
|
85
|
+
"--silent", "--show-error",
|
|
86
|
+
"--output", destPath,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
if (isWindows) {
|
|
90
|
+
// Schannel CRL check hard-fails when the CRL server is unreachable;
|
|
91
|
+
// this flag was in the original install.js and is preserved to
|
|
92
|
+
// avoid regression for users in corporate networks.
|
|
93
|
+
args.unshift("--ssl-revoke-best-effort");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// URL is always the last positional arg.
|
|
97
|
+
args.push(url);
|
|
52
98
|
|
|
53
99
|
try {
|
|
100
|
+
execFileSync("curl", args, {
|
|
101
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
102
|
+
});
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (err.code === "ENOENT") {
|
|
105
|
+
// ENOENT is NOT a NetworkError: another source won't help (curl
|
|
106
|
+
// is missing). Throw plain Error so the fallback loop re-raises
|
|
107
|
+
// instead of silently trying the next URL.
|
|
108
|
+
throw new Error(
|
|
109
|
+
"curl is required for installation but was not found in PATH. " +
|
|
110
|
+
"Install curl or manually download the binary from " +
|
|
111
|
+
`https://github.com/${REPO}/releases/tag/v${VERSION}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const stderr = err.stderr ? err.stderr.toString().trim() : "";
|
|
115
|
+
const exitCode = err.status != null ? err.status : "unknown";
|
|
116
|
+
throw new NetworkError(
|
|
117
|
+
`curl exited with code ${exitCode}${stderr ? ": " + stderr : ""}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function downloadWithFallback(urls, destPath) {
|
|
123
|
+
const attempts = [];
|
|
124
|
+
for (const url of urls) {
|
|
54
125
|
try {
|
|
55
|
-
download(
|
|
126
|
+
download(url, destPath);
|
|
127
|
+
return url;
|
|
56
128
|
} catch (err) {
|
|
57
|
-
|
|
129
|
+
if (err instanceof NetworkError) {
|
|
130
|
+
attempts.push({ url, error: err.message });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
// ChecksumError, plain Error (ENOENT), or any other type:
|
|
134
|
+
// re-raise immediately without trying the next source.
|
|
135
|
+
throw err;
|
|
58
136
|
}
|
|
137
|
+
}
|
|
138
|
+
const detail = attempts
|
|
139
|
+
.map((a) => ` - ${a.url}\n ${a.error}`)
|
|
140
|
+
.join("\n");
|
|
141
|
+
throw new NetworkError(`All download sources failed:\n${detail}`);
|
|
142
|
+
}
|
|
59
143
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
144
|
+
function extract(archivePath, tmpDir) {
|
|
145
|
+
if (isWindows) {
|
|
146
|
+
const script =
|
|
147
|
+
`$ErrorActionPreference = 'Stop'\n` +
|
|
148
|
+
`Expand-Archive -LiteralPath '${escapeSingleQuotes(archivePath)}' ` +
|
|
149
|
+
`-DestinationPath '${escapeSingleQuotes(tmpDir)}' -Force\n`;
|
|
150
|
+
|
|
151
|
+
const scriptPath = path.join(tmpDir, "extract.ps1");
|
|
152
|
+
fs.writeFileSync(scriptPath, script, { encoding: "utf-8" });
|
|
153
|
+
|
|
154
|
+
execFileSync("powershell", [
|
|
155
|
+
"-NoProfile",
|
|
156
|
+
"-NonInteractive",
|
|
157
|
+
"-ExecutionPolicy", "Bypass",
|
|
158
|
+
"-File", scriptPath,
|
|
159
|
+
], { stdio: "ignore" });
|
|
160
|
+
} else {
|
|
161
|
+
execFileSync("tar", ["-xzf", archivePath, "-C", tmpDir], {
|
|
162
|
+
stdio: "ignore",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function verifyChecksum(filePath, expectedHash) {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const hash = crypto.createHash("sha256");
|
|
170
|
+
const stream = fs.createReadStream(filePath);
|
|
171
|
+
stream.on("error", reject);
|
|
172
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
173
|
+
stream.on("end", () => {
|
|
174
|
+
const actual = hash.digest("hex");
|
|
175
|
+
const expected = expectedHash.toLowerCase();
|
|
176
|
+
if (actual !== expected) {
|
|
177
|
+
reject(new ChecksumError(
|
|
178
|
+
`SHA256 mismatch for ${path.basename(filePath)}\n` +
|
|
179
|
+
` expected: ${expected}\n` +
|
|
180
|
+
` actual: ${actual}`
|
|
181
|
+
));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
resolve();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getExpectedChecksum(archiveFilename, checksumPath = DEFAULT_CHECKSUM_PATH) {
|
|
190
|
+
if (!fs.existsSync(checksumPath)) {
|
|
191
|
+
throw new ChecksumError("checksums.txt missing from package");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const contents = fs.readFileSync(checksumPath, "utf-8");
|
|
195
|
+
const lineRegex = /^([0-9a-fA-F]{64})\s+\*?(.+)$/;
|
|
196
|
+
|
|
197
|
+
for (const rawLine of contents.split("\n")) {
|
|
198
|
+
const line = rawLine.trim();
|
|
199
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
200
|
+
|
|
201
|
+
const match = line.match(lineRegex);
|
|
202
|
+
if (!match) continue;
|
|
203
|
+
|
|
204
|
+
const [, hash, filename] = match;
|
|
205
|
+
if (filename.trim() === archiveFilename) {
|
|
206
|
+
return hash.toLowerCase();
|
|
69
207
|
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
throw new ChecksumError(`No checksum entry for ${archiveFilename}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function install() {
|
|
214
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lark-cli-"));
|
|
215
|
+
const archivePath = path.join(tmpDir, archiveName);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// 1. Early fail: if the bundled checksums.txt is broken,
|
|
219
|
+
// report now before spending bandwidth.
|
|
220
|
+
const expectedHash = getExpectedChecksum(archiveName);
|
|
70
221
|
|
|
222
|
+
// 2. Multi-source download; only NetworkError triggers fallback.
|
|
223
|
+
const sourceUrl = downloadWithFallback(SOURCES, archivePath);
|
|
224
|
+
|
|
225
|
+
// 3. Integrity check outside the fallback loop. Mismatch aborts
|
|
226
|
+
// the entire install, does NOT try the next source.
|
|
227
|
+
await verifyChecksum(archivePath, expectedHash);
|
|
228
|
+
|
|
229
|
+
// 4. Extract (safe: bytes match the official release).
|
|
230
|
+
extract(archivePath, tmpDir);
|
|
231
|
+
|
|
232
|
+
// 5. Copy binary into place and chmod.
|
|
71
233
|
const binaryName = NAME + (isWindows ? ".exe" : "");
|
|
72
234
|
const extractedBinary = path.join(tmpDir, binaryName);
|
|
73
|
-
|
|
235
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
74
236
|
fs.copyFileSync(extractedBinary, dest);
|
|
75
237
|
fs.chmodSync(dest, 0o755);
|
|
76
|
-
|
|
238
|
+
|
|
239
|
+
console.log(
|
|
240
|
+
`${NAME} v${VERSION} installed successfully ` +
|
|
241
|
+
`(from ${new URL(sourceUrl).hostname})`
|
|
242
|
+
);
|
|
77
243
|
} finally {
|
|
244
|
+
// 6. Always clean up the temp directory.
|
|
78
245
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
79
246
|
}
|
|
80
247
|
}
|
|
81
248
|
|
|
82
|
-
|
|
83
|
-
install()
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
249
|
+
if (require.main === module) {
|
|
250
|
+
install().catch((err) => {
|
|
251
|
+
if (err instanceof ChecksumError) {
|
|
252
|
+
console.error(`\n[SECURITY] ${NAME} install aborted due to integrity check failure:\n`);
|
|
253
|
+
console.error(err.message);
|
|
254
|
+
console.error(
|
|
255
|
+
`\nRetry the install; if it persists, report it and download manually:\n` +
|
|
256
|
+
` https://github.com/${REPO}/releases/tag/v${VERSION}\n`
|
|
257
|
+
);
|
|
258
|
+
} else if (err instanceof NetworkError) {
|
|
259
|
+
console.error(`\n${NAME} install failed due to network errors:\n`);
|
|
260
|
+
console.error(err.message);
|
|
261
|
+
console.error(
|
|
262
|
+
`\nIf you are behind a firewall or on a restricted network, try configuring a proxy:\n` +
|
|
263
|
+
` export https_proxy=http://your-proxy:port\n` +
|
|
264
|
+
` npm install -g @larksuite/cli\n`
|
|
265
|
+
);
|
|
266
|
+
} else {
|
|
267
|
+
console.error(`\n${NAME} install failed:\n${err.stack || err.message}`);
|
|
268
|
+
}
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
92
271
|
}
|
|
272
|
+
|
|
273
|
+
module.exports = {
|
|
274
|
+
verifyChecksum,
|
|
275
|
+
getExpectedChecksum,
|
|
276
|
+
ChecksumError,
|
|
277
|
+
NetworkError,
|
|
278
|
+
};
|