viruagent-cli 0.6.0 → 0.6.1

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.
@@ -5,12 +5,12 @@ const { readInstaCredentials } = require('./utils');
5
5
  const { createInstaWithProviderSession } = require('./session');
6
6
  const { createAskForAuthentication } = require('./auth');
7
7
 
8
- const createInstaProvider = ({ sessionPath }) => {
8
+ const createInstaProvider = ({ sessionPath, account }) => {
9
9
  const instaApi = createInstaApiClient({ sessionPath });
10
10
 
11
11
  const askForAuthentication = createAskForAuthentication({ sessionPath });
12
12
 
13
- const withProviderSession = createInstaWithProviderSession(askForAuthentication);
13
+ const withProviderSession = createInstaWithProviderSession(askForAuthentication, account);
14
14
  const smart = createSmartComment(instaApi);
15
15
 
16
16
  return {
@@ -29,7 +29,7 @@ const createInstaProvider = ({ sessionPath }) => {
29
29
  userId,
30
30
  hasSession: Boolean(sessionid?.value),
31
31
  sessionPath,
32
- metadata: getProviderMeta('insta') || {},
32
+ metadata: getProviderMeta('insta', account) || {},
33
33
  };
34
34
  } catch (error) {
35
35
  return {
@@ -37,7 +37,7 @@ const createInstaProvider = ({ sessionPath }) => {
37
37
  loggedIn: false,
38
38
  sessionPath,
39
39
  error: error.message,
40
- metadata: getProviderMeta('insta') || {},
40
+ metadata: getProviderMeta('insta', account) || {},
41
41
  };
42
42
  }
43
43
  });
@@ -65,7 +65,7 @@ const createInstaProvider = ({ sessionPath }) => {
65
65
  userId: result.userId,
66
66
  username: result.username,
67
67
  sessionPath: result.sessionPath,
68
- });
68
+ }, account);
69
69
 
70
70
  return result;
71
71
  },
@@ -509,7 +509,7 @@ const createInstaProvider = ({ sessionPath }) => {
509
509
  },
510
510
 
511
511
  async logout() {
512
- clearProviderMeta('insta');
512
+ clearProviderMeta('insta', account);
513
513
  return {
514
514
  provider: 'insta',
515
515
  loggedOut: true,
@@ -61,16 +61,13 @@ const validateInstaSession = (sessionPath) => {
61
61
  const cookiesToHeader = (cookies) =>
62
62
  cookies.map((c) => `${c.name}=${c.value}`).join('; ');
63
63
 
64
- const createInstaWithProviderSession = (askForAuthentication) => async (fn) => {
64
+ const createInstaWithProviderSession = (askForAuthentication, account) => async (fn) => {
65
65
  const credentials = readInstaCredentials();
66
66
  const hasCredentials = Boolean(credentials.username && credentials.password);
67
67
 
68
68
  try {
69
69
  const result = await fn();
70
- saveProviderMeta('insta', {
71
- loggedIn: true,
72
- lastValidatedAt: new Date().toISOString(),
73
- });
70
+ saveProviderMeta('insta', { loggedIn: true, lastValidatedAt: new Date().toISOString() }, account);
74
71
  return result;
75
72
  } catch (error) {
76
73
  if (!parseInstaSessionError(error) || !hasCredentials) {
@@ -89,7 +86,7 @@ const createInstaWithProviderSession = (askForAuthentication) => async (fn) => {
89
86
  sessionPath: loginResult.sessionPath,
90
87
  lastRefreshedAt: new Date().toISOString(),
91
88
  lastError: null,
92
- });
89
+ }, account);
93
90
 
94
91
  if (!loginResult.loggedIn) {
95
92
  throw new Error(loginResult.message || 'Login status could not be confirmed after session refresh.');
@@ -101,7 +98,7 @@ const createInstaWithProviderSession = (askForAuthentication) => async (fn) => {
101
98
  loggedIn: false,
102
99
  lastError: buildLoginErrorMessage(reloginError),
103
100
  lastValidatedAt: new Date().toISOString(),
104
- });
101
+ }, account);
105
102
  throw reloginError;
106
103
  }
107
104
  }
@@ -1,9 +1,14 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { chromium } = require('playwright');
4
3
  const { readNaverCredentials, sleep } = require('./utils');
5
4
  const { isLoggedInByCookies, persistNaverSession } = require('./session');
6
5
  const { NAVER_LOGIN_SELECTORS, NAVER_LOGIN_ERROR_PATTERNS } = require('./selectors');
6
+ const {
7
+ CHROME_PROFILE_DIR,
8
+ launchChrome,
9
+ connectChrome,
10
+ } = require('../chromeManager');
11
+ const NAVER_PROFILE_DIR = path.join(CHROME_PROFILE_DIR, 'naver');
7
12
 
8
13
  const ANTI_DETECTION_SCRIPT = `
9
14
  Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
@@ -15,7 +20,7 @@ const ANTI_DETECTION_SCRIPT = `
15
20
  const waitForNaverLoginFinish = async (page, context, timeoutMs = 45000) => {
16
21
  const deadline = Date.now() + timeoutMs;
17
22
  while (Date.now() < deadline) {
18
- if (await isLoggedInByCookies(context)) return true;
23
+ if (await isLoggedInByCookies(context, page)) return true;
19
24
 
20
25
  const url = page.url();
21
26
  if (url.includes('naver.com') && !url.includes('nid.naver.com/nidlogin')) return true;
@@ -75,27 +80,37 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
75
80
  const resolvedUsername = username || readNaverCredentials().username;
76
81
  const resolvedPassword = password || readNaverCredentials().password;
77
82
 
78
- if (!manual && (!resolvedUsername || !resolvedPassword)) {
79
- throw new Error('Naver login requires id/pw. Set the NAVER_USERNAME/NAVER_PASSWORD environment variables or use --manual mode.');
80
- }
81
-
82
- const browser = await chromium.launch({
83
- headless: manual ? false : headless,
84
- });
83
+ // Launch real Chrome with persistent profile + CDP
84
+ // TIP: openchrome (npx openchrome-mcp setup) skips 2FA by reusing existing sessions.
85
+ const port = await launchChrome(NAVER_PROFILE_DIR);
86
+ const { browser, context, page } = await connectChrome(port);
85
87
 
86
- const context = await browser.newContext({
87
- userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
88
- viewport: { width: 1366, height: 768 },
89
- locale: 'ko-KR',
90
- timezoneId: 'Asia/Seoul',
91
- });
88
+ try {
89
+ // Check if already logged in (openchrome profile may have active session)
90
+ // Navigate to myinfo — redirects to login if not authenticated, stays if logged in
91
+ await page.goto('https://nid.naver.com/user2/help/myInfo', { waitUntil: 'domcontentloaded' });
92
+ await sleep(1000);
92
93
 
93
- await context.addInitScript(ANTI_DETECTION_SCRIPT);
94
- const page = context.pages()[0] || (await context.newPage());
94
+ const afterUrl = page.url();
95
+ if (!afterUrl.includes('nidlogin') && !afterUrl.includes('nid.naver.com/nidlogin')) {
96
+ await persistNaverSession(context, page, sessionPath);
97
+ naverApi.resetState();
98
+ const blogId = await naverApi.initBlog();
99
+ return {
100
+ provider: 'naver',
101
+ loggedIn: true,
102
+ blogId,
103
+ blogUrl: `https://blog.naver.com/${blogId}`,
104
+ sessionPath,
105
+ };
106
+ }
95
107
 
96
- try {
97
108
  await page.goto('https://nid.naver.com/nidlogin.login', { waitUntil: 'domcontentloaded' });
98
- await sleep(1500);
109
+ await sleep(1000);
110
+
111
+ if (!manual && (!resolvedUsername || !resolvedPassword)) {
112
+ throw new Error('Naver login requires id/pw. Set the NAVER_USERNAME/NAVER_PASSWORD environment variables or use --manual mode.');
113
+ }
99
114
 
100
115
  let loginSuccess = false;
101
116
 
@@ -157,7 +172,7 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
157
172
  throw new Error('Naver login failed. Please verify your id/password or use --manual mode.');
158
173
  }
159
174
 
160
- await persistNaverSession(context, sessionPath);
175
+ await persistNaverSession(context, page, sessionPath);
161
176
 
162
177
  naverApi.resetState();
163
178
  const blogId = await naverApi.initBlog();
@@ -169,9 +184,8 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
169
184
  sessionPath,
170
185
  };
171
186
  } finally {
172
- if (browser) {
173
- await browser.close().catch(() => {});
174
- }
187
+ // Don't close browser — keep Chrome running with persistent profile
188
+ await browser.close().catch(() => {});
175
189
  }
176
190
  };
177
191
 
@@ -10,7 +10,7 @@ const { collectAndUploadImages } = require('./imageUpload');
10
10
  const { createNaverWithProviderSession } = require('./session');
11
11
  const { createAskForAuthentication } = require('./auth');
12
12
 
13
- const createNaverProvider = ({ sessionPath }) => {
13
+ const createNaverProvider = ({ sessionPath, account }) => {
14
14
  const naverApi = createNaverApiClient({ sessionPath });
15
15
 
16
16
  const askForAuthentication = createAskForAuthentication({
@@ -18,7 +18,7 @@ const createNaverProvider = ({ sessionPath }) => {
18
18
  naverApi,
19
19
  });
20
20
 
21
- const withProviderSession = createNaverWithProviderSession(askForAuthentication);
21
+ const withProviderSession = createNaverWithProviderSession(askForAuthentication, account);
22
22
 
23
23
  return {
24
24
  id: 'naver',
@@ -34,7 +34,7 @@ const createNaverProvider = ({ sessionPath }) => {
34
34
  blogId,
35
35
  blogUrl: `https://blog.naver.com/${blogId}`,
36
36
  sessionPath,
37
- metadata: getProviderMeta('naver') || {},
37
+ metadata: getProviderMeta('naver', account) || {},
38
38
  };
39
39
  } catch (error) {
40
40
  return {
@@ -42,7 +42,7 @@ const createNaverProvider = ({ sessionPath }) => {
42
42
  loggedIn: false,
43
43
  sessionPath,
44
44
  error: error.message,
45
- metadata: getProviderMeta('naver') || {},
45
+ metadata: getProviderMeta('naver', account) || {},
46
46
  };
47
47
  }
48
48
  });
@@ -62,17 +62,13 @@ const createNaverProvider = ({ sessionPath }) => {
62
62
  password: password || creds.password,
63
63
  };
64
64
 
65
- if (!resolved.manual && (!resolved.username || !resolved.password)) {
66
- throw new Error('Naver auto-login requires username/password. Set the NAVER_USERNAME / NAVER_PASSWORD environment variables or use --manual mode.');
67
- }
68
-
69
65
  const result = await askForAuthentication(resolved);
70
66
  saveProviderMeta('naver', {
71
67
  loggedIn: result.loggedIn,
72
68
  blogId: result.blogId,
73
69
  blogUrl: result.blogUrl,
74
70
  sessionPath: result.sessionPath,
75
- });
71
+ }, account);
76
72
  return result;
77
73
  },
78
74
 
@@ -221,7 +217,7 @@ const createNaverProvider = ({ sessionPath }) => {
221
217
  },
222
218
 
223
219
  async logout() {
224
- clearProviderMeta('naver');
220
+ clearProviderMeta('naver', account);
225
221
  return {
226
222
  provider: 'naver',
227
223
  loggedOut: true,
@@ -2,42 +2,39 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { saveProviderMeta } = require('../../storage/sessionStore');
4
4
  const { readNaverCredentials, parseNaverSessionError, buildLoginErrorMessage } = require('./utils');
5
+ const { extractAllCookies, filterCookies, cookiesToSessionFormat } = require('../chromeManager');
5
6
 
6
7
  const NAVER_COOKIE_DOMAINS = ['https://www.naver.com', 'https://nid.naver.com', 'https://blog.naver.com'];
7
8
 
8
- const isLoggedInByCookies = async (context) => {
9
- for (const domain of NAVER_COOKIE_DOMAINS) {
10
- const cookies = await context.cookies(domain);
11
- const hasAuth = cookies.some((c) => c.name === 'NID_AUT' || c.name === 'NID_SES');
12
- if (hasAuth) return true;
9
+ const isLoggedInByCookies = async (context, page) => {
10
+ try {
11
+ const all = await extractAllCookies(context, page);
12
+ return all.some((c) => c.domain.includes('naver.com') && (c.name === 'NID_AUT' || c.name === 'NID_SES'));
13
+ } catch {
14
+ for (const domain of NAVER_COOKIE_DOMAINS) {
15
+ const cookies = await context.cookies(domain);
16
+ const hasAuth = cookies.some((c) => c.name === 'NID_AUT' || c.name === 'NID_SES');
17
+ if (hasAuth) return true;
18
+ }
19
+ return false;
13
20
  }
14
- return false;
15
21
  };
16
22
 
17
- const persistNaverSession = async (context, targetSessionPath) => {
18
- const allCookies = [];
19
- for (const domain of NAVER_COOKIE_DOMAINS) {
20
- const cookies = await context.cookies(domain);
21
- allCookies.push(...cookies);
22
- }
23
+ const persistNaverSession = async (context, page, targetSessionPath) => {
24
+ // Use CDP Network.getAllCookies to capture httpOnly cookies (e.g. NID_AUT)
25
+ const all = await extractAllCookies(context, page);
26
+ const naverCookies = filterCookies(all, ['naver.com']);
23
27
 
24
28
  // Deduplicate by name+domain
25
29
  const seen = new Set();
26
- const unique = allCookies.filter((c) => {
30
+ const unique = naverCookies.filter((c) => {
27
31
  const key = `${c.name}@${c.domain}`;
28
32
  if (seen.has(key)) return false;
29
33
  seen.add(key);
30
34
  return true;
31
35
  });
32
36
 
33
- const sanitized = unique.map((cookie) => ({
34
- ...cookie,
35
- expires: Number(cookie.expires || -1),
36
- size: undefined,
37
- partitionKey: undefined,
38
- sourcePort: undefined,
39
- sourceScheme: undefined,
40
- }));
37
+ const sanitized = cookiesToSessionFormat(unique);
41
38
 
42
39
  const payload = {
43
40
  cookies: sanitized,
@@ -62,16 +59,13 @@ const validateNaverSession = async (sessionPath) => {
62
59
  cookies.some((c) => c.name === 'NID_SES' && c.value);
63
60
  };
64
61
 
65
- const createNaverWithProviderSession = (askForAuthentication) => async (fn) => {
62
+ const createNaverWithProviderSession = (askForAuthentication, account) => async (fn) => {
66
63
  const credentials = readNaverCredentials();
67
64
  const hasCredentials = Boolean(credentials.username && credentials.password);
68
65
 
69
66
  try {
70
67
  const result = await fn();
71
- saveProviderMeta('naver', {
72
- loggedIn: true,
73
- lastValidatedAt: new Date().toISOString(),
74
- });
68
+ saveProviderMeta('naver', { loggedIn: true, lastValidatedAt: new Date().toISOString() }, account);
75
69
  return result;
76
70
  } catch (error) {
77
71
  if (!parseNaverSessionError(error) || !hasCredentials) {
@@ -93,7 +87,7 @@ const createNaverWithProviderSession = (askForAuthentication) => async (fn) => {
93
87
  sessionPath: loginResult.sessionPath,
94
88
  lastRefreshedAt: new Date().toISOString(),
95
89
  lastError: null,
96
- });
90
+ }, account);
97
91
 
98
92
  if (!loginResult.loggedIn) {
99
93
  throw new Error(loginResult.message || 'Login status could not be verified after session refresh.');
@@ -105,7 +99,7 @@ const createNaverWithProviderSession = (askForAuthentication) => async (fn) => {
105
99
  loggedIn: false,
106
100
  lastError: buildLoginErrorMessage(reloginError),
107
101
  lastValidatedAt: new Date().toISOString(),
108
- });
102
+ }, account);
109
103
  throw reloginError;
110
104
  }
111
105
  }
@@ -1,9 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { chromium } = require('playwright');
4
3
  const { readCredentialsFromEnv } = require('./utils');
5
4
  const {
6
- pickValue,
7
5
  fillBySelector,
8
6
  clickSubmit,
9
7
  checkBySelector,
@@ -16,6 +14,13 @@ const {
16
14
  KAKAO_LOGIN_SELECTORS,
17
15
  KAKAO_2FA_SELECTORS,
18
16
  } = require('./selectors');
17
+ const {
18
+ CHROME_PROFILE_DIR,
19
+ launchChrome,
20
+ connectChrome,
21
+ } = require('../chromeManager');
22
+
23
+ const TISTORY_PROFILE_DIR = path.join(CHROME_PROFILE_DIR, 'tistory');
19
24
 
20
25
  /**
21
26
  * askForAuthentication factory.
@@ -38,128 +43,147 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
38
43
  throw new Error('Tistory login requires id/pw. Please provide id/pw or set TISTORY_USERNAME/TISTORY_PASSWORD environment variables.');
39
44
  }
40
45
 
41
- const browser = await chromium.launch({
42
- headless: manual ? false : headless,
43
- });
44
- const context = await browser.newContext();
46
+ // Launch real Chrome with persistent profile + CDP
47
+ // TIP: openchrome (npx openchrome-mcp setup) skips 2FA by reusing existing sessions.
48
+ const port = await launchChrome(TISTORY_PROFILE_DIR);
49
+ const { browser, context, page } = await connectChrome(port);
50
+
51
+ try {
52
+ await page.goto('https://www.tistory.com/auth/login', {
53
+ waitUntil: 'domcontentloaded',
54
+ });
55
+
56
+ // Already logged in check
57
+ if (!page.url().includes('/auth/login')) {
58
+ await persistTistorySession(context, page, sessionPath);
59
+ tistoryApi.resetState();
60
+ const blogName = await tistoryApi.initBlog();
61
+ return {
62
+ provider: 'tistory',
63
+ loggedIn: true,
64
+ blogName,
65
+ blogUrl: `https://${blogName}.tistory.com`,
66
+ sessionPath,
67
+ };
68
+ }
45
69
 
46
- const page = context.pages()[0] || (await context.newPage());
70
+ const loginId = resolvedUsername;
71
+ const loginPw = resolvedPassword;
47
72
 
48
- try {
49
- await page.goto('https://www.tistory.com/auth/login', {
50
- waitUntil: 'domcontentloaded',
51
- });
73
+ const kakaoLoginSelector = await pickValue(page, KAKAO_TRIGGER_SELECTORS);
74
+ if (!kakaoLoginSelector) {
75
+ throw new Error('Could not find the Kakao login button. Please check if the login page UI has changed.');
76
+ }
52
77
 
53
- const loginId = resolvedUsername;
54
- const loginPw = resolvedPassword;
78
+ await page.locator(kakaoLoginSelector).click({ timeout: 5000 }).catch(() => {});
79
+ await page.waitForLoadState('domcontentloaded').catch(() => {});
80
+ await page.waitForTimeout(800);
81
+
82
+ let finalLoginStatus = false;
83
+ let pendingTwoFactorAction = false;
84
+
85
+ if (manual) {
86
+ console.log('');
87
+ console.log('==============================');
88
+ console.log('Switching to manual login mode.');
89
+ console.log('Please complete ID/PW/2FA verification in the browser.');
90
+ console.log('Login or 2FA must be completed within 5 minutes.');
91
+ console.log('==============================');
92
+ finalLoginStatus = await waitForLoginFinish(page, context, 300000);
93
+ } else if (shouldAutoFill) {
94
+ const usernameFilled = await fillBySelector(page, KAKAO_LOGIN_SELECTORS.username, loginId);
95
+ const passwordFilled = await fillBySelector(page, KAKAO_LOGIN_SELECTORS.password, loginPw);
96
+ if (!usernameFilled || !passwordFilled) {
97
+ throw new Error('Could not find Kakao login form input fields. Please check if the Tistory login page has changed.');
98
+ }
55
99
 
56
- const kakaoLoginSelector = await pickValue(page, KAKAO_TRIGGER_SELECTORS);
57
- if (!kakaoLoginSelector) {
58
- throw new Error('Could not find the Kakao login button. Please check if the login page UI has changed.');
100
+ await checkBySelector(page, KAKAO_LOGIN_SELECTORS.rememberLogin);
101
+ const kakaoSubmitted = await clickSubmit(page, KAKAO_LOGIN_SELECTORS.submit);
102
+ if (!kakaoSubmitted) {
103
+ await page.keyboard.press('Enter');
59
104
  }
60
105
 
61
- await page.locator(kakaoLoginSelector).click({ timeout: 5000 }).catch(() => {});
62
- await page.waitForLoadState('domcontentloaded').catch(() => {});
63
- await page.waitForTimeout(800);
64
-
65
- let finalLoginStatus = false;
66
- let pendingTwoFactorAction = false;
67
-
68
- if (manual) {
69
- console.log('');
70
- console.log('==============================');
71
- console.log('Switching to manual login mode.');
72
- console.log('Please complete ID/PW/2FA verification in the browser.');
73
- console.log('Login or 2FA must be completed within 5 minutes.');
74
- console.log('==============================');
75
- finalLoginStatus = await waitForLoginFinish(page, context, 300000);
76
- } else if (shouldAutoFill) {
77
- const usernameFilled = await fillBySelector(page, KAKAO_LOGIN_SELECTORS.username, loginId);
78
- const passwordFilled = await fillBySelector(page, KAKAO_LOGIN_SELECTORS.password, loginPw);
79
- if (!usernameFilled || !passwordFilled) {
80
- throw new Error('Could not find Kakao login form input fields. Please check if the Tistory login page has changed.');
81
- }
106
+ finalLoginStatus = await waitForLoginFinish(page, context);
82
107
 
83
- await checkBySelector(page, KAKAO_LOGIN_SELECTORS.rememberLogin);
84
- const kakaoSubmitted = await clickSubmit(page, KAKAO_LOGIN_SELECTORS.submit);
85
- if (!kakaoSubmitted) {
86
- await page.keyboard.press('Enter');
108
+ if (!finalLoginStatus && await hasElement(page, LOGIN_OTP_SELECTORS)) {
109
+ if (!twoFactorCode) {
110
+ return pending2faResult('otp');
87
111
  }
88
-
89
- finalLoginStatus = await waitForLoginFinish(page, context);
90
-
91
- if (!finalLoginStatus && await hasElement(page, LOGIN_OTP_SELECTORS)) {
92
- if (!twoFactorCode) {
93
- return pending2faResult('otp');
112
+ const otpFilled = await fillBySelector(page, LOGIN_OTP_SELECTORS, twoFactorCode);
113
+ if (!otpFilled) {
114
+ throw new Error('Could not find the OTP input field. Please check the login page.');
115
+ }
116
+ await page.keyboard.press('Enter');
117
+ finalLoginStatus = await waitForLoginFinish(page, context, 45000);
118
+ } else if (!finalLoginStatus && (await hasElement(page, KAKAO_2FA_SELECTORS.start) || page.url().includes('tmsTwoStepVerification') || page.url().includes('emailTwoStepVerification'))) {
119
+ await checkBySelector(page, KAKAO_2FA_SELECTORS.rememberDevice);
120
+ const isEmailModeAvailable = await hasElement(page, KAKAO_2FA_SELECTORS.emailModeButton);
121
+ const hasEmailCodeInput = await hasElement(page, KAKAO_2FA_SELECTORS.codeInput);
122
+
123
+ if (hasEmailCodeInput && twoFactorCode) {
124
+ const codeFilled = await fillBySelector(page, KAKAO_2FA_SELECTORS.codeInput, twoFactorCode);
125
+ if (!codeFilled) {
126
+ throw new Error('Could not find the 2FA input field. Please check the login page.');
94
127
  }
95
- const otpFilled = await fillBySelector(page, LOGIN_OTP_SELECTORS, twoFactorCode);
96
- if (!otpFilled) {
97
- throw new Error('Could not find the OTP input field. Please check the login page.');
128
+ const confirmed = await clickSubmit(page, KAKAO_2FA_SELECTORS.confirm);
129
+ if (!confirmed) {
130
+ await page.keyboard.press('Enter');
98
131
  }
99
- await page.keyboard.press('Enter');
100
132
  finalLoginStatus = await waitForLoginFinish(page, context, 45000);
101
- } else if (!finalLoginStatus && (await hasElement(page, KAKAO_2FA_SELECTORS.start) || page.url().includes('tmsTwoStepVerification') || page.url().includes('emailTwoStepVerification'))) {
102
- await checkBySelector(page, KAKAO_2FA_SELECTORS.rememberDevice);
103
- const isEmailModeAvailable = await hasElement(page, KAKAO_2FA_SELECTORS.emailModeButton);
104
- const hasEmailCodeInput = await hasElement(page, KAKAO_2FA_SELECTORS.codeInput);
105
-
106
- if (hasEmailCodeInput && twoFactorCode) {
107
- const codeFilled = await fillBySelector(page, KAKAO_2FA_SELECTORS.codeInput, twoFactorCode);
108
- if (!codeFilled) {
109
- throw new Error('Could not find the 2FA input field. Please check the login page.');
110
- }
111
- const confirmed = await clickSubmit(page, KAKAO_2FA_SELECTORS.confirm);
112
- if (!confirmed) {
113
- await page.keyboard.press('Enter');
114
- }
115
- finalLoginStatus = await waitForLoginFinish(page, context, 45000);
116
- } else if (!twoFactorCode) {
117
- pendingTwoFactorAction = true;
118
- } else if (isEmailModeAvailable) {
119
- await clickSubmit(page, KAKAO_2FA_SELECTORS.emailModeButton).catch(() => {});
120
- await page.waitForLoadState('domcontentloaded').catch(() => {});
121
- await page.waitForTimeout(800);
122
-
123
- const codeFilled = await fillBySelector(page, KAKAO_2FA_SELECTORS.codeInput, twoFactorCode);
124
- if (!codeFilled) {
125
- throw new Error('Could not find the Kakao email verification input field. Please check the login page.');
126
- }
127
-
128
- const confirmed = await clickSubmit(page, KAKAO_2FA_SELECTORS.confirm);
129
- if (!confirmed) {
130
- await page.keyboard.press('Enter');
131
- }
132
- finalLoginStatus = await waitForLoginFinish(page, context, 45000);
133
- } else {
134
- return pending2faResult('kakao');
133
+ } else if (!twoFactorCode) {
134
+ pendingTwoFactorAction = true;
135
+ } else if (isEmailModeAvailable) {
136
+ await clickSubmit(page, KAKAO_2FA_SELECTORS.emailModeButton).catch(() => {});
137
+ await page.waitForLoadState('domcontentloaded').catch(() => {});
138
+ await page.waitForTimeout(800);
139
+
140
+ const codeFilled = await fillBySelector(page, KAKAO_2FA_SELECTORS.codeInput, twoFactorCode);
141
+ if (!codeFilled) {
142
+ throw new Error('Could not find the Kakao email verification input field. Please check the login page.');
135
143
  }
136
- }
137
- }
138
144
 
139
- if (!finalLoginStatus) {
140
- if (pendingTwoFactorAction) {
145
+ const confirmed = await clickSubmit(page, KAKAO_2FA_SELECTORS.confirm);
146
+ if (!confirmed) {
147
+ await page.keyboard.press('Enter');
148
+ }
149
+ finalLoginStatus = await waitForLoginFinish(page, context, 45000);
150
+ } else {
141
151
  return pending2faResult('kakao');
142
152
  }
143
- throw new Error('Login failed. Please verify your credentials and ensure TISTORY_USERNAME/TISTORY_PASSWORD environment variables are set correctly.');
144
153
  }
154
+ }
145
155
 
146
- await context.storageState({ path: sessionPath });
147
- await persistTistorySession(context, sessionPath);
148
-
149
- tistoryApi.resetState();
150
- const blogName = await tistoryApi.initBlog();
151
- return {
152
- provider: 'tistory',
153
- loggedIn: true,
154
- blogName,
155
- blogUrl: `https://${blogName}.tistory.com`,
156
- sessionPath,
157
- };
158
- } finally {
159
- if (browser) {
160
- await browser.close().catch(() => {});
156
+ if (!finalLoginStatus) {
157
+ if (pendingTwoFactorAction) {
158
+ return pending2faResult('kakao');
161
159
  }
160
+ throw new Error('Login failed. Please verify your credentials and ensure TISTORY_USERNAME/TISTORY_PASSWORD environment variables are set correctly.');
162
161
  }
162
+
163
+ await persistTistorySession(context, page, sessionPath);
164
+
165
+ tistoryApi.resetState();
166
+ const blogName = await tistoryApi.initBlog();
167
+ return {
168
+ provider: 'tistory',
169
+ loggedIn: true,
170
+ blogName,
171
+ blogUrl: `https://${blogName}.tistory.com`,
172
+ sessionPath,
173
+ };
174
+ } finally {
175
+ // Don't close browser — keep Chrome running with persistent profile
176
+ await browser.close().catch(() => {});
177
+ }
178
+ };
179
+
180
+ // Keep pickValue local since it's used above
181
+ const pickValue = async (page, selectors) => {
182
+ for (const selector of selectors) {
183
+ const element = await page.$(selector);
184
+ if (element) return selector;
185
+ }
186
+ return null;
163
187
  };
164
188
 
165
189
  module.exports = {