viruagent-cli 0.4.1 → 0.4.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.
package/bin/index.js CHANGED
@@ -212,6 +212,12 @@ analyzePostCmd
212
212
  .option('--post-id <shortcode>', 'Post shortcode')
213
213
  .action((opts) => execute('analyze-post', opts));
214
214
 
215
+ const resolveChallengeCmd = program
216
+ .command('resolve-challenge')
217
+ .description('Resolve Instagram challenge (auto-verify identity)');
218
+ addProviderOption(resolveChallengeCmd);
219
+ resolveChallengeCmd.action((opts) => execute('resolve-challenge', opts));
220
+
215
221
  const rateLimitCmd = program
216
222
  .command('rate-limit-status')
217
223
  .description('Show current rate limit usage');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viruagent-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "AI-agent-optimized CLI for blog publishing (Tistory, Naver) and Instagram automation",
5
5
  "private": false,
6
6
  "type": "commonjs",
@@ -174,9 +174,36 @@ const createInstaApiClient = ({ sessionPath }) => {
174
174
  if (location.includes('/accounts/login')) {
175
175
  throw new Error('세션이 만료되었습니다. 다시 로그인해 주세요.');
176
176
  }
177
+ if (location.includes('/challenge')) {
178
+ // challenge 자동 해결 시도
179
+ const resolved = await resolveChallenge();
180
+ if (resolved) {
181
+ // 해결 후 원래 요청 재시도
182
+ return fetch(url, { ...options, headers, redirect: 'manual' });
183
+ }
184
+ throw new Error('challenge_required: 본인 인증이 필요합니다. 브라우저에서 수동으로 처리해 주세요.');
185
+ }
177
186
  throw new Error(`리다이렉트 발생: ${res.status} → ${location}`);
178
187
  }
179
188
 
189
+ // challenge_required JSON 응답 처리
190
+ if (res.status === 400 && !options.allowError) {
191
+ const cloned = res.clone();
192
+ try {
193
+ const data = await cloned.json();
194
+ if (data.message === 'challenge_required') {
195
+ const resolved = await resolveChallenge();
196
+ if (resolved) {
197
+ return fetch(url, { ...options, headers, redirect: 'manual' });
198
+ }
199
+ throw new Error('challenge_required: 본인 인증이 필요합니다.');
200
+ }
201
+ } catch (e) {
202
+ if (e.message.includes('challenge_required')) throw e;
203
+ // JSON 파싱 실패는 무시하고 원래 흐름
204
+ }
205
+ }
206
+
180
207
  if (res.status === 401 || res.status === 403) {
181
208
  throw new Error(`인증 오류 (${res.status}). 다시 로그인해 주세요.`);
182
209
  }
@@ -316,6 +343,35 @@ const createInstaApiClient = ({ sessionPath }) => {
316
343
  };
317
344
  };
318
345
 
346
+ // ── Challenge 자동 해결 ──
347
+
348
+ const resolveChallenge = async () => {
349
+ try {
350
+ const cookies = getCookies();
351
+ const res = await fetch('https://www.instagram.com/api/v1/challenge/web/action/', {
352
+ method: 'POST',
353
+ headers: {
354
+ 'User-Agent': USER_AGENT,
355
+ 'X-IG-App-ID': IG_APP_ID,
356
+ 'X-CSRFToken': getCsrfToken(),
357
+ 'X-Requested-With': 'XMLHttpRequest',
358
+ 'X-Instagram-AJAX': '1',
359
+ Referer: 'https://www.instagram.com/challenge/',
360
+ Origin: 'https://www.instagram.com',
361
+ 'Content-Type': 'application/x-www-form-urlencoded',
362
+ Cookie: cookiesToHeader(cookies),
363
+ },
364
+ body: 'choice=0',
365
+ redirect: 'manual',
366
+ });
367
+ if (!res.ok) return false;
368
+ const data = await res.json();
369
+ return data.status === 'ok';
370
+ } catch {
371
+ return false;
372
+ }
373
+ };
374
+
319
375
  const parseLikeResponse = async (res) => {
320
376
  if (res.ok) return res.json();
321
377
  if (res.status === 400) {
@@ -611,6 +667,7 @@ const createInstaApiClient = ({ sessionPath }) => {
611
667
  configurePost,
612
668
  publishPost,
613
669
  deletePost,
670
+ resolveChallenge,
614
671
  resetState,
615
672
  getRateLimitStatus: () => {
616
673
  const status = {};
@@ -99,10 +99,61 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
99
99
 
100
100
  const loginData = await loginRes.json();
101
101
 
102
- if (loginData.checkpoint_url) {
103
- throw new Error(
104
- '2단계 인증(checkpoint)이 필요합니다. 브라우저에서 먼저 인증을 완료해 주세요.',
105
- );
102
+ if (loginData.checkpoint_url || loginData.message === 'challenge_required') {
103
+ // challenge 자동 해결 시도 (choice=0 = 본인입니다)
104
+ const challengeRes = await fetch('https://www.instagram.com/api/v1/challenge/web/action/', {
105
+ method: 'POST',
106
+ headers: {
107
+ 'User-Agent': USER_AGENT,
108
+ 'X-CSRFToken': csrfToken,
109
+ 'X-Requested-With': 'XMLHttpRequest',
110
+ 'X-Instagram-AJAX': '1',
111
+ 'X-IG-App-ID': IG_APP_ID,
112
+ Referer: 'https://www.instagram.com/challenge/',
113
+ Origin: 'https://www.instagram.com',
114
+ 'Content-Type': 'application/x-www-form-urlencoded',
115
+ Cookie: cookieHeader,
116
+ },
117
+ body: 'choice=0',
118
+ redirect: 'manual',
119
+ });
120
+ const challengeData = await challengeRes.json().catch(() => ({}));
121
+ if (challengeData.status !== 'ok') {
122
+ throw new Error(
123
+ 'challenge_required: 자동 해결 실패. 브라우저에서 본인 인증을 완료해 주세요. ' +
124
+ (loginData.checkpoint_url || ''),
125
+ );
126
+ }
127
+ // challenge 해결 후 재로그인
128
+ const retryRes = await fetch('https://www.instagram.com/api/v1/web/accounts/login/ajax/', {
129
+ method: 'POST',
130
+ headers: {
131
+ 'User-Agent': USER_AGENT,
132
+ 'X-CSRFToken': csrfToken,
133
+ 'X-Requested-With': 'XMLHttpRequest',
134
+ 'X-IG-App-ID': IG_APP_ID,
135
+ 'Content-Type': 'application/x-www-form-urlencoded',
136
+ Referer: 'https://www.instagram.com/accounts/login/',
137
+ Origin: 'https://www.instagram.com',
138
+ Cookie: cookieHeader,
139
+ },
140
+ body: body.toString(),
141
+ redirect: 'manual',
142
+ });
143
+ const retryCookies = parseCookiesFromHeaders(retryRes.headers);
144
+ cookies = mergeCookies(cookies, retryCookies);
145
+ const retryData = await retryRes.json().catch(() => ({}));
146
+ if (retryData.authenticated) {
147
+ saveInstaSession(sessionPath, cookies);
148
+ return {
149
+ provider: 'insta',
150
+ loggedIn: true,
151
+ userId: retryData.userId || null,
152
+ username: resolvedUsername,
153
+ sessionPath,
154
+ challengeResolved: true,
155
+ };
156
+ }
106
157
  }
107
158
 
108
159
  if (loginData.two_factor_required) {
@@ -271,6 +271,16 @@ const createInstaProvider = ({ sessionPath }) => {
271
271
  });
272
272
  },
273
273
 
274
+ async resolveChallenge() {
275
+ const resolved = await instaApi.resolveChallenge();
276
+ return {
277
+ provider: 'insta',
278
+ mode: 'resolveChallenge',
279
+ resolved,
280
+ message: resolved ? 'Challenge 해결 완료' : 'Challenge 해결 실패. 브라우저에서 수동으로 처리해 주세요.',
281
+ };
282
+ },
283
+
274
284
  rateLimitStatus() {
275
285
  return {
276
286
  provider: 'insta',
package/src/runner.js CHANGED
@@ -237,6 +237,9 @@ const runCommand = async (command, opts = {}) => {
237
237
  }
238
238
  return withProvider(() => provider.analyzePost({ postId: opts.postId }))();
239
239
 
240
+ case 'resolve-challenge':
241
+ return withProvider(() => provider.resolveChallenge())();
242
+
240
243
  case 'rate-limit-status':
241
244
  return withProvider(() => Promise.resolve(provider.rateLimitStatus()))();
242
245