@pikecode/api-key-manager 1.0.17 → 1.0.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikecode/api-key-manager",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "A CLI tool for managing and switching multiple API provider configurations",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -22,7 +22,7 @@ class CommandRegistry {
22
22
  return command;
23
23
  }
24
24
 
25
- throw new Error(`命令 '${name}' 未注册`);
25
+ throw new Error(`未知命令 '${name}'\n运行 'akm --help' 查看所有可用命令`);
26
26
  }
27
27
 
28
28
  // 执行命令
@@ -54,7 +54,7 @@ class ProviderLister {
54
54
  }
55
55
 
56
56
  if (provider.authToken) {
57
- console.log(chalk.gray(` Token: ${provider.authToken.substring(0, 10)}...`));
57
+ console.log(chalk.gray(` Token: ${provider.authToken}`));
58
58
  }
59
59
 
60
60
  if (provider.launchArgs && provider.launchArgs.length > 0) {
@@ -25,7 +25,7 @@ class EnvSwitcher extends BaseCommand {
25
25
  await this.configManager.load();
26
26
  const provider = this.configManager.getProvider(providerName);
27
27
  if (!provider) {
28
- throw new Error(`供应商 '${providerName}' 不存在`);
28
+ throw new Error(`供应商 '${providerName}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
29
29
  }
30
30
  return provider;
31
31
  }
package/src/config.js CHANGED
@@ -50,7 +50,23 @@ class ConfigManager {
50
50
  async _performLoad() {
51
51
  try {
52
52
  if (await fs.pathExists(this.configPath)) {
53
- const data = await fs.readJSON(this.configPath);
53
+ // 检查并修复文件权限(仅 Unix 系统)
54
+ await this._checkAndFixPermissions();
55
+
56
+ let data;
57
+ try {
58
+ data = await fs.readJSON(this.configPath);
59
+ } catch (jsonError) {
60
+ // 配置文件损坏,创建备份并重置
61
+ const backupPath = `${this.configPath}.corrupted.${Date.now()}`;
62
+ await fs.copy(this.configPath, backupPath);
63
+ console.log(chalk.yellow('⚠️ 配置文件损坏,已备份到:'), backupPath);
64
+
65
+ this.config = this.getDefaultConfig();
66
+ await this._performSave();
67
+ this.isLoaded = true;
68
+ return this.config;
69
+ }
54
70
 
55
71
  if (!data || typeof data !== 'object' || Array.isArray(data)) {
56
72
  // 配置文件被写成非对象内容时,重置为默认配置
@@ -84,6 +100,33 @@ class ConfigManager {
84
100
  }
85
101
  }
86
102
 
103
+ async _checkAndFixPermissions() {
104
+ // Windows 不支持 Unix 文件权限
105
+ if (process.platform === 'win32') {
106
+ return;
107
+ }
108
+
109
+ try {
110
+ const stat = await fs.stat(this.configPath);
111
+ const mode = stat.mode & 0o777;
112
+
113
+ // 检查权限是否为 600 (仅所有者可读写) 或 400 (仅所有者可读)
114
+ if (mode !== 0o600 && mode !== 0o400) {
115
+ console.log(chalk.yellow('⚠️ 配置文件权限不安全:'), mode.toString(8), chalk.gray('建议: 0600'));
116
+
117
+ try {
118
+ await fs.chmod(this.configPath, 0o600);
119
+ console.log(chalk.green('✅ 已自动修复文件权限为: 0600'));
120
+ } catch (chmodError) {
121
+ console.log(chalk.red('❌ 无法自动修复权限,请手动执行:'));
122
+ console.log(chalk.gray(` chmod 600 ${this.configPath}`));
123
+ }
124
+ }
125
+ } catch (error) {
126
+ // 忽略权限检查错误,不影响主流程
127
+ }
128
+ }
129
+
87
130
  _migrateAuthModes() {
88
131
  // 迁移旧配置以保持向后兼容
89
132
  if (this.config.providers) {
@@ -129,6 +172,12 @@ class ConfigManager {
129
172
  // 保存前确保迁移已应用
130
173
  this._migrateAuthModes();
131
174
  await fs.writeJSON(this.configPath, this.config, { spaces: 2 });
175
+
176
+ // 设置文件权限为 600 (仅所有者可读写)
177
+ if (process.platform !== 'win32') {
178
+ await fs.chmod(this.configPath, 0o600);
179
+ }
180
+
132
181
  // 更新最后修改时间
133
182
  const stat = await fs.stat(this.configPath);
134
183
  this.lastModified = stat.mtime;
@@ -185,9 +234,9 @@ class ConfigManager {
185
234
 
186
235
  async removeProvider(name) {
187
236
  await this.ensureLoaded();
188
-
237
+
189
238
  if (!this.config.providers[name]) {
190
- throw new Error(`供应商 '${name}' 不存在`);
239
+ throw new Error(`供应商 '${name}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
191
240
  }
192
241
 
193
242
  delete this.config.providers[name];
@@ -202,9 +251,9 @@ class ConfigManager {
202
251
 
203
252
  async setCurrentProvider(name) {
204
253
  await this.ensureLoaded();
205
-
254
+
206
255
  if (!this.config.providers[name]) {
207
- throw new Error(`供应商 '${name}' 不存在`);
256
+ throw new Error(`供应商 '${name}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
208
257
  }
209
258
 
210
259
  // 重置所有供应商的current状态
@@ -34,9 +34,14 @@ function openFileWithDefaultApp(filePath) {
34
34
 
35
35
  async function openAKMConfigFile() {
36
36
  if (!(await ensureConfigExists())) {
37
- throw new Error('未找到 ~/.akm-config.json,请先运行 akm add 创建配置');
37
+ throw new Error('未找到配置文件 ~/.akm-config.json\n请先运行 \'akm add\' 创建第一个供应商配置');
38
+ }
39
+
40
+ try {
41
+ await openFileWithDefaultApp(AKM_CONFIG_FILE);
42
+ } catch (error) {
43
+ throw new Error(`无法打开配置文件: ${error.message}\n配置文件位置: ${AKM_CONFIG_FILE}`);
38
44
  }
39
- await openFileWithDefaultApp(AKM_CONFIG_FILE);
40
45
  }
41
46
 
42
47
  module.exports = {
@@ -1,5 +1,26 @@
1
1
  const spawn = require('cross-spawn');
2
2
 
3
+ /**
4
+ * 清理环境变量值,移除危险字符
5
+ * @param {string} value - 要清理的值
6
+ * @returns {string} 清理后的值
7
+ */
8
+ function sanitizeEnvValue(value) {
9
+ if (typeof value !== 'string') {
10
+ throw new Error('环境变量值必须是字符串');
11
+ }
12
+
13
+ // 移除控制字符
14
+ let cleaned = value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
15
+
16
+ // 检测可能的 shell 命令注入
17
+ if (/[;&|`$()]/.test(cleaned)) {
18
+ throw new Error('环境变量值包含潜在不安全的字符');
19
+ }
20
+
21
+ return cleaned;
22
+ }
23
+
3
24
  function clearTerminal() {
4
25
  if (!process.stdout || typeof process.stdout.write !== 'function') {
5
26
  return;
@@ -24,33 +45,37 @@ function clearTerminal() {
24
45
  function buildEnvVariables(config) {
25
46
  const env = { ...process.env };
26
47
 
27
- // Claude Code 配置
28
- if (config.authMode === 'oauth_token') {
29
- env.CLAUDE_CODE_OAUTH_TOKEN = config.authToken;
30
- } else if (config.authMode === 'api_key') {
31
- env.ANTHROPIC_BASE_URL = config.baseUrl;
32
- // 根据 tokenType 选择设置哪种 token
33
- if (config.tokenType === 'auth_token') {
34
- env.ANTHROPIC_AUTH_TOKEN = config.authToken;
48
+ try {
49
+ // Claude Code 配置
50
+ if (config.authMode === 'oauth_token') {
51
+ env.CLAUDE_CODE_OAUTH_TOKEN = sanitizeEnvValue(config.authToken);
52
+ } else if (config.authMode === 'api_key') {
53
+ env.ANTHROPIC_BASE_URL = sanitizeEnvValue(config.baseUrl);
54
+ // 根据 tokenType 选择设置哪种 token
55
+ if (config.tokenType === 'auth_token') {
56
+ env.ANTHROPIC_AUTH_TOKEN = sanitizeEnvValue(config.authToken);
57
+ } else {
58
+ // 默认使用 ANTHROPIC_API_KEY
59
+ env.ANTHROPIC_API_KEY = sanitizeEnvValue(config.authToken);
60
+ }
35
61
  } else {
36
- // 默认使用 ANTHROPIC_API_KEY
37
- env.ANTHROPIC_API_KEY = config.authToken;
62
+ // auth_token 模式
63
+ env.ANTHROPIC_BASE_URL = sanitizeEnvValue(config.baseUrl);
64
+ env.ANTHROPIC_AUTH_TOKEN = sanitizeEnvValue(config.authToken);
38
65
  }
39
- } else {
40
- // auth_token 模式
41
- env.ANTHROPIC_BASE_URL = config.baseUrl;
42
- env.ANTHROPIC_AUTH_TOKEN = config.authToken;
43
- }
44
66
 
45
- if (config.models && config.models.primary) {
46
- env.ANTHROPIC_MODEL = config.models.primary;
47
- }
67
+ if (config.models && config.models.primary) {
68
+ env.ANTHROPIC_MODEL = sanitizeEnvValue(config.models.primary);
69
+ }
48
70
 
49
- if (config.models && config.models.smallFast) {
50
- env.ANTHROPIC_SMALL_FAST_MODEL = config.models.smallFast;
51
- }
71
+ if (config.models && config.models.smallFast) {
72
+ env.ANTHROPIC_SMALL_FAST_MODEL = sanitizeEnvValue(config.models.smallFast);
73
+ }
52
74
 
53
- return env;
75
+ return env;
76
+ } catch (error) {
77
+ throw new Error(`配置验证失败: ${error.message}\n请使用 'akm edit ${config.name}' 修复配置`);
78
+ }
54
79
  }
55
80
 
56
81
  async function executeWithEnv(config, launchArgs = []) {
@@ -72,11 +97,17 @@ async function executeWithEnv(config, launchArgs = []) {
72
97
  if (code === 0) {
73
98
  resolve();
74
99
  } else {
75
- reject(new Error(`Claude Code 退出,代码: ${code}`));
100
+ reject(new Error(`Claude Code 退出,退出代码: ${code}\n提示: 请检查 API 配置是否正确`));
76
101
  }
77
102
  });
78
103
 
79
- child.on('error', reject);
104
+ child.on('error', (error) => {
105
+ if (error.code === 'ENOENT') {
106
+ reject(new Error('找不到 claude 命令\n请确认已安装 Claude Code (https://claude.com/code)'));
107
+ } else {
108
+ reject(new Error(`启动 Claude Code 失败: ${error.message}`));
109
+ }
110
+ });
80
111
  });
81
112
  }
82
113
 
@@ -3,15 +3,33 @@ const validator = {
3
3
  if (!name || typeof name !== 'string') {
4
4
  return '供应商名称不能为空';
5
5
  }
6
-
6
+
7
7
  if (name.trim().length === 0) {
8
8
  return '供应商名称不能为空或只包含空格';
9
9
  }
10
-
10
+
11
+ // 禁止文件系统特殊字符
12
+ if (/[<>:"/\\|?*\x00-\x1F]/.test(name)) {
13
+ return '供应商名称包含非法字符 (不能包含: < > : " / \\ | ? *)';
14
+ }
15
+
16
+ // 禁止使用保留名称 (Windows)
17
+ const reserved = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
18
+ 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2',
19
+ 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
20
+ if (reserved.includes(name.toUpperCase())) {
21
+ return '供应商名称不能使用系统保留名称';
22
+ }
23
+
24
+ // 禁止以点或空格开头/结尾 (Windows 限制)
25
+ if (/^[. ]|[. ]$/.test(name)) {
26
+ return '供应商名称不能以点或空格开头/结尾';
27
+ }
28
+
11
29
  if (name.length > 100) {
12
30
  return '供应商名称不能超过100个字符';
13
31
  }
14
-
32
+
15
33
  return null;
16
34
  },
17
35
 
@@ -54,11 +72,27 @@ const validator = {
54
72
  if (!token || typeof token !== 'string') {
55
73
  return 'Token不能为空';
56
74
  }
57
-
58
- if (token.length < 10) {
75
+
76
+ if (token.trim().length === 0) {
77
+ return 'Token不能只包含空格';
78
+ }
79
+
80
+ if (token.trim().length < 10) {
59
81
  return 'Token长度不能少于10个字符';
60
82
  }
61
-
83
+
84
+ // 检测常见的占位符文本
85
+ const placeholders = [
86
+ 'your-key-here', 'your-token', 'your_key', 'your_token',
87
+ 'example', 'test-key', 'demo', 'placeholder', 'replace-me',
88
+ 'insert-key', 'api-key-here', 'token-here', 'xxx', 'yyy',
89
+ 'zzz', 'abc123', '123456'
90
+ ];
91
+ const lowerToken = token.toLowerCase();
92
+ if (placeholders.some(p => lowerToken.includes(p))) {
93
+ return 'Token 似乎是占位符,请输入真实的 API Token';
94
+ }
95
+
62
96
  return null;
63
97
  },
64
98