coffeeinabit 0.0.35 → 0.0.46

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/Makefile CHANGED
@@ -1,3 +1,12 @@
1
- patch-and-publish:
2
- npm version patch
3
- npm publish
1
+ patch-and-publish:
2
+ @echo "Checking npm authentication..."
3
+ @npm whoami || (echo "Error: Not logged in to npm. Run 'npm login' first." && exit 1)
4
+ @echo "Authenticated as: $$(npm whoami)"
5
+ @echo "Verifying npm token in ~/.npmrc..."
6
+ @grep -q "//registry.npmjs.org/:_authToken=" ~/.npmrc || (echo "Error: No npm token found in ~/.npmrc" && exit 1)
7
+ @echo "Token found. Publishing..."
8
+ npm version patch
9
+ npm publish --access public || (echo "" && echo "❌ Publish failed! If you see a 2FA error:" && echo " 1. Go to: https://www.npmjs.com/settings/kate_yan/tokens" && echo " 2. Create a 'Granular Access Token'" && echo " 3. Enable 'Bypass 2FA' permission" && echo " 4. Update ~/.npmrc with: //registry.npmjs.org/:_authToken=YOUR_NEW_TOKEN" && exit 1)
10
+
11
+ stop:
12
+ pm2 stop "npx coffeeinabit" && pm2 delete "npx coffeeinabit" && pkill -9 -f "npm exec coffeeinabit"; pkill -9 -f "npx.*coffeeinabit"; pkill -9 -f "node.*coffeeinabit"
package/cloud_auth.js CHANGED
@@ -1,170 +1,170 @@
1
- import axios from 'axios';
2
-
3
- export class CloudAuth {
4
- constructor() {
5
- this.sessionSecret = process.env.SESSION_SECRET || 'coffeeinabit-secret-key-change-in-production';
6
- }
7
-
8
- async getLoginUrl(redirectUri) {
9
- try {
10
- const params = new URLSearchParams({
11
- client_id: '4rhqg2pjfhjkq42mr7iilpnmsu',
12
- response_type: 'code',
13
- redirect_uri: redirectUri,
14
- scope: 'openid email profile'
15
- });
16
-
17
- const loginUrl = `https://coffeeinabit.auth.us-east-1.amazoncognito.com/login?${params.toString()}`;
18
-
19
- return {
20
- success: true,
21
- loginUrl: loginUrl
22
- };
23
- } catch (error) {
24
- return {
25
- success: false,
26
- error: error.message
27
- };
28
- }
29
- }
30
-
31
- async exchangeCodeForTokens(code, redirectUri) {
32
- try {
33
- console.log('[CloudAuth] Received authorization code:', code.substring(0, 10) + '...');
34
- console.log('[CloudAuth] Using redirect_uri:', redirectUri);
35
-
36
- const response = await axios.get(`https://api.coffeeinabit.com/auth/token?code=${encodeURIComponent(code)}&redirect_url=${encodeURIComponent(redirectUri)}`);
37
-
38
- if (response.data.error) {
39
- throw new Error(response.data.error);
40
- }
41
-
42
- const { id_token, access_token, refresh_token } = response.data;
43
-
44
- return {
45
- success: true,
46
- tokens: {
47
- idToken: id_token,
48
- accessToken: access_token,
49
- refreshToken: refresh_token
50
- },
51
- user: this.extractUserFromToken(id_token)
52
- };
53
- } catch (error) {
54
- console.error('[CloudAuth] Token exchange failed:', error.response?.status, error.response?.data);
55
- return {
56
- success: false,
57
- error: error.message
58
- };
59
- }
60
- }
61
-
62
- async refreshTokens(refreshToken) {
63
- try {
64
- const response = await axios.get(
65
- `https://api.coffeeinabit.com/auth/refresh?refresh_token=${encodeURIComponent(refreshToken)}`
66
- );
67
-
68
- if (response.data.error) {
69
- throw new Error(response.data.error);
70
- }
71
-
72
- return {
73
- success: true,
74
- tokens: {
75
- idToken: response.data.id_token,
76
- accessToken: response.data.access_token,
77
- refreshToken: response.data.refresh_token || refreshToken
78
- },
79
- user: this.extractUserFromToken(response.data.id_token)
80
- };
81
- } catch (error) {
82
- return {
83
- success: false,
84
- error: error.message
85
- };
86
- }
87
- }
88
-
89
- extractUserFromToken(idToken) {
90
- if (!idToken) return null;
91
-
92
- try {
93
- const payload = JSON.parse(Buffer.from(idToken.split('.')[1], 'base64').toString());
94
- return {
95
- email: payload.email || payload['cognito:username'],
96
- userId: payload.sub
97
- };
98
- } catch (error) {
99
- console.error('[CloudAuth] Error extracting user from token:', error.message);
100
- return null;
101
- }
102
- }
103
-
104
- isTokenExpired(token) {
105
- if (!token) return true;
106
-
107
- try {
108
- const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
109
- if (!payload || !payload.exp) return true;
110
-
111
- const bufferTime = 5 * 60;
112
- return payload.exp < (Date.now() / 1000) + bufferTime;
113
- } catch (error) {
114
- console.error('[CloudAuth] Error checking token expiration:', error.message);
115
- return true;
116
- }
117
- }
118
-
119
- async ensureValidToken(session) {
120
- if (!session.tokens?.idToken) {
121
- return { valid: false, error: 'No token available' };
122
- }
123
-
124
- if (this.isTokenExpired(session.tokens.idToken)) {
125
- console.log('[CloudAuth] Token expired or expiring soon, attempting refresh...');
126
-
127
- if (!session.tokens.refreshToken) {
128
- return { valid: false, error: 'No refresh token available' };
129
- }
130
-
131
- const result = await this.refreshTokens(session.tokens.refreshToken);
132
-
133
- if (result.success) {
134
- this.storeTokensInSession(session, result.tokens, result.user);
135
- return { valid: true };
136
- } else {
137
- return { valid: false, error: result.error };
138
- }
139
- }
140
-
141
- return { valid: true };
142
- }
143
-
144
- storeTokensInSession(session, tokens, user) {
145
- session.tokens = tokens;
146
- session.user = user;
147
- console.log('[CloudAuth] Tokens stored in session for user:', user?.email);
148
- }
149
-
150
- clearSession(session) {
151
- session.tokens = null;
152
- session.user = null;
153
- console.log('[CloudAuth] Session cleared');
154
- }
155
-
156
- isAuthenticated(session) {
157
- return !!(session.tokens && session.tokens.idToken && session.tokens.accessToken);
158
- }
159
-
160
- getAuthStatus(session) {
161
- if (this.isAuthenticated(session)) {
162
- return {
163
- authenticated: true,
164
- user: session.user || null
165
- };
166
- } else {
167
- return { authenticated: false };
168
- }
169
- }
170
- }
1
+ import axios from 'axios';
2
+
3
+ export class CloudAuth {
4
+ constructor() {
5
+ this.sessionSecret = process.env.SESSION_SECRET || 'coffeeinabit-secret-key-change-in-production';
6
+ }
7
+
8
+ async getLoginUrl(redirectUri) {
9
+ try {
10
+ const params = new URLSearchParams({
11
+ client_id: '4rhqg2pjfhjkq42mr7iilpnmsu',
12
+ response_type: 'code',
13
+ redirect_uri: redirectUri,
14
+ scope: 'openid email profile'
15
+ });
16
+
17
+ const loginUrl = `https://coffeeinabit.auth.us-east-1.amazoncognito.com/login?${params.toString()}`;
18
+
19
+ return {
20
+ success: true,
21
+ loginUrl: loginUrl
22
+ };
23
+ } catch (error) {
24
+ return {
25
+ success: false,
26
+ error: error.message
27
+ };
28
+ }
29
+ }
30
+
31
+ async exchangeCodeForTokens(code, redirectUri) {
32
+ try {
33
+ console.log('[CloudAuth] Received authorization code:', code.substring(0, 10) + '...');
34
+ console.log('[CloudAuth] Using redirect_uri:', redirectUri);
35
+
36
+ const response = await axios.get(`https://api.coffeeinabit.com/auth/token?code=${encodeURIComponent(code)}&redirect_url=${encodeURIComponent(redirectUri)}`);
37
+
38
+ if (response.data.error) {
39
+ throw new Error(response.data.error);
40
+ }
41
+
42
+ const { id_token, access_token, refresh_token } = response.data;
43
+
44
+ return {
45
+ success: true,
46
+ tokens: {
47
+ idToken: id_token,
48
+ accessToken: access_token,
49
+ refreshToken: refresh_token
50
+ },
51
+ user: this.extractUserFromToken(id_token)
52
+ };
53
+ } catch (error) {
54
+ console.error('[CloudAuth] Token exchange failed:', error.response?.status, error.response?.data);
55
+ return {
56
+ success: false,
57
+ error: error.message
58
+ };
59
+ }
60
+ }
61
+
62
+ async refreshTokens(refreshToken) {
63
+ try {
64
+ const response = await axios.get(
65
+ `https://api.coffeeinabit.com/auth/refresh?refresh_token=${encodeURIComponent(refreshToken)}`
66
+ );
67
+
68
+ if (response.data.error) {
69
+ throw new Error(response.data.error);
70
+ }
71
+
72
+ return {
73
+ success: true,
74
+ tokens: {
75
+ idToken: response.data.id_token,
76
+ accessToken: response.data.access_token,
77
+ refreshToken: response.data.refresh_token || refreshToken
78
+ },
79
+ user: this.extractUserFromToken(response.data.id_token)
80
+ };
81
+ } catch (error) {
82
+ return {
83
+ success: false,
84
+ error: error.message
85
+ };
86
+ }
87
+ }
88
+
89
+ extractUserFromToken(idToken) {
90
+ if (!idToken) return null;
91
+
92
+ try {
93
+ const payload = JSON.parse(Buffer.from(idToken.split('.')[1], 'base64').toString());
94
+ return {
95
+ email: payload.email || payload['cognito:username'],
96
+ userId: payload.sub
97
+ };
98
+ } catch (error) {
99
+ console.error('[CloudAuth] Error extracting user from token:', error.message);
100
+ return null;
101
+ }
102
+ }
103
+
104
+ isTokenExpired(token) {
105
+ if (!token) return true;
106
+
107
+ try {
108
+ const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
109
+ if (!payload || !payload.exp) return true;
110
+
111
+ const bufferTime = 5 * 60;
112
+ return payload.exp < (Date.now() / 1000) + bufferTime;
113
+ } catch (error) {
114
+ console.error('[CloudAuth] Error checking token expiration:', error.message);
115
+ return true;
116
+ }
117
+ }
118
+
119
+ async ensureValidToken(session) {
120
+ if (!session.tokens?.idToken) {
121
+ return { valid: false, error: 'No token available' };
122
+ }
123
+
124
+ if (this.isTokenExpired(session.tokens.idToken)) {
125
+ console.log('[CloudAuth] Token expired or expiring soon, attempting refresh...');
126
+
127
+ if (!session.tokens.refreshToken) {
128
+ return { valid: false, error: 'No refresh token available' };
129
+ }
130
+
131
+ const result = await this.refreshTokens(session.tokens.refreshToken);
132
+
133
+ if (result.success) {
134
+ this.storeTokensInSession(session, result.tokens, result.user);
135
+ return { valid: true };
136
+ } else {
137
+ return { valid: false, error: result.error };
138
+ }
139
+ }
140
+
141
+ return { valid: true };
142
+ }
143
+
144
+ storeTokensInSession(session, tokens, user) {
145
+ session.tokens = tokens;
146
+ session.user = user;
147
+ console.log('[CloudAuth] Tokens stored in session for user:', user?.email);
148
+ }
149
+
150
+ clearSession(session) {
151
+ session.tokens = null;
152
+ session.user = null;
153
+ console.log('[CloudAuth] Session cleared');
154
+ }
155
+
156
+ isAuthenticated(session) {
157
+ return !!(session.tokens && session.tokens.idToken && session.tokens.accessToken);
158
+ }
159
+
160
+ getAuthStatus(session) {
161
+ if (this.isAuthenticated(session)) {
162
+ return {
163
+ authenticated: true,
164
+ user: session.user || null
165
+ };
166
+ } else {
167
+ return { authenticated: false };
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,165 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Auto-Start Helper
6
+ *
7
+ * Handles automatic startup of LinkedIn automation when the server starts
8
+ * if there are existing authenticated sessions.
9
+ */
10
+
11
+ /**
12
+ * Attempt to automatically start automation if user is authenticated
13
+ * Note: Auto-start uses legacy single instance for backward compatibility.
14
+ * New implementations should wait for WebSocket connections.
15
+ *
16
+ * @param {object} sessionStore - Express session store
17
+ * @param {string} contextDir - Context directory path
18
+ * @param {object} cloudAuth - CloudAuth instance
19
+ * @param {object} linkedinAutomation - LinkedInAutomation instance
20
+ */
21
+ export async function tryAutoStartAutomation(sessionStore, contextDir, cloudAuth, linkedinAutomation) {
22
+ try {
23
+ // First, try using sessionStore.all if available
24
+ if (typeof sessionStore.all === 'function') {
25
+ sessionStore.all(async (err, sessions) => {
26
+ if (err) {
27
+ console.log('[AutoStart] Could not retrieve sessions for auto-start:', err.message);
28
+ // Fall through to file-based enumeration
29
+ await tryFileBasedAutoStart(contextDir, cloudAuth, linkedinAutomation);
30
+ return;
31
+ }
32
+
33
+ if (!sessions || Object.keys(sessions).length === 0) {
34
+ console.log('[AutoStart] No existing sessions found, skipping auto-start');
35
+ return;
36
+ }
37
+
38
+ // Try to find an authenticated session
39
+ for (const sessionId in sessions) {
40
+ const sessionData = sessions[sessionId];
41
+ if (sessionData && sessionData.tokens && sessionData.tokens.idToken && sessionData.tokens.accessToken) {
42
+ const success = await tryStartAutomationWithSession(sessionData, cloudAuth, linkedinAutomation);
43
+ if (success) return; // Successfully started, exit function
44
+ }
45
+ }
46
+
47
+ console.log('[AutoStart] No valid authenticated session found, skipping auto-start');
48
+ });
49
+ } else {
50
+ // Fall back to file-based enumeration
51
+ await tryFileBasedAutoStart(contextDir, cloudAuth, linkedinAutomation);
52
+ }
53
+ } catch (error) {
54
+ console.error('[AutoStart] Error during auto-start check:', error.message);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Try to start automation by reading session files directly from the filesystem
60
+ */
61
+ async function tryFileBasedAutoStart(contextDir, cloudAuth, linkedinAutomation) {
62
+ try {
63
+ // session-file-store may store sessions in a 'sessions' subdirectory or directly in contextDir
64
+ let sessionsDir = path.join(contextDir, 'sessions');
65
+ let sessionFiles = [];
66
+
67
+ // First try the sessions subdirectory
68
+ if (fs.existsSync(sessionsDir)) {
69
+ sessionFiles = fs.readdirSync(sessionsDir)
70
+ .filter(file => file.endsWith('.json'))
71
+ .map(file => path.join(sessionsDir, file));
72
+ }
73
+
74
+ // If no files found in subdirectory, check the context directory directly
75
+ if (sessionFiles.length === 0 && fs.existsSync(contextDir)) {
76
+ const allFiles = fs.readdirSync(contextDir).filter(file => file.endsWith('.json'));
77
+ // Filter out non-session files (like storage_state files)
78
+ sessionFiles = allFiles
79
+ .filter(file => {
80
+ const filePath = path.join(contextDir, file);
81
+ try {
82
+ const content = fs.readFileSync(filePath, 'utf8');
83
+ const data = JSON.parse(content);
84
+ // Session files have tokens and cookie structure
85
+ return data.tokens && data.cookie;
86
+ } catch {
87
+ return false;
88
+ }
89
+ })
90
+ .map(file => path.join(contextDir, file));
91
+ }
92
+
93
+ if (sessionFiles.length === 0) {
94
+ console.log('[AutoStart] No session files found, skipping auto-start');
95
+ return;
96
+ }
97
+
98
+ console.log(`[AutoStart] Found ${sessionFiles.length} session file(s), checking for authenticated sessions...`);
99
+
100
+ // Try each session file
101
+ for (const sessionPath of sessionFiles) {
102
+ try {
103
+ const fileContent = fs.readFileSync(sessionPath, 'utf8');
104
+ const sessionData = JSON.parse(fileContent);
105
+
106
+ if (sessionData && sessionData.tokens && sessionData.tokens.idToken && sessionData.tokens.accessToken) {
107
+ const success = await tryStartAutomationWithSession(sessionData, cloudAuth, linkedinAutomation);
108
+ if (success) return; // Successfully started, exit function
109
+ }
110
+ } catch (error) {
111
+ // Skip invalid session files
112
+ continue;
113
+ }
114
+ }
115
+
116
+ console.log('[AutoStart] No valid authenticated session found in session files, skipping auto-start');
117
+ } catch (error) {
118
+ console.log('[AutoStart] Error reading session files:', error.message);
119
+ console.log('[AutoStart] Automation will start when clients connect via WebSocket');
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Try to start automation with a session object
125
+ * @param {object} sessionData - Session data object with tokens and user info
126
+ * @param {object} cloudAuth - CloudAuth instance
127
+ * @param {object} linkedinAutomation - LinkedInAutomation instance
128
+ * @returns {Promise<boolean>} true if automation started successfully
129
+ */
130
+ export async function tryStartAutomationWithSession(sessionData, cloudAuth, linkedinAutomation) {
131
+ try {
132
+ // Check if automation is already running
133
+ if (linkedinAutomation.isRunning) {
134
+ console.log('[AutoStart] Automation is already running, skipping auto-start');
135
+ return false;
136
+ }
137
+
138
+ // Create a session-like object that works with cloudAuth methods
139
+ const sessionObj = {
140
+ tokens: sessionData.tokens,
141
+ user: sessionData.user
142
+ };
143
+
144
+ // Validate and refresh token if needed
145
+ const tokenCheck = await cloudAuth.ensureValidToken(sessionObj);
146
+ if (!tokenCheck.valid) {
147
+ console.log('[AutoStart] Session token invalid, skipping auto-start');
148
+ return false;
149
+ }
150
+
151
+ // Start automation with the authenticated session (legacy mode)
152
+ // Note: This uses the single instance for backward compatibility
153
+ // New implementations should wait for WebSocket connections
154
+ console.log('[AutoStart] Auto-starting automation with authenticated session (legacy mode)...');
155
+ linkedinAutomation._currentAccessToken = sessionObj.tokens.idToken;
156
+ linkedinAutomation._currentRefreshToken = sessionObj.tokens.refreshToken;
157
+ linkedinAutomation._currentUserEmail = sessionObj.user?.email || 'default_user';
158
+ await linkedinAutomation.startAutomation(true); // Start in headless mode by default
159
+ console.log('[AutoStart] ✅ Automation auto-started successfully (legacy mode)');
160
+ return true;
161
+ } catch (error) {
162
+ console.error('[AutoStart] Error during auto-start:', error.message);
163
+ return false;
164
+ }
165
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Token Synchronization Helper
3
+ *
4
+ * Handles synchronization of authentication tokens between sessions
5
+ * and LinkedIn automation processes.
6
+ */
7
+
8
+ /**
9
+ * Sync tokens from session to LinkedInAutomation
10
+ * Supports both legacy single instance and new multi-process state
11
+ *
12
+ * @param {object} session - Express session object
13
+ * @param {string|null} socketId - Optional socket ID for process-specific sync
14
+ * @param {object} appState - AppState instance (optional, for multi-process)
15
+ * @param {object} cloudAuth - CloudAuth instance
16
+ * @param {object} linkedinAutomation - Legacy LinkedInAutomation instance (optional)
17
+ */
18
+ export function syncTokensToAutomation(session, socketId, appState, cloudAuth, linkedinAutomation = null) {
19
+ if (!session?.tokens || !session?.user) {
20
+ return;
21
+ }
22
+
23
+ // If socketId provided, sync to specific process
24
+ if (socketId && appState) {
25
+ const process = appState.getProcess(socketId);
26
+ if (process && process.automation && process.automation.isRunning) {
27
+ const hadExpiredToken = process.automation._currentAccessToken &&
28
+ cloudAuth.isTokenExpired(process.automation._currentAccessToken);
29
+
30
+ process.automation._currentAccessToken = session.tokens.idToken;
31
+ process.automation._currentRefreshToken = session.tokens.refreshToken;
32
+
33
+ if (hadExpiredToken) {
34
+ console.log(`[TokenSync] Synced refreshed tokens to process ${socketId} for user:`, session.user.email);
35
+ }
36
+ }
37
+ } else if (linkedinAutomation) {
38
+ // Legacy: sync to single automation instance
39
+ if (linkedinAutomation.isRunning &&
40
+ linkedinAutomation._currentUserEmail === session.user.email) {
41
+ const hadExpiredToken = linkedinAutomation._currentAccessToken &&
42
+ cloudAuth.isTokenExpired(linkedinAutomation._currentAccessToken);
43
+
44
+ linkedinAutomation._currentAccessToken = session.tokens.idToken;
45
+ linkedinAutomation._currentRefreshToken = session.tokens.refreshToken;
46
+
47
+ if (hadExpiredToken) {
48
+ console.log('[TokenSync] Synced refreshed tokens to LinkedInAutomation for user:', session.user.email);
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Sync tokens to all processes for a specific user
56
+ *
57
+ * @param {object} session - Express session object
58
+ * @param {string} userEmail - User email address
59
+ * @param {object} appState - AppState instance
60
+ * @param {object} cloudAuth - CloudAuth instance
61
+ * @param {object} linkedinAutomation - Legacy LinkedInAutomation instance (optional)
62
+ */
63
+ export function syncTokensToAllUserProcesses(session, userEmail, appState, cloudAuth, linkedinAutomation = null) {
64
+ if (!userEmail || !appState) {
65
+ return;
66
+ }
67
+
68
+ const userProcesses = appState.getProcessesByUser(userEmail);
69
+ userProcesses.forEach(process => {
70
+ syncTokensToAutomation(session, process.socketId, appState, cloudAuth);
71
+ });
72
+
73
+ // Also sync to legacy instance if provided
74
+ if (linkedinAutomation) {
75
+ syncTokensToAutomation(session, null, null, cloudAuth, linkedinAutomation);
76
+ }
77
+ }