@@ -26,6 +26,25 @@ const DISCOVERY_POLL_MS = 250;
2626let PLATFORM_LOGIN = process . env . USER_LOGIN_INSTAGRAM || '' ;
2727let PLATFORM_PASSWORD = process . env . USER_PASSWORD_INSTAGRAM || '' ;
2828
29+ const MAX_LOGIN_ATTEMPTS = 3 ;
30+
31+ // Re-prompt up to maxAttempts times. attempt(values, attemptIndex) returns:
32+ // { ok: true, value } → return value
33+ // { ok: false, error: 'msg' } → re-prompt with `error: 'msg'`
34+ // throws → propagate (e.g. user cancelled)
35+ const promptWithRetry = async ( buildSpec , attempt , maxAttempts = MAX_LOGIN_ATTEMPTS ) => {
36+ let lastError = null ;
37+ for ( let i = 0 ; i < maxAttempts ; i ++ ) {
38+ const spec = buildSpec ( ) ;
39+ if ( lastError ) spec . error = lastError ;
40+ const values = await page . requestInput ( spec ) ;
41+ const result = await attempt ( values , i ) ;
42+ if ( result . ok ) return result . value ;
43+ lastError = result . error ;
44+ }
45+ throw new Error ( 'Too many failed attempts: ' + ( lastError || 'unknown reason' ) ) ;
46+ } ;
47+
2948// ─── In-page fetch helper ────────────────────────────────────
3049
3150const fetchApi = async ( url , options ) => {
@@ -253,7 +272,7 @@ const handleAuthPlatformChallenge = async (challengeUrl) => {
253272 await safeGoto ( fullUrl ) ;
254273 await page . sleep ( 2000 ) ;
255274
256- const { code } = await page . requestInput ( {
275+ const buildSpec = ( ) => ( {
257276 message : 'Enter Instagram 2FA code' ,
258277 schema : {
259278 type : 'object' ,
@@ -263,46 +282,52 @@ const handleAuthPlatformChallenge = async (challengeUrl) => {
263282 required : [ 'code' ] ,
264283 } ,
265284 } ) ;
266- const trimmedCode = String ( code || '' ) . trim ( ) ;
267- if ( ! / ^ \d { 4 , 8 } $ / . test ( trimmedCode ) ) {
268- throw new Error ( 'Invalid challenge code supplied: "' + code + '"' ) ;
269- }
270285
271- await page . setData ( 'status' , 'Submitting challenge code...' ) ;
272- const submitResult = await page . evaluate ( `
273- (() => {
274- const input = document.querySelector('input[type="text"]');
275- if (!input) return { ok: false, reason: 'no text input found' };
276- const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
277- setter.call(input, ${ JSON . stringify ( trimmedCode ) } );
278- input.dispatchEvent(new Event('input', { bubbles: true }));
279- input.dispatchEvent(new Event('change', { bubbles: true }));
280- const buttons = Array.from(document.querySelectorAll('[role="button"], button'));
281- const cont = buttons.find(b => (b.textContent || '').trim().toLowerCase() === 'continue');
282- if (!cont) return { ok: false, reason: 'no Continue button found' };
283- cont.click();
284- return { ok: true };
285- })()
286- ` ) ;
287- if ( ! submitResult || submitResult . ok !== true ) {
288- throw new Error (
289- 'Failed to submit challenge code: ' +
290- ( ( submitResult && submitResult . reason ) || 'unknown' )
291- ) ;
292- }
286+ try {
287+ await promptWithRetry ( buildSpec , async ( values ) => {
288+ const trimmedCode = String ( ( values && values . code ) || '' ) . trim ( ) ;
289+ if ( ! / ^ \d { 4 , 8 } $ / . test ( trimmedCode ) ) {
290+ return { ok : false , error : 'Invalid code format — must be 4-8 digits' } ;
291+ }
292+
293+ await page . setData ( 'status' , 'Submitting challenge code...' ) ;
294+ const submitResult = await page . evaluate ( `
295+ (() => {
296+ const input = document.querySelector('input[type="text"]');
297+ if (!input) return { ok: false, reason: 'no text input found' };
298+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
299+ setter.call(input, ${ JSON . stringify ( trimmedCode ) } );
300+ input.dispatchEvent(new Event('input', { bubbles: true }));
301+ input.dispatchEvent(new Event('change', { bubbles: true }));
302+ const buttons = Array.from(document.querySelectorAll('[role="button"], button'));
303+ const cont = buttons.find(b => (b.textContent || '').trim().toLowerCase() === 'continue');
304+ if (!cont) return { ok: false, reason: 'no Continue button found' };
305+ cont.click();
306+ return { ok: true };
307+ })()
308+ ` ) ;
309+ if ( ! submitResult || submitResult . ok !== true ) {
310+ const reason = ( submitResult && submitResult . reason ) || 'unknown' ;
311+ return { ok : false , error : 'Failed to submit code: ' + reason } ;
312+ }
293313
294- await page . setData ( 'status' , 'Waiting for session cookie...' ) ;
295- for ( let attempt = 0 ; attempt < 15 ; attempt ++ ) {
296- await page . sleep ( 1000 ) ;
297- const ds = await readDsUserId ( ) ;
298- if ( ds ) {
299- await page . setData ( 'status' , 'Challenge cleared' ) ;
300- return ;
314+ await page . setData ( 'status' , 'Waiting for session cookie...' ) ;
315+ for ( let pollAttempt = 0 ; pollAttempt < 15 ; pollAttempt ++ ) {
316+ await page . sleep ( 1000 ) ;
317+ const ds = await readDsUserId ( ) ;
318+ if ( ds ) {
319+ await page . setData ( 'status' , 'Challenge cleared' ) ;
320+ return { ok : true , value : undefined } ;
321+ }
322+ }
323+ return { ok : false , error : 'Code rejected — verification cookie never appeared' } ;
324+ } ) ;
325+ } catch ( e ) {
326+ if ( e && typeof e . message === 'string' && e . message . startsWith ( 'Too many failed attempts: ' ) ) {
327+ throw new Error ( 'Challenge verification failed: ' + e . message . slice ( 'Too many failed attempts: ' . length ) ) ;
301328 }
329+ throw e ;
302330 }
303- throw new Error (
304- 'Challenge code submitted but ds_user_id cookie never appeared — code may have been rejected'
305- ) ;
306331} ;
307332
308333const performLogin = async ( ) => {
@@ -311,8 +336,26 @@ const performLogin = async () => {
311336 throw new Error ( 'csrftoken cookie missing after visiting instagram.com — cannot submit login' ) ;
312337 }
313338
314- if ( ! PLATFORM_LOGIN || ! PLATFORM_PASSWORD ) {
315- const creds = await page . requestInput ( {
339+ const submitCredentials = async ( ) => {
340+ await page . setData ( 'status' , 'Submitting login credentials...' ) ;
341+ const wrappedPwd = IG_PWD_PREFIX + Math . floor ( Date . now ( ) / 1000 ) + ':' + PLATFORM_PASSWORD ;
342+ return await postLoginAjax ( LOGIN_URL , csrftoken , {
343+ username : PLATFORM_LOGIN ,
344+ enc_password : wrappedPwd ,
345+ queryParams : '{}' ,
346+ optIntoOneTap : 'false' ,
347+ trustedDeviceRecords : '{}' ,
348+ } ) ;
349+ } ;
350+
351+ let result ;
352+ if ( PLATFORM_LOGIN && PLATFORM_PASSWORD ) {
353+ result = await submitCredentials ( ) ;
354+ if ( result . kind === 'error' ) {
355+ throw new Error ( 'Instagram login failed: ' + ( result . message || 'unknown reason' ) ) ;
356+ }
357+ } else {
358+ const buildCredsSpec = ( ) => ( {
316359 message : 'Log in to Instagram' ,
317360 schema : {
318361 type : 'object' ,
@@ -323,43 +366,58 @@ const performLogin = async () => {
323366 required : [ 'username' , 'password' ] ,
324367 } ,
325368 } ) ;
326- PLATFORM_LOGIN = creds . username ;
327- PLATFORM_PASSWORD = creds . password ;
328- }
329369
330- await page . setData ( 'status' , 'Submitting login credentials...' ) ;
331- const wrappedPwd = IG_PWD_PREFIX + Math . floor ( Date . now ( ) / 1000 ) + ':' + PLATFORM_PASSWORD ;
332- const result = await postLoginAjax ( LOGIN_URL , csrftoken , {
333- username : PLATFORM_LOGIN ,
334- enc_password : wrappedPwd ,
335- queryParams : '{}' ,
336- optIntoOneTap : 'false' ,
337- trustedDeviceRecords : '{}' ,
338- } ) ;
370+ try {
371+ result = await promptWithRetry ( buildCredsSpec , async ( creds ) => {
372+ PLATFORM_LOGIN = creds . username ;
373+ PLATFORM_PASSWORD = creds . password ;
374+ const inner = await submitCredentials ( ) ;
375+ if ( inner . kind === 'ok' ) return { ok : true , value : { kind : 'ok' } } ;
376+ if ( inner . kind === 'two_factor' ) return { ok : true , value : inner } ;
377+ if ( inner . kind === 'auth_platform' ) return { ok : true , value : inner } ;
378+ if ( inner . kind === 'checkpoint' ) return { ok : true , value : inner } ;
379+ return { ok : false , error : inner . message || 'Login failed' } ;
380+ } ) ;
381+ } catch ( e ) {
382+ if ( e && typeof e . message === 'string' && e . message . startsWith ( 'Too many failed attempts: ' ) ) {
383+ throw new Error ( 'Too many failed login attempts: ' + e . message . slice ( 'Too many failed attempts: ' . length ) ) ;
384+ }
385+ throw e ;
386+ }
387+ }
339388
340389 if ( result . kind === 'ok' ) {
341390 return ;
342391 }
343392
344393 if ( result . kind === 'two_factor' ) {
345- const { code } = await page . requestInput ( {
394+ const buildCodeSpec = ( ) => ( {
346395 message : 'Enter your Instagram two-factor verification code' ,
347396 schema : {
348397 type : 'object' ,
349398 properties : { code : { type : 'string' , title : '6-digit verification code' } } ,
350399 required : [ 'code' ] ,
351400 } ,
352401 } ) ;
353- const refreshedCsrf = ( await readCsrfToken ( ) ) || csrftoken ;
354- const second = await postLoginAjax ( TWO_FACTOR_URL , refreshedCsrf , {
355- username : PLATFORM_LOGIN ,
356- verificationCode : String ( code ) . trim ( ) ,
357- identifier : result . info . twoFactorIdentifier ,
358- queryParams : '{}' ,
359- trust_signal_v2 : 'true' ,
360- } ) ;
361- if ( second . kind !== 'ok' ) {
362- throw new Error ( 'Two-factor verification failed: ' + ( second . message || second . kind ) ) ;
402+
403+ try {
404+ await promptWithRetry ( buildCodeSpec , async ( values ) => {
405+ const refreshedCsrf = ( await readCsrfToken ( ) ) || csrftoken ;
406+ const second = await postLoginAjax ( TWO_FACTOR_URL , refreshedCsrf , {
407+ username : PLATFORM_LOGIN ,
408+ verificationCode : String ( ( values && values . code ) || '' ) . trim ( ) ,
409+ identifier : result . info . twoFactorIdentifier ,
410+ queryParams : '{}' ,
411+ trust_signal_v2 : 'true' ,
412+ } ) ;
413+ if ( second . kind === 'ok' ) return { ok : true , value : undefined } ;
414+ return { ok : false , error : second . message || second . kind } ;
415+ } ) ;
416+ } catch ( e ) {
417+ if ( e && typeof e . message === 'string' && e . message . startsWith ( 'Too many failed attempts: ' ) ) {
418+ throw new Error ( 'Two-factor verification failed: ' + e . message . slice ( 'Too many failed attempts: ' . length ) ) ;
419+ }
420+ throw e ;
363421 }
364422 return ;
365423 }
0 commit comments