Appearance
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),
});readCoderuns at the moment the branch runs, after Instagram sent the email.Effect.timeoutstops the wait after five minutes; the call then rejects with the timeout, andCodeUnavailableErroris what you raise when your system cannot answer.Effect.tryPromiseis the bridge from aPromise-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.
- A challenge: in the
otherwisebranch of the screen that asks for the code, as above (Challenges: answer them once withotherwise). - A registration: between
submitPhoneNumberandsubmitConfirmationCode.
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
findAccountorrequestNewCode, beforelogInWithCode(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.