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/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 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"];
22
29
 
23
30
  export class UpdateError extends Error {
24
- 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;
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
- ["autoCheck", "autoDownload"].some((key) => raw[key] !== undefined && typeof raw[key] !== "boolean") ||
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
- /** 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
+ */
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
- if (type === 53) {
207
- if (size !== 0 || !["package/", "package/lib/", "package/locale/"].includes(path)) fail("invalid-package");
208
- } else if ((type === 0 || type === 48) && PACKAGE_FILES.test(path.slice(8))) {
209
- 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") {
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
- const required = ["package.json", "lib/index.js", "lib/client.js", "cordis.patch.yml", "locale/en.json", "locale/zh.json"];
217
- 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");
218
- if (["dependencies", "optionalDependencies", "bundledDependencies", "bundleDependencies"].some((key) => manifest[key] && Object.keys(manifest[key]).length)) fail("invalid-package");
219
- 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)}`);
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 ["autoCheck", "autoDownload"]) if (Object.hasOwn(patch, key) && typeof patch[key] !== "boolean") fail("invalid-settings");
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
@@ -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.5.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
  }