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/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 PACKAGE_FILES = /^(?:package\.json|README(?:\.en)?\.md|CHANGELOG\.md|LICENSE|icon\.svg|cordis\.patch\.yml|lib\/[A-Za-z0-9_-]+\.js|locale\/(?:en|zh)\.json)$/;
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
- constructor(code) { super(code); this.code = code; }
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
- /** Validate checksum and a restricted npm tar archive without extracting any file. */
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
- if (type === 53) {
210
- if (size !== 0 || !["package/", "package/lib/", "package/locale/"].includes(path)) fail("invalid-package");
211
- } else if ((type === 0 || type === 48) && PACKAGE_FILES.test(path.slice(8))) {
212
- if (path === "package/package.json") {
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
- const required = ["package.json", "lib/index.js", "lib/client.js", "cordis.patch.yml", "locale/en.json", "locale/zh.json"];
220
- if (!ended || required.some((p) => !seen.has(`package/${p}`)) || manifest?.name !== UPDATE_PACKAGE || manifest?.version !== expected.version || manifest?.dsh?.bundle?.patch !== "./cordis.patch.yml") fail("invalid-package");
221
- if (["dependencies", "optionalDependencies", "bundledDependencies", "bundleDependencies"].some((key) => manifest[key] && Object.keys(manifest[key]).length)) fail("invalid-package");
222
- if (Object.keys(manifest.scripts ?? {}).some((key) => /^(?:preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack)$/.test(key))) fail("invalid-package");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "CLIProxyAPI 接入与同步",
4
- "description": "接入一个或多个 CLIProxyAPI 账号(可分别启用或停用),实时同步模型与推理能力,并提供可视化配置与模型过滤面板。"
3
+ "title": "CLIProxyAPI (CPA) 接入与同步",
4
+ "description": "接入一个或多个 CLIProxyAPI(CPA)账号(可分别启用或停用),实时同步模型与推理能力,并提供可视化配置与模型过滤面板。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "dsh-cliproxyapi-pro",
3
- "version": "1.6.0",
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
  }