coffeeinabit 0.0.59 → 0.0.61

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.
@@ -60,6 +60,13 @@ export class LinkedInAutomation {
60
60
 
61
61
  // Token re-sync callback (set externally if available)
62
62
  this.tokenResyncCallback = null;
63
+
64
+ // Token sync-back callback (to persist refreshed tokens to session store)
65
+ this.tokenSyncBackCallback = null;
66
+
67
+ // Proactive token refresh timer
68
+ this._tokenRefreshInterval = null;
69
+ this._tokenRefreshIntervalMs = 45 * 60 * 1000; // 45 minutes
63
70
 
64
71
  // Lifecycle phases
65
72
  this.PHASE_IDLE = 'idle';
@@ -363,6 +370,7 @@ export class LinkedInAutomation {
363
370
  this.setupPageListeners();
364
371
  this.startAmbientMouseMovements();
365
372
  this.startScreenshotCapture();
373
+ this.startTokenRefreshTimer();
366
374
 
367
375
  logger.log('[LinkedInAutomation] Automation fully started and running');
368
376
 
@@ -638,6 +646,7 @@ export class LinkedInAutomation {
638
646
  this.stopScreenshotCapture();
639
647
  this.stopActionPolling();
640
648
  this.stopAmbientMouseMovements();
649
+ this.stopTokenRefreshTimer();
641
650
 
642
651
  await this.closeBrowser();
643
652
 
@@ -730,12 +739,15 @@ export class LinkedInAutomation {
730
739
  }
731
740
  }
732
741
 
733
- async uploadScreenshotToS3(base64Screenshot, actionId) {
742
+ async uploadScreenshotToS3(base64Screenshot, actionId, retryCount = 0) {
734
743
  try {
735
744
  const idToken = await this.getAccessToken();
736
- if (!idToken) return;
745
+ if (!idToken) {
746
+ logger.warn('[LinkedInAutomation] Screenshot upload skipped - no valid token');
747
+ return;
748
+ }
737
749
 
738
- fetch('https://api.coffeeinabit.com/app/screenshots', {
750
+ const response = await fetch('https://api.coffeeinabit.com/app/screenshots', {
739
751
  method: 'POST',
740
752
  headers: {
741
753
  'Content-Type': 'application/json',
@@ -746,11 +758,20 @@ export class LinkedInAutomation {
746
758
  action_id: actionId || null,
747
759
  timestamp: Date.now()
748
760
  })
749
- }).catch(err => {
750
- logger.error('[LinkedInAutomation] Screenshot upload failed:', err.message);
751
761
  });
762
+
763
+ if (response.status === 401 && retryCount < 1) {
764
+ logger.warn('[LinkedInAutomation] Screenshot upload got 401, refreshing token and retrying...');
765
+ this._currentAccessToken = null;
766
+ return this.uploadScreenshotToS3(base64Screenshot, actionId, retryCount + 1);
767
+ }
752
768
  } catch (error) {
753
- logger.error('[LinkedInAutomation] Screenshot upload error:', error.message);
769
+ if (retryCount < 1) {
770
+ logger.warn('[LinkedInAutomation] Screenshot upload failed, retrying in 2s:', error.message);
771
+ await new Promise(resolve => setTimeout(resolve, 2000));
772
+ return this.uploadScreenshotToS3(base64Screenshot, actionId, retryCount + 1);
773
+ }
774
+ logger.error('[LinkedInAutomation] Screenshot upload failed after retry:', error.message);
754
775
  }
755
776
  }
756
777
 
@@ -940,6 +961,7 @@ export class LinkedInAutomation {
940
961
  if (refreshResult.tokens.refreshToken) {
941
962
  this._currentRefreshToken = refreshResult.tokens.refreshToken;
942
963
  }
964
+ this.syncTokensBack();
943
965
  logger.log('[LinkedInAutomation] Token re-sync and refresh succeeded after 401, resetting failure counter');
944
966
  this.consecutiveAuthFailures = 0;
945
967
  this.authFailurePaused = false;
@@ -1313,6 +1335,58 @@ export class LinkedInAutomation {
1313
1335
  this.tokenResyncCallback = callback;
1314
1336
  }
1315
1337
 
1338
+ /**
1339
+ * Set a callback to persist refreshed tokens back to the session store
1340
+ * @param {Function} callback - Function(idToken, refreshToken) that saves tokens to session
1341
+ */
1342
+ setTokenSyncBackCallback(callback) {
1343
+ this.tokenSyncBackCallback = callback;
1344
+ }
1345
+
1346
+ /**
1347
+ * Persist refreshed tokens back to the session store so future resyncs get fresh tokens
1348
+ */
1349
+ async syncTokensBack() {
1350
+ if (!this.tokenSyncBackCallback) return;
1351
+ try {
1352
+ await this.tokenSyncBackCallback(this._currentAccessToken, this._currentRefreshToken);
1353
+ } catch (error) {
1354
+ logger.error('[LinkedInAutomation] Error syncing tokens back to session:', error.message);
1355
+ }
1356
+ }
1357
+
1358
+ /**
1359
+ * Start a periodic timer that proactively refreshes the JWT token before it expires.
1360
+ * This prevents stale tokens from accumulating during long-running automation.
1361
+ */
1362
+ startTokenRefreshTimer() {
1363
+ this.stopTokenRefreshTimer();
1364
+ logger.log(`[LinkedInAutomation] Starting proactive token refresh timer (every ${this._tokenRefreshIntervalMs / 60000} min)`);
1365
+
1366
+ this._tokenRefreshInterval = setInterval(async () => {
1367
+ if (!this.isRunning) return;
1368
+ try {
1369
+ logger.log('[LinkedInAutomation] Proactive token refresh triggered');
1370
+ const token = await this.getAccessToken();
1371
+ if (token) {
1372
+ logger.log('[LinkedInAutomation] Proactive token refresh succeeded');
1373
+ } else {
1374
+ logger.error('[LinkedInAutomation] Proactive token refresh failed - no valid token');
1375
+ }
1376
+ } catch (error) {
1377
+ logger.error('[LinkedInAutomation] Proactive token refresh error:', error.message);
1378
+ }
1379
+ }, this._tokenRefreshIntervalMs);
1380
+ }
1381
+
1382
+ stopTokenRefreshTimer() {
1383
+ if (this._tokenRefreshInterval) {
1384
+ clearInterval(this._tokenRefreshInterval);
1385
+ this._tokenRefreshInterval = null;
1386
+ logger.log('[LinkedInAutomation] Token refresh timer stopped');
1387
+ }
1388
+ }
1389
+
1316
1390
  /**
1317
1391
  * Attempt to re-sync tokens from external source (session, etc.)
1318
1392
  * @returns {Promise<boolean>} true if tokens were successfully re-synced
@@ -1338,12 +1412,13 @@ export class LinkedInAutomation {
1338
1412
  if (result.tokens.refreshToken) {
1339
1413
  this._currentRefreshToken = result.tokens.refreshToken;
1340
1414
  }
1415
+ this.syncTokensBack();
1341
1416
  } else {
1342
1417
  logger.error('[LinkedInAutomation] Failed to refresh re-synced tokens:', result.error);
1343
1418
  return false;
1344
1419
  }
1345
1420
  }
1346
-
1421
+
1347
1422
  return true;
1348
1423
  }
1349
1424
  } catch (error) {
@@ -1390,37 +1465,41 @@ export class LinkedInAutomation {
1390
1465
  if (result.tokens.refreshToken) {
1391
1466
  this._currentRefreshToken = result.tokens.refreshToken;
1392
1467
  }
1468
+ // Persist refreshed tokens back to session store
1469
+ this.syncTokensBack();
1393
1470
  // Reset failure counter on successful refresh
1394
1471
  this.consecutiveAuthFailures = 0;
1395
1472
  this.authFailurePaused = false;
1396
1473
  return this._currentAccessToken;
1397
1474
  } else {
1398
1475
  logger.error('[LinkedInAutomation] Token refresh failed:', result.error);
1399
-
1476
+
1400
1477
  // Try one more time to re-sync before giving up
1401
1478
  logger.log('[LinkedInAutomation] Attempting token re-sync after refresh failure...');
1402
1479
  const resynced = await this.attemptTokenResync();
1403
1480
  if (resynced && !this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
1404
1481
  logger.log('[LinkedInAutomation] Token re-synced successfully after refresh failure');
1482
+ this.syncTokensBack();
1405
1483
  this.consecutiveAuthFailures = 0;
1406
1484
  this.authFailurePaused = false;
1407
1485
  return this._currentAccessToken;
1408
1486
  }
1409
-
1487
+
1410
1488
  return null;
1411
1489
  }
1412
1490
  } catch (error) {
1413
1491
  logger.error('[LinkedInAutomation] Error during token refresh:', error.message);
1414
-
1492
+
1415
1493
  // Try re-sync one more time
1416
1494
  const resynced = await this.attemptTokenResync();
1417
1495
  if (resynced && !this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
1418
1496
  logger.log('[LinkedInAutomation] Token re-synced successfully after refresh error');
1497
+ this.syncTokensBack();
1419
1498
  this.consecutiveAuthFailures = 0;
1420
1499
  this.authFailurePaused = false;
1421
1500
  return this._currentAccessToken;
1422
1501
  }
1423
-
1502
+
1424
1503
  return null;
1425
1504
  }
1426
1505
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coffeeinabit",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "description": "coffeeinabit app",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/server.js CHANGED
@@ -95,6 +95,37 @@ const sessionStore = new FileStoreSession({
95
95
  retries: 0
96
96
  });
97
97
 
98
+ // Set up token sync-back callback: when automation refreshes tokens, persist them to the session store
99
+ linkedinAutomation.setTokenSyncBackCallback(async (idToken, refreshToken) => {
100
+ if (!linkedinAutomation._currentUserEmail || !idToken) return;
101
+
102
+ if (typeof sessionStore.all !== 'function') return;
103
+
104
+ return new Promise((resolve) => {
105
+ sessionStore.all((err, sessions) => {
106
+ if (err || !sessions) { resolve(); return; }
107
+
108
+ for (const sessionId in sessions) {
109
+ const sess = sessions[sessionId];
110
+ if (sess?.user?.email === linkedinAutomation._currentUserEmail && sess.tokens) {
111
+ sess.tokens.idToken = idToken;
112
+ if (refreshToken) sess.tokens.refreshToken = refreshToken;
113
+ sessionStore.set(sessionId, sess, (setErr) => {
114
+ if (setErr) {
115
+ logger.error('[Server] Failed to sync tokens back to session store:', setErr.message);
116
+ } else {
117
+ logger.log('[Server] Tokens synced back to session store for user:', sess.user.email);
118
+ }
119
+ resolve();
120
+ });
121
+ return;
122
+ }
123
+ }
124
+ resolve();
125
+ });
126
+ });
127
+ });
128
+
98
129
  // Configure Express middleware
99
130
  app.use(express.json());
100
131
  app.use(express.static(path.join(__dirname, 'public')));
@@ -1,7 +1,125 @@
1
1
  {
2
- "threads": [],
3
- "thread_count": 0,
4
- "total_messages": 0,
5
- "status": "no_history",
6
- "message": "No previous messages found with natalie-grigorchuk-48277811. Conversation overlay shows \"New message\"."
2
+ "threads": [
3
+ {
4
+ "thread_id": "urn:li:messagingThread:2-ZGU3NTUyMzMtMTJkNy00NDIxLTk4MDgtYmYxNGIwOTQ5YjFmXzEwMA==",
5
+ "participants": [
6
+ {
7
+ "name": "Alena Borikova",
8
+ "uniq_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
9
+ "is_self": true
10
+ },
11
+ {
12
+ "name": "Kate Yanchenko",
13
+ "uniq_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
14
+ "is_self": false
15
+ }
16
+ ],
17
+ "last_activity_at": 1770139934759,
18
+ "last_activity_at_iso": "2026-02-03T17:32:14.759Z",
19
+ "messages": [
20
+ {
21
+ "author": "Kate Yanchenko",
22
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
23
+ "is_self": false,
24
+ "text": "Hello!",
25
+ "timestamp": 1765865708197,
26
+ "timestamp_iso": "2025-12-16T06:15:08.197Z",
27
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
28
+ },
29
+ {
30
+ "author": "Alena Borikova",
31
+ "author_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
32
+ "is_self": true,
33
+ "text": "Hi hi ",
34
+ "timestamp": 1765867625215,
35
+ "timestamp_iso": "2025-12-16T06:47:05.215Z",
36
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
37
+ },
38
+ {
39
+ "author": "Kate Yanchenko",
40
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
41
+ "is_self": false,
42
+ "text": "Hello!",
43
+ "timestamp": 1765872088845,
44
+ "timestamp_iso": "2025-12-16T08:01:28.845Z",
45
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
46
+ },
47
+ {
48
+ "author": "Kate Yanchenko",
49
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
50
+ "is_self": false,
51
+ "text": "just testing",
52
+ "timestamp": 1765873430497,
53
+ "timestamp_iso": "2025-12-16T08:23:50.497Z",
54
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
55
+ },
56
+ {
57
+ "author": "Kate Yanchenko",
58
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
59
+ "is_self": false,
60
+ "text": "Hey, how are you?",
61
+ "timestamp": 1770138675922,
62
+ "timestamp_iso": "2026-02-03T17:11:15.922Z",
63
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
64
+ },
65
+ {
66
+ "author": "Alena Borikova",
67
+ "author_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
68
+ "is_self": true,
69
+ "text": "Fine, you? ❤️",
70
+ "timestamp": 1770138705220,
71
+ "timestamp_iso": "2026-02-03T17:11:45.220Z",
72
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
73
+ },
74
+ {
75
+ "author": "Kate Yanchenko",
76
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
77
+ "is_self": false,
78
+ "text": "Hey, how are you?",
79
+ "timestamp": 1770139276918,
80
+ "timestamp_iso": "2026-02-03T17:21:16.918Z",
81
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
82
+ },
83
+ {
84
+ "author": "Alena Borikova",
85
+ "author_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
86
+ "is_self": true,
87
+ "text": "I'm good",
88
+ "timestamp": 1770139300739,
89
+ "timestamp_iso": "2026-02-03T17:21:40.739Z",
90
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
91
+ },
92
+ {
93
+ "author": "Alena Borikova",
94
+ "author_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
95
+ "is_self": true,
96
+ "text": "Thanks for asking 😁❤️",
97
+ "timestamp": 1770139311452,
98
+ "timestamp_iso": "2026-02-03T17:21:51.452Z",
99
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
100
+ },
101
+ {
102
+ "author": "Kate Yanchenko",
103
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
104
+ "is_self": false,
105
+ "text": "Hey, how are you?",
106
+ "timestamp": 1770139531176,
107
+ "timestamp_iso": "2026-02-03T17:25:31.176Z",
108
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
109
+ },
110
+ {
111
+ "author": "Alena Borikova",
112
+ "author_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
113
+ "is_self": true,
114
+ "text": "🐥❤️",
115
+ "timestamp": 1770139934759,
116
+ "timestamp_iso": "2026-02-03T17:32:14.759Z",
117
+ "network_intercepted_at": "2026-02-21T02:53:40.617Z"
118
+ }
119
+ ],
120
+ "message_count": 11
121
+ }
122
+ ],
123
+ "thread_count": 1,
124
+ "total_messages": 11
7
125
  }