stigmergy 1.1.6 → 1.2.6
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/LICENSE +18 -18
- package/README.md +28 -223
- package/STIGMERGY.md +61 -61
- package/docs/PROJECT_CONSTITUTION.md +433 -433
- package/docs/PROJECT_STRUCTURE_CURRENT.md +80 -80
- package/examples/calculator-example.js +72 -72
- package/examples/cline_usage_examples.md +364 -364
- package/examples/encryption-example.js +67 -67
- package/examples/json-parser-example.js +120 -120
- package/examples/json-validation-example.js +64 -64
- package/examples/rest-client-example.js +52 -52
- package/examples/rest_client_example.js +54 -54
- package/package.json +15 -7
- package/scripts/build.js +74 -74
- package/scripts/post-deployment-config.js +296 -296
- package/scripts/preinstall-check.js +173 -111
- package/scripts/publish.js +58 -268
- package/scripts/run-layered-tests.js +247 -0
- package/scripts/safe-install.js +139 -139
- package/scripts/simple-publish.js +57 -59
- package/src/adapters/claude/install_claude_integration.js +292 -0
- package/src/adapters/codebuddy/install_codebuddy_integration.js +349 -0
- package/src/adapters/codex/install_codex_integration.js +395 -0
- package/src/adapters/copilot/install_copilot_integration.js +716 -0
- package/src/adapters/gemini/install_gemini_integration.js +304 -0
- package/src/adapters/iflow/install_iflow_integration.js +304 -0
- package/src/adapters/qoder/install_qoder_integration.js +1090 -0
- package/src/adapters/qwen/install_qwen_integration.js +285 -0
- package/src/auth.js +173 -173
- package/src/auth_command.js +208 -208
- package/src/calculator.js +313 -313
- package/src/cli/router.js +792 -67
- package/src/core/cache_cleaner.js +767 -0
- package/src/core/cli_help_analyzer.js +680 -674
- package/src/core/cli_parameter_handler.js +132 -127
- package/src/core/cli_tools.js +89 -89
- package/src/core/coordination/index.js +16 -16
- package/src/core/coordination/nodejs/AdapterManager.js +102 -89
- package/src/core/coordination/nodejs/CLCommunication.js +132 -124
- package/src/core/coordination/nodejs/CLIIntegrationManager.js +272 -236
- package/src/core/coordination/nodejs/HealthChecker.js +76 -77
- package/src/core/coordination/nodejs/HookDeploymentManager.js +263 -190
- package/src/core/coordination/nodejs/StatisticsCollector.js +71 -71
- package/src/core/coordination/nodejs/index.js +90 -72
- package/src/core/coordination/nodejs/utils/Logger.js +29 -29
- package/src/core/enhanced_installer.js +479 -0
- package/src/core/enhanced_uninstaller.js +638 -0
- package/src/core/error_handler.js +406 -406
- package/src/core/installer.js +816 -294
- package/src/core/memory_manager.js +83 -83
- package/src/core/rest_client.js +160 -160
- package/src/core/smart_router.js +249 -146
- package/src/core/upgrade_manager.js +420 -0
- package/src/data_encryption.js +143 -143
- package/src/data_structures.js +440 -440
- package/src/deploy.js +55 -55
- package/src/index.js +30 -30
- package/src/test/cli-availability-checker.js +194 -0
- package/src/test/test-environment.js +289 -0
- package/src/utils/helpers.js +35 -35
- package/src/utils.js +921 -915
- package/src/weatherProcessor.js +228 -228
- package/test/calculator.test.js +0 -215
- package/test/collision-test.js +0 -26
- package/test/comprehensive-execution-test.js +0 -428
- package/test/conflict-prevention-test.js +0 -95
- package/test/cross-cli-detection-test.js +0 -33
- package/test/csv-processing-test.js +0 -36
- package/test/deploy-hooks-test.js +0 -250
- package/test/e2e/claude-cli-test.js +0 -128
- package/test/e2e/collaboration-test.js +0 -75
- package/test/e2e/comprehensive-test.js +0 -431
- package/test/e2e/error-handling-test.js +0 -90
- package/test/e2e/individual-tool-test.js +0 -143
- package/test/e2e/other-cli-test.js +0 -130
- package/test/e2e/qoder-cli-test.js +0 -128
- package/test/e2e/run-e2e-tests.js +0 -73
- package/test/e2e/test-data.js +0 -88
- package/test/e2e/test-utils.js +0 -222
- package/test/encryption-simple-test.js +0 -110
- package/test/encryption.test.js +0 -129
- package/test/enhanced-main-alignment.test.js +0 -298
- package/test/error-handling-test.js +0 -341
- package/test/fibonacci.test.js +0 -178
- package/test/final-deploy-test.js +0 -221
- package/test/final-install-test.js +0 -226
- package/test/hash-table-demo.js +0 -33
- package/test/hash-table-test.js +0 -26
- package/test/hash_table_test.js +0 -114
- package/test/hook-system-integration-test.js +0 -307
- package/test/iflow-integration-test.js +0 -292
- package/test/improved-install-test.js +0 -362
- package/test/install-command-test.js +0 -370
- package/test/json-parser-test.js +0 -161
- package/test/json-validation-test.js +0 -164
- package/test/natural-language-skills-test.js +0 -320
- package/test/nl-integration-test.js +0 -179
- package/test/parameter-parsing-test.js +0 -143
- package/test/plugin-deployment-test.js +0 -316
- package/test/postinstall-test.js +0 -269
- package/test/python-plugins-test.js +0 -259
- package/test/real-test.js +0 -435
- package/test/remaining-adapters-test.js +0 -256
- package/test/rest-client-test.js +0 -56
- package/test/rest_client.test.js +0 -85
- package/test/simple-iflow-hook-test.js +0 -137
- package/test/system-compatibility-test.js +0 -467
- package/test/tdd-deploy-fix-test.js +0 -324
- package/test/tdd-fixes-test.js +0 -211
- package/test/third-party-skills-test.js +0 -321
- package/test/tool-selection-integration-test.js +0 -158
- package/test/unit/calculator-full.test.js +0 -191
- package/test/unit/calculator-simple.test.js +0 -96
- package/test/unit/calculator.test.js +0 -97
- package/test/unit/cli-scanner.test.js +0 -291
- package/test/unit/cli_parameter_handler.test.js +0 -116
- package/test/unit/cross-cli-executor.test.js +0 -399
- package/test/weather-processor.test.js +0 -104
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Qwen CLI Integration Installer - JavaScript Version
|
|
5
|
+
* Sets up basic integration for Qwen CLI with cross-CLI collaboration capabilities
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs').promises;
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
// Qwen CLI config paths
|
|
13
|
+
const QWEN_CONFIG_DIR = path.join(os.homedir(), '.qwen');
|
|
14
|
+
const QWEN_CONFIG_FILE = path.join(QWEN_CONFIG_DIR, 'config.json');
|
|
15
|
+
const QWEN_HOOKS_FILE = path.join(QWEN_CONFIG_DIR, 'hooks.json');
|
|
16
|
+
|
|
17
|
+
class QwenInstaller {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.toolName = 'qwen';
|
|
20
|
+
this.configDir = QWEN_CONFIG_DIR;
|
|
21
|
+
this.configFile = QWEN_CONFIG_FILE;
|
|
22
|
+
this.hooksFile = QWEN_HOOKS_FILE;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async createConfigDirectory() {
|
|
26
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
27
|
+
console.log(`[OK] Created Qwen config directory: ${this.configDir}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async installConfig() {
|
|
31
|
+
// Read existing config
|
|
32
|
+
let existingConfig = {};
|
|
33
|
+
try {
|
|
34
|
+
const content = await fs.readFile(this.configFile, 'utf-8');
|
|
35
|
+
existingConfig = JSON.parse(content);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log(`Warning: Failed to read existing config: ${e.message}`);
|
|
38
|
+
existingConfig = {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Define cross-CLI integration config
|
|
42
|
+
const crossCliConfig = {
|
|
43
|
+
cross_cli_enabled: true,
|
|
44
|
+
supported_clis: ["claude", "gemini", "qwen", "iflow", "qodercli", "codebuddy", "copilot", "codex"],
|
|
45
|
+
auto_detect: true,
|
|
46
|
+
timeout: 30,
|
|
47
|
+
collaboration_mode: "active",
|
|
48
|
+
qwen_oauth_integration: true
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Merge configs
|
|
52
|
+
const mergedConfig = { ...existingConfig, ...crossCliConfig };
|
|
53
|
+
|
|
54
|
+
// Write config file
|
|
55
|
+
await fs.writeFile(this.configFile, JSON.stringify(mergedConfig, null, 2));
|
|
56
|
+
console.log(`[OK] Qwen config installed: ${this.configFile}`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async installHooks() {
|
|
61
|
+
// Read existing hooks config
|
|
62
|
+
let existingHooks = {};
|
|
63
|
+
try {
|
|
64
|
+
const content = await fs.readFile(this.hooksFile, 'utf-8');
|
|
65
|
+
existingHooks = JSON.parse(content);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.log(`Warning: Failed to read existing hooks config: ${e.message}`);
|
|
68
|
+
existingHooks = {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Define cross-CLI integration hooks
|
|
72
|
+
const crossCliHooks = {
|
|
73
|
+
cross_cli_adapter: {
|
|
74
|
+
enabled: true,
|
|
75
|
+
supported_tools: ["claude", "gemini", "qwen", "iflow", "qodercli", "codebuddy", "copilot", "codex"],
|
|
76
|
+
trigger_patterns: [
|
|
77
|
+
"use\\s+(\\w+)\\s+to\\s+(.+)$",
|
|
78
|
+
"call\\s+(\\w+)\\s+(.+)$",
|
|
79
|
+
"ask\\s+(\\w+)\\s+(.+)$",
|
|
80
|
+
"stigmergy\\s+(\\w+)\\s+(.+)$"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Merge hooks configs
|
|
86
|
+
const mergedHooks = { ...existingHooks, ...crossCliHooks };
|
|
87
|
+
|
|
88
|
+
// Write hooks config file
|
|
89
|
+
await fs.writeFile(this.hooksFile, JSON.stringify(mergedHooks, null, 2));
|
|
90
|
+
console.log(`[OK] Qwen hooks installed: ${this.hooksFile}`);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async copyAdapterFiles() {
|
|
95
|
+
const currentDir = __dirname;
|
|
96
|
+
const targetDir = path.join(this.configDir, 'hooks');
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Look for adapter files to copy
|
|
100
|
+
const adapterFiles = await fs.readdir(currentDir);
|
|
101
|
+
const filesToCopy = adapterFiles.filter(file =>
|
|
102
|
+
file.endsWith('.js') || file.endsWith('.json') || file.endsWith('.py') || file === '__init__.py'
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
for (const file of filesToCopy) {
|
|
106
|
+
const srcFile = path.join(currentDir, file);
|
|
107
|
+
const dstFile = path.join(targetDir, file);
|
|
108
|
+
await fs.copyFile(srcFile, dstFile);
|
|
109
|
+
console.log(`[OK] Copied adapter file: ${file}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Create global Cross-CLI documentation
|
|
113
|
+
await this.createGlobalCrossCliDocumentation(this.configDir);
|
|
114
|
+
|
|
115
|
+
return true;
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.log(`Warning: Failed to copy adapter files: ${e.message}`);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async createGlobalCrossCliDocumentation(configDir) {
|
|
123
|
+
try {
|
|
124
|
+
const docContent = `# Qwen CLI Cross-CLI Communication Guide
|
|
125
|
+
|
|
126
|
+
## Cross-CLI Communication
|
|
127
|
+
When you need to call other AI tools, please use the following prompt <<<Execute the following command in shell: stigmergy distcli "Prompt">>>
|
|
128
|
+
|
|
129
|
+
Examples:
|
|
130
|
+
- Run in shell: stigmergy claude "write a Python function"
|
|
131
|
+
- Run in shell: stigmergy gemini "translate this text"
|
|
132
|
+
- Run in shell: stigmergy qwen "analyze this code"
|
|
133
|
+
|
|
134
|
+
Available tools: claude, gemini, qwen, iflow, qodercli, codebuddy, copilot, codex
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
*This document is automatically generated and maintained by Stigmergy CLI*
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
const docPath = path.join(configDir, 'CROSS_CLI_GUIDE.md');
|
|
141
|
+
await fs.writeFile(docPath, docContent);
|
|
142
|
+
console.log(`[OK] Created Cross-CLI Communication Guide: ${docPath}`);
|
|
143
|
+
|
|
144
|
+
// Append to qwen.md if it exists
|
|
145
|
+
const qwenMdPath = path.join(os.homedir(), '.qwen', 'qwen.md');
|
|
146
|
+
try {
|
|
147
|
+
await fs.access(qwenMdPath);
|
|
148
|
+
const crossCliContent = `
|
|
149
|
+
|
|
150
|
+
## Cross-CLI Communication
|
|
151
|
+
When you need to call other AI tools, please use the following prompt <<<Execute the following command in shell: stigmergy distcli "Prompt">>>
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
- Run in shell: stigmergy claude "write a Python function"
|
|
155
|
+
- Run in shell: stigmergy gemini "translate this text"
|
|
156
|
+
- Run in shell: stigmergy qwen "analyze this code"
|
|
157
|
+
|
|
158
|
+
Available tools: claude, gemini, qwen, iflow, qodercli, codebuddy, copilot, codex
|
|
159
|
+
`;
|
|
160
|
+
await fs.appendFile(qwenMdPath, crossCliContent);
|
|
161
|
+
console.log(`[OK] 在QWEN.md末尾追加Cross-CLI通信提示`);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// File doesn't exist, that's ok
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return true;
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.log(`Warning: Failed to create Cross-CLI Communication Guide: ${e.message}`);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async verifyInstallation() {
|
|
174
|
+
console.log('\n验证Qwen CLI集成安装...');
|
|
175
|
+
|
|
176
|
+
// Check config directory
|
|
177
|
+
if (!await fs.access(this.configDir).then(() => true).catch(() => false)) {
|
|
178
|
+
console.log(`Warning: Config directory does not exist: ${this.configDir}`);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check config file content
|
|
183
|
+
try {
|
|
184
|
+
const content = await fs.readFile(this.configFile, 'utf-8');
|
|
185
|
+
const config = JSON.parse(content);
|
|
186
|
+
|
|
187
|
+
if (config.cross_cli_enabled) {
|
|
188
|
+
console.log('[OK] Cross-CLI integration enabled');
|
|
189
|
+
} else {
|
|
190
|
+
console.log('Note: Cross-CLI integration not enabled');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log('[OK] Qwen config file verified');
|
|
194
|
+
return true;
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.log(`Warning: Failed to verify config file: ${e.message}`);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async uninstallIntegration() {
|
|
202
|
+
try {
|
|
203
|
+
// Remove cross-CLI config from config file
|
|
204
|
+
if (await fs.access(this.configFile).then(() => true).catch(() => false)) {
|
|
205
|
+
const content = await fs.readFile(this.configFile, 'utf-8');
|
|
206
|
+
const config = JSON.parse(content);
|
|
207
|
+
|
|
208
|
+
// Remove cross-CLI settings
|
|
209
|
+
delete config.cross_cli_enabled;
|
|
210
|
+
delete config.supported_clis;
|
|
211
|
+
delete config.auto_detect;
|
|
212
|
+
delete config.collaboration_mode;
|
|
213
|
+
delete config.qwen_oauth_integration;
|
|
214
|
+
|
|
215
|
+
// Save updated config
|
|
216
|
+
await fs.writeFile(this.configFile, JSON.stringify(config, null, 2));
|
|
217
|
+
console.log('[OK] Removed cross-CLI settings from Qwen config');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Remove hooks config file
|
|
221
|
+
if (await fs.access(this.hooksFile).then(() => true).catch(() => false)) {
|
|
222
|
+
await fs.unlink(this.hooksFile);
|
|
223
|
+
console.log('[OK] Removed Qwen hooks config file');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log('[OK] Qwen CLI cross-CLI integration uninstalled');
|
|
227
|
+
return true;
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.log(`Error: Uninstall failed: ${e.message}`);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async install() {
|
|
235
|
+
console.log('Qwen CLI Integration Installer');
|
|
236
|
+
console.log('==========================================');
|
|
237
|
+
|
|
238
|
+
// Execute installation
|
|
239
|
+
console.log('Step 1. 创建配置目录...');
|
|
240
|
+
await this.createConfigDirectory();
|
|
241
|
+
|
|
242
|
+
console.log('\nStep 2. 安装配置...');
|
|
243
|
+
const configSuccess = await this.installConfig();
|
|
244
|
+
|
|
245
|
+
console.log('\nStep 3. 安装钩子...');
|
|
246
|
+
const hooksSuccess = await this.installHooks();
|
|
247
|
+
|
|
248
|
+
console.log('\nStep 4. 复制适配器文件...');
|
|
249
|
+
const adapterSuccess = await this.copyAdapterFiles();
|
|
250
|
+
|
|
251
|
+
console.log('\nStep 5. 验证安装...');
|
|
252
|
+
const verificationSuccess = await this.verifyInstallation();
|
|
253
|
+
|
|
254
|
+
const overallSuccess = configSuccess && hooksSuccess && adapterSuccess && verificationSuccess;
|
|
255
|
+
if (overallSuccess) {
|
|
256
|
+
console.log('\n[SUCCESS] Qwen CLI integration installed successfully!');
|
|
257
|
+
} else {
|
|
258
|
+
console.log('\n[WARNING] Installation completed with warnings');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return overallSuccess;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Main execution
|
|
266
|
+
if (require.main === module) {
|
|
267
|
+
const installer = new QwenInstaller();
|
|
268
|
+
|
|
269
|
+
const args = process.argv.slice(2);
|
|
270
|
+
const command = args[0];
|
|
271
|
+
|
|
272
|
+
switch (command) {
|
|
273
|
+
case '--verify':
|
|
274
|
+
installer.verifyInstallation().then(success => process.exit(success ? 0 : 1));
|
|
275
|
+
break;
|
|
276
|
+
case '--uninstall':
|
|
277
|
+
installer.uninstallIntegration().then(success => process.exit(success ? 0 : 1));
|
|
278
|
+
break;
|
|
279
|
+
default:
|
|
280
|
+
installer.install().then(success => process.exit(success ? 0 : 1));
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = QwenInstaller;
|
package/src/auth.js
CHANGED
|
@@ -1,173 +1,173 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authentication module for the Stigmergy CLI system.
|
|
3
|
-
* Provides user authentication functionality including password hashing and token management.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const crypto = require('crypto');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Custom exception for authentication failures.
|
|
10
|
-
*/
|
|
11
|
-
class AuthenticationError extends Error {
|
|
12
|
-
constructor(message) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.name = 'AuthenticationError';
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Handles user authentication operations including registration, login, and session management.
|
|
20
|
-
*/
|
|
21
|
-
class UserAuthenticator {
|
|
22
|
-
/**
|
|
23
|
-
* Create a new UserAuthenticator instance.
|
|
24
|
-
*/
|
|
25
|
-
constructor() {
|
|
26
|
-
// In production, this would be stored in a secure database
|
|
27
|
-
this._users = new Map();
|
|
28
|
-
this._sessions = new Map();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Register a new user with the provided username and password.
|
|
33
|
-
*
|
|
34
|
-
* @param {string} username - The user's username
|
|
35
|
-
* @param {string} password - The user's password
|
|
36
|
-
* @returns {boolean} True if registration successful, false if username already exists
|
|
37
|
-
* @throws {Error} If username or password is invalid
|
|
38
|
-
*/
|
|
39
|
-
registerUser(username, password) {
|
|
40
|
-
if (!username || !password) {
|
|
41
|
-
throw new Error('Username and password cannot be empty');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (username.length < 3) {
|
|
45
|
-
throw new Error('Username must be at least 3 characters long');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (password.length < 8) {
|
|
49
|
-
throw new Error('Password must be at least 8 characters long');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (this._users.has(username)) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Hash the password with a salt
|
|
57
|
-
const salt = crypto.randomBytes(16).toString('hex');
|
|
58
|
-
const passwordHash = this._hashPassword(password, salt);
|
|
59
|
-
|
|
60
|
-
this._users.set(username, {
|
|
61
|
-
passwordHash: passwordHash,
|
|
62
|
-
salt: salt,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Authenticate a user with the provided credentials.
|
|
70
|
-
*
|
|
71
|
-
* @param {string} username - The user's username
|
|
72
|
-
* @param {string} password - The user's password
|
|
73
|
-
* @returns {string} Session token if authentication is successful
|
|
74
|
-
* @throws {AuthenticationError} If authentication fails
|
|
75
|
-
*/
|
|
76
|
-
authenticateUser(username, password) {
|
|
77
|
-
if (!this._users.has(username)) {
|
|
78
|
-
throw new AuthenticationError('Invalid username or password');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const userData = this._users.get(username);
|
|
82
|
-
const passwordHash = this._hashPassword(password, userData.salt);
|
|
83
|
-
|
|
84
|
-
if (passwordHash !== userData.passwordHash) {
|
|
85
|
-
throw new AuthenticationError('Invalid username or password');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Generate session token
|
|
89
|
-
const sessionToken = crypto.randomBytes(32).toString('base64url');
|
|
90
|
-
this._sessions.set(sessionToken, {
|
|
91
|
-
username: username,
|
|
92
|
-
createdAt: Date.now(),
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return sessionToken;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Validate a session token and return the associated username.
|
|
100
|
-
*
|
|
101
|
-
* @param {string} sessionToken - The session token to validate
|
|
102
|
-
* @returns {string|null} Username if session is valid, null otherwise
|
|
103
|
-
*/
|
|
104
|
-
validateSession(sessionToken) {
|
|
105
|
-
if (!this._sessions.has(sessionToken)) {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// In production, you would check session expiration here
|
|
110
|
-
return this._sessions.get(sessionToken).username;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Invalidate a session token.
|
|
115
|
-
*
|
|
116
|
-
* @param {string} sessionToken - The session token to invalidate
|
|
117
|
-
* @returns {boolean} True if session was invalidated, false if token was not found
|
|
118
|
-
*/
|
|
119
|
-
logout(sessionToken) {
|
|
120
|
-
if (this._sessions.has(sessionToken)) {
|
|
121
|
-
this._sessions.delete(sessionToken);
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Hash a password with the provided salt using PBKDF2.
|
|
129
|
-
*
|
|
130
|
-
* @param {string} password - The password to hash
|
|
131
|
-
* @param {string} salt - The salt to use for hashing
|
|
132
|
-
* @returns {string} The hashed password
|
|
133
|
-
* @private
|
|
134
|
-
*/
|
|
135
|
-
_hashPassword(password, salt) {
|
|
136
|
-
return crypto
|
|
137
|
-
.pbkdf2Sync(
|
|
138
|
-
password,
|
|
139
|
-
salt,
|
|
140
|
-
100000, // iterations
|
|
141
|
-
32, // key length
|
|
142
|
-
'sha256', // digest
|
|
143
|
-
)
|
|
144
|
-
.toString('hex');
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Helper function to authenticate a user and return a session token.
|
|
150
|
-
*
|
|
151
|
-
* @param {UserAuthenticator} authenticator - The authenticator instance
|
|
152
|
-
* @param {string} username - The user's username
|
|
153
|
-
* @param {string} password - The user's password
|
|
154
|
-
* @returns {[boolean, string]} A tuple containing success status and message/token
|
|
155
|
-
*/
|
|
156
|
-
function authenticateAndGetToken(authenticator, username, password) {
|
|
157
|
-
try {
|
|
158
|
-
const token = authenticator.authenticateUser(username, password);
|
|
159
|
-
return [true, token];
|
|
160
|
-
} catch (error) {
|
|
161
|
-
if (error instanceof AuthenticationError) {
|
|
162
|
-
return [false, error.message];
|
|
163
|
-
} else {
|
|
164
|
-
return [false, `Authentication error: ${error.message}`];
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
module.exports = {
|
|
170
|
-
UserAuthenticator,
|
|
171
|
-
AuthenticationError,
|
|
172
|
-
authenticateAndGetToken,
|
|
173
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Authentication module for the Stigmergy CLI system.
|
|
3
|
+
* Provides user authentication functionality including password hashing and token management.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom exception for authentication failures.
|
|
10
|
+
*/
|
|
11
|
+
class AuthenticationError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'AuthenticationError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handles user authentication operations including registration, login, and session management.
|
|
20
|
+
*/
|
|
21
|
+
class UserAuthenticator {
|
|
22
|
+
/**
|
|
23
|
+
* Create a new UserAuthenticator instance.
|
|
24
|
+
*/
|
|
25
|
+
constructor() {
|
|
26
|
+
// In production, this would be stored in a secure database
|
|
27
|
+
this._users = new Map();
|
|
28
|
+
this._sessions = new Map();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a new user with the provided username and password.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} username - The user's username
|
|
35
|
+
* @param {string} password - The user's password
|
|
36
|
+
* @returns {boolean} True if registration successful, false if username already exists
|
|
37
|
+
* @throws {Error} If username or password is invalid
|
|
38
|
+
*/
|
|
39
|
+
registerUser(username, password) {
|
|
40
|
+
if (!username || !password) {
|
|
41
|
+
throw new Error('Username and password cannot be empty');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (username.length < 3) {
|
|
45
|
+
throw new Error('Username must be at least 3 characters long');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (password.length < 8) {
|
|
49
|
+
throw new Error('Password must be at least 8 characters long');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (this._users.has(username)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Hash the password with a salt
|
|
57
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
58
|
+
const passwordHash = this._hashPassword(password, salt);
|
|
59
|
+
|
|
60
|
+
this._users.set(username, {
|
|
61
|
+
passwordHash: passwordHash,
|
|
62
|
+
salt: salt,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Authenticate a user with the provided credentials.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} username - The user's username
|
|
72
|
+
* @param {string} password - The user's password
|
|
73
|
+
* @returns {string} Session token if authentication is successful
|
|
74
|
+
* @throws {AuthenticationError} If authentication fails
|
|
75
|
+
*/
|
|
76
|
+
authenticateUser(username, password) {
|
|
77
|
+
if (!this._users.has(username)) {
|
|
78
|
+
throw new AuthenticationError('Invalid username or password');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const userData = this._users.get(username);
|
|
82
|
+
const passwordHash = this._hashPassword(password, userData.salt);
|
|
83
|
+
|
|
84
|
+
if (passwordHash !== userData.passwordHash) {
|
|
85
|
+
throw new AuthenticationError('Invalid username or password');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Generate session token
|
|
89
|
+
const sessionToken = crypto.randomBytes(32).toString('base64url');
|
|
90
|
+
this._sessions.set(sessionToken, {
|
|
91
|
+
username: username,
|
|
92
|
+
createdAt: Date.now(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return sessionToken;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate a session token and return the associated username.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} sessionToken - The session token to validate
|
|
102
|
+
* @returns {string|null} Username if session is valid, null otherwise
|
|
103
|
+
*/
|
|
104
|
+
validateSession(sessionToken) {
|
|
105
|
+
if (!this._sessions.has(sessionToken)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// In production, you would check session expiration here
|
|
110
|
+
return this._sessions.get(sessionToken).username;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Invalidate a session token.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} sessionToken - The session token to invalidate
|
|
117
|
+
* @returns {boolean} True if session was invalidated, false if token was not found
|
|
118
|
+
*/
|
|
119
|
+
logout(sessionToken) {
|
|
120
|
+
if (this._sessions.has(sessionToken)) {
|
|
121
|
+
this._sessions.delete(sessionToken);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Hash a password with the provided salt using PBKDF2.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} password - The password to hash
|
|
131
|
+
* @param {string} salt - The salt to use for hashing
|
|
132
|
+
* @returns {string} The hashed password
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
_hashPassword(password, salt) {
|
|
136
|
+
return crypto
|
|
137
|
+
.pbkdf2Sync(
|
|
138
|
+
password,
|
|
139
|
+
salt,
|
|
140
|
+
100000, // iterations
|
|
141
|
+
32, // key length
|
|
142
|
+
'sha256', // digest
|
|
143
|
+
)
|
|
144
|
+
.toString('hex');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Helper function to authenticate a user and return a session token.
|
|
150
|
+
*
|
|
151
|
+
* @param {UserAuthenticator} authenticator - The authenticator instance
|
|
152
|
+
* @param {string} username - The user's username
|
|
153
|
+
* @param {string} password - The user's password
|
|
154
|
+
* @returns {[boolean, string]} A tuple containing success status and message/token
|
|
155
|
+
*/
|
|
156
|
+
function authenticateAndGetToken(authenticator, username, password) {
|
|
157
|
+
try {
|
|
158
|
+
const token = authenticator.authenticateUser(username, password);
|
|
159
|
+
return [true, token];
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof AuthenticationError) {
|
|
162
|
+
return [false, error.message];
|
|
163
|
+
} else {
|
|
164
|
+
return [false, `Authentication error: ${error.message}`];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
UserAuthenticator,
|
|
171
|
+
AuthenticationError,
|
|
172
|
+
authenticateAndGetToken,
|
|
173
|
+
};
|