@startanaicompany/cli 1.0.0 → 1.1.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/CLAUDE.md +253 -0
- package/README.md +36 -6
- package/auth_session_update.md +785 -0
- package/bin/saac.js +14 -1
- package/package.json +1 -1
- package/src/commands/login.js +38 -44
- package/src/commands/logout.js +41 -3
- package/src/commands/logoutAll.js +74 -0
- package/src/commands/register.js +46 -34
- package/src/commands/sessions.js +75 -0
- package/src/commands/verify.js +32 -4
- package/src/lib/api.js +37 -1
- package/src/lib/config.js +52 -4
- package/test-session-token.js +117 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
# Session Token Authentication - Implementation Guide
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-01-25
|
|
4
|
+
**From**: Coolify Wrapper API Team
|
|
5
|
+
**To**: SAAC CLI Team
|
|
6
|
+
**Subject**: New Session-Based Authentication System
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Executive Summary
|
|
11
|
+
|
|
12
|
+
The Coolify Wrapper API now supports **session-based authentication** with 1-year expiring tokens. This provides better security than permanent API keys while maintaining a great user experience.
|
|
13
|
+
|
|
14
|
+
**Key Changes**:
|
|
15
|
+
- New `/auth/login` endpoint to exchange API keys for session tokens
|
|
16
|
+
- Session tokens expire after 1 year (vs permanent API keys)
|
|
17
|
+
- Multiple concurrent sessions per user (laptop, desktop, etc.)
|
|
18
|
+
- Server-side revocation capability
|
|
19
|
+
- Backward compatible - permanent API keys still work
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Why Session Tokens?
|
|
24
|
+
|
|
25
|
+
### Current Problem
|
|
26
|
+
```javascript
|
|
27
|
+
// Current flow - LESS SECURE
|
|
28
|
+
~/.saac/config.json:
|
|
29
|
+
{
|
|
30
|
+
"email": "user@example.com",
|
|
31
|
+
"apiKey": "cw_permanent_key..." // Never expires, full access forever
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Risk**: If `~/.saac/config.json` is compromised, attacker has permanent access until user manually regenerates API key.
|
|
36
|
+
|
|
37
|
+
### New Solution
|
|
38
|
+
```javascript
|
|
39
|
+
// Session token flow - MORE SECURE
|
|
40
|
+
~/.saac/config.json:
|
|
41
|
+
{
|
|
42
|
+
"email": "user@example.com",
|
|
43
|
+
"sessionToken": "st_xyz...", // Expires in 1 year
|
|
44
|
+
"expiresAt": "2026-01-25T...",
|
|
45
|
+
"verified": true
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Benefits**:
|
|
50
|
+
- ✅ Limited time window (1 year vs forever)
|
|
51
|
+
- ✅ Server can revoke tokens remotely
|
|
52
|
+
- ✅ User can see all active sessions
|
|
53
|
+
- ✅ Matches industry standards (GitHub CLI, AWS CLI, Vercel CLI)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## New API Endpoints
|
|
58
|
+
|
|
59
|
+
### 1. POST /api/v1/auth/login
|
|
60
|
+
|
|
61
|
+
**Purpose**: Exchange permanent API key for a session token (1-year expiry)
|
|
62
|
+
|
|
63
|
+
**Request**:
|
|
64
|
+
```bash
|
|
65
|
+
POST https://apps.startanaicompany.com/api/v1/auth/login
|
|
66
|
+
Headers:
|
|
67
|
+
X-API-Key: cw_RJ1gH8Sd1nvmPF4lWigu2g3Nkjt1mwEJXYd2aycD0IIniNPhImE5XgWaz3Tcz
|
|
68
|
+
Content-Type: application/json
|
|
69
|
+
|
|
70
|
+
Body (optional):
|
|
71
|
+
{
|
|
72
|
+
"email": "ryan88@goryan.io" // For validation
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Response (Success - 200)**:
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"session_token": "st_abc123defghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTU",
|
|
80
|
+
"expires_at": "2026-01-25T12:34:56.789Z",
|
|
81
|
+
"user": {
|
|
82
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
83
|
+
"email": "ryan88@goryan.io",
|
|
84
|
+
"verified": true,
|
|
85
|
+
"max_applications": 50
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Response (Error - 401)**:
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"error": "Invalid API key",
|
|
94
|
+
"code": "UNAUTHORIZED",
|
|
95
|
+
"timestamp": "2026-01-25T12:34:56.789Z"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Token Format**:
|
|
100
|
+
- Prefix: `st_`
|
|
101
|
+
- Length: 64 characters total
|
|
102
|
+
- Pattern: `st_` + 61 random alphanumeric characters
|
|
103
|
+
- Example: `st_kgzfNByNNrtrDsAW07h6ORwTtP3POK6O98klH9Rm8jTt9ByHojeH7zDmGwaF`
|
|
104
|
+
|
|
105
|
+
**Security Notes**:
|
|
106
|
+
- Session token is returned in plaintext **only once**
|
|
107
|
+
- Server stores SHA-256 hash of the token
|
|
108
|
+
- Token expires exactly 1 year after creation
|
|
109
|
+
- Token can be revoked remotely by user or server
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### 2. POST /api/v1/auth/logout
|
|
114
|
+
|
|
115
|
+
**Purpose**: Revoke the current session token
|
|
116
|
+
|
|
117
|
+
**Request**:
|
|
118
|
+
```bash
|
|
119
|
+
POST https://apps.startanaicompany.com/api/v1/auth/logout
|
|
120
|
+
Headers:
|
|
121
|
+
X-Session-Token: st_abc123...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Response (Success - 200)**:
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"success": true,
|
|
128
|
+
"message": "Session revoked successfully"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Use Case**: User wants to logout from current device only
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### 3. POST /api/v1/auth/logout-all
|
|
137
|
+
|
|
138
|
+
**Purpose**: Revoke ALL sessions for the current user
|
|
139
|
+
|
|
140
|
+
**Request**:
|
|
141
|
+
```bash
|
|
142
|
+
POST https://apps.startanaicompany.com/api/v1/auth/logout-all
|
|
143
|
+
Headers:
|
|
144
|
+
X-Session-Token: st_abc123...
|
|
145
|
+
# OR
|
|
146
|
+
X-API-Key: cw_permanent_key...
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Response (Success - 200)**:
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"success": true,
|
|
153
|
+
"sessions_revoked": 3,
|
|
154
|
+
"message": "3 session(s) revoked successfully"
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Use Cases**:
|
|
159
|
+
- Device lost or stolen
|
|
160
|
+
- Security breach suspected
|
|
161
|
+
- User wants to force re-login on all devices
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### 4. GET /api/v1/auth/sessions
|
|
166
|
+
|
|
167
|
+
**Purpose**: List all active sessions for the current user
|
|
168
|
+
|
|
169
|
+
**Request**:
|
|
170
|
+
```bash
|
|
171
|
+
GET https://apps.startanaicompany.com/api/v1/auth/sessions
|
|
172
|
+
Headers:
|
|
173
|
+
X-Session-Token: st_abc123...
|
|
174
|
+
# OR
|
|
175
|
+
X-API-Key: cw_permanent_key...
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Response (Success - 200)**:
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"sessions": [
|
|
182
|
+
{
|
|
183
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
184
|
+
"expires_at": "2026-01-25T12:34:56.789Z",
|
|
185
|
+
"created_at": "2025-01-25T12:34:56.789Z",
|
|
186
|
+
"last_used_at": "2025-01-25T14:22:10.123Z",
|
|
187
|
+
"created_ip": "192.168.1.100",
|
|
188
|
+
"created_user_agent": "saac-cli/1.0.0 (Darwin; x64)"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"id": "660e9511-f39c-52e5-b827-557766551111",
|
|
192
|
+
"expires_at": "2026-01-20T08:15:30.456Z",
|
|
193
|
+
"created_at": "2025-01-20T08:15:30.456Z",
|
|
194
|
+
"last_used_at": "2025-01-23T09:45:22.789Z",
|
|
195
|
+
"created_ip": "192.168.1.105",
|
|
196
|
+
"created_user_agent": "saac-cli/1.0.0 (Linux; x64)"
|
|
197
|
+
}
|
|
198
|
+
],
|
|
199
|
+
"total": 2
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Use Case**: User wants to see which devices are logged in
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### 5. DELETE /api/v1/auth/sessions/:sessionId
|
|
208
|
+
|
|
209
|
+
**Purpose**: Revoke a specific session by ID
|
|
210
|
+
|
|
211
|
+
**Request**:
|
|
212
|
+
```bash
|
|
213
|
+
DELETE https://apps.startanaicompany.com/api/v1/auth/sessions/550e8400-e29b-41d4-a716-446655440000
|
|
214
|
+
Headers:
|
|
215
|
+
X-Session-Token: st_abc123...
|
|
216
|
+
# OR
|
|
217
|
+
X-API-Key: cw_permanent_key...
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Response (Success - 200)**:
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"success": true,
|
|
224
|
+
"message": "Session revoked successfully"
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Use Case**: User sees unfamiliar session and wants to revoke it
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Authentication Header Priority
|
|
233
|
+
|
|
234
|
+
The middleware checks headers in this order:
|
|
235
|
+
|
|
236
|
+
1. **X-Session-Token** (session token) - Checked first
|
|
237
|
+
2. **X-API-Key** (permanent API key) - Fallback
|
|
238
|
+
|
|
239
|
+
**Examples**:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# Option 1: Use session token (recommended for CLI users)
|
|
243
|
+
curl https://apps.startanaicompany.com/api/v1/users/me \
|
|
244
|
+
-H "X-Session-Token: st_abc123..."
|
|
245
|
+
|
|
246
|
+
# Option 2: Use permanent API key (for CI/CD, scripts)
|
|
247
|
+
curl https://apps.startanaicompany.com/api/v1/users/me \
|
|
248
|
+
-H "X-API-Key: cw_abc123..."
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Note**: If both headers are provided, `X-Session-Token` takes priority.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## CLI Implementation Guide
|
|
256
|
+
|
|
257
|
+
### Recommended Changes
|
|
258
|
+
|
|
259
|
+
#### 1. Update `src/lib/api.js`
|
|
260
|
+
|
|
261
|
+
```javascript
|
|
262
|
+
const axios = require('axios');
|
|
263
|
+
const { getUser } = require('./config');
|
|
264
|
+
|
|
265
|
+
function createClient() {
|
|
266
|
+
const user = getUser();
|
|
267
|
+
const envApiKey = process.env.SAAC_API_KEY; // For CI/CD
|
|
268
|
+
|
|
269
|
+
const headers = {
|
|
270
|
+
'Content-Type': 'application/json',
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Priority order:
|
|
274
|
+
// 1. Environment variable (for CI/CD, scripts)
|
|
275
|
+
// 2. Session token (for CLI users)
|
|
276
|
+
// 3. API key (backward compatibility)
|
|
277
|
+
if (envApiKey) {
|
|
278
|
+
headers['X-API-Key'] = envApiKey;
|
|
279
|
+
} else if (user?.sessionToken) {
|
|
280
|
+
headers['X-Session-Token'] = user.sessionToken;
|
|
281
|
+
} else if (user?.apiKey) {
|
|
282
|
+
headers['X-API-Key'] = user.apiKey;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return axios.create({
|
|
286
|
+
baseURL: 'https://apps.startanaicompany.com/api/v1',
|
|
287
|
+
timeout: 30000,
|
|
288
|
+
headers,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Login and get session token
|
|
294
|
+
*/
|
|
295
|
+
async function login(email, apiKey) {
|
|
296
|
+
const client = axios.create({
|
|
297
|
+
baseURL: 'https://apps.startanaicompany.com/api/v1',
|
|
298
|
+
timeout: 30000,
|
|
299
|
+
headers: {
|
|
300
|
+
'Content-Type': 'application/json',
|
|
301
|
+
'X-API-Key': apiKey, // Use API key for login
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const response = await client.post('/auth/login', { email });
|
|
306
|
+
return response.data;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = {
|
|
310
|
+
createClient,
|
|
311
|
+
login,
|
|
312
|
+
};
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### 2. Update `src/lib/config.js`
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
const Conf = require('conf');
|
|
319
|
+
const config = new Conf();
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Save user data including session token
|
|
323
|
+
*/
|
|
324
|
+
function saveUser(userData) {
|
|
325
|
+
config.set('user', {
|
|
326
|
+
email: userData.email,
|
|
327
|
+
userId: userData.userId,
|
|
328
|
+
sessionToken: userData.sessionToken, // NEW
|
|
329
|
+
expiresAt: userData.expiresAt, // NEW
|
|
330
|
+
verified: userData.verified,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Check if user is authenticated and token is valid
|
|
336
|
+
*/
|
|
337
|
+
function isAuthenticated() {
|
|
338
|
+
const user = getUser();
|
|
339
|
+
|
|
340
|
+
if (!user || !user.email) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check for session token
|
|
345
|
+
if (user.sessionToken) {
|
|
346
|
+
// Check if token is expired
|
|
347
|
+
if (user.expiresAt) {
|
|
348
|
+
const expirationDate = new Date(user.expiresAt);
|
|
349
|
+
const now = new Date();
|
|
350
|
+
|
|
351
|
+
if (now >= expirationDate) {
|
|
352
|
+
return false; // Token expired
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Fallback: Check for API key (backward compatibility)
|
|
359
|
+
return !!user.apiKey;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Check if session token expires soon (within 7 days)
|
|
364
|
+
*/
|
|
365
|
+
function isTokenExpiringSoon() {
|
|
366
|
+
const user = getUser();
|
|
367
|
+
if (!user?.expiresAt) return false;
|
|
368
|
+
|
|
369
|
+
const expirationDate = new Date(user.expiresAt);
|
|
370
|
+
const now = new Date();
|
|
371
|
+
const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
372
|
+
|
|
373
|
+
return expirationDate <= sevenDaysFromNow;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
module.exports = {
|
|
377
|
+
saveUser,
|
|
378
|
+
getUser,
|
|
379
|
+
isAuthenticated,
|
|
380
|
+
isTokenExpiringSoon,
|
|
381
|
+
};
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### 3. Update `src/commands/login.js`
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
const inquirer = require('inquirer');
|
|
388
|
+
const validator = require('validator');
|
|
389
|
+
const api = require('../lib/api');
|
|
390
|
+
const { saveUser } = require('../lib/config');
|
|
391
|
+
const logger = require('../lib/logger');
|
|
392
|
+
|
|
393
|
+
async function login(options) {
|
|
394
|
+
try {
|
|
395
|
+
logger.section('Login to StartAnAiCompany');
|
|
396
|
+
|
|
397
|
+
// Get credentials
|
|
398
|
+
let email = options.email;
|
|
399
|
+
let apiKey = options.apiKey;
|
|
400
|
+
|
|
401
|
+
if (!email) {
|
|
402
|
+
const answers = await inquirer.prompt([
|
|
403
|
+
{
|
|
404
|
+
type: 'input',
|
|
405
|
+
name: 'email',
|
|
406
|
+
message: 'Email address:',
|
|
407
|
+
validate: (input) => validator.isEmail(input) || 'Invalid email',
|
|
408
|
+
},
|
|
409
|
+
]);
|
|
410
|
+
email = answers.email;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!apiKey) {
|
|
414
|
+
const answers = await inquirer.prompt([
|
|
415
|
+
{
|
|
416
|
+
type: 'password',
|
|
417
|
+
name: 'apiKey',
|
|
418
|
+
message: 'API Key:',
|
|
419
|
+
mask: '*',
|
|
420
|
+
},
|
|
421
|
+
]);
|
|
422
|
+
apiKey = answers.apiKey;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const spin = logger.spinner('Logging in...').start();
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
// Call new /auth/login endpoint
|
|
429
|
+
const result = await api.login(email, apiKey);
|
|
430
|
+
|
|
431
|
+
spin.succeed('Login successful!');
|
|
432
|
+
|
|
433
|
+
// Save session token and expiration
|
|
434
|
+
saveUser({
|
|
435
|
+
email: result.user.email || email,
|
|
436
|
+
userId: result.user.id,
|
|
437
|
+
sessionToken: result.session_token, // NEW: Store session token
|
|
438
|
+
expiresAt: result.expires_at, // NEW: Store expiration
|
|
439
|
+
verified: result.user.verified,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
logger.newline();
|
|
443
|
+
logger.success('You are now logged in!');
|
|
444
|
+
logger.newline();
|
|
445
|
+
logger.field('Email', email);
|
|
446
|
+
logger.field('Verified', result.user.verified ? 'Yes' : 'No');
|
|
447
|
+
|
|
448
|
+
// Show expiration date
|
|
449
|
+
if (result.expires_at) {
|
|
450
|
+
const expirationDate = new Date(result.expires_at);
|
|
451
|
+
logger.field('Session expires', expirationDate.toLocaleDateString());
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
} catch (error) {
|
|
455
|
+
spin.fail('Login failed');
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
logger.error(error.response?.data?.message || error.message);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
module.exports = login;
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
#### 4. Add New Command: `src/commands/logout.js`
|
|
468
|
+
|
|
469
|
+
```javascript
|
|
470
|
+
const api = require('../lib/api');
|
|
471
|
+
const { clearUser } = require('../lib/config');
|
|
472
|
+
const logger = require('../lib/logger');
|
|
473
|
+
|
|
474
|
+
async function logout() {
|
|
475
|
+
try {
|
|
476
|
+
logger.section('Logout from StartAnAiCompany');
|
|
477
|
+
|
|
478
|
+
const spin = logger.spinner('Logging out...').start();
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
// Revoke current session on server
|
|
482
|
+
const client = api.createClient();
|
|
483
|
+
await client.post('/auth/logout');
|
|
484
|
+
|
|
485
|
+
// Clear local config
|
|
486
|
+
clearUser();
|
|
487
|
+
|
|
488
|
+
spin.succeed('Logout successful!');
|
|
489
|
+
logger.success('You have been logged out.');
|
|
490
|
+
|
|
491
|
+
} catch (error) {
|
|
492
|
+
// Even if server call fails, clear local config
|
|
493
|
+
clearUser();
|
|
494
|
+
spin.warn('Logged out locally (server error)');
|
|
495
|
+
}
|
|
496
|
+
} catch (error) {
|
|
497
|
+
logger.error(error.message);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
module.exports = logout;
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## Migration Strategy
|
|
508
|
+
|
|
509
|
+
### Phase 1: Backward Compatible (Recommended)
|
|
510
|
+
|
|
511
|
+
**Support both authentication methods** during transition:
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
// CLI automatically uses the best available authentication:
|
|
515
|
+
// 1. SAAC_API_KEY env var (for CI/CD)
|
|
516
|
+
// 2. Session token (for CLI users)
|
|
517
|
+
// 3. API key (legacy, still works)
|
|
518
|
+
|
|
519
|
+
createClient() {
|
|
520
|
+
if (process.env.SAAC_API_KEY) {
|
|
521
|
+
return { 'X-API-Key': process.env.SAAC_API_KEY };
|
|
522
|
+
}
|
|
523
|
+
if (user.sessionToken) {
|
|
524
|
+
return { 'X-Session-Token': user.sessionToken };
|
|
525
|
+
}
|
|
526
|
+
if (user.apiKey) {
|
|
527
|
+
return { 'X-API-Key': user.apiKey };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Benefits**:
|
|
533
|
+
- ✅ No breaking changes
|
|
534
|
+
- ✅ Users can upgrade CLI whenever convenient
|
|
535
|
+
- ✅ CI/CD pipelines continue working
|
|
536
|
+
- ✅ Gradual migration
|
|
537
|
+
|
|
538
|
+
### Phase 2: Encourage Session Tokens
|
|
539
|
+
|
|
540
|
+
**Show warnings for API key usage**:
|
|
541
|
+
|
|
542
|
+
```javascript
|
|
543
|
+
if (user.apiKey && !user.sessionToken) {
|
|
544
|
+
logger.warn('You are using a permanent API key.');
|
|
545
|
+
logger.warn('Run `saac login` to get a session token (more secure).');
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Phase 3: Optional - Force Migration
|
|
550
|
+
|
|
551
|
+
**After 6-12 months**, optionally deprecate permanent API keys for CLI usage:
|
|
552
|
+
|
|
553
|
+
```javascript
|
|
554
|
+
if (user.apiKey && !user.sessionToken) {
|
|
555
|
+
logger.error('Permanent API keys are deprecated for CLI usage.');
|
|
556
|
+
logger.error('Please run `saac login` to get a session token.');
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Note**: Keep permanent API keys working for CI/CD via environment variable!
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Testing the Implementation
|
|
566
|
+
|
|
567
|
+
### Test 1: Login Flow
|
|
568
|
+
|
|
569
|
+
```bash
|
|
570
|
+
# Clear existing config
|
|
571
|
+
rm -rf ~/.saac
|
|
572
|
+
|
|
573
|
+
# Login with API key
|
|
574
|
+
saac login -e ryan88@goryan.io -k cw_RJ1gH8Sd1nvmPF4lWigu2g3Nkjt1mwEJXYd2aycD0IIniNPhImE5XgWaz3Tcz
|
|
575
|
+
|
|
576
|
+
# Verify session token saved
|
|
577
|
+
cat ~/.saac/config.json | jq
|
|
578
|
+
# Should show sessionToken and expiresAt
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Test 2: Authenticated Request
|
|
582
|
+
|
|
583
|
+
```bash
|
|
584
|
+
# Get user info (should use session token)
|
|
585
|
+
saac whoami
|
|
586
|
+
|
|
587
|
+
# Verify it works
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Test 3: Token Expiration
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
# Manually set expired token in config
|
|
594
|
+
# Edit ~/.saac/config.json:
|
|
595
|
+
{
|
|
596
|
+
"sessionToken": "st_abc123...",
|
|
597
|
+
"expiresAt": "2020-01-01T00:00:00.000Z" // Past date
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
# Try authenticated request
|
|
601
|
+
saac whoami
|
|
602
|
+
# Should prompt to login again
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Test 4: Multiple Sessions
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
# Login on device 1
|
|
609
|
+
saac login -e user@example.com -k cw_key1
|
|
610
|
+
|
|
611
|
+
# Login on device 2
|
|
612
|
+
saac login -e user@example.com -k cw_key1
|
|
613
|
+
|
|
614
|
+
# List sessions
|
|
615
|
+
saac sessions
|
|
616
|
+
# Should show 2 active sessions
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Test 5: Logout
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
# Logout from current session
|
|
623
|
+
saac logout
|
|
624
|
+
|
|
625
|
+
# Try authenticated request
|
|
626
|
+
saac whoami
|
|
627
|
+
# Should prompt to login
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## Environment Variable Support (CI/CD)
|
|
633
|
+
|
|
634
|
+
For CI/CD pipelines and scripts, **permanent API keys via environment variable still work**:
|
|
635
|
+
|
|
636
|
+
```bash
|
|
637
|
+
# Set API key as environment variable
|
|
638
|
+
export SAAC_API_KEY="cw_RJ1gH8Sd1nvmPF4lWigu2g3Nkjt1mwEJXYd2aycD0IIniNPhImE5XgWaz3Tcz"
|
|
639
|
+
|
|
640
|
+
# Run CLI commands (no login required)
|
|
641
|
+
saac create myapp
|
|
642
|
+
saac deploy myapp
|
|
643
|
+
saac logs myapp
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
**This approach is recommended for**:
|
|
647
|
+
- GitHub Actions
|
|
648
|
+
- GitLab CI
|
|
649
|
+
- Jenkins
|
|
650
|
+
- Docker containers
|
|
651
|
+
- Automated scripts
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## Security Best Practices
|
|
656
|
+
|
|
657
|
+
### For CLI Users
|
|
658
|
+
|
|
659
|
+
1. ✅ **Use session tokens** (via `saac login`)
|
|
660
|
+
2. ✅ **Don't commit** `~/.saac/config.json` to git
|
|
661
|
+
3. ✅ **Set file permissions**: `chmod 600 ~/.saac/config.json`
|
|
662
|
+
4. ✅ **Revoke sessions** when changing devices
|
|
663
|
+
5. ✅ **Use `saac logout-all`** if device is lost
|
|
664
|
+
|
|
665
|
+
### For CI/CD
|
|
666
|
+
|
|
667
|
+
1. ✅ **Use environment variables** for API keys
|
|
668
|
+
2. ✅ **Use secrets management** (GitHub Secrets, GitLab Variables)
|
|
669
|
+
3. ✅ **Never log API keys** in CI output
|
|
670
|
+
4. ✅ **Rotate keys periodically**
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Timeline
|
|
675
|
+
|
|
676
|
+
- ✅ **2026-01-25**: Session token system deployed to production
|
|
677
|
+
- ⏳ **Next Week**: CLI team implements session token support
|
|
678
|
+
- ⏳ **Next Month**: Gradual user migration to session tokens
|
|
679
|
+
- ⏳ **6-12 Months**: Consider deprecating API keys for CLI (keep for CI/CD)
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## FAQ
|
|
684
|
+
|
|
685
|
+
### Q: Do existing API keys still work?
|
|
686
|
+
|
|
687
|
+
**A**: Yes! Permanent API keys (`cw_...`) continue to work exactly as before. The server accepts both `X-API-Key` and `X-Session-Token` headers.
|
|
688
|
+
|
|
689
|
+
### Q: Can I have multiple active sessions?
|
|
690
|
+
|
|
691
|
+
**A**: Yes! Each device can have its own session token. Great for users with multiple computers.
|
|
692
|
+
|
|
693
|
+
### Q: What happens when a session token expires?
|
|
694
|
+
|
|
695
|
+
**A**: The CLI will receive a 401 error. You should detect this and prompt the user to login again via `saac login`.
|
|
696
|
+
|
|
697
|
+
### Q: Can I revoke a session remotely?
|
|
698
|
+
|
|
699
|
+
**A**: Yes! Use `POST /auth/logout-all` to revoke all sessions, or `DELETE /auth/sessions/:id` to revoke a specific session.
|
|
700
|
+
|
|
701
|
+
### Q: How do I rotate my permanent API key?
|
|
702
|
+
|
|
703
|
+
**A**: Use the existing `POST /users/regenerate-key` endpoint. This doesn't affect session tokens (they remain valid until expiry).
|
|
704
|
+
|
|
705
|
+
### Q: Are session tokens secure?
|
|
706
|
+
|
|
707
|
+
**A**: Yes! They are:
|
|
708
|
+
- 64 characters of cryptographically random data
|
|
709
|
+
- Hashed with SHA-256 before storage
|
|
710
|
+
- Transmitted over HTTPS only
|
|
711
|
+
- Automatically expire after 1 year
|
|
712
|
+
- Revocable server-side
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## Support
|
|
717
|
+
|
|
718
|
+
If you have questions about implementing session tokens in the CLI:
|
|
719
|
+
|
|
720
|
+
1. Check this documentation
|
|
721
|
+
2. Test the endpoints manually with `curl`
|
|
722
|
+
3. Contact the Coolify Wrapper API team
|
|
723
|
+
4. Review the implementation in `/home/milko/projects/coolifywrapper/src/`
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## Appendix: Complete Example
|
|
728
|
+
|
|
729
|
+
Here's a complete example of the login flow:
|
|
730
|
+
|
|
731
|
+
```bash
|
|
732
|
+
# 1. User runs login command
|
|
733
|
+
$ saac login -e ryan88@goryan.io -k cw_RJ1gH8Sd1nvmPF4lWigu2g3Nkjt1mwEJXYd2aycD0IIniNPhImE5XgWaz3Tcz
|
|
734
|
+
|
|
735
|
+
# 2. CLI calls API
|
|
736
|
+
POST https://apps.startanaicompany.com/api/v1/auth/login
|
|
737
|
+
Headers: X-API-Key: cw_RJ1gH8Sd1nvmPF4lWigu2g3Nkjt1mwEJXYd2aycD0IIniNPhImE5XgWaz3Tcz
|
|
738
|
+
|
|
739
|
+
# 3. Server responds
|
|
740
|
+
{
|
|
741
|
+
"session_token": "st_kgzfNByNNrtrDsAW07h6ORwTtP3POK6O98klH9Rm8jTt9ByHojeH7zDmGwaF",
|
|
742
|
+
"expires_at": "2026-01-25T12:34:56.789Z",
|
|
743
|
+
"user": {
|
|
744
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
745
|
+
"email": "ryan88@goryan.io",
|
|
746
|
+
"verified": true,
|
|
747
|
+
"max_applications": 50
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
# 4. CLI saves to ~/.saac/config.json
|
|
752
|
+
{
|
|
753
|
+
"user": {
|
|
754
|
+
"email": "ryan88@goryan.io",
|
|
755
|
+
"userId": "550e8400-e29b-41d4-a716-446655440000",
|
|
756
|
+
"sessionToken": "st_kgzfNByNNrtrDsAW07h6ORwTtP3POK6O98klH9Rm8jTt9ByHojeH7zDmGwaF",
|
|
757
|
+
"expiresAt": "2026-01-25T12:34:56.789Z",
|
|
758
|
+
"verified": true
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
# 5. User runs other commands
|
|
763
|
+
$ saac whoami
|
|
764
|
+
|
|
765
|
+
# 6. CLI uses session token
|
|
766
|
+
GET https://apps.startanaicompany.com/api/v1/users/me
|
|
767
|
+
Headers: X-Session-Token: st_kgzfNByNNrtrDsAW07h6ORwTtP3POK6O98klH9Rm8jTt9ByHojeH7zDmGwaF
|
|
768
|
+
|
|
769
|
+
# 7. Server validates and responds
|
|
770
|
+
{
|
|
771
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
772
|
+
"email": "ryan88@goryan.io",
|
|
773
|
+
"gitea_username": "ryan",
|
|
774
|
+
"created_at": "2025-01-20T08:15:30.456Z",
|
|
775
|
+
"application_count": 5,
|
|
776
|
+
"max_applications": 50,
|
|
777
|
+
"email_verified": true
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
---
|
|
782
|
+
|
|
783
|
+
**End of Report**
|
|
784
|
+
|
|
785
|
+
Good luck with the implementation! The session token system is live and ready to use. 🚀
|