@zhangferry-dev/tokendash 1.6.2 → 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.
@@ -2259,6 +2259,32 @@ var QuotaService = class {
2259
2259
  }
2260
2260
  return this.fetchAll();
2261
2261
  }
2262
+ /**
2263
+ * Validate a credential without caching it or writing it to disk. This keeps
2264
+ * the settings form transactional: only credentials accepted upstream are
2265
+ * persisted by the native app.
2266
+ */
2267
+ async validateCredential(provider, credential) {
2268
+ const adapter = this.registry.get(provider);
2269
+ if (!adapter) {
2270
+ return {
2271
+ provider,
2272
+ valid: false,
2273
+ status: { state: "not_configured", message: "Unsupported provider" }
2274
+ };
2275
+ }
2276
+ try {
2277
+ const snapshot = await withTimeout(
2278
+ adapter.fetch({ credential }),
2279
+ this.fetchTimeoutMs,
2280
+ provider
2281
+ );
2282
+ const validated = validateQuotaSnapshot(snapshot);
2283
+ return { provider, valid: validated.status.state === "ok", status: validated.status };
2284
+ } catch (err) {
2285
+ return { provider, valid: false, status: statusForError(err, this.fetchTimeoutMs) };
2286
+ }
2287
+ }
2262
2288
  async fetchWithTimeout(adapter) {
2263
2289
  try {
2264
2290
  const snapshot = await withTimeout(adapter.fetch(), this.fetchTimeoutMs, adapter.provider);
@@ -2270,14 +2296,7 @@ var QuotaService = class {
2270
2296
  }
2271
2297
  }
2272
2298
  handleFailure(adapter, err) {
2273
- let status;
2274
- if (err instanceof QuotaError) {
2275
- status = err.status;
2276
- } else if (err instanceof TimeoutError) {
2277
- status = { state: "timed_out", message: `upstream did not respond within ${this.fetchTimeoutMs}ms` };
2278
- } else {
2279
- status = { state: "error", message: redact(err), category: "unexpected" };
2280
- }
2299
+ const status = statusForError(err, this.fetchTimeoutMs);
2281
2300
  const stale = this.cache.getStale(adapter.provider);
2282
2301
  if (stale) {
2283
2302
  return { ...stale, freshness: "stale", status };
@@ -2292,6 +2311,13 @@ var QuotaService = class {
2292
2311
  };
2293
2312
  }
2294
2313
  };
2314
+ function statusForError(err, timeoutMs) {
2315
+ if (err instanceof QuotaError) return err.status;
2316
+ if (err instanceof TimeoutError) {
2317
+ return { state: "timed_out", message: `upstream did not respond within ${timeoutMs}ms` };
2318
+ }
2319
+ return { state: "error", message: redact(err), category: "unexpected" };
2320
+ }
2295
2321
  var TimeoutError = class extends Error {
2296
2322
  constructor(provider) {
2297
2323
  super(`quota fetch timed out: ${provider}`);
@@ -2416,8 +2442,7 @@ var codexAdapter = {
2416
2442
  provider: "codex",
2417
2443
  displayName: "OpenAI Codex",
2418
2444
  async isConfigured() {
2419
- if ((0, import_node_fs8.existsSync)((0, import_node_path8.join)(codexHome(), "auth.json"))) return true;
2420
- return await codexBinaryAvailable();
2445
+ return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(codexHome(), "auth.json")) && resolveCodexBinary() !== null;
2421
2446
  },
2422
2447
  async fetch() {
2423
2448
  const result = await queryRateLimits();
@@ -2460,10 +2485,16 @@ function capitalize(s) {
2460
2485
  return s.charAt(0).toUpperCase() + s.slice(1);
2461
2486
  }
2462
2487
  async function queryRateLimits() {
2463
- if (!await codexBinaryAvailable()) {
2464
- throw new QuotaError({ state: "not_configured", message: "codex CLI not found on PATH" });
2465
- }
2466
- const proc = (0, import_node_child_process2.spawn)("codex", ["app-server"], { stdio: ["pipe", "pipe", "pipe"] });
2488
+ const codexBinary = resolveCodexBinary();
2489
+ if (!codexBinary) {
2490
+ throw new QuotaError({ state: "not_configured", message: "official Codex CLI not found" });
2491
+ }
2492
+ const binaryDir = (0, import_node_path8.dirname)(codexBinary);
2493
+ const childPath = [binaryDir, process.env.PATH].filter(Boolean).join(":");
2494
+ const proc = (0, import_node_child_process2.spawn)(codexBinary, ["app-server"], {
2495
+ stdio: ["pipe", "pipe", "pipe"],
2496
+ env: { ...process.env, PATH: childPath }
2497
+ });
2467
2498
  const client = new JsonRpcClient(proc);
2468
2499
  try {
2469
2500
  await client.request("initialize", {
@@ -2552,24 +2583,44 @@ var JsonRpcClient = class {
2552
2583
  this.pending.clear();
2553
2584
  }
2554
2585
  };
2555
- var cachedCodexPath;
2556
- async function codexBinaryAvailable() {
2557
- if (cachedCodexPath !== void 0) return cachedCodexPath !== null;
2558
- return new Promise((resolve2) => {
2559
- const proc = (0, import_node_child_process2.spawn)("which", ["codex"], { stdio: ["ignore", "pipe", "ignore"] });
2560
- let out = "";
2561
- proc.stdout.on("data", (c) => {
2562
- out += c;
2563
- });
2564
- proc.on("close", () => {
2565
- cachedCodexPath = out.trim() || null;
2566
- resolve2(cachedCodexPath !== null);
2567
- });
2568
- proc.on("error", () => {
2569
- cachedCodexPath = null;
2570
- resolve2(false);
2571
- });
2572
- });
2586
+ function resolveCodexBinary(options = {}) {
2587
+ const home = options.home ?? (0, import_node_os8.homedir)();
2588
+ const path = options.path ?? process.env.PATH ?? "";
2589
+ const explicitBinary = options.explicitBinary ?? process.env.CODEX_BIN;
2590
+ const isExecutable = options.isExecutable ?? defaultIsExecutable;
2591
+ const nvmRoot = (0, import_node_path8.join)(home, ".nvm", "versions", "node");
2592
+ const nvmVersions = options.nvmVersions ?? readDirectoryNames(nvmRoot);
2593
+ const candidates = [
2594
+ explicitBinary,
2595
+ "/Applications/Codex.app/Contents/Resources/codex",
2596
+ (0, import_node_path8.join)(home, "Applications", "Codex.app", "Contents", "Resources", "codex"),
2597
+ "/opt/homebrew/bin/codex",
2598
+ "/usr/local/bin/codex",
2599
+ (0, import_node_path8.join)(home, ".local", "bin", "codex"),
2600
+ (0, import_node_path8.join)(home, ".volta", "bin", "codex"),
2601
+ (0, import_node_path8.join)(home, ".bun", "bin", "codex"),
2602
+ ...nvmVersions.sort((a, b) => b.localeCompare(a, void 0, { numeric: true })).map((version) => (0, import_node_path8.join)(nvmRoot, version, "bin", "codex")),
2603
+ ...path.split(":").filter(Boolean).map((directory) => (0, import_node_path8.join)(directory, "codex"))
2604
+ ];
2605
+ for (const candidate of candidates) {
2606
+ if (candidate && isExecutable(candidate)) return candidate;
2607
+ }
2608
+ return null;
2609
+ }
2610
+ function defaultIsExecutable(candidate) {
2611
+ try {
2612
+ (0, import_node_fs8.accessSync)(candidate, import_node_fs8.constants.X_OK);
2613
+ return true;
2614
+ } catch {
2615
+ return false;
2616
+ }
2617
+ }
2618
+ function readDirectoryNames(directory) {
2619
+ try {
2620
+ return (0, import_node_fs8.readdirSync)(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2621
+ } catch {
2622
+ return [];
2623
+ }
2573
2624
  }
2574
2625
 
2575
2626
  // src/server/quota/adapters/claude.ts
@@ -2577,6 +2628,7 @@ var import_node_fs9 = require("node:fs");
2577
2628
  var import_node_path9 = require("node:path");
2578
2629
  var import_node_os9 = require("node:os");
2579
2630
  var import_node_child_process3 = require("node:child_process");
2631
+ var import_node_crypto = require("node:crypto");
2580
2632
  var claudeAdapter = {
2581
2633
  provider: "claude",
2582
2634
  displayName: "Claude Code",
@@ -2645,7 +2697,7 @@ function readClaudeToken() {
2645
2697
  if ((0, import_node_fs9.existsSync)(credPath)) {
2646
2698
  try {
2647
2699
  const parsed = JSON.parse((0, import_node_fs9.readFileSync)(credPath, "utf8"));
2648
- return parsed?.claudeAiOauth?.accessToken ?? null;
2700
+ return extractClaudeAccessToken(parsed);
2649
2701
  } catch {
2650
2702
  return null;
2651
2703
  }
@@ -2653,33 +2705,55 @@ function readClaudeToken() {
2653
2705
  return null;
2654
2706
  }
2655
2707
  function readFromKeychain() {
2656
- const candidates = ["Claude Code-credentials"];
2708
+ const candidates = claudeKeychainServiceNames(process.env.CLAUDE_CONFIG_DIR);
2657
2709
  try {
2658
2710
  const list = (0, import_node_child_process3.execFileSync)("security", ["dump-keychain"], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" });
2659
- for (const m of list.matchAll(/"srvname"<blob>="([^"]*Claude Code-credentials[^"]*)"/g)) {
2711
+ for (const m of list.matchAll(/"svce"<blob>="([^"]*Claude Code-credentials[^"]*)"/g)) {
2660
2712
  if (m[1] && !candidates.includes(m[1])) candidates.push(m[1]);
2661
2713
  }
2662
2714
  } catch {
2663
2715
  }
2716
+ const accounts = [safeUsername(), void 0];
2664
2717
  for (const name of candidates) {
2665
- try {
2666
- const raw = (0, import_node_child_process3.execFileSync)("security", ["find-generic-password", "-s", name, "-w"], {
2667
- stdio: ["ignore", "pipe", "ignore"],
2668
- encoding: "utf8"
2669
- }).trim();
2670
- if (!raw) continue;
2718
+ for (const account of accounts) {
2671
2719
  try {
2672
- const parsed = JSON.parse(raw);
2673
- return parsed?.claudeAiOauth?.accessToken ?? null;
2720
+ const args = ["find-generic-password", "-s", name];
2721
+ if (account) args.push("-a", account);
2722
+ args.push("-w");
2723
+ const raw = (0, import_node_child_process3.execFileSync)("/usr/bin/security", args, {
2724
+ stdio: ["ignore", "pipe", "ignore"],
2725
+ encoding: "utf8",
2726
+ timeout: 2e3
2727
+ }).trim();
2728
+ if (!raw) continue;
2729
+ const token = extractClaudeAccessToken(JSON.parse(raw));
2730
+ if (token) return token;
2674
2731
  } catch {
2675
- return raw;
2732
+ continue;
2676
2733
  }
2677
- } catch {
2678
- continue;
2679
2734
  }
2680
2735
  }
2681
2736
  return null;
2682
2737
  }
2738
+ function claudeKeychainServiceNames(configDir) {
2739
+ if (!configDir) return ["Claude Code-credentials"];
2740
+ const hash = (0, import_node_crypto.createHash)("sha256").update(configDir).digest("hex").slice(0, 8);
2741
+ return [`Claude Code-credentials-${hash}`, "Claude Code-credentials"];
2742
+ }
2743
+ function extractClaudeAccessToken(value) {
2744
+ if (!value || typeof value !== "object") return null;
2745
+ const root = value;
2746
+ const nested = root.claudeAiOauth;
2747
+ const credentials = nested && typeof nested === "object" ? nested : root;
2748
+ return typeof credentials.accessToken === "string" && credentials.accessToken ? credentials.accessToken : null;
2749
+ }
2750
+ function safeUsername() {
2751
+ try {
2752
+ return (0, import_node_os9.userInfo)().username?.trim() || void 0;
2753
+ } catch {
2754
+ return void 0;
2755
+ }
2756
+ }
2683
2757
 
2684
2758
  // src/server/quota/adapters/glm.ts
2685
2759
  var import_node_fs11 = require("node:fs");
@@ -2715,8 +2789,8 @@ var glmAdapter = {
2715
2789
  async isConfigured() {
2716
2790
  return !!resolveCredential();
2717
2791
  },
2718
- async fetch() {
2719
- const cred = resolveCredential();
2792
+ async fetch(options) {
2793
+ const cred = resolveCredential(options?.credential);
2720
2794
  if (!cred) {
2721
2795
  throw new QuotaError({ state: "not_configured", message: "set ZAI_API_KEY or ZHIPU_API_KEY" });
2722
2796
  }
@@ -2772,7 +2846,13 @@ function classifyFetchError2(err) {
2772
2846
  const msg = err instanceof Error ? err.message : String(err);
2773
2847
  return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2774
2848
  }
2775
- function resolveCredential() {
2849
+ function resolveCredential(proposed) {
2850
+ if (proposed?.apiKey) {
2851
+ return {
2852
+ key: proposed.apiKey,
2853
+ base: proposed.baseUrl || "https://open.bigmodel.cn"
2854
+ };
2855
+ }
2776
2856
  const stored = readStoredCredential("glm");
2777
2857
  if (stored) return { key: stored.apiKey, base: stored.baseUrl || "https://open.bigmodel.cn" };
2778
2858
  const zai = envOrConfig("ZAI_API_KEY");
@@ -2827,8 +2907,8 @@ var minimaxAdapter = {
2827
2907
  async isConfigured() {
2828
2908
  return !!resolveCredential2();
2829
2909
  },
2830
- async fetch() {
2831
- const cred = resolveCredential2();
2910
+ async fetch(options) {
2911
+ const cred = resolveCredential2(options?.credential);
2832
2912
  if (!cred) {
2833
2913
  throw new QuotaError({ state: "not_configured", message: "set MINIMAX_API_KEY (Subscription Key)" });
2834
2914
  }
@@ -2887,7 +2967,12 @@ function classifyFetchError3(err) {
2887
2967
  const msg = err instanceof Error ? err.message : String(err);
2888
2968
  return new QuotaError({ state: "upstream_unavailable", message: msg.slice(0, 200) });
2889
2969
  }
2890
- function resolveCredential2() {
2970
+ function resolveCredential2(proposed) {
2971
+ if (proposed?.apiKey) {
2972
+ const region2 = (process.env.MINIMAX_REGION || "").toLowerCase();
2973
+ const base2 = proposed.baseUrl || (region2 === "cn" ? "https://www.minimaxi.com" : "https://www.minimax.io");
2974
+ return { key: proposed.apiKey, base: base2 };
2975
+ }
2891
2976
  const stored = readStoredCredential("minimax");
2892
2977
  if (stored) {
2893
2978
  const region2 = (process.env.MINIMAX_REGION || "").toLowerCase();
@@ -2915,9 +3000,9 @@ var kimiAdapter = {
2915
3000
  const cred = readCredentials();
2916
3001
  return !!cred && !!cred.access_token;
2917
3002
  },
2918
- async fetch() {
3003
+ async fetch(options) {
2919
3004
  const credPath = credentialsPath();
2920
- let cred = readCredentials();
3005
+ let cred = options?.credential?.apiKey ? { access_token: options.credential.apiKey, token_type: "Bearer" } : readCredentials();
2921
3006
  if (!cred || !cred.access_token) {
2922
3007
  throw new QuotaError({ state: "not_configured", message: "run `kimi` to log in first" });
2923
3008
  }
@@ -3069,6 +3154,24 @@ async function getQuota(_req, res) {
3069
3154
  res.status(500).json({ error: "Failed to fetch quota", hint: message });
3070
3155
  }
3071
3156
  }
3157
+ var editableQuotaProviders = /* @__PURE__ */ new Set(["glm", "kimi", "minimax"]);
3158
+ async function validateQuotaCredential(req, res) {
3159
+ const provider = req.body?.provider;
3160
+ const apiKey = typeof req.body?.apiKey === "string" ? req.body.apiKey.trim() : "";
3161
+ const baseUrl = typeof req.body?.baseUrl === "string" ? req.body.baseUrl.trim() : void 0;
3162
+ if (!editableQuotaProviders.has(provider) || !apiKey) {
3163
+ res.status(400).json({
3164
+ error: "Invalid credential request",
3165
+ hint: "A supported provider and non-empty token are required."
3166
+ });
3167
+ return;
3168
+ }
3169
+ const result = await quotaService.validateCredential(provider, {
3170
+ apiKey,
3171
+ baseUrl: baseUrl || void 0
3172
+ });
3173
+ res.status(result.valid ? 200 : 422).json(result);
3174
+ }
3072
3175
  function getAgents(_req, res) {
3073
3176
  try {
3074
3177
  const agents = detectAvailableAgents();
@@ -3102,6 +3205,7 @@ function registerApiRoutes(router, appInfo) {
3102
3205
  router.get("/blocks", getBlocks);
3103
3206
  router.get("/analytics", getAnalytics);
3104
3207
  router.get("/quota", getQuota);
3208
+ router.post("/quota/validate", validateQuotaCredential);
3105
3209
  }
3106
3210
 
3107
3211
  // src/server/index.ts
@@ -3235,6 +3339,7 @@ function resolveStaticAssetBaseDir(moduleUrl = __esbuild_import_meta_url, baseDi
3235
3339
  function createApp(_port, baseDir) {
3236
3340
  const app = (0, import_express.default)();
3237
3341
  const router = import_express.default.Router();
3342
+ app.use(import_express.default.json({ limit: "16kb" }));
3238
3343
  registerApiRoutes(router, {
3239
3344
  packageName: PACKAGE_NAME,
3240
3345
  version: getPackageVersion(),