viruagent-cli 0.6.0 → 0.6.2

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.
@@ -3,19 +3,67 @@ const path = require('path');
3
3
  const { saveProviderMeta } = require('../../storage/sessionStore');
4
4
  const { sleep, readCredentialsFromEnv, parseSessionError, buildLoginErrorMessage } = require('./utils');
5
5
  const { clickKakaoAccountContinue } = require('./browserHelpers');
6
+ const { extractAllCookies, filterCookies, cookiesToSessionFormat } = require('../chromeManager');
6
7
 
7
- const isLoggedInByCookies = async (context) => {
8
- const cookies = await context.cookies('https://www.tistory.com');
9
- return cookies.some((cookie) => {
10
- const name = cookie.name.toLowerCase();
11
- return name.includes('tistory') || name.includes('access') || name.includes('login');
12
- });
8
+ // ── Rate Limit (일일 발행 제한) ──
9
+ const DAILY_LIMIT = { publish: 15 };
10
+
11
+ const readSessionFile = (sessionPath) => {
12
+ if (!fs.existsSync(sessionPath)) return null;
13
+ try { return JSON.parse(fs.readFileSync(sessionPath, 'utf-8')); } catch { return null; }
14
+ };
15
+ const writeSessionFile = (sessionPath, data) => {
16
+ fs.mkdirSync(path.dirname(sessionPath), { recursive: true });
17
+ fs.writeFileSync(sessionPath, JSON.stringify(data, null, 2), 'utf-8');
18
+ };
19
+
20
+ const checkAndIncrementRateLimit = (sessionPath, type) => {
21
+ const raw = readSessionFile(sessionPath) || {};
22
+ if (!raw.rateLimits) raw.rateLimits = {};
23
+ const c = raw.rateLimits[type] || { daily: 0, dayStart: Date.now() };
24
+
25
+ const now = Date.now();
26
+ if (now - c.dayStart > 86400000) { c.daily = 0; c.dayStart = now; }
27
+
28
+ const dailyMax = DAILY_LIMIT[type];
29
+ if (dailyMax && c.daily >= dailyMax) {
30
+ throw new Error(`daily_limit: ${type} exceeded daily limit of ${dailyMax}. Try again tomorrow.`);
31
+ }
32
+
33
+ c.daily++;
34
+ raw.rateLimits[type] = { ...c, savedAt: new Date().toISOString() };
35
+ writeSessionFile(sessionPath, raw);
36
+ };
37
+
38
+ const getRateLimitStatus = (sessionPath) => {
39
+ const raw = readSessionFile(sessionPath) || {};
40
+ const result = {};
41
+ for (const [type, max] of Object.entries(DAILY_LIMIT)) {
42
+ const c = raw.rateLimits?.[type] || { daily: 0 };
43
+ result[type] = { daily: `${c.daily}/${max}` };
44
+ }
45
+ return result;
46
+ };
47
+
48
+ const isLoggedInByCookies = async (context, page) => {
49
+ try {
50
+ // Use CDP to get all cookies including httpOnly (TSSESSION)
51
+ const all = await extractAllCookies(context, page);
52
+ return all.some((c) => c.domain.includes('tistory') && c.name === 'TSSESSION');
53
+ } catch {
54
+ // Fallback to context.cookies if CDP fails
55
+ const cookies = await context.cookies('https://www.tistory.com');
56
+ return cookies.some((cookie) => {
57
+ const name = cookie.name.toLowerCase();
58
+ return name.includes('tistory') || name.includes('access') || name.includes('login');
59
+ });
60
+ }
13
61
  };
14
62
 
15
63
  const waitForLoginFinish = async (page, context, timeoutMs = 45000) => {
16
64
  const deadline = Date.now() + timeoutMs;
17
65
  while (Date.now() < deadline) {
18
- if (await isLoggedInByCookies(context)) {
66
+ if (await isLoggedInByCookies(context, page)) {
19
67
  return true;
20
68
  }
21
69
 
@@ -33,16 +81,11 @@ const waitForLoginFinish = async (page, context, timeoutMs = 45000) => {
33
81
  return false;
34
82
  };
35
83
 
36
- const persistTistorySession = async (context, targetSessionPath) => {
37
- const cookies = await context.cookies('https://www.tistory.com');
38
- const sanitized = cookies.map((cookie) => ({
39
- ...cookie,
40
- expires: Number(cookie.expires || -1),
41
- size: undefined,
42
- partitionKey: undefined,
43
- sourcePort: undefined,
44
- sourceScheme: undefined,
45
- }));
84
+ const persistTistorySession = async (context, page, targetSessionPath) => {
85
+ // Use CDP Network.getAllCookies to capture httpOnly cookies (e.g. TSSESSION)
86
+ const all = await extractAllCookies(context, page);
87
+ const tistoryCookies = filterCookies(all, ['tistory.com']);
88
+ const sanitized = cookiesToSessionFormat(tistoryCookies);
46
89
 
47
90
  const payload = {
48
91
  cookies: sanitized,
@@ -60,16 +103,13 @@ const persistTistorySession = async (context, targetSessionPath) => {
60
103
  * withProviderSession factory.
61
104
  * Receives askForAuthentication via dependency injection to avoid scope issues.
62
105
  */
63
- const createWithProviderSession = (askForAuthentication) => async (fn) => {
106
+ const createWithProviderSession = (askForAuthentication, account) => async (fn) => {
64
107
  const credentials = readCredentialsFromEnv();
65
108
  const hasCredentials = Boolean(credentials.username && credentials.password);
66
109
 
67
110
  try {
68
111
  const result = await fn();
69
- saveProviderMeta('tistory', {
70
- loggedIn: true,
71
- lastValidatedAt: new Date().toISOString(),
72
- });
112
+ saveProviderMeta('tistory', { loggedIn: true, lastValidatedAt: new Date().toISOString() }, account);
73
113
  return result;
74
114
  } catch (error) {
75
115
  if (!parseSessionError(error) || !hasCredentials) {
@@ -91,7 +131,7 @@ const createWithProviderSession = (askForAuthentication) => async (fn) => {
91
131
  sessionPath: loginResult.sessionPath,
92
132
  lastRefreshedAt: new Date().toISOString(),
93
133
  lastError: null,
94
- });
134
+ }, account);
95
135
 
96
136
  if (!loginResult.loggedIn) {
97
137
  throw new Error(loginResult.message || 'Login status could not be confirmed after session refresh.');
@@ -103,7 +143,7 @@ const createWithProviderSession = (askForAuthentication) => async (fn) => {
103
143
  loggedIn: false,
104
144
  lastError: buildLoginErrorMessage(reloginError),
105
145
  lastValidatedAt: new Date().toISOString(),
106
- });
146
+ }, account);
107
147
  throw reloginError;
108
148
  }
109
149
  }
@@ -114,4 +154,6 @@ module.exports = {
114
154
  waitForLoginFinish,
115
155
  persistTistorySession,
116
156
  createWithProviderSession,
157
+ checkAndIncrementRateLimit,
158
+ getRateLimitStatus,
117
159
  };
package/src/runner.js CHANGED
@@ -86,9 +86,10 @@ const runCommand = async (command, opts = {}) => {
86
86
  }
87
87
 
88
88
  const providerName = opts.provider || 'tistory';
89
+ const account = opts.account || undefined;
89
90
  let provider;
90
91
  try {
91
- provider = manager.getProvider(providerName);
92
+ provider = manager.getProvider(providerName, account);
92
93
  } catch {
93
94
  throw createError(
94
95
  'PROVIDER_NOT_FOUND',
@@ -15,23 +15,25 @@ const providers = ['tistory', 'naver', 'insta'];
15
15
  const createProviderManager = () => {
16
16
  const cache = new Map();
17
17
 
18
- const getProvider = (provider = 'tistory') => {
18
+ const getProvider = (provider = 'tistory', account) => {
19
19
  const normalized = String(provider || 'tistory').toLowerCase();
20
20
  if (!providerFactory[normalized]) {
21
21
  throw new Error(`Unsupported provider: ${provider}. Available options: ${providers.join(', ')}`);
22
22
  }
23
23
 
24
- if (!cache.has(normalized)) {
25
- const sessionPath = getSessionPath(normalized);
24
+ const cacheKey = account ? `${normalized}:${String(account).toLowerCase()}` : normalized;
25
+ if (!cache.has(cacheKey)) {
26
+ const sessionPath = getSessionPath(normalized, account);
26
27
  const options = {
27
28
  provider: normalized,
28
29
  sessionPath,
30
+ account: account || undefined,
29
31
  };
30
32
  const providerInstance = providerFactory[normalized](options);
31
- cache.set(normalized, providerInstance);
33
+ cache.set(cacheKey, providerInstance);
32
34
  }
33
35
 
34
- return cache.get(normalized);
36
+ return cache.get(cacheKey);
35
37
  };
36
38
 
37
39
  const providerNames = { tistory: 'Tistory', naver: 'Naver Blog', insta: 'Instagram' };
@@ -27,9 +27,11 @@ const writeJson = (target, data) => {
27
27
  fs.writeFileSync(target, JSON.stringify(data, null, 2), 'utf-8');
28
28
  };
29
29
 
30
- const getSessionPath = (provider) => {
30
+ const getSessionPath = (provider, account) => {
31
31
  ensureDir(SESSION_DIR);
32
- return path.join(SESSION_DIR, `${normalizeProvider(provider)}-session.json`);
32
+ const base = normalizeProvider(provider);
33
+ const suffix = account ? `-${String(account).toLowerCase().replace(/[^a-z0-9._-]/g, '_')}` : '';
34
+ return path.join(SESSION_DIR, `${base}${suffix}-session.json`);
33
35
  };
34
36
 
35
37
  const getProvidersMeta = () => {
@@ -37,25 +39,32 @@ const getProvidersMeta = () => {
37
39
  return readJson(META_FILE);
38
40
  };
39
41
 
40
- const saveProviderMeta = (provider, patch) => {
42
+ const metaKey = (provider, account) => {
43
+ const base = normalizeProvider(provider);
44
+ return account ? `${base}:${String(account).toLowerCase()}` : base;
45
+ };
46
+
47
+ const saveProviderMeta = (provider, patch, account) => {
41
48
  const meta = getProvidersMeta();
42
- meta[normalizeProvider(provider)] = {
43
- ...(meta[normalizeProvider(provider)] || {}),
49
+ const key = metaKey(provider, account);
50
+ meta[key] = {
51
+ ...(meta[key] || {}),
44
52
  ...patch,
45
53
  provider: normalizeProvider(provider),
54
+ ...(account ? { account: String(account).toLowerCase() } : {}),
46
55
  updatedAt: new Date().toISOString(),
47
56
  };
48
57
  writeJson(META_FILE, meta);
49
58
  };
50
59
 
51
- const getProviderMeta = (provider) => {
60
+ const getProviderMeta = (provider, account) => {
52
61
  const meta = getProvidersMeta();
53
- return meta[normalizeProvider(provider)] || null;
62
+ return meta[metaKey(provider, account)] || null;
54
63
  };
55
64
 
56
- const clearProviderMeta = (provider) => {
65
+ const clearProviderMeta = (provider, account) => {
57
66
  const meta = getProvidersMeta();
58
- delete meta[normalizeProvider(provider)];
67
+ delete meta[metaKey(provider, account)];
59
68
  writeJson(META_FILE, meta);
60
69
  };
61
70