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.
Files changed (118) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +28 -223
  3. package/STIGMERGY.md +61 -61
  4. package/docs/PROJECT_CONSTITUTION.md +433 -433
  5. package/docs/PROJECT_STRUCTURE_CURRENT.md +80 -80
  6. package/examples/calculator-example.js +72 -72
  7. package/examples/cline_usage_examples.md +364 -364
  8. package/examples/encryption-example.js +67 -67
  9. package/examples/json-parser-example.js +120 -120
  10. package/examples/json-validation-example.js +64 -64
  11. package/examples/rest-client-example.js +52 -52
  12. package/examples/rest_client_example.js +54 -54
  13. package/package.json +15 -7
  14. package/scripts/build.js +74 -74
  15. package/scripts/post-deployment-config.js +296 -296
  16. package/scripts/preinstall-check.js +173 -111
  17. package/scripts/publish.js +58 -268
  18. package/scripts/run-layered-tests.js +247 -0
  19. package/scripts/safe-install.js +139 -139
  20. package/scripts/simple-publish.js +57 -59
  21. package/src/adapters/claude/install_claude_integration.js +292 -0
  22. package/src/adapters/codebuddy/install_codebuddy_integration.js +349 -0
  23. package/src/adapters/codex/install_codex_integration.js +395 -0
  24. package/src/adapters/copilot/install_copilot_integration.js +716 -0
  25. package/src/adapters/gemini/install_gemini_integration.js +304 -0
  26. package/src/adapters/iflow/install_iflow_integration.js +304 -0
  27. package/src/adapters/qoder/install_qoder_integration.js +1090 -0
  28. package/src/adapters/qwen/install_qwen_integration.js +285 -0
  29. package/src/auth.js +173 -173
  30. package/src/auth_command.js +208 -208
  31. package/src/calculator.js +313 -313
  32. package/src/cli/router.js +792 -67
  33. package/src/core/cache_cleaner.js +767 -0
  34. package/src/core/cli_help_analyzer.js +680 -674
  35. package/src/core/cli_parameter_handler.js +132 -127
  36. package/src/core/cli_tools.js +89 -89
  37. package/src/core/coordination/index.js +16 -16
  38. package/src/core/coordination/nodejs/AdapterManager.js +102 -89
  39. package/src/core/coordination/nodejs/CLCommunication.js +132 -124
  40. package/src/core/coordination/nodejs/CLIIntegrationManager.js +272 -236
  41. package/src/core/coordination/nodejs/HealthChecker.js +76 -77
  42. package/src/core/coordination/nodejs/HookDeploymentManager.js +263 -190
  43. package/src/core/coordination/nodejs/StatisticsCollector.js +71 -71
  44. package/src/core/coordination/nodejs/index.js +90 -72
  45. package/src/core/coordination/nodejs/utils/Logger.js +29 -29
  46. package/src/core/enhanced_installer.js +479 -0
  47. package/src/core/enhanced_uninstaller.js +638 -0
  48. package/src/core/error_handler.js +406 -406
  49. package/src/core/installer.js +816 -294
  50. package/src/core/memory_manager.js +83 -83
  51. package/src/core/rest_client.js +160 -160
  52. package/src/core/smart_router.js +249 -146
  53. package/src/core/upgrade_manager.js +420 -0
  54. package/src/data_encryption.js +143 -143
  55. package/src/data_structures.js +440 -440
  56. package/src/deploy.js +55 -55
  57. package/src/index.js +30 -30
  58. package/src/test/cli-availability-checker.js +194 -0
  59. package/src/test/test-environment.js +289 -0
  60. package/src/utils/helpers.js +35 -35
  61. package/src/utils.js +921 -915
  62. package/src/weatherProcessor.js +228 -228
  63. package/test/calculator.test.js +0 -215
  64. package/test/collision-test.js +0 -26
  65. package/test/comprehensive-execution-test.js +0 -428
  66. package/test/conflict-prevention-test.js +0 -95
  67. package/test/cross-cli-detection-test.js +0 -33
  68. package/test/csv-processing-test.js +0 -36
  69. package/test/deploy-hooks-test.js +0 -250
  70. package/test/e2e/claude-cli-test.js +0 -128
  71. package/test/e2e/collaboration-test.js +0 -75
  72. package/test/e2e/comprehensive-test.js +0 -431
  73. package/test/e2e/error-handling-test.js +0 -90
  74. package/test/e2e/individual-tool-test.js +0 -143
  75. package/test/e2e/other-cli-test.js +0 -130
  76. package/test/e2e/qoder-cli-test.js +0 -128
  77. package/test/e2e/run-e2e-tests.js +0 -73
  78. package/test/e2e/test-data.js +0 -88
  79. package/test/e2e/test-utils.js +0 -222
  80. package/test/encryption-simple-test.js +0 -110
  81. package/test/encryption.test.js +0 -129
  82. package/test/enhanced-main-alignment.test.js +0 -298
  83. package/test/error-handling-test.js +0 -341
  84. package/test/fibonacci.test.js +0 -178
  85. package/test/final-deploy-test.js +0 -221
  86. package/test/final-install-test.js +0 -226
  87. package/test/hash-table-demo.js +0 -33
  88. package/test/hash-table-test.js +0 -26
  89. package/test/hash_table_test.js +0 -114
  90. package/test/hook-system-integration-test.js +0 -307
  91. package/test/iflow-integration-test.js +0 -292
  92. package/test/improved-install-test.js +0 -362
  93. package/test/install-command-test.js +0 -370
  94. package/test/json-parser-test.js +0 -161
  95. package/test/json-validation-test.js +0 -164
  96. package/test/natural-language-skills-test.js +0 -320
  97. package/test/nl-integration-test.js +0 -179
  98. package/test/parameter-parsing-test.js +0 -143
  99. package/test/plugin-deployment-test.js +0 -316
  100. package/test/postinstall-test.js +0 -269
  101. package/test/python-plugins-test.js +0 -259
  102. package/test/real-test.js +0 -435
  103. package/test/remaining-adapters-test.js +0 -256
  104. package/test/rest-client-test.js +0 -56
  105. package/test/rest_client.test.js +0 -85
  106. package/test/simple-iflow-hook-test.js +0 -137
  107. package/test/system-compatibility-test.js +0 -467
  108. package/test/tdd-deploy-fix-test.js +0 -324
  109. package/test/tdd-fixes-test.js +0 -211
  110. package/test/third-party-skills-test.js +0 -321
  111. package/test/tool-selection-integration-test.js +0 -158
  112. package/test/unit/calculator-full.test.js +0 -191
  113. package/test/unit/calculator-simple.test.js +0 -96
  114. package/test/unit/calculator.test.js +0 -97
  115. package/test/unit/cli-scanner.test.js +0 -291
  116. package/test/unit/cli_parameter_handler.test.js +0 -116
  117. package/test/unit/cross-cli-executor.test.js +0 -399
  118. 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
+ };