coffeeinabit 0.0.47 → 0.0.49

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.
@@ -12,7 +12,6 @@ import { executeSendMessages } from './tools/send_messages.js';
12
12
  import { executeCommentPost } from './tools/comment_post.js';
13
13
  import { executeGetMessages, closeAllConversationBubbles } from './tools/get_messages.js';
14
14
  import { executeLinkedInSearch } from './tools/get_linkedin_search_results.js';
15
- import { executeGetDailyLinkedInConnections } from './tools/get_daily_linkedin_connections.js';
16
15
  import { executeGetNewMessages } from './tools/get_new_messages.js';
17
16
  import { executeGetLinkedInUpdates } from './tools/get_linkedin_updates.js';
18
17
  import { safeGoto } from './tools/navigation.js';
@@ -42,6 +41,16 @@ export class LinkedInAutomation {
42
41
  this.ambientMouseTimeout = null;
43
42
  this.cloudAuth = new CloudAuth();
44
43
  this._currentRefreshToken = null;
44
+ this._currentAccessToken = null; // Track access token explicitly
45
+
46
+ // Authentication failure tracking
47
+ this.consecutiveAuthFailures = 0;
48
+ this.maxAuthFailures = 3; // Stop polling after 3 consecutive failures
49
+ this.authFailurePaused = false;
50
+ this.lastAuthFailureTime = null;
51
+
52
+ // Token re-sync callback (set externally if available)
53
+ this.tokenResyncCallback = null;
45
54
 
46
55
  // Lifecycle phases
47
56
  this.PHASE_IDLE = 'idle';
@@ -350,6 +359,7 @@ export class LinkedInAutomation {
350
359
  });
351
360
 
352
361
  this.page.on('framenavigated', (frame) => {
362
+ if (!this.page) return;
353
363
  if (frame === this.page.mainFrame()) {
354
364
  this.updatePageInfo();
355
365
  }
@@ -730,13 +740,43 @@ export class LinkedInAutomation {
730
740
  if (!this.ensureBrowserAvailable('pollForActions')) {
731
741
  return;
732
742
  }
743
+
744
+ // Check if polling is paused due to auth failures
745
+ if (this.authFailurePaused) {
746
+ console.log('[LinkedInAutomation] Polling paused due to authentication failures. Attempting recovery...');
747
+
748
+ // Try to re-sync tokens and recover
749
+ const idToken = await this.getAccessToken();
750
+ if (idToken && !this.cloudAuth.isTokenExpired(idToken)) {
751
+ console.log('[LinkedInAutomation] Authentication recovered, resuming polling');
752
+ this.authFailurePaused = false;
753
+ this.consecutiveAuthFailures = 0;
754
+ // Continue with normal polling below
755
+ } else {
756
+ console.log('[LinkedInAutomation] Still unable to authenticate, skipping poll');
757
+
758
+ // Emit event to notify UI
759
+ if (this.io) {
760
+ this.io.emit('automation:auth-failure', {
761
+ message: 'Authentication failed. Please re-authenticate.',
762
+ paused: true,
763
+ consecutiveFailures: this.consecutiveAuthFailures,
764
+ lastFailureTime: this.lastAuthFailureTime
765
+ });
766
+ }
767
+
768
+ return false;
769
+ }
770
+ }
771
+
733
772
  try {
734
773
  console.log('[LinkedInAutomation] Polling for actions from backend...');
735
774
 
736
775
  const idToken = await this.getAccessToken();
737
776
  if (!idToken) {
738
777
  console.error('[LinkedInAutomation] No id_token available for authentication');
739
- return;
778
+ this.handleAuthFailure('No token available');
779
+ return false;
740
780
  }
741
781
 
742
782
  console.log('[LinkedInAutomation] Using id_token for authentication:', idToken.substring(0, 20) + '...');
@@ -760,25 +800,116 @@ export class LinkedInAutomation {
760
800
  for (const action of actions) {
761
801
  await this.executeAction(action);
762
802
  }
803
+ // Reset failure counter on successful request
804
+ this.consecutiveAuthFailures = 0;
805
+ this.authFailurePaused = false;
763
806
  // Indicate there was work
764
807
  return true;
765
808
  } else {
766
809
  console.log('[LinkedInAutomation] No actions to execute, continuing to poll...');
810
+ // Reset failure counter on successful request (even with no actions)
811
+ this.consecutiveAuthFailures = 0;
812
+ this.authFailurePaused = false;
767
813
  return false;
768
814
  }
815
+ } else if (response.status === 401) {
816
+ // Handle 401 Unauthorized specifically
817
+ const errorText = await response.text();
818
+ let errorMessage;
819
+ try {
820
+ const errorJson = JSON.parse(errorText);
821
+ errorMessage = errorJson.message || 'Unauthorized';
822
+ } catch {
823
+ errorMessage = errorText || 'Unauthorized';
824
+ }
825
+
826
+ console.error('[LinkedInAutomation] Authentication failed (401 Unauthorized):', errorMessage);
827
+
828
+ // Clear current token since it's invalid
829
+ this._currentAccessToken = null;
830
+
831
+ // Try to re-sync tokens one more time
832
+ console.log('[LinkedInAutomation] Attempting token re-sync after 401 error...');
833
+ const resynced = await this.attemptTokenResync();
834
+
835
+ if (!resynced) {
836
+ this.handleAuthFailure(`401 Unauthorized: ${errorMessage}`);
837
+ } else {
838
+ // If re-sync worked, try to refresh the token
839
+ if (this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
840
+ const refreshResult = await this.cloudAuth.refreshTokens(this._currentRefreshToken);
841
+ if (!refreshResult.success) {
842
+ this.handleAuthFailure(`401 Unauthorized: Token re-sync succeeded but refresh failed`);
843
+ } else {
844
+ this._currentAccessToken = refreshResult.tokens.idToken;
845
+ if (refreshResult.tokens.refreshToken) {
846
+ this._currentRefreshToken = refreshResult.tokens.refreshToken;
847
+ }
848
+ console.log('[LinkedInAutomation] Token re-sync and refresh succeeded after 401, resetting failure counter');
849
+ this.consecutiveAuthFailures = 0;
850
+ this.authFailurePaused = false;
851
+ }
852
+ } else {
853
+ console.log('[LinkedInAutomation] Token re-sync succeeded after 401, resetting failure counter');
854
+ this.consecutiveAuthFailures = 0;
855
+ this.authFailurePaused = false;
856
+ }
857
+ }
858
+
859
+ return false;
769
860
  } else {
770
861
  console.error('[LinkedInAutomation] Failed to fetch actions. Status:', response.status, response.statusText);
771
862
  const errorText = await response.text();
772
863
  console.error('[LinkedInAutomation] Error response:', errorText);
864
+ // Non-auth errors don't count as auth failures
773
865
  return false;
774
866
  }
775
867
  } catch (error) {
776
868
  console.error('[LinkedInAutomation] Error polling for actions:', error.message);
777
869
  console.error('[LinkedInAutomation] Full error:', error);
870
+
871
+ // Check if error is auth-related
872
+ if (error.message.includes('401') || error.message.includes('Unauthorized') || error.message.includes('token')) {
873
+ this.handleAuthFailure(`Network error: ${error.message}`);
874
+ }
875
+
778
876
  return false;
779
877
  }
780
878
  }
781
879
 
880
+ /**
881
+ * Handle authentication failure - track consecutive failures and pause polling if threshold reached
882
+ * @param {string} reason - Reason for the auth failure
883
+ */
884
+ handleAuthFailure(reason) {
885
+ this.consecutiveAuthFailures++;
886
+ this.lastAuthFailureTime = new Date().toISOString();
887
+
888
+ console.warn(`[LinkedInAutomation] Authentication failure #${this.consecutiveAuthFailures}: ${reason}`);
889
+
890
+ if (this.consecutiveAuthFailures >= this.maxAuthFailures) {
891
+ if (!this.authFailurePaused) {
892
+ this.authFailurePaused = true;
893
+ console.error(`[LinkedInAutomation] ⚠️ STOPPING polling after ${this.consecutiveAuthFailures} consecutive authentication failures`);
894
+ console.error('[LinkedInAutomation] Polling will resume when authentication is restored.');
895
+ console.error('[LinkedInAutomation] Please re-authenticate or check your session.');
896
+
897
+ // Emit event to notify UI
898
+ if (this.io) {
899
+ this.io.emit('automation:auth-failure', {
900
+ message: `Authentication failed after ${this.consecutiveAuthFailures} attempts. Polling paused.`,
901
+ paused: true,
902
+ consecutiveFailures: this.consecutiveAuthFailures,
903
+ lastFailureTime: this.lastAuthFailureTime,
904
+ reason: reason
905
+ });
906
+ }
907
+ }
908
+ } else {
909
+ console.warn(`[LinkedInAutomation] Will pause polling after ${this.maxAuthFailures - this.consecutiveAuthFailures} more failures`);
910
+ }
911
+ }
912
+
782
913
  async executeAction(action) {
783
914
  const actionLogs = [];
784
915
  const originalConsoleLog = console.log;
@@ -858,10 +989,6 @@ export class LinkedInAutomation {
858
989
  console.log('[LinkedInAutomation] Executing get_linkedin_search_results action...');
859
990
  result = await executeLinkedInSearch(this.page, action);
860
991
  break;
861
- case 'get_daily_linkedin_connections':
862
- console.log('[LinkedInAutomation] Executing get_daily_linkedin_connections action...');
863
- result = await executeGetDailyLinkedInConnections(this.page, action);
864
- break;
865
992
  case 'get_new_messages':
866
993
  console.log('[LinkedInAutomation] Executing get_new_messages action...');
867
994
  result = await executeGetNewMessages(this.page, action, await this.getAccessToken());
@@ -1053,11 +1180,64 @@ export class LinkedInAutomation {
1053
1180
  }
1054
1181
  }
1055
1182
 
1183
+ /**
1184
+ * Set a callback function to re-sync tokens from external source (e.g., session store)
1185
+ * @param {Function} callback - Function that returns Promise<{idToken, refreshToken}> or null
1186
+ */
1187
+ setTokenResyncCallback(callback) {
1188
+ this.tokenResyncCallback = callback;
1189
+ }
1190
+
1191
+ /**
1192
+ * Attempt to re-sync tokens from external source (session, etc.)
1193
+ * @returns {Promise<boolean>} true if tokens were successfully re-synced
1194
+ */
1195
+ async attemptTokenResync() {
1196
+ if (!this.tokenResyncCallback) {
1197
+ return false;
1198
+ }
1199
+
1200
+ try {
1201
+ const tokens = await this.tokenResyncCallback();
1202
+ if (tokens && tokens.idToken && tokens.refreshToken) {
1203
+ console.log('[LinkedInAutomation] Tokens re-synced from external source');
1204
+ this._currentAccessToken = tokens.idToken;
1205
+ this._currentRefreshToken = tokens.refreshToken;
1206
+
1207
+ // If tokens are expired, try to refresh them
1208
+ if (this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
1209
+ console.log('[LinkedInAutomation] Re-synced tokens are expired, attempting refresh...');
1210
+ const result = await this.cloudAuth.refreshTokens(this._currentRefreshToken);
1211
+ if (result.success) {
1212
+ this._currentAccessToken = result.tokens.idToken;
1213
+ if (result.tokens.refreshToken) {
1214
+ this._currentRefreshToken = result.tokens.refreshToken;
1215
+ }
1216
+ } else {
1217
+ console.error('[LinkedInAutomation] Failed to refresh re-synced tokens:', result.error);
1218
+ return false;
1219
+ }
1220
+ }
1221
+
1222
+ return true;
1223
+ }
1224
+ } catch (error) {
1225
+ console.error('[LinkedInAutomation] Error during token re-sync:', error.message);
1226
+ }
1227
+
1228
+ return false;
1229
+ }
1230
+
1056
1231
  async getAccessToken() {
1057
- // Check if token exists
1232
+ // First, try to get current token
1058
1233
  if (!this._currentAccessToken) {
1059
- console.error('[LinkedInAutomation] No access token available');
1060
- return null;
1234
+ console.log('[LinkedInAutomation] No access token available, attempting re-sync...');
1235
+ const resynced = await this.attemptTokenResync();
1236
+ if (!resynced) {
1237
+ console.error('[LinkedInAutomation] No access token available and re-sync failed');
1238
+ return null;
1239
+ }
1240
+ // Token was re-synced, continue to validation below
1061
1241
  }
1062
1242
 
1063
1243
  // Check if token is expired or expiring soon (within 5 minutes)
@@ -1066,8 +1246,12 @@ export class LinkedInAutomation {
1066
1246
 
1067
1247
  // Check if refresh token is available
1068
1248
  if (!this._currentRefreshToken) {
1069
- console.error('[LinkedInAutomation] No refresh token available, cannot refresh');
1070
- return null;
1249
+ console.log('[LinkedInAutomation] No refresh token available, attempting re-sync...');
1250
+ const resynced = await this.attemptTokenResync();
1251
+ if (!resynced || !this._currentRefreshToken) {
1252
+ console.error('[LinkedInAutomation] No refresh token available and re-sync failed');
1253
+ return null;
1254
+ }
1071
1255
  }
1072
1256
 
1073
1257
  // Attempt to refresh the token
@@ -1081,17 +1265,48 @@ export class LinkedInAutomation {
1081
1265
  if (result.tokens.refreshToken) {
1082
1266
  this._currentRefreshToken = result.tokens.refreshToken;
1083
1267
  }
1268
+ // Reset failure counter on successful refresh
1269
+ this.consecutiveAuthFailures = 0;
1270
+ this.authFailurePaused = false;
1084
1271
  return this._currentAccessToken;
1085
1272
  } else {
1086
1273
  console.error('[LinkedInAutomation] Token refresh failed:', result.error);
1274
+
1275
+ // Try one more time to re-sync before giving up
1276
+ console.log('[LinkedInAutomation] Attempting token re-sync after refresh failure...');
1277
+ const resynced = await this.attemptTokenResync();
1278
+ if (resynced && !this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
1279
+ console.log('[LinkedInAutomation] Token re-synced successfully after refresh failure');
1280
+ this.consecutiveAuthFailures = 0;
1281
+ this.authFailurePaused = false;
1282
+ return this._currentAccessToken;
1283
+ }
1284
+
1087
1285
  return null;
1088
1286
  }
1089
1287
  } catch (error) {
1090
1288
  console.error('[LinkedInAutomation] Error during token refresh:', error.message);
1289
+
1290
+ // Try re-sync one more time
1291
+ const resynced = await this.attemptTokenResync();
1292
+ if (resynced && !this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
1293
+ console.log('[LinkedInAutomation] Token re-synced successfully after refresh error');
1294
+ this.consecutiveAuthFailures = 0;
1295
+ this.authFailurePaused = false;
1296
+ return this._currentAccessToken;
1297
+ }
1298
+
1091
1299
  return null;
1092
1300
  }
1093
1301
  }
1094
1302
 
1303
+ // Token is valid, reset failure counter
1304
+ if (this.consecutiveAuthFailures > 0) {
1305
+ console.log('[LinkedInAutomation] Authentication recovered, resetting failure counter');
1306
+ this.consecutiveAuthFailures = 0;
1307
+ this.authFailurePaused = false;
1308
+ }
1309
+
1095
1310
  return this._currentAccessToken;
1096
1311
  }
1097
1312
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coffeeinabit",
3
- "version": "0.0.47",
3
+ "version": "0.0.49",
4
4
  "description": "coffeeinabit app",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "axios": "^1.6.0",
25
+ "coffeeinabit": "^0.0.47",
25
26
  "dotenv": "^16.3.1",
26
27
  "express": "^4.18.2",
27
28
  "express-session": "^1.17.3",
package/server.js CHANGED
@@ -37,9 +37,53 @@ const appState = new AppState();
37
37
  // Legacy single automation instance (for backward compatibility during migration)
38
38
  // TODO: Remove after full migration to multi-process state
39
39
  const linkedinAutomation = new LinkedInAutomation(io);
40
- linkedinAutomation.getAccessToken = function() {
41
- return this._currentAccessToken || null;
42
- };
40
+
41
+ // Set up token re-sync callback for legacy automation instance
42
+ // This allows the automation to re-sync tokens from the session store when they expire
43
+ linkedinAutomation.setTokenResyncCallback(async () => {
44
+ try {
45
+ // Try to get tokens from session store for the user
46
+ if (!linkedinAutomation._currentUserEmail) {
47
+ return null;
48
+ }
49
+
50
+ // Get all sessions and find one for this user
51
+ if (typeof sessionStore.all === 'function') {
52
+ return new Promise((resolve) => {
53
+ sessionStore.all((err, sessions) => {
54
+ if (err || !sessions) {
55
+ resolve(null);
56
+ return;
57
+ }
58
+
59
+ // Find a session for this user
60
+ for (const sessionId in sessions) {
61
+ const session = sessions[sessionId];
62
+ if (session && session.user && session.user.email === linkedinAutomation._currentUserEmail) {
63
+ // Ensure tokens are valid
64
+ cloudAuth.ensureValidToken(session).then(result => {
65
+ if (result.valid && session.tokens) {
66
+ resolve({
67
+ idToken: session.tokens.idToken,
68
+ refreshToken: session.tokens.refreshToken
69
+ });
70
+ } else {
71
+ resolve(null);
72
+ }
73
+ }).catch(() => resolve(null));
74
+ return;
75
+ }
76
+ }
77
+ resolve(null);
78
+ });
79
+ });
80
+ }
81
+ } catch (error) {
82
+ console.error('[Server] Error in token re-sync callback:', error.message);
83
+ return null;
84
+ }
85
+ return null;
86
+ });
43
87
 
44
88
  // Initialize session store
45
89
  const FileStoreSession = FileStore(session);
@@ -95,6 +139,9 @@ app.use('/auth', createAuthRoutes(cloudAuth, PORT, appState, linkedinAutomation)
95
139
  app.use('/api/automation', createAutomationRoutes(cloudAuth, appState, linkedinAutomation, io));
96
140
  app.use('/api/state', createStateRoutes(cloudAuth, appState));
97
141
 
142
+ // Set up token re-sync dependencies for AppState
143
+ appState.setTokenResyncDependencies(cloudAuth, sessionStore);
144
+
98
145
  // Setup WebSocket handlers
99
146
  setupSocketHandlers(io, appState);
100
147
 
@@ -18,6 +18,10 @@ export class AppState {
18
18
  // Private state: Map of socketId -> process state
19
19
  this._processes = new Map();
20
20
 
21
+ // Dependencies for token re-sync (set externally)
22
+ this._cloudAuth = null;
23
+ this._sessionStore = null;
24
+
21
25
  // Statistics
22
26
  this._stats = {
23
27
  totalProcessesCreated: 0,
@@ -26,6 +30,16 @@ export class AppState {
26
30
  };
27
31
  }
28
32
 
33
+ /**
34
+ * Set dependencies for token re-sync
35
+ * @param {object} cloudAuth - CloudAuth instance
36
+ * @param {object} sessionStore - Session store instance
37
+ */
38
+ setTokenResyncDependencies(cloudAuth, sessionStore) {
39
+ this._cloudAuth = cloudAuth;
40
+ this._sessionStore = sessionStore;
41
+ }
42
+
29
43
  /**
30
44
  * Create a new automation process for a socket connection
31
45
  * @param {string} socketId - WebSocket connection ID
@@ -40,9 +54,54 @@ export class AppState {
40
54
  }
41
55
 
42
56
  const automation = new LinkedInAutomation(io);
43
- automation.getAccessToken = function() {
44
- return this._currentAccessToken || null;
45
- };
57
+
58
+ // Set up token re-sync callback if dependencies are available
59
+ if (this._cloudAuth && this._sessionStore) {
60
+ const userEmail = session?.user?.email;
61
+ automation.setTokenResyncCallback(async () => {
62
+ try {
63
+ if (!userEmail) {
64
+ return null;
65
+ }
66
+
67
+ // Try to get tokens from session store for this user
68
+ if (typeof this._sessionStore.all === 'function') {
69
+ return new Promise((resolve) => {
70
+ this._sessionStore.all((err, sessions) => {
71
+ if (err || !sessions) {
72
+ resolve(null);
73
+ return;
74
+ }
75
+
76
+ // Find a session for this user
77
+ for (const sessionId in sessions) {
78
+ const sess = sessions[sessionId];
79
+ if (sess && sess.user && sess.user.email === userEmail) {
80
+ // Ensure tokens are valid
81
+ this._cloudAuth.ensureValidToken(sess).then(result => {
82
+ if (result.valid && sess.tokens) {
83
+ resolve({
84
+ idToken: sess.tokens.idToken,
85
+ refreshToken: sess.tokens.refreshToken
86
+ });
87
+ } else {
88
+ resolve(null);
89
+ }
90
+ }).catch(() => resolve(null));
91
+ return;
92
+ }
93
+ }
94
+ resolve(null);
95
+ });
96
+ });
97
+ }
98
+ } catch (error) {
99
+ console.error('[AppState] Error in token re-sync callback:', error.message);
100
+ return null;
101
+ }
102
+ return null;
103
+ });
104
+ }
46
105
 
47
106
  const processState = {
48
107
  socketId,