dsh-cliproxyapi-pro 1.5.1 → 1.7.0
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/CHANGELOG.md +93 -51
- package/README.en.md +79 -80
- package/README.md +80 -79
- package/lib/client.js +3371 -653
- package/lib/index.js +170 -182
- package/lib/quota.js +1646 -0
- package/lib/updater.js +90 -23
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +9 -4
package/lib/updater.js
CHANGED
|
@@ -13,22 +13,58 @@ export const UPDATE_REGISTRIES = Object.freeze({
|
|
|
13
13
|
export const UPDATE_TAGS = Object.freeze({ stable: "latest", beta: "beta", dev: "dev" });
|
|
14
14
|
export const UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
15
15
|
export const DEFAULT_UPDATE_SETTINGS = Object.freeze({
|
|
16
|
-
autoCheck: true, autoDownload: false, channel: "stable", region: "auto",
|
|
16
|
+
autoCheck: true, autoDownload: false, autoInstall: false, channel: "stable", region: "auto",
|
|
17
17
|
});
|
|
18
|
+
const BOOLEAN_SETTINGS = ["autoCheck", "autoDownload", "autoInstall"];
|
|
18
19
|
const MAX_METADATA_BYTES = 512 * 1024;
|
|
19
20
|
const MAX_PACKAGE_BYTES = 8 * 1024 * 1024;
|
|
20
21
|
const MAX_UNPACKED_BYTES = 16 * 1024 * 1024;
|
|
21
|
-
const
|
|
22
|
+
const MAX_PACKAGE_ENTRIES = 1000;
|
|
23
|
+
const MAX_PATH_SEGMENTS = 6;
|
|
24
|
+
// Plain names only: no dotfiles, spaces, shell/glob characters or non-ASCII.
|
|
25
|
+
const SAFE_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,99}$/;
|
|
26
|
+
// npm packs these root files whether or not `files` lists them.
|
|
27
|
+
const ALWAYS_PACKED = /^(?:package\.json|(?:README|LICEN[CS]E|NOTICE)(?:\.[A-Za-z0-9_-]+)*)$/i;
|
|
28
|
+
const REQUIRED_FILES = ["package.json", "lib/index.js", "lib/client.js", "cordis.patch.yml", "locale/en.json", "locale/zh.json"];
|
|
22
29
|
|
|
23
30
|
export class UpdateError extends Error {
|
|
24
|
-
|
|
31
|
+
/** `detail` is a short diagnostic for logs only; the UI shows `code`. */
|
|
32
|
+
constructor(code, detail) { super(code); this.code = code; if (detail) this.detail = detail; }
|
|
33
|
+
}
|
|
34
|
+
const fail = (code, detail) => { throw new UpdateError(code, detail); };
|
|
35
|
+
const printable = (value) => String(value).replace(/[^\x20-\x7e]/g, "?").slice(0, 160);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compile the package's own `files` field (npm semantics: a matched directory
|
|
39
|
+
* includes its contents, `*` stays within a segment, `**` spans segments).
|
|
40
|
+
* Negations only remove files from what npm packs, so they are ignored here;
|
|
41
|
+
* absolute or traversing patterns never match.
|
|
42
|
+
*/
|
|
43
|
+
export function packageFilePatterns(files) {
|
|
44
|
+
if (!Array.isArray(files) || files.length === 0 || files.length > 200) fail("invalid-package", "package.json has no usable \"files\" list");
|
|
45
|
+
const patterns = [];
|
|
46
|
+
for (const raw of files) {
|
|
47
|
+
if (typeof raw !== "string") fail("invalid-package", "package.json \"files\" contains a non-string entry");
|
|
48
|
+
const pattern = raw.trim().replace(/^\.\//, "").replace(/\/+$/, "");
|
|
49
|
+
if (!pattern || pattern.startsWith("!") || pattern.startsWith("/") || pattern.includes("\\") ||
|
|
50
|
+
pattern.split("/").some((part) => part === "" || part === "." || part === "..")) continue;
|
|
51
|
+
const parts = pattern.split("/");
|
|
52
|
+
const body = parts.map((part, i) => {
|
|
53
|
+
const last = i === parts.length - 1;
|
|
54
|
+
if (part === "**") return last ? ".*" : "(?:[^/]+/)*";
|
|
55
|
+
return part.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]") + (last ? "" : "/");
|
|
56
|
+
}).join("");
|
|
57
|
+
patterns.push(new RegExp(`^${body}(?:/.*)?$`));
|
|
58
|
+
}
|
|
59
|
+
return patterns;
|
|
25
60
|
}
|
|
26
|
-
const fail = (code) => { throw new UpdateError(code); };
|
|
27
61
|
|
|
28
62
|
export function normalizeUpdateSettings(raw = {}) {
|
|
29
63
|
return {
|
|
30
64
|
autoCheck: typeof raw?.autoCheck === "boolean" ? raw.autoCheck : true,
|
|
31
65
|
autoDownload: raw?.autoDownload === true,
|
|
66
|
+
// Consumed by the Web client, which delegates installation to dshmarket.
|
|
67
|
+
autoInstall: raw?.autoInstall === true,
|
|
32
68
|
channel: Object.hasOwn(UPDATE_TAGS, raw?.channel ?? "") ? raw.channel : "stable",
|
|
33
69
|
region: ["auto", "global", "cn"].includes(raw?.region) ? raw.region : "auto",
|
|
34
70
|
};
|
|
@@ -38,7 +74,7 @@ export function loadUpdateSettings(file) {
|
|
|
38
74
|
try {
|
|
39
75
|
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
40
76
|
if (!raw || typeof raw !== "object" || Array.isArray(raw) || raw.version !== 1 ||
|
|
41
|
-
|
|
77
|
+
BOOLEAN_SETTINGS.some((key) => raw[key] !== undefined && typeof raw[key] !== "boolean") ||
|
|
42
78
|
raw.channel !== undefined && !Object.hasOwn(UPDATE_TAGS, raw.channel) ||
|
|
43
79
|
raw.region !== undefined && !["auto", "global", "cn"].includes(raw.region)) fail("invalid-settings");
|
|
44
80
|
return normalizeUpdateSettings(raw);
|
|
@@ -170,53 +206,82 @@ async function fetchBytes(fetcher, url, signal, limit, download = null) {
|
|
|
170
206
|
}
|
|
171
207
|
}
|
|
172
208
|
|
|
173
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* Validate checksum and a restricted npm tar archive without extracting any file.
|
|
211
|
+
* Which files may appear is not hard-coded: every regular file must be declared
|
|
212
|
+
* by the package's own `files` field (or be one npm always packs), so a future
|
|
213
|
+
* release can ship new files without this version rejecting it. Structural
|
|
214
|
+
* safety rules (paths, entry types, sizes, dependencies, scripts) are fixed.
|
|
215
|
+
*/
|
|
174
216
|
export function verifyUpdatePackage(bytes, expected) {
|
|
175
217
|
const digest = createHash("sha512").update(bytes).digest();
|
|
176
218
|
if (!timingSafeEqual(digest, integrityDigest(expected.integrity))) fail("integrity-error");
|
|
177
219
|
let tar;
|
|
178
220
|
try { tar = gunzipSync(bytes, { maxOutputLength: MAX_UNPACKED_BYTES }); }
|
|
179
|
-
catch { fail("invalid-package"); }
|
|
221
|
+
catch { fail("invalid-package", "not a gzip archive within the size limit"); }
|
|
180
222
|
const seen = new Set();
|
|
223
|
+
const files = [];
|
|
224
|
+
const directories = [];
|
|
181
225
|
let manifest;
|
|
182
226
|
let ended = false;
|
|
183
227
|
const text = (buffer) => buffer.toString("utf8").replace(/\0.*$/s, "");
|
|
184
228
|
for (let offset = 0; offset + 512 <= tar.length;) {
|
|
185
229
|
const header = tar.subarray(offset, offset + 512);
|
|
186
230
|
if (header.every((b) => b === 0)) {
|
|
187
|
-
if (tar.subarray(offset).some((b) => b !== 0)) fail("invalid-package");
|
|
231
|
+
if (tar.subarray(offset).some((b) => b !== 0)) fail("invalid-package", "data after the tar terminator");
|
|
188
232
|
ended = true;
|
|
189
233
|
break;
|
|
190
234
|
}
|
|
191
235
|
const octal = (buffer) => {
|
|
192
236
|
const value = text(buffer).trim();
|
|
193
|
-
if (!/^[0-7]+$/.test(value)) fail("invalid-package");
|
|
237
|
+
if (!/^[0-7]+$/.test(value)) fail("invalid-package", "malformed tar header");
|
|
194
238
|
return parseInt(value, 8);
|
|
195
239
|
};
|
|
196
240
|
const size = octal(header.subarray(124, 136));
|
|
197
241
|
const checksum = octal(header.subarray(148, 156));
|
|
198
242
|
let sum = 0;
|
|
199
243
|
for (let i = 0; i < 512; i++) sum += i >= 148 && i < 156 ? 32 : header[i];
|
|
200
|
-
if (sum !== checksum || size > MAX_UNPACKED_BYTES || offset + 512 + size > tar.length) fail("invalid-package");
|
|
244
|
+
if (sum !== checksum || size > MAX_UNPACKED_BYTES || offset + 512 + size > tar.length) fail("invalid-package", "corrupt tar entry");
|
|
201
245
|
const prefix = text(header.subarray(345, 500));
|
|
202
246
|
const path = `${prefix ? `${prefix}/` : ""}${text(header.subarray(0, 100))}`;
|
|
203
|
-
if (!path.startsWith("package/") || path.includes("\\") || path.split("/").some((p) => p === ".." || p === ".") || seen.has(path)) fail("invalid-package");
|
|
204
|
-
seen.add(path);
|
|
205
247
|
const type = header[156];
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
248
|
+
const directory = type === 53;
|
|
249
|
+
let relative = path.startsWith("package/") ? path.slice(8) : null;
|
|
250
|
+
if (directory && relative?.endsWith("/")) relative = relative.slice(0, -1);
|
|
251
|
+
const parts = relative?.split("/") ?? [];
|
|
252
|
+
if (relative === null || (relative !== "" || !directory) && (parts.length > MAX_PATH_SEGMENTS ||
|
|
253
|
+
parts.some((part) => !SAFE_SEGMENT.test(part) || part === "node_modules")) || seen.has(path)) {
|
|
254
|
+
fail("invalid-package", `unsafe path ${printable(path)}`);
|
|
255
|
+
}
|
|
256
|
+
seen.add(path);
|
|
257
|
+
if (seen.size > MAX_PACKAGE_ENTRIES) fail("invalid-package", "too many entries");
|
|
258
|
+
if (directory) {
|
|
259
|
+
if (size !== 0) fail("invalid-package", `non-empty directory entry ${printable(path)}`);
|
|
260
|
+
directories.push(relative);
|
|
261
|
+
} else if (type === 0 || type === 48) {
|
|
262
|
+
files.push(relative);
|
|
263
|
+
if (relative === "package.json") {
|
|
210
264
|
try { manifest = JSON.parse(tar.subarray(offset + 512, offset + 512 + size).toString("utf8")); }
|
|
211
|
-
catch { fail("invalid-package"); }
|
|
265
|
+
catch { fail("invalid-package", "package.json is not valid JSON"); }
|
|
212
266
|
}
|
|
213
|
-
} else fail("invalid-package");
|
|
267
|
+
} else fail("invalid-package", `link or special entry ${printable(path)}`);
|
|
214
268
|
offset += 512 + Math.ceil(size / 512) * 512;
|
|
215
269
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
if (
|
|
270
|
+
if (!ended) fail("invalid-package", "missing tar terminator");
|
|
271
|
+
const missing = REQUIRED_FILES.find((p) => !files.includes(p));
|
|
272
|
+
if (missing) fail("invalid-package", `missing ${missing}`);
|
|
273
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest) || manifest.name !== UPDATE_PACKAGE ||
|
|
274
|
+
manifest.version !== expected.version || manifest.dsh?.bundle?.patch !== "./cordis.patch.yml") {
|
|
275
|
+
fail("invalid-package", "package.json does not describe this plugin version");
|
|
276
|
+
}
|
|
277
|
+
if (["dependencies", "optionalDependencies", "bundledDependencies", "bundleDependencies"].some((key) => manifest[key] && Object.keys(manifest[key]).length)) fail("invalid-package", "declares dependencies");
|
|
278
|
+
const script = Object.keys(manifest.scripts ?? {}).find((key) => /^(?:preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack)$/.test(key));
|
|
279
|
+
if (script) fail("invalid-package", `declares lifecycle script ${printable(script)}`);
|
|
280
|
+
const patterns = packageFilePatterns(manifest.files);
|
|
281
|
+
const undeclared = files.find((file) => !(!file.includes("/") && ALWAYS_PACKED.test(file)) && !patterns.some((re) => re.test(file)));
|
|
282
|
+
if (undeclared) fail("invalid-package", `file not declared in package.json "files": ${printable(undeclared)}`);
|
|
283
|
+
const stray = directories.find((dir) => dir !== "" && !files.some((file) => file.startsWith(`${dir}/`)));
|
|
284
|
+
if (stray !== undefined) fail("invalid-package", `directory without files ${printable(stray)}`);
|
|
220
285
|
return manifest;
|
|
221
286
|
}
|
|
222
287
|
|
|
@@ -252,6 +317,8 @@ export function createPluginUpdater(deps) {
|
|
|
252
317
|
const code = error instanceof UpdateError ? error.code : "network-error";
|
|
253
318
|
state.error = code;
|
|
254
319
|
state.errorCode = code;
|
|
320
|
+
// The page shows only the localized code; the concrete reason goes to the Host log.
|
|
321
|
+
if (error instanceof UpdateError && error.detail) deps.logger?.warn?.(`[dsh-cliproxyapi] update ${code}: ${error.detail}`);
|
|
255
322
|
};
|
|
256
323
|
|
|
257
324
|
const lookup = async (region, snapshot, signal) => {
|
|
@@ -340,7 +407,7 @@ export function createPluginUpdater(deps) {
|
|
|
340
407
|
if (!patch || typeof patch !== "object" || Array.isArray(patch)) fail("invalid-settings");
|
|
341
408
|
if (Object.keys(patch).some((key) => !Object.hasOwn(DEFAULT_UPDATE_SETTINGS, key))) fail("invalid-settings");
|
|
342
409
|
if (Object.hasOwn(patch, "channel") && !Object.hasOwn(UPDATE_TAGS, patch.channel) || Object.hasOwn(patch, "region") && !["auto", "global", "cn"].includes(patch.region)) fail("invalid-settings");
|
|
343
|
-
for (const key of
|
|
410
|
+
for (const key of BOOLEAN_SETTINGS) if (Object.hasOwn(patch, key) && typeof patch[key] !== "boolean") fail("invalid-settings");
|
|
344
411
|
const next = normalizeUpdateSettings({ ...settings, ...patch });
|
|
345
412
|
saving = true;
|
|
346
413
|
try {
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
|
-
"title": "CLIProxyAPI Provider",
|
|
4
|
-
"description": "Connect one or more CLIProxyAPI accounts, enable or disable each, synchronize their models and reasoning capabilities into DSH, and manage settings visually."
|
|
3
|
+
"title": "CLIProxyAPI (CPA) Provider",
|
|
4
|
+
"description": "Connect one or more CLIProxyAPI (CPA) accounts, enable or disable each, synchronize their models and reasoning capabilities into DSH, and manage settings visually."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cliproxyapi-pro",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Auto-discovery CLIProxyAPI provider and management UI for DeepSeek Harness — multiple accounts, dynamic model sync, capability detection, and visual settings.",
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"description": "Auto-discovery CLIProxyAPI (CPA) provider and management UI for DeepSeek Harness — multiple accounts, dynamic model sync, capability detection, and visual settings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"deepseek-harness",
|
|
8
8
|
"dsh-plugin",
|
|
9
9
|
"cliproxyapi",
|
|
10
|
+
"cli-proxy-api",
|
|
11
|
+
"cpa",
|
|
10
12
|
"llm-provider"
|
|
11
13
|
],
|
|
12
14
|
"author": "bowen (https://github.com/TinyBOOOOM)",
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
"lib/index.js",
|
|
39
41
|
"lib/client.js",
|
|
40
42
|
"lib/updater.js",
|
|
43
|
+
"lib/quota.js",
|
|
41
44
|
"locale/*.json",
|
|
42
45
|
"cordis.patch.yml",
|
|
43
46
|
"icon.svg",
|
|
@@ -59,13 +62,15 @@
|
|
|
59
62
|
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
60
63
|
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
61
64
|
"@deepseek-ai/dsh-client-ui-theme",
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-renderer"
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-model-selection",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
63
68
|
]
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
71
|
"scripts": {
|
|
67
72
|
"test": "node --test tests/*.test.mjs",
|
|
68
|
-
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/updater.js && node --test tests/*.test.mjs",
|
|
73
|
+
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/updater.js && node --check lib/quota.js && node --test tests/*.test.mjs",
|
|
69
74
|
"pack:check": "npm pack --dry-run"
|
|
70
75
|
}
|
|
71
76
|
}
|