diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 3064e6dd541..6a9c1a1ff07 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -20,6 +20,7 @@ import Database from '../../../../db' import { didDocForSession } from './util' import { getPdsEndpoint } from '../../../../pds-agents' import { isThisPds } from '../../../proxy' +import { dbLogger as log } from '../../../../logger' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.createAccount({ @@ -45,33 +46,11 @@ export default function (server: Server, ctx: AppContext) { const now = new Date().toISOString() const passwordScrypt = await scrypt.genSaltAndHash(password) - let verificationPhone: string | undefined = undefined - if (ctx.cfg.phoneVerification.required && ctx.twilio) { - if (!input.body.verificationPhone) { - throw new InvalidRequestError( - `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, - 'InvalidPhoneVerification', - ) - } else if (!input.body.verificationCode) { - throw new InvalidRequestError( - `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, - 'InvalidPhoneVerification', - ) - } - verificationPhone = ctx.twilio.normalizePhoneNumber( - input.body.verificationPhone, - ) - const verified = await ctx.twilio.verifyCode( - verificationPhone, - input.body.verificationCode.trim(), - ) - if (!verified) { - throw new InvalidRequestError( - 'Could not verify phone number. Please try again.', - 'InvalidPhoneVerification', - ) - } - } + const verificationPhone = await ensurePhoneVerification( + ctx, + input.body.verificationPhone, + input.body.verificationCode?.trim(), + ) const result = await ctx.db.transaction(async (dbTxn) => { const actorTxn = ctx.services.account(dbTxn) @@ -99,19 +78,6 @@ export default function (server: Server, ctx: AppContext) { throw err } - // Generate a real did with PLC - if (plcOp && !entrywayAssignedPds) { - try { - await ctx.plcClient.sendOperation(did, plcOp) - } catch (err) { - req.log.error( - { didKey: ctx.plcRotationKey.did(), handle }, - 'failed to create did:plc', - ) - throw err - } - } - // insert invite code use if (ctx.cfg.invites.required && inviteCode) { await ensureCodeIsAvailable(dbTxn, inviteCode, true) @@ -135,6 +101,10 @@ export default function (server: Server, ctx: AppContext) { .execute() } + if (!entrywayAssignedPds) { + await repoTxn.createRepo(did, [], now) + } + const { access, refresh } = await ctx.services .auth(dbTxn) .createSession({ @@ -144,6 +114,15 @@ export default function (server: Server, ctx: AppContext) { deactivated: !hasAvailability, }) + return { + did, + pdsDid: entrywayAssignedPds?.did ?? null, + accessJwt: access, + refreshJwt: refresh, + } + }) + + try { if (entrywayAssignedPds) { const agent = ctx.pdsAgents.get(entrywayAssignedPds.host) await agent.com.atproto.server.createAccount({ @@ -153,17 +132,21 @@ export default function (server: Server, ctx: AppContext) { recoveryKey: input.body.recoveryKey, }) } else { - // Setup repo root - await repoTxn.createRepo(did, [], now) - } - - return { - did, - pdsDid: entrywayAssignedPds?.did ?? null, - accessJwt: access, - refreshJwt: refresh, + assert(plcOp) + try { + await ctx.plcClient.sendOperation(did, plcOp) + } catch (err) { + req.log.error( + { didKey: ctx.plcRotationKey.did(), handle }, + 'failed to create did:plc', + ) + throw err + } } - }) + } catch (err) { + await cleanupUncreatedAccount(ctx, did) + throw err + } const didDoc = await didDocForSession(ctx, result) @@ -490,6 +473,42 @@ const ensureUnusedHandleAndEmail = async ( } } +const ensurePhoneVerification = async ( + ctx: AppContext, + phone?: string, + code?: string, +): Promise => { + if (!ctx.cfg.phoneVerification.required || !ctx.twilio) { + return + } + + if (!phone) { + throw new InvalidRequestError( + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, + 'InvalidPhoneVerification', + ) + } + if (ctx.cfg.phoneVerification.bypassPhoneNumber === phone) { + return undefined + } + + if (!code) { + throw new InvalidRequestError( + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, + 'InvalidPhoneVerification', + ) + } + const normalizedPhone = ctx.twilio.normalizePhoneNumber(phone) + const verified = await ctx.twilio.verifyCode(normalizedPhone, code) + if (!verified) { + throw new InvalidRequestError( + 'Could not verify phone number. Please try again.', + 'InvalidPhoneVerification', + ) + } + return normalizedPhone +} + const randomIndexByWeight = (weights) => { let sum = 0 const cumulative = weights.map((weight) => { @@ -500,3 +519,21 @@ const randomIndexByWeight = (weights) => { const rand = Math.random() * sum return cumulative.findIndex((item) => item >= rand) } + +const cleanupUncreatedAccount = async ( + ctx: AppContext, + did: string, + tries = 0, +) => { + if (tries > 3) return + try { + await Promise.all([ + ctx.services.account(ctx.db).deleteAccount(did), + ctx.services.record(ctx.db).deleteForActor(did), + ctx.services.repo(ctx.db).deleteRepo(did), + ]) + } catch (err) { + log.error({ err, did, tries }, 'failed to clean up partially created user') + return cleanupUncreatedAccount(ctx, did, tries + 1) + } +} diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index 6652a2a29eb..a96462e05ab 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -137,6 +137,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { twilioAccountSid: env.twilioAccountSid, twilioServiceSid: env.twilioServiceSid, accountsPerPhoneNumber: env.accountsPerPhoneNumber ?? 3, + bypassPhoneNumber: env.bypassPhoneNumber, } } @@ -322,6 +323,7 @@ export type PhoneVerificationConfig = twilioAccountSid: string twilioServiceSid: string accountsPerPhoneNumber: number + bypassPhoneNumber?: string } | { required: false diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 1d332f9b8ff..324d74889db 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -52,6 +52,7 @@ export const readEnv = (): ServerEnvironment => { // phone verification phoneVerificationRequired: envBool('PDS_PHONE_VERIFICATION_REQUIRED'), accountsPerPhoneNumber: envInt('PDS_ACCOUNTS_PER_PHONE_NUMBER'), + bypassPhoneNumber: envStr('PDS_BYPASS_PHONE_NUMBER'), twilioAccountSid: envStr('PDS_TWILIO_ACCOUNT_SID'), twilioAuthToken: envStr('PDS_TWILIO_AUTH_TOKEN'), twilioServiceSid: envStr('PDS_TWILIO_SERVICE_SID'), @@ -166,6 +167,7 @@ export type ServerEnvironment = { // phone verification phoneVerificationRequired?: boolean accountsPerPhoneNumber?: number + bypassPhoneNumber?: string twilioAccountSid?: string twilioAuthToken?: string twilioServiceSid?: string