diff --git a/packages/waas/src/auth.ts b/packages/waas/src/auth.ts index a27dc2826..b3a8dc7a8 100644 --- a/packages/waas/src/auth.ts +++ b/packages/waas/src/auth.ts @@ -22,9 +22,11 @@ import { AdoptChildWalletArgs } from './intents' import { + ConfirmationRequiredResponse, FeeOptionsResponse, isChildWalletAdoptedResponse, isCloseSessionResponse, + isConfirmationRequiredResponse, isFeeOptionsResponse, isFinishValidateSessionResponse, isGetAdopterResponse, @@ -168,7 +170,9 @@ export class SequenceWaaS { private validationRequiredCallback: (() => void)[] = [] private emailConflictCallback: ((info: EmailConflictInfo, forceCreate: () => Promise) => Promise)[] = [] private emailAuthCodeRequiredCallback: ((respondWithCode: (code: string) => Promise) => Promise)[] = [] + private confirmationRequiredCallback: ((respondWithCode: (code: string) => Promise) => Promise)[] = [] private validationRequiredSalt: string + private lastConfirmationAttemptAt: Date | undefined public readonly config: Required & Required & ExtendedSequenceConfig @@ -313,6 +317,13 @@ export class SequenceWaaS { } } + onConfirmationRequired(callback: (respondWithCode: (code: string) => Promise) => Promise) { + this.confirmationRequiredCallback.push(callback) + return () => { + this.confirmationRequiredCallback = this.confirmationRequiredCallback.filter(c => c !== callback) + } + } + private async handleValidationRequired({ onValidationRequired }: ValidationArgs = {}): Promise { const proceed = onValidationRequired ? onValidationRequired() : true if (!proceed) { @@ -333,6 +344,45 @@ export class SequenceWaaS { return this.waitForSessionValid() } + private async handleConfirmationRequired(response: ConfirmationRequiredResponse) { + if (this.confirmationRequiredCallback.length === 0) { + throw new Error('Missing confirmationRequired callback') + } + + return new Promise((resolve, reject) => { + const respondToChallenge = async (answer: string) => { + if (this.lastConfirmationAttemptAt) { + const timeSinceLastAttempt = new Date().getTime() - this.lastConfirmationAttemptAt.getTime() + if (timeSinceLastAttempt < 32000) { + console.info(`Waiting ${Math.ceil((32000 - timeSinceLastAttempt) / 1000)}s before retrying confirmation attempt`) + await new Promise(resolve => setTimeout(resolve, 32000 - timeSinceLastAttempt)) + } + } + + this.lastConfirmationAttemptAt = new Date() + + const intent = await this.waas.confirmIntent(response.data.salt, answer) + + try { + const response2 = await this.sendIntent(intent) + resolve(response2) + } catch (e) { + if (e instanceof AnswerIncorrectError) { + // This will NOT resolve NOR reject the top-level promise returned from signIn, it'll keep being pending + // It allows the caller to retry calling the respondToChallenge callback + throw e + } else { + reject(e) + } + } + } + + for (const callback of this.confirmationRequiredCallback) { + callback(respondToChallenge) + } + }) + } + private headers() { return { 'X-Access-Key': this.config.projectAccessKey @@ -775,6 +825,15 @@ export class SequenceWaaS { return response } + if (isConfirmationRequiredResponse(response)) { + const response2 = await this.handleConfirmationRequired(response) + if (isExpectedResponse(response2)) { + return response2 + } else { + throw new Error(JSON.stringify(response2)) + } + } + if (isValidationRequiredResponse(response)) { const proceed = await this.handleValidationRequired(args.validation) diff --git a/packages/waas/src/base.ts b/packages/waas/src/base.ts index cdde0f509..7b6ec4296 100644 --- a/packages/waas/src/base.ts +++ b/packages/waas/src/base.ts @@ -4,6 +4,7 @@ import { changeIntentTime, closeSession, combineTransactionIntents, + confirmIntent, feeOptions, finishValidateSession, getAdopter, @@ -50,7 +51,8 @@ import { IntentDataOpenSession, IntentDataSendTransaction, IntentDataSignMessage, - IntentDataValidateSession + IntentDataValidateSession, + IntentDataConfirmIntent, } from './clients/intent.gen' import { getDefaultSubtleCryptoBackend, SubtleCryptoBackend } from './subtle-crypto' import { getDefaultSecureStoreBackend, SecureStoreBackend } from './secure-store' @@ -513,6 +515,17 @@ export class SequenceWaaSBase { return this.signIntent(intent) } + async confirmIntent(salt: string, secretCode: string): Promise> { + const intent = confirmIntent({ + lifespan: DEFAULT_LIFESPAN, + wallet: await this.getWalletAddress(), + confirmationID: salt, + challengeAnswer: ethers.id(salt + secretCode), + }) + + return this.signIntent(intent) + } + async getSession(): Promise> { const sessionId = await this.sessionId.get() if (!sessionId) { diff --git a/packages/waas/src/intents/responses.ts b/packages/waas/src/intents/responses.ts index 6af89ba43..7a1829e00 100644 --- a/packages/waas/src/intents/responses.ts +++ b/packages/waas/src/intents/responses.ts @@ -10,7 +10,8 @@ import { IntentResponseGetSession, IntentResponseIdToken, IntentResponseValidationFinished, - IntentResponseValidationStarted + IntentResponseValidationStarted, + IntentResponseConfirmationRequired, } from '../clients/intent.gen' import { WebrpcEndpointError, WebrpcError } from '../clients/authenticator.gen' @@ -127,6 +128,7 @@ export interface Response { export type InitiateAuthResponse = Response export type ValidateSessionResponse = Response +export type ConfirmationRequiredResponse = Response export type FinishValidateSessionResponse = Response export type GetSessionResponse = Response export type LinkAccountResponse = Response @@ -249,6 +251,15 @@ export function isFinishValidateSessionResponse(receipt: any): receipt is Finish return typeof receipt === 'object' && receipt.code === IntentResponseCode.validationFinished && typeof receipt.data === 'object' } +export function isConfirmationRequiredResponse(receipt: any): receipt is ConfirmationRequiredResponse { + return ( + typeof receipt === 'object' && + receipt.code === IntentResponseCode.confirmationRequired && + typeof receipt.data === 'object' && + typeof receipt.data.salt === 'string' + ) +} + export function isCloseSessionResponse(receipt: any): receipt is CloseSessionResponse { return typeof receipt === 'object' && typeof receipt.code === 'string' && receipt.code === 'sessionClosed' } diff --git a/packages/waas/src/intents/session.ts b/packages/waas/src/intents/session.ts index e8028fe77..4a534bbc4 100644 --- a/packages/waas/src/intents/session.ts +++ b/packages/waas/src/intents/session.ts @@ -11,7 +11,8 @@ import { IntentDataGetIdToken, IntentName, IntentDataAdoptChildWallet, - IntentDataGetAdopter + IntentDataGetAdopter, + IntentDataConfirmIntent, } from '../clients/intent.gen' interface BaseArgs { @@ -83,3 +84,9 @@ export type GetAdopterArgs = BaseArgs & IntentDataGetAdopter export function getAdopter({ lifespan, ...data }: GetAdopterArgs): Intent { return makeIntent(IntentName.getAdopter, lifespan, data) } + +export type ConfirmIntentArgs = BaseArgs & IntentDataConfirmIntent + +export function confirmIntent({ lifespan, ...data }: ConfirmIntentArgs): Intent { + return makeIntent(IntentName.confirmIntent, lifespan, data) +}