dsh-cliproxyapi-pro 1.6.0 → 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 +88 -58
- package/README.en.md +52 -68
- package/README.md +53 -67
- package/lib/client.js +2732 -618
- package/lib/index.js +146 -173
- package/lib/quota.js +1646 -0
- package/lib/updater.js +84 -20
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +9 -4
package/lib/updater.js
CHANGED
|
@@ -19,12 +19,45 @@ const BOOLEAN_SETTINGS = ["autoCheck", "autoDownload", "autoInstall"];
|
|
|
19
19
|
const MAX_METADATA_BYTES = 512 * 1024;
|
|
20
20
|
const MAX_PACKAGE_BYTES = 8 * 1024 * 1024;
|
|
21
21
|
const MAX_UNPACKED_BYTES = 16 * 1024 * 1024;
|
|
22
|
-
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"];
|
|
23
29
|
|
|
24
30
|
export class UpdateError extends Error {
|
|
25
|
-
|
|
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;
|
|
26
60
|
}
|
|
27
|
-
const fail = (code) => { throw new UpdateError(code); };
|
|
28
61
|
|
|
29
62
|
export function normalizeUpdateSettings(raw = {}) {
|
|
30
63
|
return {
|
|
@@ -173,53 +206,82 @@ async function fetchBytes(fetcher, url, signal, limit, download = null) {
|
|
|
173
206
|
}
|
|
174
207
|
}
|
|
175
208
|
|
|
176
|
-
/**
|
|
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
|
+
*/
|
|
177
216
|
export function verifyUpdatePackage(bytes, expected) {
|
|
178
217
|
const digest = createHash("sha512").update(bytes).digest();
|
|
179
218
|
if (!timingSafeEqual(digest, integrityDigest(expected.integrity))) fail("integrity-error");
|
|
180
219
|
let tar;
|
|
181
220
|
try { tar = gunzipSync(bytes, { maxOutputLength: MAX_UNPACKED_BYTES }); }
|
|
182
|
-
catch { fail("invalid-package"); }
|
|
221
|
+
catch { fail("invalid-package", "not a gzip archive within the size limit"); }
|
|
183
222
|
const seen = new Set();
|
|
223
|
+
const files = [];
|
|
224
|
+
const directories = [];
|
|
184
225
|
let manifest;
|
|
185
226
|
let ended = false;
|
|
186
227
|
const text = (buffer) => buffer.toString("utf8").replace(/\0.*$/s, "");
|
|
187
228
|
for (let offset = 0; offset + 512 <= tar.length;) {
|
|
188
229
|
const header = tar.subarray(offset, offset + 512);
|
|
189
230
|
if (header.every((b) => b === 0)) {
|
|
190
|
-
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");
|
|
191
232
|
ended = true;
|
|
192
233
|
break;
|
|
193
234
|
}
|
|
194
235
|
const octal = (buffer) => {
|
|
195
236
|
const value = text(buffer).trim();
|
|
196
|
-
if (!/^[0-7]+$/.test(value)) fail("invalid-package");
|
|
237
|
+
if (!/^[0-7]+$/.test(value)) fail("invalid-package", "malformed tar header");
|
|
197
238
|
return parseInt(value, 8);
|
|
198
239
|
};
|
|
199
240
|
const size = octal(header.subarray(124, 136));
|
|
200
241
|
const checksum = octal(header.subarray(148, 156));
|
|
201
242
|
let sum = 0;
|
|
202
243
|
for (let i = 0; i < 512; i++) sum += i >= 148 && i < 156 ? 32 : header[i];
|
|
203
|
-
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");
|
|
204
245
|
const prefix = text(header.subarray(345, 500));
|
|
205
246
|
const path = `${prefix ? `${prefix}/` : ""}${text(header.subarray(0, 100))}`;
|
|
206
|
-
if (!path.startsWith("package/") || path.includes("\\") || path.split("/").some((p) => p === ".." || p === ".") || seen.has(path)) fail("invalid-package");
|
|
207
|
-
seen.add(path);
|
|
208
247
|
const type = header[156];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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") {
|
|
213
264
|
try { manifest = JSON.parse(tar.subarray(offset + 512, offset + 512 + size).toString("utf8")); }
|
|
214
|
-
catch { fail("invalid-package"); }
|
|
265
|
+
catch { fail("invalid-package", "package.json is not valid JSON"); }
|
|
215
266
|
}
|
|
216
|
-
} else fail("invalid-package");
|
|
267
|
+
} else fail("invalid-package", `link or special entry ${printable(path)}`);
|
|
217
268
|
offset += 512 + Math.ceil(size / 512) * 512;
|
|
218
269
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
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)}`);
|
|
223
285
|
return manifest;
|
|
224
286
|
}
|
|
225
287
|
|
|
@@ -255,6 +317,8 @@ export function createPluginUpdater(deps) {
|
|
|
255
317
|
const code = error instanceof UpdateError ? error.code : "network-error";
|
|
256
318
|
state.error = code;
|
|
257
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}`);
|
|
258
322
|
};
|
|
259
323
|
|
|
260
324
|
const lookup = async (region, snapshot, signal) => {
|
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
|
}
|