@kikkimo/claude-launcher 1.0.0 → 2.0.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/CHANGELOG.md +55 -0
- package/README.md +100 -41
- package/claude-launcher +1017 -576
- package/docs/README-zh.md +104 -45
- package/lib/api-manager.js +449 -0
- package/lib/auth/password-input.js +144 -0
- package/lib/auth/password-strength.js +154 -0
- package/lib/auth/password-validator.js +255 -0
- package/lib/crypto.js +85 -0
- package/lib/i18n/formatter.js +62 -0
- package/lib/i18n/index.js +218 -0
- package/lib/i18n/language-manager.js +160 -0
- package/lib/i18n/locales/de.js +523 -0
- package/lib/i18n/locales/en.js +524 -0
- package/lib/i18n/locales/es.js +523 -0
- package/lib/i18n/locales/fr.js +523 -0
- package/lib/i18n/locales/it.js +523 -0
- package/lib/i18n/locales/ja.js +523 -0
- package/lib/i18n/locales/ko.js +523 -0
- package/lib/i18n/locales/pt.js +523 -0
- package/lib/i18n/locales/ru.js +523 -0
- package/lib/i18n/locales/zh-TW.js +523 -0
- package/lib/i18n/locales/zh.js +523 -0
- package/lib/launcher.js +253 -0
- package/lib/presets/providers.js +104 -0
- package/lib/ui/colors.js +32 -0
- package/lib/ui/interactive-table.js +260 -0
- package/lib/ui/menu.js +314 -0
- package/lib/ui/prompts.js +540 -0
- package/lib/utils/string-width.js +180 -0
- package/lib/utils/version-checker.js +240 -0
- package/lib/validators.js +130 -0
- package/package.json +2 -2
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure Password Input Module
|
|
3
|
+
* Handles masked password input without showing plaintext characters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get password input with proper masking (no plaintext display)
|
|
10
|
+
* @param {string} prompt - The prompt message to display
|
|
11
|
+
* @returns {Promise<string>} - The entered password
|
|
12
|
+
*/
|
|
13
|
+
function getPasswordInput(prompt) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
// Save original stdin state
|
|
16
|
+
const originalRawMode = process.stdin.isRaw;
|
|
17
|
+
const originalPaused = process.stdin.isPaused();
|
|
18
|
+
|
|
19
|
+
let password = '';
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Display prompt - this is necessary for password input
|
|
23
|
+
process.stdout.write(prompt);
|
|
24
|
+
|
|
25
|
+
if (!process.stdin.isTTY) {
|
|
26
|
+
// Non-TTY environment fallback
|
|
27
|
+
const rl = readline.createInterface({
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stdout
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
rl.question('', (answer) => {
|
|
33
|
+
rl.close();
|
|
34
|
+
resolve(answer.trim());
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Set up raw mode for character-by-character input
|
|
40
|
+
process.stdin.setRawMode(true);
|
|
41
|
+
process.stdin.resume();
|
|
42
|
+
process.stdin.setEncoding('utf8');
|
|
43
|
+
|
|
44
|
+
const cleanup = () => {
|
|
45
|
+
try {
|
|
46
|
+
process.stdin.removeListener('data', handleKeyPress);
|
|
47
|
+
if (process.stdin.isTTY) {
|
|
48
|
+
process.stdin.setRawMode(originalRawMode);
|
|
49
|
+
if (originalPaused) {
|
|
50
|
+
process.stdin.pause();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Ignore cleanup errors
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleKeyPress = (data) => {
|
|
59
|
+
const key = data.toString();
|
|
60
|
+
|
|
61
|
+
// Handle different key combinations
|
|
62
|
+
switch (key) {
|
|
63
|
+
case '\u0003': // Ctrl+C
|
|
64
|
+
cleanup();
|
|
65
|
+
reject(new Error('Password input cancelled by user'));
|
|
66
|
+
return;
|
|
67
|
+
|
|
68
|
+
case '\r': // Enter (CR)
|
|
69
|
+
case '\n': // Line Feed (LF)
|
|
70
|
+
case '\r\n': // CRLF
|
|
71
|
+
process.stdout.write('\n');
|
|
72
|
+
cleanup();
|
|
73
|
+
resolve(password);
|
|
74
|
+
return;
|
|
75
|
+
|
|
76
|
+
case '\u007f': // Backspace (DEL)
|
|
77
|
+
case '\b': // Backspace (BS)
|
|
78
|
+
if (password.length > 0) {
|
|
79
|
+
password = password.slice(0, -1);
|
|
80
|
+
// Clear the last asterisk
|
|
81
|
+
process.stdout.write('\b \b');
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
|
|
85
|
+
case '\u001b': // Escape
|
|
86
|
+
cleanup();
|
|
87
|
+
reject(new Error('Password input cancelled'));
|
|
88
|
+
return;
|
|
89
|
+
|
|
90
|
+
default:
|
|
91
|
+
// Filter out control characters (except printable ones)
|
|
92
|
+
if (key.charCodeAt(0) >= 32 && key.charCodeAt(0) < 127) {
|
|
93
|
+
password += key;
|
|
94
|
+
process.stdout.write('*');
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
process.stdin.on('data', handleKeyPress);
|
|
101
|
+
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Restore stdin state on error
|
|
104
|
+
try {
|
|
105
|
+
if (process.stdin.isTTY) {
|
|
106
|
+
process.stdin.setRawMode(originalRawMode);
|
|
107
|
+
if (originalPaused) {
|
|
108
|
+
process.stdin.pause();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (cleanupError) {
|
|
112
|
+
// Ignore cleanup errors
|
|
113
|
+
}
|
|
114
|
+
reject(error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get password input with confirmation
|
|
121
|
+
* @param {string} prompt - The initial prompt message
|
|
122
|
+
* @param {string} confirmPrompt - The confirmation prompt message
|
|
123
|
+
* @returns {Promise<string>} - The confirmed password
|
|
124
|
+
*/
|
|
125
|
+
async function getPasswordWithConfirmation(prompt, confirmPrompt = 'Confirm Password: ') {
|
|
126
|
+
const password = await getPasswordInput(prompt);
|
|
127
|
+
|
|
128
|
+
if (!password) {
|
|
129
|
+
throw new Error('Password cannot be empty');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const confirmPassword = await getPasswordInput(confirmPrompt);
|
|
133
|
+
|
|
134
|
+
if (password !== confirmPassword) {
|
|
135
|
+
throw new Error('Passwords do not match');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return password;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
getPasswordInput,
|
|
143
|
+
getPasswordWithConfirmation
|
|
144
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Strength Validation Module
|
|
3
|
+
* Enforces strong password requirements for security
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const i18n = require('../i18n');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate password strength according to security requirements
|
|
10
|
+
* @param {string} password - The password to validate
|
|
11
|
+
* @returns {Object} - Validation result with success status and detailed feedback
|
|
12
|
+
*/
|
|
13
|
+
function validatePasswordStrength(password) {
|
|
14
|
+
const result = {
|
|
15
|
+
valid: false,
|
|
16
|
+
score: 0,
|
|
17
|
+
errors: [],
|
|
18
|
+
suggestions: []
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Basic validation
|
|
22
|
+
if (!password) {
|
|
23
|
+
result.errors.push(i18n.tSync('errors.password.empty'));
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Length requirement (≥6 characters)
|
|
28
|
+
if (password.length < 6) {
|
|
29
|
+
result.errors.push(i18n.tSync('errors.password.too_short'));
|
|
30
|
+
} else {
|
|
31
|
+
result.score += 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ASCII only validation
|
|
35
|
+
if (!/^[\x20-\x7E]*$/.test(password)) {
|
|
36
|
+
result.errors.push(i18n.tSync('errors.password.non_ascii'));
|
|
37
|
+
} else {
|
|
38
|
+
result.score += 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// No spaces or unusual whitespace characters
|
|
42
|
+
if (/\s/.test(password)) {
|
|
43
|
+
result.errors.push(i18n.tSync('errors.password.contains_spaces'));
|
|
44
|
+
} else {
|
|
45
|
+
result.score += 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Character type requirements (at least 2 of 4 types)
|
|
49
|
+
let characterTypes = 0;
|
|
50
|
+
const hasLowercase = /[a-z]/.test(password);
|
|
51
|
+
const hasUppercase = /[A-Z]/.test(password);
|
|
52
|
+
const hasNumbers = /[0-9]/.test(password);
|
|
53
|
+
const hasSpecialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]/.test(password);
|
|
54
|
+
|
|
55
|
+
if (hasLowercase) characterTypes++;
|
|
56
|
+
if (hasUppercase) characterTypes++;
|
|
57
|
+
if (hasNumbers) characterTypes++;
|
|
58
|
+
if (hasSpecialChars) characterTypes++;
|
|
59
|
+
|
|
60
|
+
if (characterTypes < 2) {
|
|
61
|
+
result.errors.push(i18n.tSync('errors.password.insufficient_types'));
|
|
62
|
+
|
|
63
|
+
// Provide specific suggestions
|
|
64
|
+
if (!hasLowercase) result.suggestions.push(i18n.tSync('errors.password.suggest_lowercase'));
|
|
65
|
+
if (!hasUppercase) result.suggestions.push(i18n.tSync('errors.password.suggest_uppercase'));
|
|
66
|
+
if (!hasNumbers) result.suggestions.push(i18n.tSync('errors.password.suggest_numbers'));
|
|
67
|
+
if (!hasSpecialChars) result.suggestions.push(i18n.tSync('errors.password.suggest_special'));
|
|
68
|
+
} else {
|
|
69
|
+
result.score += characterTypes; // Bonus points for more character types
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Common weak password patterns - exact matches only for common weak passwords
|
|
73
|
+
const weakPatterns = [
|
|
74
|
+
/^123456$/,
|
|
75
|
+
/^1234567$/,
|
|
76
|
+
/^12345678$/,
|
|
77
|
+
/^password$/i,
|
|
78
|
+
/^admin$/i,
|
|
79
|
+
/^qwerty$/i,
|
|
80
|
+
/^abc123$/i,
|
|
81
|
+
/^111111$/,
|
|
82
|
+
/^000000$/,
|
|
83
|
+
/^(.)\1{5,}/, // Repeated characters (6+ times)
|
|
84
|
+
/^(012|123|234|345|456|567|678|789|890){3,}/, // Sequential numbers (3+ repetitions)
|
|
85
|
+
/^(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz){3,}/i // Sequential letters (3+ repetitions)
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const pattern of weakPatterns) {
|
|
89
|
+
if (pattern.test(password)) {
|
|
90
|
+
result.errors.push(i18n.tSync('errors.password.weak_pattern'));
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Additional strength scoring
|
|
96
|
+
if (password.length >= 8) result.score += 1;
|
|
97
|
+
if (password.length >= 12) result.score += 1;
|
|
98
|
+
if (characterTypes >= 3) result.score += 1;
|
|
99
|
+
if (characterTypes === 4) result.score += 1;
|
|
100
|
+
|
|
101
|
+
// Strength classification
|
|
102
|
+
if (result.score >= 8) {
|
|
103
|
+
result.strength = 'Very Strong';
|
|
104
|
+
} else if (result.score >= 6) {
|
|
105
|
+
result.strength = 'Strong';
|
|
106
|
+
} else if (result.score >= 4) {
|
|
107
|
+
result.strength = 'Good';
|
|
108
|
+
} else if (result.score >= 2) {
|
|
109
|
+
result.strength = 'Weak';
|
|
110
|
+
} else {
|
|
111
|
+
result.strength = 'Very Weak';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Final validation - require Good level or above (score >= 4) and no errors
|
|
115
|
+
// Explicitly reject Weak and Very Weak passwords
|
|
116
|
+
if (result.strength === 'Weak' || result.strength === 'Very Weak') {
|
|
117
|
+
result.errors.push(i18n.tSync('errors.password.strength_insufficient', result.strength));
|
|
118
|
+
result.suggestions.push(i18n.tSync('errors.password.suggest_longer'));
|
|
119
|
+
result.suggestions.push(i18n.tSync('errors.password.suggest_more_types'));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
result.valid = result.errors.length === 0 && result.score >= 4;
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get password requirements description for user guidance
|
|
129
|
+
* @returns {Array} - Array of requirement strings
|
|
130
|
+
*/
|
|
131
|
+
function getPasswordRequirements() {
|
|
132
|
+
return i18n.tSync('ui.general.password_requirements_list');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate example of a strong password for user reference
|
|
137
|
+
* @returns {string} - Example strong password
|
|
138
|
+
*/
|
|
139
|
+
function generatePasswordExample() {
|
|
140
|
+
const examples = [
|
|
141
|
+
'MyStr0ng!Pass',
|
|
142
|
+
'Secure#123Pass',
|
|
143
|
+
'Good$Pass456',
|
|
144
|
+
'Safe&Strong7',
|
|
145
|
+
'Best!Choice9'
|
|
146
|
+
];
|
|
147
|
+
return examples[Math.floor(Math.random() * examples.length)];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
validatePasswordStrength,
|
|
152
|
+
getPasswordRequirements,
|
|
153
|
+
generatePasswordExample
|
|
154
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Validation Module
|
|
3
|
+
* Handles all password verification scenarios
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { getPasswordInput } = require('./password-input');
|
|
7
|
+
const colors = require('../ui/colors');
|
|
8
|
+
const { validatePasswordStrength, getPasswordRequirements, generatePasswordExample } = require('./password-strength');
|
|
9
|
+
const i18n = require('../i18n');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Force cleanup stdin state to prevent navigation issues
|
|
13
|
+
*/
|
|
14
|
+
function forceStdinCleanup() {
|
|
15
|
+
try {
|
|
16
|
+
if (process.stdin.isTTY) {
|
|
17
|
+
process.stdin.setRawMode(false);
|
|
18
|
+
process.stdin.removeAllListeners('data');
|
|
19
|
+
process.stdin.removeAllListeners('keypress');
|
|
20
|
+
process.stdin.pause();
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// Ignore cleanup errors
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert strength string to translated version
|
|
29
|
+
* @param {string} strength - English strength string
|
|
30
|
+
* @returns {string} - Translated strength string
|
|
31
|
+
*/
|
|
32
|
+
function getTranslatedStrength(strength) {
|
|
33
|
+
const strengthMap = {
|
|
34
|
+
'Very Weak': 'very_weak',
|
|
35
|
+
'Weak': 'weak',
|
|
36
|
+
'Good': 'good',
|
|
37
|
+
'Strong': 'strong',
|
|
38
|
+
'Very Strong': 'very_strong'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const strengthKey = strengthMap[strength] || 'weak';
|
|
42
|
+
return i18n.tSync(`password.strength.${strengthKey}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Verify user password for export/import operations
|
|
47
|
+
* @param {Object} apiManager - The API manager instance
|
|
48
|
+
* @param {string} operation - The operation being performed (for error messages)
|
|
49
|
+
* @returns {Promise<boolean>} - True if password is verified, false otherwise
|
|
50
|
+
*/
|
|
51
|
+
async function verifyExportPassword(apiManager, operation = 'operation') {
|
|
52
|
+
try {
|
|
53
|
+
const password = await getPasswordInput(i18n.tSync('messages.prompts.enter_password'));
|
|
54
|
+
|
|
55
|
+
if (!password) {
|
|
56
|
+
forceStdinCleanup();
|
|
57
|
+
console.log(colors.red + i18n.tSync('errors.password.empty') + colors.reset);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!apiManager.verifyExportPassword(password)) {
|
|
62
|
+
forceStdinCleanup();
|
|
63
|
+
console.log(colors.red + '❌ ' + i18n.tSync('errors.password.verification_failed') + colors.reset);
|
|
64
|
+
console.log(colors.gray + i18n.tSync('errors.general.operation_failed', operation) + colors.reset);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
forceStdinCleanup();
|
|
71
|
+
if (error.message.includes('cancelled')) {
|
|
72
|
+
console.log(colors.yellow + '\n' + i18n.tSync('errors.general.cancelled_by_user').replace('{0}', operation) + colors.reset);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(colors.red + `❌ Password verification error: ${error.message}` + colors.reset);
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Verify current password before changing it
|
|
82
|
+
* @param {Object} apiManager - The API manager instance
|
|
83
|
+
* @returns {Promise<boolean>} - True if current password is verified, false otherwise
|
|
84
|
+
*/
|
|
85
|
+
async function verifyCurrentPassword(apiManager) {
|
|
86
|
+
try {
|
|
87
|
+
const currentPassword = await getPasswordInput(i18n.tSync('messages.prompts.enter_current_password'));
|
|
88
|
+
|
|
89
|
+
if (!currentPassword) {
|
|
90
|
+
forceStdinCleanup();
|
|
91
|
+
console.log(colors.red + i18n.tSync('errors.password.empty') + colors.reset);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!apiManager.verifyExportPassword(currentPassword)) {
|
|
96
|
+
forceStdinCleanup();
|
|
97
|
+
console.log(colors.red + '❌ ' + i18n.tSync('errors.password.current_incorrect') + colors.reset);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return true;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
forceStdinCleanup();
|
|
104
|
+
if (error.message.includes('cancelled')) {
|
|
105
|
+
console.log(colors.yellow + '\n' + i18n.tSync('errors.password.verification_cancelled') + colors.reset);
|
|
106
|
+
} else {
|
|
107
|
+
console.log(colors.red + `❌ Password verification error: ${error.message}` + colors.reset);
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Prompt user to set up a new password with validation
|
|
115
|
+
* @param {Object} apiManager - The API manager instance
|
|
116
|
+
* @param {boolean} isFirstTime - Whether this is the first time setting a password
|
|
117
|
+
* @returns {Promise<boolean>} - True if password is successfully set, false otherwise
|
|
118
|
+
*/
|
|
119
|
+
async function setupNewPassword(apiManager, isFirstTime = false) {
|
|
120
|
+
try {
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(colors.cyan + (isFirstTime ? i18n.tSync('password.setup.title') : i18n.tSync('password.setup.change_title')) + colors.reset);
|
|
123
|
+
console.log('');
|
|
124
|
+
|
|
125
|
+
if (!isFirstTime) {
|
|
126
|
+
console.log(colors.yellow + '⚠️ ' + i18n.tSync('password.setup.warning') + colors.reset);
|
|
127
|
+
console.log('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Display password requirements
|
|
131
|
+
console.log(colors.cyan + i18n.tSync('ui.general.password_requirements_title') + colors.reset);
|
|
132
|
+
const requirements = getPasswordRequirements();
|
|
133
|
+
requirements.forEach(req => {
|
|
134
|
+
console.log(colors.gray + ' ' + req + colors.reset);
|
|
135
|
+
});
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log(colors.gray + i18n.tSync('ui.general.example_strong_password', generatePasswordExample()) + colors.reset);
|
|
138
|
+
console.log('');
|
|
139
|
+
|
|
140
|
+
let attempts = 0;
|
|
141
|
+
const maxAttempts = 3;
|
|
142
|
+
|
|
143
|
+
while (attempts < maxAttempts) {
|
|
144
|
+
attempts++;
|
|
145
|
+
|
|
146
|
+
// Get new password
|
|
147
|
+
const passwordPrompt = i18n.tSync('ui.general.new_password_attempt', attempts, maxAttempts);
|
|
148
|
+
const newPassword = await getPasswordInput(passwordPrompt);
|
|
149
|
+
|
|
150
|
+
if (!newPassword) {
|
|
151
|
+
forceStdinCleanup();
|
|
152
|
+
console.log(colors.red + i18n.tSync('errors.password.empty') + colors.reset);
|
|
153
|
+
if (attempts < maxAttempts) {
|
|
154
|
+
console.log('');
|
|
155
|
+
continue;
|
|
156
|
+
} else {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Validate password strength
|
|
162
|
+
const validation = validatePasswordStrength(newPassword);
|
|
163
|
+
|
|
164
|
+
if (!validation.valid) {
|
|
165
|
+
console.log(colors.red + '❌ ' + i18n.tSync('errors.password.requirements_not_met') + colors.reset);
|
|
166
|
+
validation.errors.forEach(error => {
|
|
167
|
+
console.log(colors.red + ' • ' + error + colors.reset);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (validation.suggestions.length > 0) {
|
|
171
|
+
console.log(colors.yellow + '💡 ' + i18n.tSync('ui.general.suggestions') + colors.reset);
|
|
172
|
+
validation.suggestions.forEach(suggestion => {
|
|
173
|
+
console.log(colors.yellow + ' • ' + suggestion + colors.reset);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// If password is invalid, force display strength as "Weak" regardless of technical score
|
|
177
|
+
const strengthKey = validation.valid ? validation.strength : 'Weak';
|
|
178
|
+
const translatedStrength = getTranslatedStrength(strengthKey);
|
|
179
|
+
console.log(colors.gray + i18n.tSync('ui.general.current_password_strength', translatedStrength) + colors.reset);
|
|
180
|
+
|
|
181
|
+
if (attempts < maxAttempts) {
|
|
182
|
+
console.log('');
|
|
183
|
+
continue;
|
|
184
|
+
} else {
|
|
185
|
+
console.log(colors.red + i18n.tSync('ui.general.max_attempts_password_failed') + colors.reset);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Confirm password
|
|
191
|
+
const confirmPassword = await getPasswordInput(i18n.tSync('password.setup.confirm_password_prompt'));
|
|
192
|
+
|
|
193
|
+
if (newPassword !== confirmPassword) {
|
|
194
|
+
forceStdinCleanup();
|
|
195
|
+
console.log(colors.red + i18n.tSync('ui.general.passwords_mismatch') + colors.reset);
|
|
196
|
+
if (attempts < maxAttempts) {
|
|
197
|
+
console.log('');
|
|
198
|
+
continue;
|
|
199
|
+
} else {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Success - set the password
|
|
205
|
+
apiManager.setExportPassword(newPassword);
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(colors.green + '✓ ' + i18n.tSync('password.setup.password_success', getTranslatedStrength(validation.strength)) + colors.reset);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return false;
|
|
212
|
+
|
|
213
|
+
} catch (error) {
|
|
214
|
+
forceStdinCleanup();
|
|
215
|
+
if (error.message.includes('cancelled')) {
|
|
216
|
+
console.log(colors.yellow + '\n' + i18n.tSync('errors.password.setup_cancelled') + colors.reset);
|
|
217
|
+
} else {
|
|
218
|
+
console.log(colors.red + `❌ Failed to set password: ${error.message}` + colors.reset);
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Handle password change operation
|
|
226
|
+
* @param {Object} apiManager - The API manager instance
|
|
227
|
+
* @returns {Promise<boolean>} - True if password is successfully changed, false otherwise
|
|
228
|
+
*/
|
|
229
|
+
async function changePassword(apiManager) {
|
|
230
|
+
try {
|
|
231
|
+
// First verify current password
|
|
232
|
+
const currentVerified = await verifyCurrentPassword(apiManager);
|
|
233
|
+
if (!currentVerified) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log(colors.green + i18n.tSync('errors.password.current_password_verified') + colors.reset);
|
|
238
|
+
console.log('');
|
|
239
|
+
|
|
240
|
+
// Set new password
|
|
241
|
+
return await setupNewPassword(apiManager, false);
|
|
242
|
+
|
|
243
|
+
} catch (error) {
|
|
244
|
+
forceStdinCleanup();
|
|
245
|
+
console.log(colors.red + `❌ Password change failed: ${error.message}` + colors.reset);
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
verifyExportPassword,
|
|
252
|
+
verifyCurrentPassword,
|
|
253
|
+
setupNewPassword,
|
|
254
|
+
changePassword
|
|
255
|
+
};
|
package/lib/crypto.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto Module - Handles encryption and decryption for sensitive data
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate encryption key from machine-specific data
|
|
10
|
+
*/
|
|
11
|
+
function getEncryptionKey() {
|
|
12
|
+
const machineId = os.hostname() + os.userInfo().username + os.platform();
|
|
13
|
+
return crypto.pbkdf2Sync(machineId, 'claude-launcher-salt', 10000, 32, 'sha256');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Encrypt data using AES-256-CBC
|
|
18
|
+
* @param {string} plaintext - The text to encrypt
|
|
19
|
+
* @returns {object} Result object with success status and encrypted value or error
|
|
20
|
+
*/
|
|
21
|
+
function encrypt(plaintext) {
|
|
22
|
+
try {
|
|
23
|
+
const key = getEncryptionKey();
|
|
24
|
+
const iv = crypto.randomBytes(16);
|
|
25
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
26
|
+
|
|
27
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
28
|
+
encrypted += cipher.final('hex');
|
|
29
|
+
|
|
30
|
+
const result = iv.toString('hex') + ':' + encrypted;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
success: true,
|
|
34
|
+
value: result,
|
|
35
|
+
error: null
|
|
36
|
+
};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
value: null,
|
|
41
|
+
error: error.message
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decrypt data using AES-256-CBC
|
|
48
|
+
* @param {string} encryptedData - The encrypted data to decrypt
|
|
49
|
+
* @returns {object} Result object with success status and decrypted value or error
|
|
50
|
+
*/
|
|
51
|
+
function decrypt(encryptedData) {
|
|
52
|
+
try {
|
|
53
|
+
const key = getEncryptionKey();
|
|
54
|
+
|
|
55
|
+
const parts = encryptedData.split(':');
|
|
56
|
+
if (parts.length !== 2) {
|
|
57
|
+
throw new Error('Invalid encrypted data format');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
61
|
+
const encrypted = parts[1];
|
|
62
|
+
|
|
63
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
64
|
+
|
|
65
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
66
|
+
decrypted += decipher.final('utf8');
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
success: true,
|
|
70
|
+
value: decrypted,
|
|
71
|
+
error: null
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
value: null,
|
|
77
|
+
error: error.message
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
encrypt,
|
|
84
|
+
decrypt
|
|
85
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Formatter Module
|
|
3
|
+
* Handles dynamic content formatting with placeholders
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class MessageFormatter {
|
|
7
|
+
/**
|
|
8
|
+
* Format message with placeholders like {0}, {1}, etc.
|
|
9
|
+
* @param {string} template - Message template with placeholders
|
|
10
|
+
* @param {...any} args - Arguments to replace placeholders
|
|
11
|
+
* @returns {string} - Formatted message
|
|
12
|
+
*/
|
|
13
|
+
static format(template, ...args) {
|
|
14
|
+
if (!template) return '';
|
|
15
|
+
|
|
16
|
+
return template.replace(/\{(\d+)\}/g, (match, index) => {
|
|
17
|
+
const argIndex = parseInt(index);
|
|
18
|
+
return args[argIndex] !== undefined ? args[argIndex] : match;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handle plural forms (extension feature)
|
|
24
|
+
* @param {number} count - Count number
|
|
25
|
+
* @param {string} singular - Singular form
|
|
26
|
+
* @param {string} plural - Plural form
|
|
27
|
+
* @returns {string} - Appropriate form based on count
|
|
28
|
+
*/
|
|
29
|
+
static plural(count, singular, plural) {
|
|
30
|
+
return count === 1 ? singular : (plural || singular + 's');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format numbers according to locale (extension feature)
|
|
35
|
+
* @param {number} number - Number to format
|
|
36
|
+
* @param {string} langCode - Language code
|
|
37
|
+
* @returns {string} - Formatted number
|
|
38
|
+
*/
|
|
39
|
+
static formatNumber(number, langCode) {
|
|
40
|
+
const locale = langCode === 'zh' ? 'zh-CN' : 'en-US';
|
|
41
|
+
return new Intl.NumberFormat(locale).format(number);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format dates according to locale (extension feature)
|
|
46
|
+
* @param {string|Date} date - Date to format
|
|
47
|
+
* @param {string} langCode - Language code
|
|
48
|
+
* @returns {string} - Formatted date
|
|
49
|
+
*/
|
|
50
|
+
static formatDate(date, langCode) {
|
|
51
|
+
const locale = langCode === 'zh' ? 'zh-CN' : 'en-US';
|
|
52
|
+
return new Intl.DateTimeFormat(locale, {
|
|
53
|
+
year: 'numeric',
|
|
54
|
+
month: 'short',
|
|
55
|
+
day: 'numeric',
|
|
56
|
+
hour: '2-digit',
|
|
57
|
+
minute: '2-digit'
|
|
58
|
+
}).format(new Date(date));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = MessageFormatter;
|