@startanaicompany/cli 1.4.14 → 1.4.16

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/CLAUDE.md CHANGED
@@ -395,6 +395,101 @@ saac deploy --force
395
395
 
396
396
  **Note:** The `--force` flag is defined in the CLI but not currently used by the API.
397
397
 
398
+ ### OTP-Based Login (NEW in 1.4.16)
399
+
400
+ The login command now supports two authentication methods:
401
+
402
+ **Method 1: Login with API Key (Fast Path)**
403
+ ```bash
404
+ saac login -e user@example.com -k cw_abc123
405
+ # → Immediate session token, no email verification needed
406
+ ```
407
+
408
+ **Method 2: Login with OTP (Recovery Path)**
409
+ ```bash
410
+ # Step 1: Request OTP
411
+ saac login -e user@example.com
412
+ # → Verification code sent to email (6 digits, 5-minute expiration)
413
+
414
+ # Step 2: Verify OTP
415
+ saac login -e user@example.com --otp 123456
416
+ # → Session token created, user is now logged in
417
+ ```
418
+
419
+ **Why This Feature?**
420
+
421
+ Solves the **API key lockout problem**:
422
+ - User loses API key
423
+ - All sessions expire
424
+ - Cannot login (no API key)
425
+ - Cannot regenerate key (requires login)
426
+ - **LOCKED OUT** ❌
427
+
428
+ With OTP login:
429
+ - User can always recover via email ✅
430
+ - No support tickets needed ✅
431
+ - Self-service account recovery ✅
432
+
433
+ **Implementation Details:**
434
+
435
+ The `login.js` command now has three modes:
436
+ 1. **API key login** - If `-k` flag provided (existing flow)
437
+ 2. **OTP request** - If no `-k` or `--otp` flag (new flow)
438
+ 3. **OTP verification** - If `--otp` flag provided (new flow)
439
+
440
+ **Backend Requirements:**
441
+ - `POST /api/v1/auth/login-otp` - Generate and send OTP
442
+ - `POST /api/v1/auth/verify-otp` - Verify OTP and create session
443
+
444
+ See `/data/sharedinfo/login-feature-update.md` for complete API specifications.
445
+
446
+ ### API Key Management (NEW in 1.4.16)
447
+
448
+ Users can now regenerate their API keys if lost or compromised.
449
+
450
+ **Commands:**
451
+
452
+ ```bash
453
+ # Regenerate API key (requires authentication)
454
+ saac keys regenerate
455
+ # → Shows new API key (only once!)
456
+
457
+ # Show API key info
458
+ saac keys show
459
+ # → Displays key prefix, created date, last used
460
+ ```
461
+
462
+ **Recovery Flow:**
463
+
464
+ ```bash
465
+ # 1. User loses API key but is not logged in
466
+ saac login -e user@example.com
467
+ # → OTP sent to email
468
+
469
+ # 2. Verify OTP
470
+ saac login -e user@example.com --otp 123456
471
+ # → Logged in with session token
472
+
473
+ # 3. Generate new API key
474
+ saac keys regenerate
475
+ # → New API key: cw_new_key_xyz...
476
+
477
+ # 4. On next machine, use new API key
478
+ saac login -e user@example.com -k cw_new_key_xyz...
479
+ ```
480
+
481
+ **Security Notes:**
482
+ - Regenerating API key invalidates the old key immediately
483
+ - Existing session tokens remain valid (no disruption)
484
+ - Email notification sent when key is regenerated
485
+ - Full API key shown only once (must be saved)
486
+
487
+ **Backend Requirements:**
488
+ - `POST /api/v1/users/regenerate-key` - Generate new API key
489
+ - `GET /api/v1/users/api-key` - Get API key info (optional)
490
+
491
+ See `/data/sharedinfo/login-feature-update.md` for complete specifications.
492
+
398
493
  ### Init Command Implementation
399
494
 
400
495
  The `init` command links an existing SAAC application to the current directory.
@@ -493,19 +588,26 @@ The wrapper API expects Git repositories to be hosted on the StartAnAiCompany Gi
493
588
  - During registration, Gitea username can be auto-detected or manually provided
494
589
  - Applications reference repositories in the format: `git@git.startanaicompany.com:user/repo.git`
495
590
 
496
- ## Current Status - Version 1.4.14
591
+ ## Current Status - Version 1.4.16
497
592
 
498
593
  ### Completed Features
499
594
 
500
595
  **Authentication & Sessions:**
501
596
  - ✅ `saac register` - Register with email only (git_username auto-detected from email)
502
- - ✅ `saac login` - Login with email + API key, receives 1-year session token
597
+ - ✅ `saac login` - Two methods: API key (fast) or OTP (recovery)
598
+ - `saac login -e email -k api-key` - Login with API key
599
+ - `saac login -e email` - Request OTP via email
600
+ - `saac login -e email --otp code` - Verify OTP and login
503
601
  - ✅ `saac verify` - Email verification, shows FULL API key for user to save
504
602
  - ✅ `saac logout` - Logout from current device
505
603
  - ✅ `saac logout-all` - Revoke all sessions
506
604
  - ✅ `saac sessions` - List all active sessions
507
605
  - ✅ `saac whoami` - Show current user info
508
606
 
607
+ **API Key Management (NEW in 1.4.16):**
608
+ - ✅ `saac keys regenerate` - Generate new API key (invalidates old one)
609
+ - ✅ `saac keys show` - Show API key information (prefix, created, last used)
610
+
509
611
  **Git OAuth (NEW in 1.4.0):**
510
612
  - ✅ `saac git connect [host]` - OAuth flow for Git authentication
511
613
  - ✅ `saac git list` - List connected Git accounts
@@ -706,4 +808,4 @@ Before publishing to npm:
706
808
  - `dotenv` - Environment variables
707
809
  - `open` - Open browser for OAuth (v8.4.2 for compatibility with chalk v4)
708
810
 
709
- **Version:** 1.4.14 (current)
811
+ **Version:** 1.4.16 (current)
package/bin/saac.js CHANGED
@@ -16,6 +16,7 @@ const verify = require('../src/commands/verify');
16
16
  const logout = require('../src/commands/logout');
17
17
  const logoutAll = require('../src/commands/logoutAll');
18
18
  const sessions = require('../src/commands/sessions');
19
+ const keys = require('../src/commands/keys');
19
20
  const git = require('../src/commands/git');
20
21
  const init = require('../src/commands/init');
21
22
  const create = require('../src/commands/create');
@@ -56,7 +57,8 @@ program
56
57
  .command('login')
57
58
  .description('Login with existing account')
58
59
  .option('-e, --email <email>', 'Email address')
59
- .option('-k, --api-key <key>', 'API key')
60
+ .option('-k, --api-key <key>', 'API key (for fast login)')
61
+ .option('--otp <code>', 'One-time password from email')
60
62
  .action(login);
61
63
 
62
64
  program
@@ -80,6 +82,22 @@ program
80
82
  .description('List all active sessions')
81
83
  .action(sessions);
82
84
 
85
+ // API Key management
86
+ const keysCommand = program
87
+ .command('keys')
88
+ .description('Manage API keys');
89
+
90
+ keysCommand
91
+ .command('regenerate')
92
+ .description('Generate a new API key (invalidates old one)')
93
+ .action(keys.regenerate);
94
+
95
+ keysCommand
96
+ .command('show')
97
+ .alias('info')
98
+ .description('Show API key information')
99
+ .action(keys.show);
100
+
83
101
  // Git OAuth commands
84
102
  const gitCommand = program
85
103
  .command('git')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.4.14",
3
+ "version": "1.4.16",
4
4
  "description": "Official CLI for StartAnAiCompany.com - Deploy AI recruitment sites with ease",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -3,10 +3,11 @@
3
3
  */
4
4
 
5
5
  const api = require('../lib/api');
6
- const { isAuthenticated, saveProjectConfig, getUser } = require('../lib/config');
6
+ const { isAuthenticated, saveProjectConfig, getUser, getProjectConfig } = require('../lib/config');
7
7
  const logger = require('../lib/logger');
8
8
  const oauth = require('../lib/oauth');
9
9
  const inquirer = require('inquirer');
10
+ const { execSync } = require('child_process');
10
11
 
11
12
  async function create(name, options) {
12
13
  try {
@@ -19,6 +20,65 @@ async function create(name, options) {
19
20
  process.exit(1);
20
21
  }
21
22
 
23
+ // Check if application already exists in this directory
24
+ const existingConfig = getProjectConfig();
25
+ if (existingConfig) {
26
+ logger.error('Application already published');
27
+ logger.newline();
28
+ logger.info('This directory is already linked to an application:');
29
+ if (existingConfig.applicationName) {
30
+ logger.field('Name', existingConfig.applicationName);
31
+ }
32
+ if (existingConfig.subdomain && existingConfig.domainSuffix) {
33
+ const domain = `https://${existingConfig.subdomain}.${existingConfig.domainSuffix}`;
34
+ logger.field('Domain', domain);
35
+ logger.newline();
36
+ logger.info(`Your application should be available at: ${domain}`);
37
+ }
38
+ logger.newline();
39
+ logger.info('To manage this application, use:');
40
+ logger.log(' saac deploy Deploy changes');
41
+ logger.log(' saac update [options] Update configuration');
42
+ logger.log(' saac env set KEY=VALUE Set environment variables');
43
+ logger.log(' saac logs --follow View logs');
44
+ logger.log(' saac status Check status');
45
+ logger.newline();
46
+ logger.warn('To create a new application, use a different directory');
47
+ process.exit(1);
48
+ }
49
+
50
+ // Check current git branch
51
+ let currentBranch = null;
52
+ try {
53
+ currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
54
+ encoding: 'utf8',
55
+ stdio: ['pipe', 'pipe', 'ignore']
56
+ }).trim();
57
+ } catch (error) {
58
+ // Not in a git repository or git not available - continue anyway
59
+ }
60
+
61
+ if (currentBranch && currentBranch !== 'master' && currentBranch !== 'main') {
62
+ const specifiedBranch = options.branch;
63
+
64
+ if (!specifiedBranch || specifiedBranch !== currentBranch) {
65
+ logger.error(`You are currently on branch: ${logger.chalk.yellow(currentBranch)}`);
66
+ logger.newline();
67
+ logger.warn('This is not the master or main branch!');
68
+ logger.newline();
69
+ logger.info('If you really want to use this branch, confirm by specifying it explicitly:');
70
+ logger.log(` saac create ${name} -s ${options.subdomain || '<subdomain>'} -r ${options.repository || '<repository>'} --branch ${currentBranch}`);
71
+ logger.newline();
72
+ logger.info('Or switch to master/main branch:');
73
+ logger.log(' git checkout master');
74
+ logger.log(' git checkout main');
75
+ process.exit(1);
76
+ } else {
77
+ logger.warn(`Using branch: ${logger.chalk.yellow(currentBranch)}`);
78
+ logger.newline();
79
+ }
80
+ }
81
+
22
82
  // Validate required fields
23
83
  if (!name) {
24
84
  logger.error('Application name is required');
@@ -0,0 +1,143 @@
1
+ /**
2
+ * API Key management commands
3
+ */
4
+
5
+ const api = require('../lib/api');
6
+ const { getUser, isAuthenticated } = require('../lib/config');
7
+ const logger = require('../lib/logger');
8
+ const inquirer = require('inquirer');
9
+
10
+ /**
11
+ * Regenerate API key
12
+ */
13
+ async function regenerate() {
14
+ try {
15
+ // Must be authenticated (via session token)
16
+ if (!isAuthenticated()) {
17
+ logger.error('Not logged in');
18
+ logger.newline();
19
+ logger.info('You must be logged in to regenerate your API key');
20
+ logger.newline();
21
+ logger.info('Login using email verification:');
22
+ logger.log(' saac login -e <email> # Request OTP');
23
+ logger.log(' saac login -e <email> --otp <code> # Verify OTP');
24
+ process.exit(1);
25
+ }
26
+
27
+ const user = getUser();
28
+
29
+ logger.section('Regenerate API Key');
30
+ logger.newline();
31
+
32
+ logger.warn('This will generate a new API key and invalidate your current one.');
33
+ logger.warn('Your active session tokens will continue to work.');
34
+ logger.newline();
35
+
36
+ // Confirmation prompt
37
+ const { confirm } = await inquirer.prompt([
38
+ {
39
+ type: 'confirm',
40
+ name: 'confirm',
41
+ message: 'Are you sure you want to regenerate your API key?',
42
+ default: false,
43
+ },
44
+ ]);
45
+
46
+ if (!confirm) {
47
+ logger.info('Cancelled');
48
+ return;
49
+ }
50
+
51
+ const spin = logger.spinner('Generating new API key...').start();
52
+
53
+ try {
54
+ const result = await api.regenerateApiKey();
55
+
56
+ spin.succeed('New API key generated!');
57
+
58
+ logger.newline();
59
+ logger.success('Your new API key:');
60
+ logger.newline();
61
+
62
+ // Show FULL API key (it's only shown once)
63
+ logger.field('API Key', result.api_key);
64
+
65
+ logger.newline();
66
+ logger.warn('Save this key securely. It will not be shown again.');
67
+ logger.newline();
68
+
69
+ logger.info('Your existing sessions remain active.');
70
+ logger.info('Use this new key for future logins or CI/CD.');
71
+ logger.newline();
72
+
73
+ logger.info('To login with the new key:');
74
+ logger.log(` saac login -e ${user.email} -k ${result.api_key}`);
75
+
76
+ } catch (error) {
77
+ spin.fail('Failed to regenerate API key');
78
+ throw error;
79
+ }
80
+ } catch (error) {
81
+ logger.error(error.response?.data?.message || error.message);
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Show API key info (without revealing full key)
88
+ */
89
+ async function show() {
90
+ try {
91
+ if (!isAuthenticated()) {
92
+ logger.error('Not logged in');
93
+ logger.newline();
94
+ logger.info('Login first:');
95
+ logger.log(' saac login -e <email>');
96
+ process.exit(1);
97
+ }
98
+
99
+ logger.section('API Key Information');
100
+ logger.newline();
101
+
102
+ const spin = logger.spinner('Fetching API key info...').start();
103
+
104
+ try {
105
+ const result = await api.getApiKeyInfo();
106
+
107
+ spin.succeed('API key info retrieved');
108
+
109
+ logger.newline();
110
+ logger.field('Key Prefix', result.key_prefix); // e.g., "cw_RJ1gH8..."
111
+ logger.field('Created', new Date(result.created_at).toLocaleDateString());
112
+ logger.field('Last Used', result.last_used_at
113
+ ? new Date(result.last_used_at).toLocaleString()
114
+ : 'Never');
115
+
116
+ logger.newline();
117
+ logger.info('Commands:');
118
+ logger.log(' saac keys regenerate Generate new API key');
119
+ logger.log(' saac sessions View active sessions');
120
+
121
+ } catch (error) {
122
+ spin.fail('Failed to fetch API key info');
123
+
124
+ // If endpoint doesn't exist yet, show helpful message
125
+ if (error.response?.status === 404) {
126
+ logger.newline();
127
+ logger.warn('API key info endpoint not available yet');
128
+ logger.info('You can still regenerate your key with:');
129
+ logger.log(' saac keys regenerate');
130
+ } else {
131
+ throw error;
132
+ }
133
+ }
134
+ } catch (error) {
135
+ logger.error(error.response?.data?.message || error.message);
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ module.exports = {
141
+ regenerate,
142
+ show,
143
+ };
@@ -1,5 +1,7 @@
1
1
  /**
2
- * Login command
2
+ * Login command - Two methods:
3
+ * 1. With API key (fast): saac login -e email -k api_key
4
+ * 2. With OTP (recovery): saac login -e email, then saac login -e email --otp code
3
5
  */
4
6
 
5
7
  const inquirer = require('inquirer');
@@ -10,21 +12,23 @@ const logger = require('../lib/logger');
10
12
 
11
13
  async function login(options) {
12
14
  try {
13
- // Require both email and API key flags (no interactive prompts)
14
- if (!options.email || !options.apiKey) {
15
- logger.error('Email and API key are required');
15
+ // Require email flag
16
+ if (!options.email) {
17
+ logger.error('Email is required');
16
18
  logger.newline();
17
19
  logger.info('Usage:');
18
- logger.log(' saac login -e <email> -k <api-key>');
19
- logger.log(' saac login --email <email> --api-key <api-key>');
20
+ logger.log(' saac login -e <email> -k <api-key> # Login with API key');
21
+ logger.log(' saac login -e <email> # Request OTP via email');
22
+ logger.log(' saac login -e <email> --otp <code> # Verify OTP and login');
20
23
  logger.newline();
21
- logger.info('Example:');
22
- logger.log(' saac login -e user@example.com -k cw_your_api_key');
24
+ logger.info('Examples:');
25
+ logger.log(' saac login -e user@example.com -k cw_abc123 # Fast login');
26
+ logger.log(' saac login -e user@example.com # Send OTP to email');
27
+ logger.log(' saac login -e user@example.com --otp 123456 # Verify OTP');
23
28
  process.exit(1);
24
29
  }
25
30
 
26
31
  const email = options.email;
27
- const apiKey = options.apiKey;
28
32
 
29
33
  // Validate email format
30
34
  if (!validator.isEmail(email)) {
@@ -33,46 +37,143 @@ async function login(options) {
33
37
  }
34
38
 
35
39
  logger.section('Login to StartAnAiCompany');
40
+ logger.newline();
36
41
 
37
- // Login and get session token
38
- const spin = logger.spinner('Logging in...').start();
42
+ // CASE 1: Login with API key (fast path)
43
+ if (options.apiKey) {
44
+ return await loginWithApiKey(email, options.apiKey);
45
+ }
39
46
 
40
- try {
41
- // Call /auth/login endpoint to get session token
42
- const result = await api.login(email, apiKey);
47
+ // CASE 2: Verify OTP (second step)
48
+ if (options.otp) {
49
+ return await verifyOtpAndLogin(email, options.otp);
50
+ }
43
51
 
44
- spin.succeed('Login successful!');
52
+ // CASE 3: Request OTP (first step)
53
+ return await requestOtp(email);
45
54
 
46
- // Save session token and expiration
47
- saveUser({
48
- email: result.user.email || email,
49
- userId: result.user.id,
50
- sessionToken: result.session_token,
51
- expiresAt: result.expires_at,
52
- verified: result.user.verified,
53
- });
55
+ } catch (error) {
56
+ logger.error(error.response?.data?.message || error.message);
57
+ process.exit(1);
58
+ }
59
+ }
54
60
 
55
- logger.newline();
56
- logger.success('You are now logged in!');
57
- logger.newline();
58
- logger.field('Email', result.user.email || email);
59
- logger.field('Verified', result.user.verified ? 'Yes' : 'No');
60
-
61
- // Show expiration date
62
- if (result.expires_at) {
63
- const expirationDate = new Date(result.expires_at);
64
- logger.field('Session expires', expirationDate.toLocaleDateString());
65
- }
66
-
67
- } catch (error) {
68
- spin.fail('Login failed');
69
- throw error;
61
+ /**
62
+ * Login with API key (existing flow)
63
+ */
64
+ async function loginWithApiKey(email, apiKey) {
65
+ const spin = logger.spinner('Logging in with API key...').start();
66
+
67
+ try {
68
+ const result = await api.login(email, apiKey);
69
+
70
+ spin.succeed('Login successful!');
71
+
72
+ // Save session token
73
+ saveUser({
74
+ email: result.user.email || email,
75
+ userId: result.user.id,
76
+ sessionToken: result.session_token,
77
+ expiresAt: result.expires_at,
78
+ verified: result.user.verified,
79
+ });
80
+
81
+ logger.newline();
82
+ logger.success('You are now logged in!');
83
+ logger.newline();
84
+ logger.field('Email', result.user.email || email);
85
+ logger.field('Verified', result.user.verified ? 'Yes' : 'No');
86
+
87
+ if (result.expires_at) {
88
+ const expirationDate = new Date(result.expires_at);
89
+ logger.field('Session expires', expirationDate.toLocaleDateString());
70
90
  }
91
+
71
92
  } catch (error) {
72
- logger.error(
73
- error.response?.data?.message || 'Invalid credentials'
74
- );
75
- process.exit(1);
93
+ spin.fail('Login failed');
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Request OTP via email (new flow - step 1)
100
+ */
101
+ async function requestOtp(email) {
102
+ const spin = logger.spinner('Sending verification code to email...').start();
103
+
104
+ try {
105
+ const result = await api.requestLoginOtp(email);
106
+
107
+ spin.succeed('Verification code sent!');
108
+
109
+ logger.newline();
110
+ logger.success(`A verification code has been sent to ${logger.chalk.cyan(email)}`);
111
+ logger.newline();
112
+ logger.info('Check your email and run:');
113
+ logger.log(` ${logger.chalk.yellow(`saac login -e ${email} --otp <code>`)}`);
114
+ logger.newline();
115
+ logger.warn('Note: Check MailHog at https://mailhog.goryan.io for the verification code');
116
+ logger.newline();
117
+
118
+ if (result.otp_expires_at) {
119
+ const expiresAt = new Date(result.otp_expires_at);
120
+ const now = new Date();
121
+ const minutesLeft = Math.ceil((expiresAt - now) / 60000);
122
+ logger.info(`Code expires in ${minutesLeft} minutes`);
123
+ }
124
+
125
+ } catch (error) {
126
+ spin.fail('Failed to send verification code');
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Verify OTP and complete login (new flow - step 2)
133
+ */
134
+ async function verifyOtpAndLogin(email, otpCode) {
135
+ const spin = logger.spinner('Verifying code...').start();
136
+
137
+ try {
138
+ const result = await api.verifyLoginOtp(email, otpCode);
139
+
140
+ spin.succeed('Verification successful!');
141
+
142
+ // Save session token
143
+ saveUser({
144
+ email: result.user.email || email,
145
+ userId: result.user.id,
146
+ sessionToken: result.session_token,
147
+ expiresAt: result.expires_at,
148
+ verified: result.user.verified,
149
+ });
150
+
151
+ logger.newline();
152
+ logger.success('You are now logged in!');
153
+ logger.newline();
154
+ logger.field('Email', result.user.email || email);
155
+ logger.field('Verified', result.user.verified ? 'Yes' : 'No');
156
+
157
+ if (result.expires_at) {
158
+ const expirationDate = new Date(result.expires_at);
159
+ logger.field('Session expires', expirationDate.toLocaleDateString());
160
+ }
161
+
162
+ logger.newline();
163
+ logger.info('💡 Tip: Generate an API key for faster future logins:');
164
+ logger.log(' saac keys regenerate');
165
+
166
+ } catch (error) {
167
+ spin.fail('Verification failed');
168
+
169
+ if (error.response?.status === 401) {
170
+ logger.newline();
171
+ logger.error('Invalid or expired verification code');
172
+ logger.info('Request a new code:');
173
+ logger.log(` saac login -e ${email}`);
174
+ }
175
+
176
+ throw error;
76
177
  }
77
178
  }
78
179
 
@@ -92,12 +92,13 @@ async function status() {
92
92
  const hasMore = applications.length > 5;
93
93
 
94
94
  const data = [
95
- ['Name', 'Domain', 'Status', 'Created'],
95
+ ['Name', 'Domain', 'Status', 'Branch', 'Created'],
96
96
  ];
97
97
 
98
98
  displayApps.forEach((app) => {
99
99
  const created = new Date(app.created_at).toLocaleDateString();
100
100
  const status = app.status || 'unknown';
101
+ const branch = app.git_branch || 'unknown';
101
102
 
102
103
  // Status with icons (handle both Coolify format and documented format)
103
104
  let statusDisplay;
@@ -128,6 +129,7 @@ async function status() {
128
129
  app.name,
129
130
  app.domain || `${app.subdomain}.startanaicompany.com`,
130
131
  statusDisplay,
132
+ branch,
131
133
  created
132
134
  ]);
133
135
  });
package/src/lib/api.js CHANGED
@@ -181,6 +181,61 @@ async function healthCheck() {
181
181
  return response.data;
182
182
  }
183
183
 
184
+ /**
185
+ * Request login OTP (no API key required)
186
+ */
187
+ async function requestLoginOtp(email) {
188
+ const client = axios.create({
189
+ baseURL: getApiUrl(),
190
+ timeout: 30000,
191
+ headers: {
192
+ 'Content-Type': 'application/json',
193
+ 'User-Agent': `saac-cli/${pkg.version} (${os.platform()}; ${os.arch()})`,
194
+ },
195
+ });
196
+
197
+ const response = await client.post('/auth/login-otp', { email });
198
+ return response.data;
199
+ }
200
+
201
+ /**
202
+ * Verify login OTP and get session token
203
+ */
204
+ async function verifyLoginOtp(email, otpCode) {
205
+ const client = axios.create({
206
+ baseURL: getApiUrl(),
207
+ timeout: 30000,
208
+ headers: {
209
+ 'Content-Type': 'application/json',
210
+ 'User-Agent': `saac-cli/${pkg.version} (${os.platform()}; ${os.arch()})`,
211
+ },
212
+ });
213
+
214
+ const response = await client.post('/auth/verify-otp', {
215
+ email,
216
+ otp_code: otpCode,
217
+ });
218
+ return response.data;
219
+ }
220
+
221
+ /**
222
+ * Regenerate API key (requires authentication)
223
+ */
224
+ async function regenerateApiKey() {
225
+ const client = createClient();
226
+ const response = await client.post('/users/regenerate-key');
227
+ return response.data;
228
+ }
229
+
230
+ /**
231
+ * Get API key info (prefix, created date, last used)
232
+ */
233
+ async function getApiKeyInfo() {
234
+ const client = createClient();
235
+ const response = await client.get('/users/api-key');
236
+ return response.data;
237
+ }
238
+
184
239
  module.exports = {
185
240
  createClient,
186
241
  login,
@@ -197,4 +252,8 @@ module.exports = {
197
252
  updateDomain,
198
253
  deleteApplication,
199
254
  healthCheck,
255
+ requestLoginOtp,
256
+ verifyLoginOtp,
257
+ regenerateApiKey,
258
+ getApiKeyInfo,
200
259
  };