shiva-code 0.7.2 → 0.7.3
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/dist/chunk-UAP4ZKEJ.js +1918 -0
- package/dist/chunk-UZKDLLPA.js +891 -0
- package/dist/hook-SZAVQGEA.js +11 -0
- package/dist/index.js +1469 -3904
- package/dist/login-ZEKMSKVH.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import {
|
|
2
|
+
colors,
|
|
3
|
+
log
|
|
4
|
+
} from "./chunk-Z6NXFC4Q.js";
|
|
5
|
+
import {
|
|
6
|
+
api
|
|
7
|
+
} from "./chunk-QQZRCJZK.js";
|
|
8
|
+
import {
|
|
9
|
+
clearAuth,
|
|
10
|
+
getConfig,
|
|
11
|
+
isAuthenticated,
|
|
12
|
+
setAuth
|
|
13
|
+
} from "./chunk-OP4HYQZZ.js";
|
|
14
|
+
import {
|
|
15
|
+
__require
|
|
16
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
17
|
+
|
|
18
|
+
// src/commands/auth/login.ts
|
|
19
|
+
import { Command } from "commander";
|
|
20
|
+
import inquirer2 from "inquirer";
|
|
21
|
+
|
|
22
|
+
// src/services/auth/auth.ts
|
|
23
|
+
import inquirer from "inquirer";
|
|
24
|
+
import open from "open";
|
|
25
|
+
import http from "http";
|
|
26
|
+
import { URL } from "url";
|
|
27
|
+
|
|
28
|
+
// src/services/auth/two-factor.ts
|
|
29
|
+
import * as crypto from "crypto";
|
|
30
|
+
function generateSecret(length = 20) {
|
|
31
|
+
const buffer = crypto.randomBytes(length);
|
|
32
|
+
return base32Encode(buffer);
|
|
33
|
+
}
|
|
34
|
+
function base32Encode(buffer) {
|
|
35
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
36
|
+
let result = "";
|
|
37
|
+
let bits = 0;
|
|
38
|
+
let value = 0;
|
|
39
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
40
|
+
value = value << 8 | buffer[i];
|
|
41
|
+
bits += 8;
|
|
42
|
+
while (bits >= 5) {
|
|
43
|
+
result += alphabet[value >>> bits - 5 & 31];
|
|
44
|
+
bits -= 5;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (bits > 0) {
|
|
48
|
+
result += alphabet[value << 5 - bits & 31];
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
function base32Decode(encoded) {
|
|
53
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
54
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
55
|
+
for (let i = 0; i < alphabet.length; i++) {
|
|
56
|
+
lookup.set(alphabet[i], i);
|
|
57
|
+
}
|
|
58
|
+
const bytes = [];
|
|
59
|
+
let bits = 0;
|
|
60
|
+
let value = 0;
|
|
61
|
+
for (const char of encoded.toUpperCase()) {
|
|
62
|
+
if (char === "=") continue;
|
|
63
|
+
const index = lookup.get(char);
|
|
64
|
+
if (index === void 0) continue;
|
|
65
|
+
value = value << 5 | index;
|
|
66
|
+
bits += 5;
|
|
67
|
+
if (bits >= 8) {
|
|
68
|
+
bytes.push(value >>> bits - 8 & 255);
|
|
69
|
+
bits -= 8;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return Buffer.from(bytes);
|
|
73
|
+
}
|
|
74
|
+
function generateTOTP(secret, timeStep = 30) {
|
|
75
|
+
const time = Math.floor(Date.now() / 1e3 / timeStep);
|
|
76
|
+
const key = base32Decode(secret);
|
|
77
|
+
const counter = Buffer.alloc(8);
|
|
78
|
+
counter.writeBigUInt64BE(BigInt(time));
|
|
79
|
+
const hmac = crypto.createHmac("sha1", key);
|
|
80
|
+
hmac.update(counter);
|
|
81
|
+
const hash = hmac.digest();
|
|
82
|
+
const offset = hash[hash.length - 1] & 15;
|
|
83
|
+
const code = ((hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255) % 1e6;
|
|
84
|
+
return code.toString().padStart(6, "0");
|
|
85
|
+
}
|
|
86
|
+
function verifyTOTP(secret, code, timeStep = 30) {
|
|
87
|
+
for (let i = -1; i <= 1; i++) {
|
|
88
|
+
const time = Math.floor(Date.now() / 1e3 / timeStep) + i;
|
|
89
|
+
const expectedCode = generateTOTPForTime(secret, time, timeStep);
|
|
90
|
+
if (expectedCode === code) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function generateTOTPForTime(secret, time, timeStep) {
|
|
97
|
+
const key = base32Decode(secret);
|
|
98
|
+
const counter = Buffer.alloc(8);
|
|
99
|
+
counter.writeBigUInt64BE(BigInt(time));
|
|
100
|
+
const hmac = crypto.createHmac("sha1", key);
|
|
101
|
+
hmac.update(counter);
|
|
102
|
+
const hash = hmac.digest();
|
|
103
|
+
const offset = hash[hash.length - 1] & 15;
|
|
104
|
+
const code = ((hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255) % 1e6;
|
|
105
|
+
return code.toString().padStart(6, "0");
|
|
106
|
+
}
|
|
107
|
+
function generateOtpAuthUrl(secret, email, issuer = "SHIVA Code") {
|
|
108
|
+
const encodedIssuer = encodeURIComponent(issuer);
|
|
109
|
+
const encodedEmail = encodeURIComponent(email);
|
|
110
|
+
return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;
|
|
111
|
+
}
|
|
112
|
+
function generateBackupCodes(count = 10) {
|
|
113
|
+
const codes = [];
|
|
114
|
+
for (let i = 0; i < count; i++) {
|
|
115
|
+
const buffer = crypto.randomBytes(6);
|
|
116
|
+
const code = buffer.toString("hex").toUpperCase();
|
|
117
|
+
codes.push(`${code.slice(0, 3)}-${code.slice(3, 6)}-${code.slice(6, 9)}-${code.slice(9, 12)}`);
|
|
118
|
+
}
|
|
119
|
+
return codes;
|
|
120
|
+
}
|
|
121
|
+
function generateDeviceFingerprint() {
|
|
122
|
+
const os = __require("os");
|
|
123
|
+
const components = [
|
|
124
|
+
os.hostname(),
|
|
125
|
+
os.platform(),
|
|
126
|
+
os.arch(),
|
|
127
|
+
os.cpus()[0]?.model || "unknown",
|
|
128
|
+
os.userInfo().username
|
|
129
|
+
];
|
|
130
|
+
const fingerprint = crypto.createHash("sha256").update(components.join("|")).digest("hex").slice(0, 32);
|
|
131
|
+
return fingerprint;
|
|
132
|
+
}
|
|
133
|
+
function getDeviceName() {
|
|
134
|
+
const os = __require("os");
|
|
135
|
+
const hostname = os.hostname();
|
|
136
|
+
const platform = os.platform();
|
|
137
|
+
const platformNames = {
|
|
138
|
+
darwin: "macOS",
|
|
139
|
+
linux: "Linux",
|
|
140
|
+
win32: "Windows"
|
|
141
|
+
};
|
|
142
|
+
return `${hostname} (${platformNames[platform] || platform})`;
|
|
143
|
+
}
|
|
144
|
+
var TwoFactorService = class {
|
|
145
|
+
/**
|
|
146
|
+
* Start 2FA setup - generate secret and QR code
|
|
147
|
+
*/
|
|
148
|
+
async startSetup(email) {
|
|
149
|
+
try {
|
|
150
|
+
const response = await api.setup2FA();
|
|
151
|
+
if (!response.setup) {
|
|
152
|
+
throw new Error(response.message || "2FA-Setup fehlgeschlagen");
|
|
153
|
+
}
|
|
154
|
+
return response.setup;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const secret = generateSecret();
|
|
157
|
+
const otpAuthUrl = generateOtpAuthUrl(secret, email);
|
|
158
|
+
const backupCodes = generateBackupCodes();
|
|
159
|
+
return {
|
|
160
|
+
secret,
|
|
161
|
+
qrCodeDataUrl: otpAuthUrl,
|
|
162
|
+
// In real implementation, would be a data URL
|
|
163
|
+
backupCodes
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Verify TOTP code and complete setup
|
|
169
|
+
*/
|
|
170
|
+
async verifySetup(code) {
|
|
171
|
+
try {
|
|
172
|
+
const response = await api.verify2FA(code);
|
|
173
|
+
return response.success;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Verify TOTP code during login
|
|
180
|
+
*/
|
|
181
|
+
async verifyCode(code) {
|
|
182
|
+
try {
|
|
183
|
+
const response = await api.verify2FACode(code);
|
|
184
|
+
return response.success;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Verify backup code
|
|
191
|
+
*/
|
|
192
|
+
async verifyBackupCode(code) {
|
|
193
|
+
try {
|
|
194
|
+
const response = await api.verifyBackupCode(code);
|
|
195
|
+
return response.success;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get 2FA status
|
|
202
|
+
*/
|
|
203
|
+
async getStatus() {
|
|
204
|
+
try {
|
|
205
|
+
const response = await api.get2FAStatus();
|
|
206
|
+
return response;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return {
|
|
209
|
+
enabled: false,
|
|
210
|
+
backupCodesRemaining: 0
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Disable 2FA
|
|
216
|
+
*/
|
|
217
|
+
async disable(code) {
|
|
218
|
+
try {
|
|
219
|
+
const response = await api.disable2FA(code);
|
|
220
|
+
return response.success;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Generate new backup codes
|
|
227
|
+
*/
|
|
228
|
+
async regenerateBackupCodes() {
|
|
229
|
+
try {
|
|
230
|
+
const response = await api.regenerateBackupCodes();
|
|
231
|
+
return response.codes;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Verify TOTP code locally (for testing)
|
|
238
|
+
*/
|
|
239
|
+
verifyLocal(secret, code) {
|
|
240
|
+
return verifyTOTP(secret, code);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Generate TOTP code locally (for testing)
|
|
244
|
+
*/
|
|
245
|
+
generateLocal(secret) {
|
|
246
|
+
return generateTOTP(secret);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var DeviceTrustService = class {
|
|
250
|
+
/**
|
|
251
|
+
* Get current device fingerprint
|
|
252
|
+
*/
|
|
253
|
+
getFingerprint() {
|
|
254
|
+
return generateDeviceFingerprint();
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Get current device name
|
|
258
|
+
*/
|
|
259
|
+
getDeviceName() {
|
|
260
|
+
return getDeviceName();
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Register current device as trusted
|
|
264
|
+
*/
|
|
265
|
+
async trustDevice(name) {
|
|
266
|
+
const fingerprint = this.getFingerprint();
|
|
267
|
+
const deviceName = name || this.getDeviceName();
|
|
268
|
+
try {
|
|
269
|
+
const response = await api.trustDevice({
|
|
270
|
+
name: deviceName,
|
|
271
|
+
fingerprint
|
|
272
|
+
});
|
|
273
|
+
return response.device;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if current device is trusted
|
|
280
|
+
*/
|
|
281
|
+
async isDeviceTrusted() {
|
|
282
|
+
const fingerprint = this.getFingerprint();
|
|
283
|
+
try {
|
|
284
|
+
const devices = await api.getDevices();
|
|
285
|
+
return devices.devices.some((d) => d.fingerprint === fingerprint);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* List all trusted devices
|
|
292
|
+
*/
|
|
293
|
+
async listDevices() {
|
|
294
|
+
try {
|
|
295
|
+
const response = await api.getDevices();
|
|
296
|
+
const currentFingerprint = this.getFingerprint();
|
|
297
|
+
return response.devices.map((d) => ({
|
|
298
|
+
...d,
|
|
299
|
+
isCurrent: d.fingerprint === currentFingerprint
|
|
300
|
+
}));
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Revoke a trusted device
|
|
307
|
+
*/
|
|
308
|
+
async revokeDevice(deviceId) {
|
|
309
|
+
try {
|
|
310
|
+
const response = await api.revokeDevice(deviceId);
|
|
311
|
+
return response.success;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Revoke all devices except current
|
|
318
|
+
*/
|
|
319
|
+
async revokeAllDevices() {
|
|
320
|
+
try {
|
|
321
|
+
const response = await api.revokeAllDevices();
|
|
322
|
+
return response.revokedCount;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
var twoFactorService = new TwoFactorService();
|
|
329
|
+
var deviceTrustService = new DeviceTrustService();
|
|
330
|
+
function displayQRCode(url) {
|
|
331
|
+
log.newline();
|
|
332
|
+
log.info("Scan diesen QR-Code mit deiner Authenticator App:");
|
|
333
|
+
log.newline();
|
|
334
|
+
log.dim("(QR-Code-Anzeige erfordert qrcode-terminal Paket)");
|
|
335
|
+
log.newline();
|
|
336
|
+
log.plain(`URL: ${url}`);
|
|
337
|
+
log.newline();
|
|
338
|
+
}
|
|
339
|
+
function displayBackupCodes(codes) {
|
|
340
|
+
log.newline();
|
|
341
|
+
console.log(colors.orange.bold("Backup-Codes (speichern!)"));
|
|
342
|
+
console.log(colors.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
343
|
+
log.newline();
|
|
344
|
+
for (let i = 0; i < codes.length; i++) {
|
|
345
|
+
console.log(` ${i + 1}. ${codes[i]}`);
|
|
346
|
+
}
|
|
347
|
+
log.newline();
|
|
348
|
+
log.warn("Diese Codes k\xF6nnen nur einmal verwendet werden!");
|
|
349
|
+
log.dim("Speichere sie an einem sicheren Ort.");
|
|
350
|
+
log.newline();
|
|
351
|
+
}
|
|
352
|
+
function displayDevices(devices) {
|
|
353
|
+
log.newline();
|
|
354
|
+
console.log(colors.orange.bold("Vertrauensw\xFCrdige Ger\xE4te"));
|
|
355
|
+
console.log(colors.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
356
|
+
log.newline();
|
|
357
|
+
if (devices.length === 0) {
|
|
358
|
+
log.dim("Keine Ger\xE4te registriert");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
for (const device of devices) {
|
|
362
|
+
const current = device.isCurrent ? colors.green(" (aktuell)") : "";
|
|
363
|
+
console.log(` ${device.name}${current}`);
|
|
364
|
+
log.dim(` ID: ${device.id}`);
|
|
365
|
+
log.dim(` Zuletzt: ${formatRelativeTime(device.lastUsed)}`);
|
|
366
|
+
if (device.expiresAt) {
|
|
367
|
+
log.dim(` L\xE4uft ab: ${formatRelativeTime(device.expiresAt)}`);
|
|
368
|
+
} else {
|
|
369
|
+
log.dim(" L\xE4uft ab: nie");
|
|
370
|
+
}
|
|
371
|
+
log.newline();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function formatRelativeTime(dateStr) {
|
|
375
|
+
const date = new Date(dateStr);
|
|
376
|
+
const now = /* @__PURE__ */ new Date();
|
|
377
|
+
const diffMs = now.getTime() - date.getTime();
|
|
378
|
+
const diffSec = Math.floor(diffMs / 1e3);
|
|
379
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
380
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
381
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
382
|
+
if (diffSec < 60) return "gerade eben";
|
|
383
|
+
if (diffMin < 60) return `vor ${diffMin} Minuten`;
|
|
384
|
+
if (diffHour < 24) return `vor ${diffHour} Stunden`;
|
|
385
|
+
if (diffDay < 30) return `vor ${diffDay} Tagen`;
|
|
386
|
+
return date.toLocaleDateString("de-DE");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/services/auth/auth.ts
|
|
390
|
+
var PORT_RANGE_START = 9876;
|
|
391
|
+
var PORT_RANGE_END = 9886;
|
|
392
|
+
async function loginWithOtp(email) {
|
|
393
|
+
try {
|
|
394
|
+
log.info(`Sende Login-Code an ${email}...`);
|
|
395
|
+
const otpResponse = await api.requestOtp(email);
|
|
396
|
+
if (!otpResponse.success) {
|
|
397
|
+
log.error(otpResponse.message || "Fehler beim Senden des Codes");
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
log.success("Code wurde gesendet!");
|
|
401
|
+
log.dim(`Verbleibende Versuche: ${otpResponse.remainingAttempts}`);
|
|
402
|
+
log.newline();
|
|
403
|
+
const { otp } = await inquirer.prompt([
|
|
404
|
+
{
|
|
405
|
+
type: "input",
|
|
406
|
+
name: "otp",
|
|
407
|
+
message: "Code eingeben:",
|
|
408
|
+
validate: (input) => {
|
|
409
|
+
if (!/^\d{6}$/.test(input)) {
|
|
410
|
+
return "Bitte gib den 6-stelligen Code ein";
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
]);
|
|
416
|
+
const authResponse = await api.verifyOtp(otpResponse.token, otp);
|
|
417
|
+
if (!authResponse.success || !authResponse.token || !authResponse.user) {
|
|
418
|
+
log.error(authResponse.message || "Ung\xFCltiger Code");
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
setAuth(authResponse.token, authResponse.user);
|
|
422
|
+
try {
|
|
423
|
+
const tfaStatus = await twoFactorService.getStatus();
|
|
424
|
+
if (tfaStatus.enabled) {
|
|
425
|
+
log.newline();
|
|
426
|
+
log.info("2FA ist aktiviert. Bitte verifiziere dich.");
|
|
427
|
+
log.newline();
|
|
428
|
+
const isDeviceTrusted = await deviceTrustService.isDeviceTrusted();
|
|
429
|
+
if (!isDeviceTrusted) {
|
|
430
|
+
const verified = await prompt2FAVerification();
|
|
431
|
+
if (!verified) {
|
|
432
|
+
clearAuth();
|
|
433
|
+
log.error("2FA-Verifizierung fehlgeschlagen");
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
const { trustDevice } = await inquirer.prompt([{
|
|
437
|
+
type: "confirm",
|
|
438
|
+
name: "trustDevice",
|
|
439
|
+
message: "Diesem Ger\xE4t vertrauen? (Kein 2FA bei zuk\xFCnftigen Logins)",
|
|
440
|
+
default: true
|
|
441
|
+
}]);
|
|
442
|
+
if (trustDevice) {
|
|
443
|
+
try {
|
|
444
|
+
await deviceTrustService.trustDevice();
|
|
445
|
+
log.success("Ger\xE4t als vertrauensw\xFCrdig markiert");
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
log.dim("Ger\xE4t ist vertrauensw\xFCrdig - 2FA \xFCbersprungen");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
log.newline();
|
|
456
|
+
log.success(`Angemeldet als ${authResponse.user.email} (${authResponse.user.tier.toUpperCase()})`);
|
|
457
|
+
return true;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
|
460
|
+
log.error(`Login fehlgeschlagen: ${message}`);
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function findAvailablePort() {
|
|
465
|
+
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
|
466
|
+
const isAvailable = await checkPort(port);
|
|
467
|
+
if (isAvailable) {
|
|
468
|
+
return port;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
throw new Error("Kein freier Port f\xFCr Callback-Server gefunden");
|
|
472
|
+
}
|
|
473
|
+
function checkPort(port) {
|
|
474
|
+
return new Promise((resolve) => {
|
|
475
|
+
const server = http.createServer();
|
|
476
|
+
server.once("error", () => resolve(false));
|
|
477
|
+
server.once("listening", () => {
|
|
478
|
+
server.close();
|
|
479
|
+
resolve(true);
|
|
480
|
+
});
|
|
481
|
+
server.listen(port, "127.0.0.1");
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function getCallbackHtml(type, data) {
|
|
485
|
+
const baseStyles = `
|
|
486
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
487
|
+
body {
|
|
488
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
489
|
+
min-height: 100vh;
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
justify-content: center;
|
|
493
|
+
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%);
|
|
494
|
+
color: #fff;
|
|
495
|
+
}
|
|
496
|
+
.container {
|
|
497
|
+
text-align: center;
|
|
498
|
+
padding: 3rem;
|
|
499
|
+
max-width: 420px;
|
|
500
|
+
}
|
|
501
|
+
.logo {
|
|
502
|
+
width: 80px;
|
|
503
|
+
height: 80px;
|
|
504
|
+
margin: 0 auto 1.5rem;
|
|
505
|
+
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
|
506
|
+
border-radius: 20px;
|
|
507
|
+
display: flex;
|
|
508
|
+
align-items: center;
|
|
509
|
+
justify-content: center;
|
|
510
|
+
box-shadow: 0 10px 40px rgba(249, 115, 22, 0.3);
|
|
511
|
+
}
|
|
512
|
+
.logo svg {
|
|
513
|
+
width: 48px;
|
|
514
|
+
height: 48px;
|
|
515
|
+
}
|
|
516
|
+
h1 {
|
|
517
|
+
font-size: 1.75rem;
|
|
518
|
+
font-weight: 700;
|
|
519
|
+
margin-bottom: 0.75rem;
|
|
520
|
+
}
|
|
521
|
+
.subtitle {
|
|
522
|
+
color: #9ca3af;
|
|
523
|
+
font-size: 1rem;
|
|
524
|
+
line-height: 1.5;
|
|
525
|
+
}
|
|
526
|
+
.email {
|
|
527
|
+
color: #f97316;
|
|
528
|
+
font-weight: 600;
|
|
529
|
+
}
|
|
530
|
+
.status-icon {
|
|
531
|
+
width: 64px;
|
|
532
|
+
height: 64px;
|
|
533
|
+
margin: 0 auto 1.5rem;
|
|
534
|
+
border-radius: 50%;
|
|
535
|
+
display: flex;
|
|
536
|
+
align-items: center;
|
|
537
|
+
justify-content: center;
|
|
538
|
+
}
|
|
539
|
+
.status-icon.success {
|
|
540
|
+
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
|
541
|
+
box-shadow: 0 10px 40px rgba(34, 197, 94, 0.3);
|
|
542
|
+
}
|
|
543
|
+
.status-icon.error {
|
|
544
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
545
|
+
box-shadow: 0 10px 40px rgba(239, 68, 68, 0.3);
|
|
546
|
+
}
|
|
547
|
+
.status-icon svg {
|
|
548
|
+
width: 32px;
|
|
549
|
+
height: 32px;
|
|
550
|
+
}
|
|
551
|
+
.spinner {
|
|
552
|
+
width: 48px;
|
|
553
|
+
height: 48px;
|
|
554
|
+
margin: 0 auto 1.5rem;
|
|
555
|
+
border: 3px solid rgba(249, 115, 22, 0.2);
|
|
556
|
+
border-top-color: #f97316;
|
|
557
|
+
border-radius: 50%;
|
|
558
|
+
animation: spin 1s linear infinite;
|
|
559
|
+
}
|
|
560
|
+
@keyframes spin {
|
|
561
|
+
to { transform: rotate(360deg); }
|
|
562
|
+
}
|
|
563
|
+
.hint {
|
|
564
|
+
margin-top: 1.5rem;
|
|
565
|
+
padding-top: 1.5rem;
|
|
566
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
567
|
+
color: #6b7280;
|
|
568
|
+
font-size: 0.875rem;
|
|
569
|
+
}
|
|
570
|
+
`;
|
|
571
|
+
const shivaLogo = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`;
|
|
572
|
+
const checkIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
573
|
+
const xIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
|
574
|
+
if (type === "success") {
|
|
575
|
+
return `<!DOCTYPE html>
|
|
576
|
+
<html lang="de">
|
|
577
|
+
<head>
|
|
578
|
+
<meta charset="UTF-8">
|
|
579
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
580
|
+
<title>SHIVA CLI - Angemeldet</title>
|
|
581
|
+
<style>${baseStyles}</style>
|
|
582
|
+
</head>
|
|
583
|
+
<body>
|
|
584
|
+
<div class="container">
|
|
585
|
+
<div class="status-icon success">${checkIcon}</div>
|
|
586
|
+
<h1>Erfolgreich angemeldet!</h1>
|
|
587
|
+
<p class="subtitle">Willkommen zur\xFCck, <span class="email">${data.email}</span></p>
|
|
588
|
+
<p class="hint">Du kannst dieses Fenster jetzt schlie\xDFen und zum Terminal zur\xFCckkehren.</p>
|
|
589
|
+
</div>
|
|
590
|
+
<script>setTimeout(() => window.close(), 3000);</script>
|
|
591
|
+
</body>
|
|
592
|
+
</html>`;
|
|
593
|
+
}
|
|
594
|
+
if (type === "error") {
|
|
595
|
+
return `<!DOCTYPE html>
|
|
596
|
+
<html lang="de">
|
|
597
|
+
<head>
|
|
598
|
+
<meta charset="UTF-8">
|
|
599
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
600
|
+
<title>SHIVA CLI - Fehler</title>
|
|
601
|
+
<style>${baseStyles}</style>
|
|
602
|
+
</head>
|
|
603
|
+
<body>
|
|
604
|
+
<div class="container">
|
|
605
|
+
<div class="status-icon error">${xIcon}</div>
|
|
606
|
+
<h1>Anmeldung fehlgeschlagen</h1>
|
|
607
|
+
<p class="subtitle">${data.message || "Ein unbekannter Fehler ist aufgetreten."}</p>
|
|
608
|
+
<p class="hint">Du kannst dieses Fenster schlie\xDFen und es erneut versuchen.</p>
|
|
609
|
+
</div>
|
|
610
|
+
</body>
|
|
611
|
+
</html>`;
|
|
612
|
+
}
|
|
613
|
+
return `<!DOCTYPE html>
|
|
614
|
+
<html lang="de">
|
|
615
|
+
<head>
|
|
616
|
+
<meta charset="UTF-8">
|
|
617
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
618
|
+
<title>SHIVA CLI</title>
|
|
619
|
+
<style>${baseStyles}</style>
|
|
620
|
+
</head>
|
|
621
|
+
<body>
|
|
622
|
+
<div class="container">
|
|
623
|
+
<div class="logo">${shivaLogo}</div>
|
|
624
|
+
<div class="spinner"></div>
|
|
625
|
+
<h1>SHIVA CLI</h1>
|
|
626
|
+
<p class="subtitle">Warte auf Login-Callback...</p>
|
|
627
|
+
</div>
|
|
628
|
+
</body>
|
|
629
|
+
</html>`;
|
|
630
|
+
}
|
|
631
|
+
function startCallbackServer(port) {
|
|
632
|
+
return new Promise((resolve, reject) => {
|
|
633
|
+
const server = http.createServer((req, res) => {
|
|
634
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
635
|
+
if (url.pathname === "/callback") {
|
|
636
|
+
const token = url.searchParams.get("token");
|
|
637
|
+
const userId = url.searchParams.get("userId");
|
|
638
|
+
const email = url.searchParams.get("email");
|
|
639
|
+
const tier = url.searchParams.get("tier");
|
|
640
|
+
const error = url.searchParams.get("error");
|
|
641
|
+
if (error) {
|
|
642
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
643
|
+
res.end(getCallbackHtml("error", { message: error }));
|
|
644
|
+
server.close();
|
|
645
|
+
reject(new Error(error));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (!token || !userId || !email) {
|
|
649
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
650
|
+
res.end(getCallbackHtml("error", { message: "Token oder Benutzer-Daten fehlen." }));
|
|
651
|
+
server.close();
|
|
652
|
+
reject(new Error("Ung\xFCltige Callback-Parameter"));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
656
|
+
res.end(getCallbackHtml("success", { email }));
|
|
657
|
+
server.close();
|
|
658
|
+
resolve({
|
|
659
|
+
token,
|
|
660
|
+
user: {
|
|
661
|
+
id: parseInt(userId, 10),
|
|
662
|
+
email,
|
|
663
|
+
tier: tier || "free"
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
669
|
+
res.end(getCallbackHtml("waiting", {}));
|
|
670
|
+
});
|
|
671
|
+
const timeout = setTimeout(() => {
|
|
672
|
+
server.close();
|
|
673
|
+
reject(new Error("Login-Timeout - keine Antwort erhalten"));
|
|
674
|
+
}, 12e4);
|
|
675
|
+
server.once("close", () => {
|
|
676
|
+
clearTimeout(timeout);
|
|
677
|
+
});
|
|
678
|
+
server.once("error", (err) => {
|
|
679
|
+
clearTimeout(timeout);
|
|
680
|
+
reject(err);
|
|
681
|
+
});
|
|
682
|
+
server.listen(port, "127.0.0.1");
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
async function loginWithBrowser() {
|
|
686
|
+
const config = getConfig();
|
|
687
|
+
try {
|
|
688
|
+
const port = await findAvailablePort();
|
|
689
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
690
|
+
const baseUrl = config.apiEndpoint.replace("/api", "");
|
|
691
|
+
const loginUrl = `${baseUrl}/auth/cli-login?callback=${encodeURIComponent(callbackUrl)}`;
|
|
692
|
+
const serverPromise = startCallbackServer(port);
|
|
693
|
+
log.newline();
|
|
694
|
+
log.info("\xD6ffne Browser zur Anmeldung...");
|
|
695
|
+
try {
|
|
696
|
+
await open(loginUrl);
|
|
697
|
+
} catch {
|
|
698
|
+
}
|
|
699
|
+
log.newline();
|
|
700
|
+
log.plain("Bitte \xF6ffne diese URL in deinem Browser um die Anmeldung abzuschlie\xDFen:");
|
|
701
|
+
log.newline();
|
|
702
|
+
console.log(` ${colors.cyan(loginUrl)}`);
|
|
703
|
+
log.newline();
|
|
704
|
+
log.dim("Warte auf Anmeldung... (Timeout: 2 Minuten)");
|
|
705
|
+
log.newline();
|
|
706
|
+
const result = await serverPromise;
|
|
707
|
+
setAuth(result.token, result.user);
|
|
708
|
+
try {
|
|
709
|
+
const tfaStatus = await twoFactorService.getStatus();
|
|
710
|
+
if (tfaStatus.enabled) {
|
|
711
|
+
log.newline();
|
|
712
|
+
log.info("2FA ist aktiviert. Bitte verifiziere dich.");
|
|
713
|
+
log.newline();
|
|
714
|
+
const isDeviceTrusted = await deviceTrustService.isDeviceTrusted();
|
|
715
|
+
if (!isDeviceTrusted) {
|
|
716
|
+
const verified = await prompt2FAVerification();
|
|
717
|
+
if (!verified) {
|
|
718
|
+
clearAuth();
|
|
719
|
+
log.error("2FA-Verifizierung fehlgeschlagen");
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
const { trustDevice } = await inquirer.prompt([{
|
|
723
|
+
type: "confirm",
|
|
724
|
+
name: "trustDevice",
|
|
725
|
+
message: "Diesem Ger\xE4t vertrauen?",
|
|
726
|
+
default: true
|
|
727
|
+
}]);
|
|
728
|
+
if (trustDevice) {
|
|
729
|
+
try {
|
|
730
|
+
await deviceTrustService.trustDevice();
|
|
731
|
+
log.success("Ger\xE4t als vertrauensw\xFCrdig markiert");
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
} else {
|
|
736
|
+
log.dim("Ger\xE4t ist vertrauensw\xFCrdig - 2FA \xFCbersprungen");
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
} catch {
|
|
740
|
+
}
|
|
741
|
+
log.success(`Angemeldet als ${result.user.email} (${result.user.tier.toUpperCase()})`);
|
|
742
|
+
return true;
|
|
743
|
+
} catch (error) {
|
|
744
|
+
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
|
745
|
+
log.error(`Login fehlgeschlagen: ${message}`);
|
|
746
|
+
log.newline();
|
|
747
|
+
log.info("Fallback: Manueller Token-Eintrag");
|
|
748
|
+
const loginUrl = `${config.apiEndpoint.replace("/api", "")}/auth/cli-login`;
|
|
749
|
+
log.dim(`URL: ${loginUrl}`);
|
|
750
|
+
log.newline();
|
|
751
|
+
const { useManual } = await inquirer.prompt([{
|
|
752
|
+
type: "confirm",
|
|
753
|
+
name: "useManual",
|
|
754
|
+
message: "Token manuell eingeben?",
|
|
755
|
+
default: true
|
|
756
|
+
}]);
|
|
757
|
+
if (!useManual) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
await open(loginUrl);
|
|
762
|
+
} catch {
|
|
763
|
+
log.dim("Browser konnte nicht ge\xF6ffnet werden");
|
|
764
|
+
log.dim(`\xD6ffne manuell: ${loginUrl}`);
|
|
765
|
+
}
|
|
766
|
+
const { token } = await inquirer.prompt([{
|
|
767
|
+
type: "password",
|
|
768
|
+
name: "token",
|
|
769
|
+
message: "Token:",
|
|
770
|
+
mask: "*"
|
|
771
|
+
}]);
|
|
772
|
+
if (!token) {
|
|
773
|
+
log.error("Kein Token eingegeben");
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
setAuth(token, { id: 0, email: "", tier: "free" });
|
|
777
|
+
try {
|
|
778
|
+
const response = await api.getCurrentUser();
|
|
779
|
+
setAuth(token, response.user);
|
|
780
|
+
log.success(`Angemeldet als ${response.user.email}`);
|
|
781
|
+
return true;
|
|
782
|
+
} catch {
|
|
783
|
+
clearAuth();
|
|
784
|
+
log.error("Ung\xFCltiger Token");
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function logout() {
|
|
790
|
+
clearAuth();
|
|
791
|
+
log.success("Abgemeldet");
|
|
792
|
+
}
|
|
793
|
+
async function prompt2FAVerification() {
|
|
794
|
+
const MAX_ATTEMPTS = 3;
|
|
795
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
796
|
+
const { method } = await inquirer.prompt([{
|
|
797
|
+
type: "list",
|
|
798
|
+
name: "method",
|
|
799
|
+
message: "Verifizierungsmethode:",
|
|
800
|
+
choices: [
|
|
801
|
+
{ name: "Authenticator App Code", value: "totp" },
|
|
802
|
+
{ name: "Backup Code", value: "backup" },
|
|
803
|
+
{ name: "Abbrechen", value: "cancel" }
|
|
804
|
+
]
|
|
805
|
+
}]);
|
|
806
|
+
if (method === "cancel") {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
const promptMessage = method === "totp" ? "6-stelliger Code aus deiner Authenticator App:" : "Backup-Code (Format: XXX-XXX-XXX-XXX):";
|
|
810
|
+
const { code } = await inquirer.prompt([{
|
|
811
|
+
type: "input",
|
|
812
|
+
name: "code",
|
|
813
|
+
message: promptMessage,
|
|
814
|
+
validate: (input) => {
|
|
815
|
+
if (method === "totp") {
|
|
816
|
+
return /^\d{6}$/.test(input) || "Bitte gib einen 6-stelligen Code ein";
|
|
817
|
+
}
|
|
818
|
+
return input.length > 0 || "Bitte gib einen Backup-Code ein";
|
|
819
|
+
}
|
|
820
|
+
}]);
|
|
821
|
+
try {
|
|
822
|
+
let verified = false;
|
|
823
|
+
if (method === "totp") {
|
|
824
|
+
verified = await twoFactorService.verifyCode(code);
|
|
825
|
+
} else {
|
|
826
|
+
verified = await twoFactorService.verifyBackupCode(code);
|
|
827
|
+
}
|
|
828
|
+
if (verified) {
|
|
829
|
+
log.success("2FA-Verifizierung erfolgreich");
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
} catch (error) {
|
|
833
|
+
}
|
|
834
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
835
|
+
log.error(`Ung\xFCltiger Code. Noch ${MAX_ATTEMPTS - attempt} Versuche.`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/commands/auth/login.ts
|
|
842
|
+
var loginCommand = new Command("login").description("Mit shiva.li anmelden").option("-e, --email <email>", "Email-Adresse f\xFCr OTP-Login").option("--otp", "OTP-Login statt Browser verwenden").action(async (options) => {
|
|
843
|
+
if (isAuthenticated()) {
|
|
844
|
+
const config = getConfig();
|
|
845
|
+
log.info(`Bereits angemeldet als ${config.email}`);
|
|
846
|
+
const { relogin } = await inquirer2.prompt([
|
|
847
|
+
{
|
|
848
|
+
type: "confirm",
|
|
849
|
+
name: "relogin",
|
|
850
|
+
message: "M\xF6chtest du dich neu anmelden?",
|
|
851
|
+
default: false
|
|
852
|
+
}
|
|
853
|
+
]);
|
|
854
|
+
if (!relogin) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
log.brand();
|
|
859
|
+
if (options.email) {
|
|
860
|
+
await loginWithOtp(options.email);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (options.otp) {
|
|
864
|
+
const { email } = await inquirer2.prompt([
|
|
865
|
+
{
|
|
866
|
+
type: "input",
|
|
867
|
+
name: "email",
|
|
868
|
+
message: "Email-Adresse:",
|
|
869
|
+
validate: (input) => {
|
|
870
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)) {
|
|
871
|
+
return "Bitte gib eine g\xFCltige Email-Adresse ein";
|
|
872
|
+
}
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
]);
|
|
877
|
+
await loginWithOtp(email);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
await loginWithBrowser();
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
export {
|
|
884
|
+
twoFactorService,
|
|
885
|
+
deviceTrustService,
|
|
886
|
+
displayQRCode,
|
|
887
|
+
displayBackupCodes,
|
|
888
|
+
displayDevices,
|
|
889
|
+
logout,
|
|
890
|
+
loginCommand
|
|
891
|
+
};
|