dsh-wechat-remote 1.7.6 → 1.7.9
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 -6
- package/assets/plugin.tgz +0 -0
- package/assets/release.json +11 -51
- package/bin/dsh-discovery.mjs +14 -4
- package/bin/native-recovery.mjs +44 -0
- package/bin/release-selection.mjs +2 -2
- package/bin/setup.mjs +100 -24
- package/lib/install-control.js +53 -29
- package/lib/install-profile.js +114 -159
- package/lib/install-runtime.js +17 -0
- package/lib/native-recovery.js +7478 -0
- package/lib/update-download.js +162 -36
- package/lib/update-policy.js +9 -1
- package/lib/update-worker.js +255 -186
- package/package.json +1 -1
package/lib/update-download.js
CHANGED
|
@@ -5,6 +5,12 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
import { gunzipSync } from "node:zlib";
|
|
6
6
|
|
|
7
7
|
// src/update-policy.ts
|
|
8
|
+
var versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
9
|
+
function validVersion(value) {
|
|
10
|
+
if (typeof value !== "string" || value.length > 80) return false;
|
|
11
|
+
const match = versionPattern.exec(value);
|
|
12
|
+
return Boolean(match && !(match[4] || "").split(".").some((p) => /^0\d+$/.test(p)) && (!value.includes("+") || /^[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*$/.test(value.split("+")[1])));
|
|
13
|
+
}
|
|
8
14
|
function trustedReleaseAsset(asset, version) {
|
|
9
15
|
if (!asset || !/^[a-f0-9]{64}$/.test(asset.sha256) || !Number.isSafeInteger(asset.bytes) || asset.bytes < 1 || asset.bytes > 32 * 1024 * 1024) return false;
|
|
10
16
|
try {
|
|
@@ -14,55 +20,119 @@ function trustedReleaseAsset(asset, version) {
|
|
|
14
20
|
return false;
|
|
15
21
|
}
|
|
16
22
|
}
|
|
23
|
+
function trustedNpmInstaller(source) {
|
|
24
|
+
if (!source || !validVersion(source.version) || !/^[a-f0-9]{64}$/.test(source.sha256) || !Number.isSafeInteger(source.bytes) || source.bytes < 1 || source.bytes > 32 * 1024 * 1024) return false;
|
|
25
|
+
return source.url === `https://registry.npmjs.org/dsh-wechat-remote/-/dsh-wechat-remote-${source.version}.tgz`;
|
|
26
|
+
}
|
|
17
27
|
|
|
18
28
|
// src/update-download.ts
|
|
19
|
-
|
|
29
|
+
var DownloadUnavailableError = class extends Error {
|
|
30
|
+
constructor(message = "\u66F4\u65B0\u5305\u6682\u65F6\u65E0\u6CD5\u4E0B\u8F7D\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\uFF1B\u5F53\u524D\u63D2\u4EF6\u5C1A\u672A\u66FF\u6362", options) {
|
|
31
|
+
super(message, options);
|
|
32
|
+
this.name = "DownloadUnavailableError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var githubHosts = ["github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com"];
|
|
36
|
+
async function fetchBytes(url, maxBytes, fetcher, hosts, deadline, options) {
|
|
20
37
|
let next = url;
|
|
21
38
|
for (let i = 0; i < 5; i++) {
|
|
39
|
+
options.signal?.throwIfAborted();
|
|
22
40
|
const u = new URL(next);
|
|
23
|
-
if (u.protocol !== "https:" || u.username || u.password || u.port || !
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!res.ok || !res.body) {
|
|
33
|
-
await res.body?.cancel();
|
|
34
|
-
throw new Error("\u66F4\u65B0\u670D\u52A1\u6682\u4E0D\u53EF\u7528");
|
|
35
|
-
}
|
|
36
|
-
if (Number(res.headers.get("content-length")) > maxBytes) {
|
|
37
|
-
await res.body.cancel();
|
|
38
|
-
throw new Error("\u66F4\u65B0\u54CD\u5E94\u8D85\u8FC7\u5927\u5C0F\u9650\u5236");
|
|
39
|
-
}
|
|
40
|
-
const reader = res.body.getReader(), chunks = [];
|
|
41
|
-
let size = 0;
|
|
41
|
+
if (u.protocol !== "https:" || u.username || u.password || u.port || !hosts.includes(u.hostname)) throw new Error("\u66F4\u65B0\u4E0B\u8F7D\u6765\u6E90\u4E0D\u53D7\u4FE1\u4EFB");
|
|
42
|
+
const remaining = deadline - Date.now();
|
|
43
|
+
if (remaining <= 0) throw new DownloadUnavailableError();
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const cancel = () => controller.abort(options.signal.reason);
|
|
46
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
47
|
+
const totalTimer = setTimeout(() => controller.abort(new Error("\u66F4\u65B0\u4E0B\u8F7D\u8D85\u65F6")), remaining);
|
|
48
|
+
let activityTimer = setTimeout(() => controller.abort(new Error("\u66F4\u65B0\u6765\u6E90\u54CD\u5E94\u8D85\u65F6")), Math.min(remaining, options.responseTimeoutMs ?? 1e4));
|
|
49
|
+
let reader;
|
|
42
50
|
try {
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetcher(next, { signal: controller.signal, redirect: "manual" });
|
|
54
|
+
} catch (cause) {
|
|
55
|
+
options.signal?.throwIfAborted();
|
|
56
|
+
throw new DownloadUnavailableError(void 0, { cause });
|
|
57
|
+
} finally {
|
|
58
|
+
clearTimeout(activityTimer);
|
|
59
|
+
}
|
|
60
|
+
if ([301, 302, 303, 307, 308].includes(res.status)) {
|
|
61
|
+
await res.body?.cancel();
|
|
62
|
+
const location = res.headers.get("location");
|
|
63
|
+
if (!location) throw new Error("\u66F4\u65B0\u6765\u6E90\u91CD\u5B9A\u5411\u65E0\u6548");
|
|
64
|
+
next = new URL(location, next).href;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!res.ok || !res.body) {
|
|
68
|
+
await res.body?.cancel();
|
|
69
|
+
throw new DownloadUnavailableError();
|
|
70
|
+
}
|
|
71
|
+
if (Number(res.headers.get("content-length")) > maxBytes) {
|
|
72
|
+
await res.body.cancel();
|
|
73
|
+
throw new Error("\u66F4\u65B0\u54CD\u5E94\u8D85\u8FC7\u5927\u5C0F\u9650\u5236");
|
|
74
|
+
}
|
|
75
|
+
reader = res.body.getReader();
|
|
76
|
+
activityTimer = setTimeout(() => controller.abort(new Error("\u66F4\u65B0\u4E0B\u8F7D\u957F\u65F6\u95F4\u6CA1\u6709\u8FDB\u5C55")), options.idleTimeoutMs ?? 15e3);
|
|
77
|
+
const chunks = [];
|
|
78
|
+
let size = 0;
|
|
43
79
|
for (; ; ) {
|
|
44
|
-
|
|
80
|
+
let part;
|
|
81
|
+
try {
|
|
82
|
+
part = await reader.read();
|
|
83
|
+
} catch (cause) {
|
|
84
|
+
options.signal?.throwIfAborted();
|
|
85
|
+
throw new DownloadUnavailableError(void 0, { cause });
|
|
86
|
+
}
|
|
87
|
+
const { done, value } = part;
|
|
45
88
|
if (done) break;
|
|
46
89
|
size += value.length;
|
|
47
90
|
if (size > maxBytes) throw new Error("\u66F4\u65B0\u54CD\u5E94\u8D85\u8FC7\u5927\u5C0F\u9650\u5236");
|
|
48
91
|
chunks.push(value);
|
|
92
|
+
if (value.length) activityTimer.refresh();
|
|
49
93
|
}
|
|
94
|
+
options.signal?.throwIfAborted();
|
|
95
|
+
return Buffer.concat(chunks);
|
|
50
96
|
} catch (error) {
|
|
51
|
-
|
|
97
|
+
try {
|
|
98
|
+
await reader?.cancel();
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
52
101
|
throw error;
|
|
102
|
+
} finally {
|
|
103
|
+
clearTimeout(totalTimer);
|
|
104
|
+
clearTimeout(activityTimer);
|
|
105
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
106
|
+
reader?.releaseLock();
|
|
53
107
|
}
|
|
54
|
-
return Buffer.concat(chunks);
|
|
55
108
|
}
|
|
56
109
|
throw new Error("\u66F4\u65B0\u6765\u6E90\u91CD\u5B9A\u5411\u8FC7\u591A");
|
|
57
110
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
111
|
+
async function boundedFetch(url, maxBytes, fetcher = fetch) {
|
|
112
|
+
return fetchBytes(
|
|
113
|
+
url,
|
|
114
|
+
maxBytes,
|
|
115
|
+
fetcher,
|
|
116
|
+
[...githubHosts, "relay.xyxfood.xyz"],
|
|
117
|
+
Date.now() + (maxBytes <= 256 * 1024 ? 1e4 : 6e4),
|
|
118
|
+
{}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
function verifyBytes(archive, expected) {
|
|
122
|
+
if (!expected || archive.length !== expected.bytes || createHash("sha256").update(archive).digest("hex") !== expected.sha256) throw new Error("\u66F4\u65B0\u5305\u6821\u9A8C\u5931\u8D25\uFF0C\u672A\u4FEE\u6539\u5F53\u524D\u63D2\u4EF6");
|
|
123
|
+
}
|
|
124
|
+
function visitArchive(archive, visit) {
|
|
60
125
|
const tar = gunzipSync(archive, { maxOutputLength: 64 * 1024 * 1024 });
|
|
61
|
-
|
|
62
|
-
|
|
126
|
+
if (tar.length % 512 !== 0) throw new Error("\u66F4\u65B0\u5305\u8BB0\u5F55\u4E0D\u5B8C\u6574");
|
|
127
|
+
const seen = /* @__PURE__ */ new Set(), files = /* @__PURE__ */ new Set();
|
|
128
|
+
let entries = 0, ended = false;
|
|
63
129
|
for (let offset = 0; offset + 512 <= tar.length; ) {
|
|
64
130
|
const header = tar.subarray(offset, offset + 512);
|
|
65
|
-
if (header.every((b) => b === 0))
|
|
131
|
+
if (header.every((b) => b === 0)) {
|
|
132
|
+
if (tar.subarray(offset).some((b) => b !== 0)) throw new Error("\u66F4\u65B0\u5305\u7ED3\u675F\u6807\u8BB0\u65E0\u6548");
|
|
133
|
+
ended = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
66
136
|
if (++entries > 5e3) throw new Error("\u66F4\u65B0\u5305\u6587\u4EF6\u6570\u91CF\u5F02\u5E38");
|
|
67
137
|
const field = (start, size2) => header.subarray(start, start + size2).toString("utf8").replace(/\0.*$/s, "");
|
|
68
138
|
const name = (field(345, 155) ? field(345, 155) + "/" : "") + field(0, 100);
|
|
@@ -73,26 +143,82 @@ function auditArchive(archive, release) {
|
|
|
73
143
|
const sumText = field(148, 8).trim();
|
|
74
144
|
const sum = [...header].reduce((n, b, i) => n + (i >= 148 && i < 156 ? 32 : b), 0);
|
|
75
145
|
if (!/^[0-7]+$/.test(sumText) || parseInt(sumText, 8) !== sum) throw new Error("\u66F4\u65B0\u5305\u5934\u6821\u9A8C\u5931\u8D25");
|
|
76
|
-
if (!name.startsWith("package/") || /[\\:\x00-\x1f]/.test(name) || name.split("/").some((part) => part === ".." || part === ".") || name.split("/").some((part) => /[. ]$/.test(part) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)) || !["0", "", "5"].includes(type) || seen.has(name.toLowerCase()) || !Number.isSafeInteger(size) || size < 0 || offset + 512 + size > tar.length) throw new Error("\u66F4\u65B0\u5305\u5305\u542B\u4E0D\u5B89\u5168\u7684\u8DEF\u5F84\u6216\u6587\u4EF6\u7C7B\u578B");
|
|
146
|
+
if (!name.startsWith("package/") || /[\\:\x00-\x1f]/.test(name) || name.split("/").some((part) => part === ".." || part === ".") || name.split("/").some((part) => /[. ]$/.test(part) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)) || !["0", "", "5"].includes(type) || seen.has(name.toLowerCase()) || !Number.isSafeInteger(size) || size < 0 || type === "5" && size !== 0 || offset + 512 + Math.ceil(size / 512) * 512 > tar.length) throw new Error("\u66F4\u65B0\u5305\u5305\u542B\u4E0D\u5B89\u5168\u7684\u8DEF\u5F84\u6216\u6587\u4EF6\u7C7B\u578B");
|
|
77
147
|
seen.add(name.toLowerCase());
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
148
|
+
if (type !== "5") {
|
|
149
|
+
files.add(name);
|
|
150
|
+
visit(name, tar.subarray(offset + 512, offset + 512 + size));
|
|
81
151
|
}
|
|
82
152
|
offset += 512 + Math.ceil(size / 512) * 512;
|
|
83
153
|
}
|
|
84
|
-
if (!
|
|
154
|
+
if (!ended) throw new Error("\u66F4\u65B0\u5305\u7F3A\u5C11\u7ED3\u675F\u6807\u8BB0");
|
|
155
|
+
return files;
|
|
156
|
+
}
|
|
157
|
+
function readManifest(data) {
|
|
158
|
+
if (data.length > 65536) throw new Error("\u66F4\u65B0\u5305\u6E05\u5355\u8FC7\u5927");
|
|
159
|
+
return JSON.parse(data.toString("utf8"));
|
|
160
|
+
}
|
|
161
|
+
function auditArchive(archive, release) {
|
|
162
|
+
verifyBytes(archive, release.asset);
|
|
163
|
+
let manifest;
|
|
164
|
+
const files = visitArchive(archive, (name, data) => {
|
|
165
|
+
if (name === "package/package.json") manifest = readManifest(data);
|
|
166
|
+
});
|
|
167
|
+
if (!manifest || manifest.name !== "@harness-remote/dsh-wechat-remote" || manifest.version !== release.version || !files.has("package/lib/index.js") || !files.has("package/lib/client.js")) throw new Error("\u66F4\u65B0\u5305\u540D\u79F0\u3001\u7248\u672C\u6216\u5165\u53E3\u4E0D\u5339\u914D");
|
|
85
168
|
const scripts = manifest.scripts;
|
|
86
169
|
if (["preinstall", "install", "postinstall", "prepare"].some((name) => scripts?.[name])) throw new Error("\u66F4\u65B0\u5305\u5305\u542B\u4E0D\u5141\u8BB8\u7684\u5B89\u88C5\u811A\u672C");
|
|
87
170
|
}
|
|
88
|
-
|
|
171
|
+
function pluginFromInstaller(archive, release) {
|
|
172
|
+
if (!trustedNpmInstaller(release.npmInstaller)) throw new Error("\u5907\u7528\u5B89\u88C5\u5305\u6765\u6E90\u4E0D\u53D7\u4FE1\u4EFB");
|
|
173
|
+
verifyBytes(archive, release.npmInstaller);
|
|
174
|
+
let manifest, plugin;
|
|
175
|
+
visitArchive(archive, (name, data) => {
|
|
176
|
+
if (name === "package/package.json") manifest = readManifest(data);
|
|
177
|
+
if (name === "package/assets/plugin.tgz") {
|
|
178
|
+
if (data.length !== release.asset?.bytes) throw new Error("\u5185\u7F6E\u63D2\u4EF6\u5927\u5C0F\u4E0D\u5339\u914D");
|
|
179
|
+
plugin = Buffer.from(data);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
if (!manifest || manifest.name !== "dsh-wechat-remote" || manifest.version !== release.npmInstaller.version || !plugin) throw new Error("\u5907\u7528\u5B89\u88C5\u5305\u540D\u79F0\u3001\u7248\u672C\u6216\u5185\u7F6E\u63D2\u4EF6\u4E0D\u5339\u914D");
|
|
183
|
+
auditArchive(plugin, release);
|
|
184
|
+
return plugin;
|
|
185
|
+
}
|
|
186
|
+
async function downloadNpmRelease(release, fetcher = fetch, options = {}) {
|
|
187
|
+
if (!trustedReleaseAsset(release.asset, release.version) || !trustedNpmInstaller(release.npmInstaller)) throw new Error("\u5907\u7528\u5B89\u88C5\u5305\u6765\u6E90\u4E0D\u53D7\u4FE1\u4EFB");
|
|
188
|
+
const source = release.npmInstaller;
|
|
189
|
+
const body = await fetchBytes(source.url, source.bytes, fetcher, ["registry.npmjs.org"], Date.now() + (options.timeoutMs ?? 6e4), options);
|
|
190
|
+
options.signal?.throwIfAborted();
|
|
191
|
+
return pluginFromInstaller(body, release);
|
|
192
|
+
}
|
|
193
|
+
async function downloadRelease(release, fetcher = fetch, options = {}) {
|
|
89
194
|
if (!trustedReleaseAsset(release.asset, release.version)) throw new Error("\u6682\u65E0\u53EF\u9A8C\u8BC1\u7684\u6B63\u5F0F\u66F4\u65B0\u5305");
|
|
90
|
-
|
|
195
|
+
if (release.npmInstaller !== void 0 && !trustedNpmInstaller(release.npmInstaller)) throw new Error("\u5907\u7528\u5B89\u88C5\u5305\u6765\u6E90\u4E0D\u53D7\u4FE1\u4EFB");
|
|
196
|
+
const deadline = Date.now() + (options.timeoutMs ?? 12e4);
|
|
197
|
+
let body;
|
|
198
|
+
try {
|
|
199
|
+
body = await fetchBytes(
|
|
200
|
+
release.asset.url,
|
|
201
|
+
release.asset.bytes,
|
|
202
|
+
fetcher,
|
|
203
|
+
githubHosts,
|
|
204
|
+
Math.min(deadline, Date.now() + 6e4),
|
|
205
|
+
options
|
|
206
|
+
);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
options.signal?.throwIfAborted();
|
|
209
|
+
if (!(error instanceof DownloadUnavailableError) || !release.npmInstaller) throw error;
|
|
210
|
+
const remaining = deadline - Date.now();
|
|
211
|
+
if (remaining <= 0) throw error;
|
|
212
|
+
return downloadNpmRelease(release, fetcher, { ...options, timeoutMs: Math.min(remaining, 6e4) });
|
|
213
|
+
}
|
|
91
214
|
auditArchive(body, release);
|
|
92
215
|
return body;
|
|
93
216
|
}
|
|
94
217
|
export {
|
|
218
|
+
DownloadUnavailableError,
|
|
95
219
|
auditArchive,
|
|
96
220
|
boundedFetch,
|
|
97
|
-
|
|
221
|
+
downloadNpmRelease,
|
|
222
|
+
downloadRelease,
|
|
223
|
+
pluginFromInstaller
|
|
98
224
|
};
|
package/lib/update-policy.js
CHANGED
|
@@ -35,13 +35,20 @@ function trustedReleaseAsset(asset, version) {
|
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
function trustedNpmInstaller(source) {
|
|
39
|
+
if (!source || !validVersion(source.version) || !/^[a-f0-9]{64}$/.test(source.sha256) || !Number.isSafeInteger(source.bytes) || source.bytes < 1 || source.bytes > 32 * 1024 * 1024) return false;
|
|
40
|
+
return source.url === `https://registry.npmjs.org/dsh-wechat-remote/-/dsh-wechat-remote-${source.version}.tgz`;
|
|
41
|
+
}
|
|
38
42
|
function validateCatalog(value) {
|
|
39
43
|
const c = value;
|
|
40
44
|
const strings = (v, max = 100) => Array.isArray(v) && v.length <= max && v.every((s) => typeof s === "string" && s.length > 0 && s.length <= 100);
|
|
41
45
|
if (!c || c.schemaVersion !== 1 || typeof c.revision !== "string" || !/^[\w.-]{1,80}$/.test(c.revision) || !Number.isSafeInteger(c.issuedAt) || !Number.isSafeInteger(c.expiresAt) || c.expiresAt <= c.issuedAt || c.expiresAt - c.issuedAt > 32 * 864e5 || !Array.isArray(c.releases) || c.releases.length > 100 || !Array.isArray(c.blocked) || c.blocked.length > 100 || !strings(c.retiredDsh)) throw new Error("Invalid update catalog");
|
|
42
46
|
const versions = /* @__PURE__ */ new Set();
|
|
43
47
|
for (const r of c.releases) {
|
|
44
|
-
if (!r || !validVersion(r.version) || versions.has(r.version) || !["stable", "preview"].includes(r.channel) || r.channel === "stable" && versionPattern.exec(r.version)[4] || !strings(r.dsh) || !r.dsh.
|
|
48
|
+
if (!r || !validVersion(r.version) || versions.has(r.version) || !["stable", "preview"].includes(r.channel) || r.channel === "stable" && versionPattern.exec(r.version)[4] || !strings(r.dsh) || !r.dsh.every(validVersion) || !strings(r.platforms, 3) || !r.platforms.every((p) => ["windows", "macos", "linux"].includes(p)) || !strings(r.architectures, 32) || !r.architectures.every((a) => /^[a-z0-9_-]{1,32}$/.test(a)) || r.asset && !trustedReleaseAsset(r.asset, r.version) || r.npmInstaller !== void 0 && (!r.asset || !trustedNpmInstaller(r.npmInstaller))) throw new Error("Invalid release entry");
|
|
49
|
+
const hasEvidence = r.dsh.length > 0 && r.platforms.length > 0 && r.architectures.length > 0;
|
|
50
|
+
const pendingPreview = r.channel === "preview" && Boolean(versionPattern.exec(r.version)[4]) && r.dsh.length === 0 && r.platforms.length === 0 && r.architectures.length === 0;
|
|
51
|
+
if (!hasEvidence && !pendingPreview) throw new Error("Invalid release evidence");
|
|
45
52
|
versions.add(r.version);
|
|
46
53
|
if (r.targets !== void 0) {
|
|
47
54
|
if (!Array.isArray(r.targets) || !r.targets.length || r.targets.length > 12) throw new Error("Invalid release targets");
|
|
@@ -141,6 +148,7 @@ export {
|
|
|
141
148
|
assessUpdate,
|
|
142
149
|
compareVersions,
|
|
143
150
|
releaseMatches,
|
|
151
|
+
trustedNpmInstaller,
|
|
144
152
|
trustedReleaseAsset,
|
|
145
153
|
validateCatalog
|
|
146
154
|
};
|