Skip to content

Codes that arrive during the run ​

Some steps send you a code and wait for you to type it: the SMS of a registration, the email of a challenge or of an account recovery. The code exists only after the step has asked for it. This page shows how a program obtains such a code at the moment it exists.

The problem ​

A code read from an environment variable at start-up cannot work: when the program starts, Instagram has not sent it yet. The one value that can be read early is the authenticator setup key, because the code is computed from it (The authenticator code).

The pattern ​

Write readCode(kind) once, as an Effect that waits for the code from your own system: an inbox API, an SMS provider's webhook, a database row, or a person at the terminal. Give it a timeout, and turn its failure into an error of your own.

ts
import {
  AndroidIgpapi,
  ChallengeSelectVerifyMethodScreen,
  ChallengeVerifyCodeScreen,
  defineOtherwise,
  FeedTimelineScreen,
} from '@igpapi/android';
import { Data, Effect } from 'effect';
import { readFile } from 'node:fs/promises';
import { createInterface } from 'node:readline/promises';

/** The code did not arrive: nobody typed it, or your system did not answer. */
class CodeUnavailableError extends Data.TaggedError('CodeUnavailableError')<{
  readonly kind: 'email' | 'sms';
}> {}

/**
 * Waits for a code at the moment it exists. This one asks a person at the terminal;
 * your program can read an inbox, an SMS provider or a database row here instead.
 */
const readCode = (kind: 'email' | 'sms') =>
  Effect.tryPromise({
    try: async () => {
      const terminal = createInterface({ input: process.stdin, output: process.stdout });
      try {
        return (await terminal.question(`The ${kind} code: `)).trim();
      } finally {
        terminal.close();
      }
    },
    catch: () => new CodeUnavailableError({ kind }),
  }).pipe(Effect.timeout('5 minutes'));

// The branch runs when Instagram asks for the code, after it sent it.
const challenges = defineOtherwise({
  [ChallengeSelectVerifyMethodScreen.id]: method => method.chooseVerificationMethod('email'),
  [ChallengeVerifyCodeScreen.id]: screen => Effect.flatMap(readCode('email'), code => screen.submitCode(code)),
});

const saved = JSON.parse(await readFile('session.json', 'utf8'));

await AndroidIgpapi.launch({
  checkpoint: saved,
  screen: FeedTimelineScreen,
  otherwise: challenges,
  main: feed => Effect.sync(() => feed.items.length),
});
  • readCode runs at the moment the branch runs, after Instagram sent the email.
  • Effect.timeout stops the wait after five minutes; the call then rejects with the timeout, and CodeUnavailableError is what you raise when your system cannot answer.
  • Effect.tryPromise is the bridge from a Promise-based source (an HTTP call to your mailbox, node:readline) to an Effect with a typed error.

Where to call it ​

Inside the step, right after the action that made Instagram send the code.

ts
import { AndroidIgpapi } from '@igpapi/android';
import { Data, Effect } from 'effect';
import { createInterface } from 'node:readline/promises';

/** The code did not arrive: nobody typed it, or your system did not answer. */
class CodeUnavailableError extends Data.TaggedError('CodeUnavailableError')<{
  readonly kind: 'email' | 'sms';
}> {}

/** Waits for a code at the moment it exists (here: a person types it; see the previous program). */
const readCode = (kind: 'email' | 'sms') =>
  Effect.tryPromise({
    try: async () => {
      const terminal = createInterface({ input: process.stdin, output: process.stdout });
      try {
        return (await terminal.question(`The ${kind} code: `)).trim();
      } finally {
        terminal.close();
      }
    },
    catch: () => new CodeUnavailableError({ kind }),
  }).pipe(Effect.timeout('5 minutes'));

const outcome = await AndroidIgpapi.install({
  seed: 'new-account',
  main: home =>
    Effect.gen(function* () {
      const phoneNumber = yield* home.createNewAccount();
      // This step makes Instagram send the SMS ...
      const confirmationCode = yield* phoneNumber.submitPhoneNumber('+15550000001');
      // ... so the code is read now, after it exists, and not before the program started.
      const smsCode = yield* readCode('sms');
      const password = yield* confirmationCode.submitConfirmationCode(smsCode);
      return password.id;
    }),
});
console.log(outcome);
  • An account recovery: after findAccount or requestNewCode, before logInWithCode (Account recovery).

Approvals on another device ​

When a login waits for an approval on another device, no code is typed: you poll checkApproval() and own the pace (Log in).

The authenticator app is different ​

An authenticator code is computed, not received. Call totpCodeOf(secret) at the moment you submit it: it rotates every 30 seconds (The authenticator code).

A code that was wrong or expired ​

A wrong or expired code is a typed error of the action that submitted it (RegistrationSmsCodeError, IgChallengeNavigationVerifyCodeInvalidCodeError, LoginTwoFactorAuthenticationAppCodeRejectedError; all in Errors). Ask for another code with the screen's own action (resendCode() on a challenge, requestNewCode() on a recovery), read it again with readCode, and submit it.

The pages of this site are the README files of the repository. The API reference is generated from the source.