@l4yercak3/cli 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -121,6 +121,26 @@ class BackendClient {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Revoke CLI session (logout on backend)
|
|
126
|
+
* This cleans up the session from the database
|
|
127
|
+
*/
|
|
128
|
+
async revokeSession() {
|
|
129
|
+
try {
|
|
130
|
+
const session = configManager.getSession();
|
|
131
|
+
if (!session || !session.token) {
|
|
132
|
+
return { success: true }; // Nothing to revoke
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return await this.request('POST', '/api/v1/auth/cli/revoke');
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// Even if revoke fails, we'll clear locally
|
|
138
|
+
// Log but don't throw - user still wants to logout
|
|
139
|
+
console.error('Warning: Could not revoke session on backend:', error.message);
|
|
140
|
+
return { success: false, error: error.message };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
124
144
|
/**
|
|
125
145
|
* Get CLI login URL with state parameter for CSRF protection
|
|
126
146
|
* Browser login is served by Next.js at APP_URL (not the Convex API URL)
|
|
@@ -129,7 +149,8 @@ class BackendClient {
|
|
|
129
149
|
* @returns {string} The login URL
|
|
130
150
|
*/
|
|
131
151
|
getLoginUrl(state, provider = null) {
|
|
132
|
-
|
|
152
|
+
// Use port 3333 to avoid conflicts with Next.js dev server (port 3000)
|
|
153
|
+
const callbackUrl = 'http://localhost:3333/callback';
|
|
133
154
|
|
|
134
155
|
if (provider) {
|
|
135
156
|
// Direct OAuth provider URL
|
package/src/commands/login.js
CHANGED
|
@@ -39,6 +39,9 @@ function generateRetroPage({ title, icon, heading, headingColor, message, submes
|
|
|
39
39
|
</html>`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// CLI callback port - different from Next.js dev server (3000)
|
|
43
|
+
const CLI_CALLBACK_PORT = 3333;
|
|
44
|
+
|
|
42
45
|
/**
|
|
43
46
|
* Start local server to receive OAuth callback
|
|
44
47
|
* @param {string} expectedState - The state token to verify against
|
|
@@ -48,7 +51,7 @@ function startCallbackServer(expectedState) {
|
|
|
48
51
|
const http = require('http');
|
|
49
52
|
|
|
50
53
|
const server = http.createServer((req, res) => {
|
|
51
|
-
const url = new URL(req.url,
|
|
54
|
+
const url = new URL(req.url, `http://localhost:${CLI_CALLBACK_PORT}`);
|
|
52
55
|
|
|
53
56
|
if (url.pathname === '/callback') {
|
|
54
57
|
const token = url.searchParams.get('token');
|
|
@@ -104,7 +107,16 @@ function startCallbackServer(expectedState) {
|
|
|
104
107
|
}
|
|
105
108
|
});
|
|
106
109
|
|
|
107
|
-
server
|
|
110
|
+
// Handle server errors (e.g., port already in use)
|
|
111
|
+
server.on('error', (err) => {
|
|
112
|
+
if (err.code === 'EADDRINUSE') {
|
|
113
|
+
reject(new Error(`Port ${CLI_CALLBACK_PORT} is already in use. Please close any other l4yercak3 processes and try again.`));
|
|
114
|
+
} else {
|
|
115
|
+
reject(err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
server.listen(CLI_CALLBACK_PORT, 'localhost', () => {
|
|
108
120
|
console.log(chalk.gray(' Waiting for authentication...'));
|
|
109
121
|
});
|
|
110
122
|
|
|
@@ -144,9 +156,12 @@ async function handleLogin() {
|
|
|
144
156
|
await promptSetupWizard();
|
|
145
157
|
return;
|
|
146
158
|
} else {
|
|
147
|
-
// Session is invalid on backend -
|
|
159
|
+
// Session is invalid on backend - revoke it and proceed to login
|
|
148
160
|
console.log(chalk.yellow(' ⚠️ Your session has expired or is invalid'));
|
|
149
|
-
console.log(chalk.gray('
|
|
161
|
+
console.log(chalk.gray(' Revoking old session and starting fresh login...\n'));
|
|
162
|
+
|
|
163
|
+
// Try to revoke the old session on backend (cleanup)
|
|
164
|
+
await backendClient.revokeSession();
|
|
150
165
|
configManager.clearSession();
|
|
151
166
|
}
|
|
152
167
|
}
|
package/src/commands/logout.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Logout Command
|
|
3
|
-
* Clears CLI session
|
|
3
|
+
* Clears CLI session both locally and on the backend
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const configManager = require('../config/config-manager');
|
|
7
|
+
const backendClient = require('../api/backend-client');
|
|
7
8
|
const chalk = require('chalk');
|
|
8
9
|
|
|
9
10
|
async function handleLogout() {
|
|
@@ -12,6 +13,11 @@ async function handleLogout() {
|
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
// Revoke session on backend first (cleans up database)
|
|
17
|
+
console.log(chalk.gray(' Revoking session...'));
|
|
18
|
+
await backendClient.revokeSession();
|
|
19
|
+
|
|
20
|
+
// Clear local session
|
|
15
21
|
configManager.clearSession();
|
|
16
22
|
console.log(chalk.green(' ✅ Successfully logged out\n'));
|
|
17
23
|
}
|
|
@@ -248,6 +248,58 @@ describe('BackendClient', () => {
|
|
|
248
248
|
});
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
describe('revokeSession', () => {
|
|
252
|
+
it('calls revoke endpoint when session exists', async () => {
|
|
253
|
+
configManager.getSession.mockReturnValue({ token: 'test-token' });
|
|
254
|
+
const mockResponse = {
|
|
255
|
+
ok: true,
|
|
256
|
+
json: jest.fn().mockResolvedValue({ success: true }),
|
|
257
|
+
};
|
|
258
|
+
fetch.mockResolvedValue(mockResponse);
|
|
259
|
+
|
|
260
|
+
const result = await BackendClient.revokeSession();
|
|
261
|
+
|
|
262
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
263
|
+
expect.stringContaining('/api/v1/auth/cli/revoke'),
|
|
264
|
+
expect.objectContaining({ method: 'POST' })
|
|
265
|
+
);
|
|
266
|
+
expect(result.success).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('returns success when no session exists', async () => {
|
|
270
|
+
configManager.getSession.mockReturnValue(null);
|
|
271
|
+
|
|
272
|
+
const result = await BackendClient.revokeSession();
|
|
273
|
+
|
|
274
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
275
|
+
expect(result.success).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns success when session has no token', async () => {
|
|
279
|
+
configManager.getSession.mockReturnValue({ expiresAt: Date.now() });
|
|
280
|
+
|
|
281
|
+
const result = await BackendClient.revokeSession();
|
|
282
|
+
|
|
283
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
284
|
+
expect(result.success).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('returns failure but does not throw on error', async () => {
|
|
288
|
+
configManager.getSession.mockReturnValue({ token: 'test-token' });
|
|
289
|
+
fetch.mockRejectedValue(new Error('Revoke failed'));
|
|
290
|
+
|
|
291
|
+
// Suppress console.error for this test
|
|
292
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
293
|
+
|
|
294
|
+
const result = await BackendClient.revokeSession();
|
|
295
|
+
|
|
296
|
+
expect(result.success).toBe(false);
|
|
297
|
+
expect(result.error).toBe('Revoke failed');
|
|
298
|
+
|
|
299
|
+
consoleSpy.mockRestore();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
251
303
|
describe('generateState', () => {
|
|
252
304
|
it('generates a 64-character hex string', () => {
|
|
253
305
|
const state = BackendClient.generateState();
|
|
@@ -290,7 +342,7 @@ describe('BackendClient', () => {
|
|
|
290
342
|
const state = 'test-state-token';
|
|
291
343
|
const url = BackendClient.getLoginUrl(state, 'github');
|
|
292
344
|
|
|
293
|
-
expect(url).toContain(encodeURIComponent('http://localhost:
|
|
345
|
+
expect(url).toContain(encodeURIComponent('http://localhost:3333/callback'));
|
|
294
346
|
});
|
|
295
347
|
});
|
|
296
348
|
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
jest.mock('../../src/config/config-manager');
|
|
6
|
+
jest.mock('../../src/api/backend-client');
|
|
6
7
|
jest.mock('chalk', () => ({
|
|
7
8
|
yellow: (str) => str,
|
|
8
9
|
green: (str) => str,
|
|
10
|
+
gray: (str) => str,
|
|
9
11
|
}));
|
|
10
12
|
|
|
11
13
|
const configManager = require('../../src/config/config-manager');
|
|
14
|
+
const backendClient = require('../../src/api/backend-client');
|
|
12
15
|
const logoutCommand = require('../../src/commands/logout');
|
|
13
16
|
|
|
14
17
|
describe('Logout Command', () => {
|
|
@@ -21,6 +24,8 @@ describe('Logout Command', () => {
|
|
|
21
24
|
console.log = jest.fn((...args) => {
|
|
22
25
|
consoleOutput.push(args.join(' '));
|
|
23
26
|
});
|
|
27
|
+
// Mock revokeSession to return success
|
|
28
|
+
backendClient.revokeSession.mockResolvedValue({ success: true });
|
|
24
29
|
});
|
|
25
30
|
|
|
26
31
|
afterEach(() => {
|
|
@@ -59,6 +64,15 @@ describe('Logout Command', () => {
|
|
|
59
64
|
expect(configManager.clearSession).toHaveBeenCalled();
|
|
60
65
|
});
|
|
61
66
|
|
|
67
|
+
it('revokes session on backend before clearing locally', async () => {
|
|
68
|
+
configManager.isLoggedIn.mockReturnValue(true);
|
|
69
|
+
|
|
70
|
+
await logoutCommand.handler();
|
|
71
|
+
|
|
72
|
+
expect(backendClient.revokeSession).toHaveBeenCalled();
|
|
73
|
+
expect(configManager.clearSession).toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
62
76
|
it('shows success message after logout', async () => {
|
|
63
77
|
configManager.isLoggedIn.mockReturnValue(true);
|
|
64
78
|
|