From f56ef4f74ff96e65f544e86492f697bb98997513 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 16 Jan 2024 18:02:37 -0600 Subject: [PATCH 01/23] basic twilio phone verification flow --- .../com/atproto/server/createAccount.json | 2 + .../com/atproto/server/describeServer.json | 1 + .../temp/requestPhoneVerification.json | 20 +++ packages/api/src/client/index.ts | 115 ++++++++------- packages/api/src/client/lexicons.ts | 31 ++++ .../types/com/atproto/server/createAccount.ts | 1 + .../com/atproto/server/describeServer.ts | 1 + .../atproto/temp/requestPhoneVerification.ts | 32 ++++ packages/bsky/src/lexicon/index.ts | 114 +++++++------- packages/bsky/src/lexicon/lexicons.ts | 31 ++++ .../types/com/atproto/server/createAccount.ts | 1 + .../com/atproto/server/describeServer.ts | 1 + .../atproto/temp/requestPhoneVerification.ts | 38 +++++ packages/pds/package.json | 1 + .../api/com/atproto/server/createAccount.ts | 24 +++ .../api/com/atproto/server/describeServer.ts | 2 + .../pds/src/api/com/atproto/temp/index.ts | 7 + .../atproto/temp/requestPhoneVerification.ts | 25 ++++ packages/pds/src/config/config.ts | 25 ++++ packages/pds/src/config/env.ts | 12 ++ packages/pds/src/config/secrets.ts | 2 + packages/pds/src/context.ts | 16 ++ packages/pds/src/lexicon/index.ts | 114 +++++++------- packages/pds/src/lexicon/lexicons.ts | 34 +++++ .../types/com/atproto/server/createAccount.ts | 2 + .../com/atproto/server/describeServer.ts | 1 + .../atproto/temp/requestPhoneVerification.ts | 38 +++++ packages/pds/src/twilio.ts | 30 ++++ pnpm-lock.yaml | 139 +++++++++++++++++- 29 files changed, 705 insertions(+), 155 deletions(-) create mode 100644 lexicons/com/atproto/temp/requestPhoneVerification.json create mode 100644 packages/api/src/client/types/com/atproto/temp/requestPhoneVerification.ts create mode 100644 packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts create mode 100644 packages/pds/src/api/com/atproto/temp/index.ts create mode 100644 packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts create mode 100644 packages/pds/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts create mode 100644 packages/pds/src/twilio.ts diff --git a/lexicons/com/atproto/server/createAccount.json b/lexicons/com/atproto/server/createAccount.json index 8d927163951..d1456e095ae 100644 --- a/lexicons/com/atproto/server/createAccount.json +++ b/lexicons/com/atproto/server/createAccount.json @@ -15,6 +15,8 @@ "handle": { "type": "string", "format": "handle" }, "did": { "type": "string", "format": "did" }, "inviteCode": { "type": "string" }, + "verificationCode": { "type": "string" }, + "verificationPhone": { "type": "string" }, "password": { "type": "string" }, "recoveryKey": { "type": "string" }, "plcOp": { "type": "unknown" } diff --git a/lexicons/com/atproto/server/describeServer.json b/lexicons/com/atproto/server/describeServer.json index b19b1504020..3c60a58ecaf 100644 --- a/lexicons/com/atproto/server/describeServer.json +++ b/lexicons/com/atproto/server/describeServer.json @@ -12,6 +12,7 @@ "required": ["availableUserDomains"], "properties": { "inviteCodeRequired": { "type": "boolean" }, + "phoneVerificationRequired": { "type": "boolean" }, "availableUserDomains": { "type": "array", "items": { "type": "string" } diff --git a/lexicons/com/atproto/temp/requestPhoneVerification.json b/lexicons/com/atproto/temp/requestPhoneVerification.json new file mode 100644 index 00000000000..56beeb81acc --- /dev/null +++ b/lexicons/com/atproto/temp/requestPhoneVerification.json @@ -0,0 +1,20 @@ +{ + "lexicon": 1, + "id": "com.atproto.temp.requestPhoneVerification", + "defs": { + "main": { + "type": "procedure", + "description": "Request a verification code to be sent to the supplied phone number", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["phoneNumber"], + "properties": { + "phoneNumber": { "type": "string" } + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index fb56cd251a0..bfb1be016c9 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -80,6 +80,7 @@ import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscrib import * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels' import * as ComAtprotoTempImportRepo from './types/com/atproto/temp/importRepo' import * as ComAtprotoTempPushBlob from './types/com/atproto/temp/pushBlob' +import * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification' import * as ComAtprotoTempTransferAccount from './types/com/atproto/temp/transferAccount' import * as AppBskyActorDefs from './types/app/bsky/actor/defs' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' @@ -220,6 +221,7 @@ export * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscrib export * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels' export * as ComAtprotoTempImportRepo from './types/com/atproto/temp/importRepo' export * as ComAtprotoTempPushBlob from './types/com/atproto/temp/pushBlob' +export * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification' export * as ComAtprotoTempTransferAccount from './types/com/atproto/temp/transferAccount' export * as AppBskyActorDefs from './types/app/bsky/actor/defs' export * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' @@ -338,39 +340,39 @@ export class AtpServiceClient { export class ComNS { _service: AtpServiceClient - atproto: AtprotoNS + atproto: ComAtprotoNS constructor(service: AtpServiceClient) { this._service = service - this.atproto = new AtprotoNS(service) + this.atproto = new ComAtprotoNS(service) } } -export class AtprotoNS { +export class ComAtprotoNS { _service: AtpServiceClient - admin: AdminNS - identity: IdentityNS - label: LabelNS - moderation: ModerationNS - repo: RepoNS - server: ServerNS - sync: SyncNS - temp: TempNS + admin: ComAtprotoAdminNS + identity: ComAtprotoIdentityNS + label: ComAtprotoLabelNS + moderation: ComAtprotoModerationNS + repo: ComAtprotoRepoNS + server: ComAtprotoServerNS + sync: ComAtprotoSyncNS + temp: ComAtprotoTempNS constructor(service: AtpServiceClient) { this._service = service - this.admin = new AdminNS(service) - this.identity = new IdentityNS(service) - this.label = new LabelNS(service) - this.moderation = new ModerationNS(service) - this.repo = new RepoNS(service) - this.server = new ServerNS(service) - this.sync = new SyncNS(service) - this.temp = new TempNS(service) + this.admin = new ComAtprotoAdminNS(service) + this.identity = new ComAtprotoIdentityNS(service) + this.label = new ComAtprotoLabelNS(service) + this.moderation = new ComAtprotoModerationNS(service) + this.repo = new ComAtprotoRepoNS(service) + this.server = new ComAtprotoServerNS(service) + this.sync = new ComAtprotoSyncNS(service) + this.temp = new ComAtprotoTempNS(service) } } -export class AdminNS { +export class ComAtprotoAdminNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -592,7 +594,7 @@ export class AdminNS { } } -export class IdentityNS { +export class ComAtprotoIdentityNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -622,7 +624,7 @@ export class IdentityNS { } } -export class LabelNS { +export class ComAtprotoLabelNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -641,7 +643,7 @@ export class LabelNS { } } -export class ModerationNS { +export class ComAtprotoModerationNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -660,7 +662,7 @@ export class ModerationNS { } } -export class RepoNS { +export class ComAtprotoRepoNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -756,7 +758,7 @@ export class RepoNS { } } -export class ServerNS { +export class ComAtprotoServerNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -995,7 +997,7 @@ export class ServerNS { } } -export class SyncNS { +export class ComAtprotoSyncNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -1124,7 +1126,7 @@ export class SyncNS { } } -export class TempNS { +export class ComAtprotoTempNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -1164,6 +1166,17 @@ export class TempNS { }) } + requestPhoneVerification( + data?: ComAtprotoTempRequestPhoneVerification.InputSchema, + opts?: ComAtprotoTempRequestPhoneVerification.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.temp.requestPhoneVerification', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoTempRequestPhoneVerification.toKnownErr(e) + }) + } + transferAccount( data?: ComAtprotoTempTransferAccount.InputSchema, opts?: ComAtprotoTempTransferAccount.CallOptions, @@ -1178,37 +1191,37 @@ export class TempNS { export class AppNS { _service: AtpServiceClient - bsky: BskyNS + bsky: AppBskyNS constructor(service: AtpServiceClient) { this._service = service - this.bsky = new BskyNS(service) + this.bsky = new AppBskyNS(service) } } -export class BskyNS { +export class AppBskyNS { _service: AtpServiceClient - actor: ActorNS - embed: EmbedNS - feed: FeedNS - graph: GraphNS - notification: NotificationNS - richtext: RichtextNS - unspecced: UnspeccedNS + actor: AppBskyActorNS + embed: AppBskyEmbedNS + feed: AppBskyFeedNS + graph: AppBskyGraphNS + notification: AppBskyNotificationNS + richtext: AppBskyRichtextNS + unspecced: AppBskyUnspeccedNS constructor(service: AtpServiceClient) { this._service = service - this.actor = new ActorNS(service) - this.embed = new EmbedNS(service) - this.feed = new FeedNS(service) - this.graph = new GraphNS(service) - this.notification = new NotificationNS(service) - this.richtext = new RichtextNS(service) - this.unspecced = new UnspeccedNS(service) + this.actor = new AppBskyActorNS(service) + this.embed = new AppBskyEmbedNS(service) + this.feed = new AppBskyFeedNS(service) + this.graph = new AppBskyGraphNS(service) + this.notification = new AppBskyNotificationNS(service) + this.richtext = new AppBskyRichtextNS(service) + this.unspecced = new AppBskyUnspeccedNS(service) } } -export class ActorNS { +export class AppBskyActorNS { _service: AtpServiceClient profile: ProfileRecord @@ -1356,7 +1369,7 @@ export class ProfileRecord { } } -export class EmbedNS { +export class AppBskyEmbedNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -1364,7 +1377,7 @@ export class EmbedNS { } } -export class FeedNS { +export class AppBskyFeedNS { _service: AtpServiceClient generator: GeneratorRecord like: LikeRecord @@ -1867,7 +1880,7 @@ export class ThreadgateRecord { } } -export class GraphNS { +export class AppBskyGraphNS { _service: AtpServiceClient block: BlockRecord follow: FollowRecord @@ -2342,7 +2355,7 @@ export class ListitemRecord { } } -export class NotificationNS { +export class AppBskyNotificationNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -2394,7 +2407,7 @@ export class NotificationNS { } } -export class RichtextNS { +export class AppBskyRichtextNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -2402,7 +2415,7 @@ export class RichtextNS { } } -export class UnspeccedNS { +export class AppBskyUnspeccedNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 258d297c69e..059c99be925 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -2664,6 +2664,9 @@ export const schemaDict = { inviteCode: { type: 'string', }, + verificationCode: { + type: 'string', + }, password: { type: 'string', }, @@ -3068,6 +3071,9 @@ export const schemaDict = { inviteCodeRequired: { type: 'boolean', }, + phoneVerificationRequired: { + type: 'boolean', + }, availableUserDomains: { type: 'array', items: { @@ -4140,6 +4146,29 @@ export const schemaDict = { }, }, }, + ComAtprotoTempRequestPhoneVerification: { + lexicon: 1, + id: 'com.atproto.temp.requestPhoneVerification', + defs: { + main: { + type: 'procedure', + description: + 'Request a verification code to be sent to the supplied phone number', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['phonenumber'], + properties: { + phonenumber: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoTempTransferAccount: { lexicon: 1, id: 'com.atproto.temp.transferAccount', @@ -7992,6 +8021,8 @@ export const ids = { ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels', ComAtprotoTempImportRepo: 'com.atproto.temp.importRepo', ComAtprotoTempPushBlob: 'com.atproto.temp.pushBlob', + ComAtprotoTempRequestPhoneVerification: + 'com.atproto.temp.requestPhoneVerification', ComAtprotoTempTransferAccount: 'com.atproto.temp.transferAccount', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', diff --git a/packages/api/src/client/types/com/atproto/server/createAccount.ts b/packages/api/src/client/types/com/atproto/server/createAccount.ts index 7631727ef19..2e3ec305f2b 100644 --- a/packages/api/src/client/types/com/atproto/server/createAccount.ts +++ b/packages/api/src/client/types/com/atproto/server/createAccount.ts @@ -14,6 +14,7 @@ export interface InputSchema { handle: string did?: string inviteCode?: string + verificationCode?: string password?: string recoveryKey?: string plcOp?: {} diff --git a/packages/api/src/client/types/com/atproto/server/describeServer.ts b/packages/api/src/client/types/com/atproto/server/describeServer.ts index ed3b870225d..fb6c9d5c662 100644 --- a/packages/api/src/client/types/com/atproto/server/describeServer.ts +++ b/packages/api/src/client/types/com/atproto/server/describeServer.ts @@ -13,6 +13,7 @@ export type InputSchema = undefined export interface OutputSchema { inviteCodeRequired?: boolean + phoneVerificationRequired?: boolean availableUserDomains: string[] links?: Links [k: string]: unknown diff --git a/packages/api/src/client/types/com/atproto/temp/requestPhoneVerification.ts b/packages/api/src/client/types/com/atproto/temp/requestPhoneVerification.ts new file mode 100644 index 00000000000..9144fc8b344 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/temp/requestPhoneVerification.ts @@ -0,0 +1,32 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + phonenumber: string + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers + qp?: QueryParams + encoding: 'application/json' +} + +export interface Response { + success: boolean + headers: Headers +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 386f77196e7..1dd381f180f 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -77,6 +77,7 @@ import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscrib import * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels' import * as ComAtprotoTempImportRepo from './types/com/atproto/temp/importRepo' import * as ComAtprotoTempPushBlob from './types/com/atproto/temp/pushBlob' +import * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification' import * as ComAtprotoTempTransferAccount from './types/com/atproto/temp/transferAccount' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' @@ -161,39 +162,39 @@ export class Server { export class ComNS { _server: Server - atproto: AtprotoNS + atproto: ComAtprotoNS constructor(server: Server) { this._server = server - this.atproto = new AtprotoNS(server) + this.atproto = new ComAtprotoNS(server) } } -export class AtprotoNS { +export class ComAtprotoNS { _server: Server - admin: AdminNS - identity: IdentityNS - label: LabelNS - moderation: ModerationNS - repo: RepoNS - server: ServerNS - sync: SyncNS - temp: TempNS + admin: ComAtprotoAdminNS + identity: ComAtprotoIdentityNS + label: ComAtprotoLabelNS + moderation: ComAtprotoModerationNS + repo: ComAtprotoRepoNS + server: ComAtprotoServerNS + sync: ComAtprotoSyncNS + temp: ComAtprotoTempNS constructor(server: Server) { this._server = server - this.admin = new AdminNS(server) - this.identity = new IdentityNS(server) - this.label = new LabelNS(server) - this.moderation = new ModerationNS(server) - this.repo = new RepoNS(server) - this.server = new ServerNS(server) - this.sync = new SyncNS(server) - this.temp = new TempNS(server) + this.admin = new ComAtprotoAdminNS(server) + this.identity = new ComAtprotoIdentityNS(server) + this.label = new ComAtprotoLabelNS(server) + this.moderation = new ComAtprotoModerationNS(server) + this.repo = new ComAtprotoRepoNS(server) + this.server = new ComAtprotoServerNS(server) + this.sync = new ComAtprotoSyncNS(server) + this.temp = new ComAtprotoTempNS(server) } } -export class AdminNS { +export class ComAtprotoAdminNS { _server: Server constructor(server: Server) { @@ -410,7 +411,7 @@ export class AdminNS { } } -export class IdentityNS { +export class ComAtprotoIdentityNS { _server: Server constructor(server: Server) { @@ -440,7 +441,7 @@ export class IdentityNS { } } -export class LabelNS { +export class ComAtprotoLabelNS { _server: Server constructor(server: Server) { @@ -470,7 +471,7 @@ export class LabelNS { } } -export class ModerationNS { +export class ComAtprotoModerationNS { _server: Server constructor(server: Server) { @@ -489,7 +490,7 @@ export class ModerationNS { } } -export class RepoNS { +export class ComAtprotoRepoNS { _server: Server constructor(server: Server) { @@ -585,7 +586,7 @@ export class RepoNS { } } -export class ServerNS { +export class ComAtprotoServerNS { _server: Server constructor(server: Server) { @@ -824,7 +825,7 @@ export class ServerNS { } } -export class SyncNS { +export class ComAtprotoSyncNS { _server: Server constructor(server: Server) { @@ -964,7 +965,7 @@ export class SyncNS { } } -export class TempNS { +export class ComAtprotoTempNS { _server: Server constructor(server: Server) { @@ -1004,6 +1005,17 @@ export class TempNS { return this._server.xrpc.method(nsid, cfg) } + requestPhoneVerification( + cfg: ConfigOf< + AV, + ComAtprotoTempRequestPhoneVerification.Handler>, + ComAtprotoTempRequestPhoneVerification.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.temp.requestPhoneVerification' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + transferAccount( cfg: ConfigOf< AV, @@ -1018,37 +1030,37 @@ export class TempNS { export class AppNS { _server: Server - bsky: BskyNS + bsky: AppBskyNS constructor(server: Server) { this._server = server - this.bsky = new BskyNS(server) + this.bsky = new AppBskyNS(server) } } -export class BskyNS { +export class AppBskyNS { _server: Server - actor: ActorNS - embed: EmbedNS - feed: FeedNS - graph: GraphNS - notification: NotificationNS - richtext: RichtextNS - unspecced: UnspeccedNS + actor: AppBskyActorNS + embed: AppBskyEmbedNS + feed: AppBskyFeedNS + graph: AppBskyGraphNS + notification: AppBskyNotificationNS + richtext: AppBskyRichtextNS + unspecced: AppBskyUnspeccedNS constructor(server: Server) { this._server = server - this.actor = new ActorNS(server) - this.embed = new EmbedNS(server) - this.feed = new FeedNS(server) - this.graph = new GraphNS(server) - this.notification = new NotificationNS(server) - this.richtext = new RichtextNS(server) - this.unspecced = new UnspeccedNS(server) + this.actor = new AppBskyActorNS(server) + this.embed = new AppBskyEmbedNS(server) + this.feed = new AppBskyFeedNS(server) + this.graph = new AppBskyGraphNS(server) + this.notification = new AppBskyNotificationNS(server) + this.richtext = new AppBskyRichtextNS(server) + this.unspecced = new AppBskyUnspeccedNS(server) } } -export class ActorNS { +export class AppBskyActorNS { _server: Server constructor(server: Server) { @@ -1133,7 +1145,7 @@ export class ActorNS { } } -export class EmbedNS { +export class AppBskyEmbedNS { _server: Server constructor(server: Server) { @@ -1141,7 +1153,7 @@ export class EmbedNS { } } -export class FeedNS { +export class AppBskyFeedNS { _server: Server constructor(server: Server) { @@ -1325,7 +1337,7 @@ export class FeedNS { } } -export class GraphNS { +export class AppBskyGraphNS { _server: Server constructor(server: Server) { @@ -1476,7 +1488,7 @@ export class GraphNS { } } -export class NotificationNS { +export class AppBskyNotificationNS { _server: Server constructor(server: Server) { @@ -1528,7 +1540,7 @@ export class NotificationNS { } } -export class RichtextNS { +export class AppBskyRichtextNS { _server: Server constructor(server: Server) { @@ -1536,7 +1548,7 @@ export class RichtextNS { } } -export class UnspeccedNS { +export class AppBskyUnspeccedNS { _server: Server constructor(server: Server) { diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 258d297c69e..059c99be925 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -2664,6 +2664,9 @@ export const schemaDict = { inviteCode: { type: 'string', }, + verificationCode: { + type: 'string', + }, password: { type: 'string', }, @@ -3068,6 +3071,9 @@ export const schemaDict = { inviteCodeRequired: { type: 'boolean', }, + phoneVerificationRequired: { + type: 'boolean', + }, availableUserDomains: { type: 'array', items: { @@ -4140,6 +4146,29 @@ export const schemaDict = { }, }, }, + ComAtprotoTempRequestPhoneVerification: { + lexicon: 1, + id: 'com.atproto.temp.requestPhoneVerification', + defs: { + main: { + type: 'procedure', + description: + 'Request a verification code to be sent to the supplied phone number', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['phonenumber'], + properties: { + phonenumber: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoTempTransferAccount: { lexicon: 1, id: 'com.atproto.temp.transferAccount', @@ -7992,6 +8021,8 @@ export const ids = { ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels', ComAtprotoTempImportRepo: 'com.atproto.temp.importRepo', ComAtprotoTempPushBlob: 'com.atproto.temp.pushBlob', + ComAtprotoTempRequestPhoneVerification: + 'com.atproto.temp.requestPhoneVerification', ComAtprotoTempTransferAccount: 'com.atproto.temp.transferAccount', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts index 109d34cf202..b7fa352007d 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts @@ -15,6 +15,7 @@ export interface InputSchema { handle: string did?: string inviteCode?: string + verificationCode?: string password?: string recoveryKey?: string plcOp?: {} diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts b/packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts index bc73d541a04..bb574dba9ff 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts @@ -14,6 +14,7 @@ export type InputSchema = undefined export interface OutputSchema { inviteCodeRequired?: boolean + phoneVerificationRequired?: boolean availableUserDomains: string[] links?: Links [k: string]: unknown diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts b/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts new file mode 100644 index 00000000000..05aecc1574b --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts @@ -0,0 +1,38 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + phonenumber: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/package.json b/packages/pds/package.json index 6cadab50dad..e575d094400 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -67,6 +67,7 @@ "pino": "^8.15.0", "pino-http": "^8.2.1", "sharp": "^0.32.6", + "twilio": "^4.20.1", "typed-emitter": "^2.1.0", "uint8arrays": "3.0.0", "zod": "^3.21.4" diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 6a20e523ff0..e7f8f1890c0 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -40,6 +40,30 @@ export default function (server: Server, ctx: AppContext) { const now = new Date().toISOString() const passwordScrypt = await scrypt.genSaltAndHash(password) + if (ctx.cfg.phoneVerification.required && ctx.twilio) { + if (!input.body.verificationPhone) { + throw new InvalidRequestError( + 'Phone number verification is required on this server and none was provided.', + 'InvalidPhoneVerification', + ) + } else if (!input.body.verificationCode) { + throw new InvalidRequestError( + 'Phone number verification is required on this server and none was provided.', + 'InvalidPhoneVerification', + ) + } + const verified = await ctx.twilio.verifyCode( + input.body.verificationPhone, + input.body.verificationCode, + ) + if (!verified) { + throw new InvalidRequestError( + 'Could not verify phone number. Please try again.', + 'InvalidPhoneVerification', + ) + } + } + const result = await ctx.db.transaction(async (dbTxn) => { const actorTxn = ctx.services.account(dbTxn) const repoTxn = ctx.services.repo(dbTxn) diff --git a/packages/pds/src/api/com/atproto/server/describeServer.ts b/packages/pds/src/api/com/atproto/server/describeServer.ts index 0ad3b2d66eb..0314cf0d6d8 100644 --- a/packages/pds/src/api/com/atproto/server/describeServer.ts +++ b/packages/pds/src/api/com/atproto/server/describeServer.ts @@ -5,6 +5,7 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.server.describeServer(() => { const availableUserDomains = ctx.cfg.identity.serviceHandleDomains const inviteCodeRequired = ctx.cfg.invites.required + const phoneVerificationRequired = ctx.cfg.phoneVerification.required const privacyPolicy = ctx.cfg.service.privacyPolicyUrl const termsOfService = ctx.cfg.service.termsOfServiceUrl @@ -13,6 +14,7 @@ export default function (server: Server, ctx: AppContext) { body: { availableUserDomains, inviteCodeRequired, + phoneVerificationRequired, links: { privacyPolicy, termsOfService }, }, } diff --git a/packages/pds/src/api/com/atproto/temp/index.ts b/packages/pds/src/api/com/atproto/temp/index.ts new file mode 100644 index 00000000000..db34f17bf29 --- /dev/null +++ b/packages/pds/src/api/com/atproto/temp/index.ts @@ -0,0 +1,7 @@ +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' +import requestPhoneVerification from './requestPhoneVerification' + +export default function (server: Server, ctx: AppContext) { + requestPhoneVerification(server, ctx) +} diff --git a/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts b/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts new file mode 100644 index 00000000000..9170b4c6cea --- /dev/null +++ b/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts @@ -0,0 +1,25 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { HOUR, MINUTE } from '@atproto/common' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.temp.requestPhoneVerification({ + rateLimit: [ + { + durationMs: 5 * MINUTE, + points: 50, + }, + { + durationMs: HOUR, + points: 100, + }, + ], + handler: async ({ input }) => { + if (!ctx.twilio) { + throw new InvalidRequestError('phone verification not enabled') + } + await ctx.twilio.sendCode(input.body.phoneNumber) + }, + }) +} diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index d8ec5a031a2..5be9201f26e 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -126,6 +126,19 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { epoch: env.inviteEpoch ?? 0, } + let phoneVerificationCfg: ServerConfig['phoneVerification'] = { + required: false, + } + if (env.phoneVerificationRequired) { + assert(env.twilioAccountSid) + assert(env.twilioServiceSid) + phoneVerificationCfg = { + required: true, + twilioAccountSid: env.twilioAccountSid, + twilioServiceSid: env.twilioServiceSid, + } + } + let emailCfg: ServerConfig['email'] if (!env.emailFromAddress && !env.emailSmtpUrl) { emailCfg = null @@ -204,6 +217,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { blobstore: blobstoreCfg, identity: identityCfg, invites: invitesCfg, + phoneVerification: phoneVerificationCfg, email: emailCfg, moderationEmail: moderationEmailCfg, subscription: subscriptionCfg, @@ -221,6 +235,7 @@ export type ServerConfig = { blobstore: S3BlobstoreConfig | DiskBlobstoreConfig identity: IdentityConfig invites: InvitesConfig + phoneVerification: PhoneVerificationConfig email: EmailConfig | null moderationEmail: EmailConfig | null subscription: SubscriptionConfig @@ -300,6 +315,16 @@ export type InvitesConfig = required: false } +export type PhoneVerificationConfig = + | { + required: true + twilioAccountSid: string + twilioServiceSid: string + } + | { + required: false + } + export type EmailConfig = { smtpUrl: string fromAddress: string diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 0645e8b47f3..1bcfa11a3e7 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -49,6 +49,12 @@ export const readEnv = (): ServerEnvironment => { inviteInterval: envInt('PDS_INVITE_INTERVAL'), inviteEpoch: envInt('PDS_INVITE_EPOCH'), + // phone verification + phoneVerificationRequired: envBool('PDS_PHONE_VERIFICATION_REQUIRED'), + twilioAccountSid: envStr('PDS_TWILIO_ACCOUNT_SID'), + twilioAuthToken: envStr('PDS_TWILIO_AUTH_TOKEN'), + twilioServiceSid: envStr('TWILIO_SERVICE_SID'), + // email emailSmtpUrl: envStr('PDS_EMAIL_SMTP_URL'), emailFromAddress: envStr('PDS_EMAIL_FROM_ADDRESS'), @@ -156,6 +162,12 @@ export type ServerEnvironment = { inviteInterval?: number inviteEpoch?: number + // phone verification + phoneVerificationRequired?: boolean + twilioAccountSid?: string + twilioAuthToken?: string + twilioServiceSid?: string + // email emailSmtpUrl?: string emailFromAddress?: string diff --git a/packages/pds/src/config/secrets.ts b/packages/pds/src/config/secrets.ts index db9e188630b..cddc6b6b7e8 100644 --- a/packages/pds/src/config/secrets.ts +++ b/packages/pds/src/config/secrets.ts @@ -65,6 +65,7 @@ export const envToSecrets = (env: ServerEnvironment): ServerSecrets => { moderatorPassword: env.moderatorPassword ?? env.adminPassword, triagePassword: env.triagePassword ?? env.moderatorPassword ?? env.adminPassword, + twilioAuthToken: env.twilioAuthToken, repoSigningKey, plcRotationKey, } @@ -77,6 +78,7 @@ export type ServerSecrets = { adminPassword: string moderatorPassword: string triagePassword: string + twilioAuthToken?: string repoSigningKey: SigningKeyKms | SigningKeyMemory plcRotationKey: SigningKeyKms | SigningKeyMemory } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index bc2b1bd3a4b..15ee684e28b 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -21,6 +21,8 @@ import { DiskBlobStore } from './storage' import { getRedisClient } from './redis' import { RuntimeFlags } from './runtime-flags' import { PdsAgents } from './pds-agents' +import { TwilioClient } from './twilio' +import assert from 'assert' export type AppContextOptions = { db: Database @@ -43,6 +45,7 @@ export type AppContextOptions = { pdsAgents: PdsAgents repoSigningKey: crypto.Keypair plcRotationKey: crypto.Keypair + twilio?: TwilioClient cfg: ServerConfig } @@ -67,6 +70,7 @@ export class AppContext { public pdsAgents: PdsAgents public repoSigningKey: crypto.Keypair public plcRotationKey: crypto.Keypair + public twilio?: TwilioClient public cfg: ServerConfig constructor(opts: AppContextOptions) { @@ -90,6 +94,7 @@ export class AppContext { this.pdsAgents = opts.pdsAgents this.repoSigningKey = opts.repoSigningKey this.plcRotationKey = opts.plcRotationKey + this.twilio = opts.twilio this.cfg = opts.cfg } @@ -207,6 +212,16 @@ export class AppContext { crawlers, }) + let twilio: TwilioClient | undefined = undefined + if (cfg.phoneVerification.required) { + assert(secrets.twilioAuthToken) + twilio = new TwilioClient({ + accountSid: cfg.phoneVerification.twilioAccountSid, + serviceSid: cfg.phoneVerification.twilioServiceSid, + authToken: secrets.twilioAuthToken, + }) + } + const pdsAgents = new PdsAgents() return new AppContext({ @@ -230,6 +245,7 @@ export class AppContext { repoSigningKey, plcRotationKey, pdsAgents, + twilio, cfg, ...(overrides ?? {}), }) diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 386f77196e7..1dd381f180f 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -77,6 +77,7 @@ import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscrib import * as ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels' import * as ComAtprotoTempImportRepo from './types/com/atproto/temp/importRepo' import * as ComAtprotoTempPushBlob from './types/com/atproto/temp/pushBlob' +import * as ComAtprotoTempRequestPhoneVerification from './types/com/atproto/temp/requestPhoneVerification' import * as ComAtprotoTempTransferAccount from './types/com/atproto/temp/transferAccount' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' @@ -161,39 +162,39 @@ export class Server { export class ComNS { _server: Server - atproto: AtprotoNS + atproto: ComAtprotoNS constructor(server: Server) { this._server = server - this.atproto = new AtprotoNS(server) + this.atproto = new ComAtprotoNS(server) } } -export class AtprotoNS { +export class ComAtprotoNS { _server: Server - admin: AdminNS - identity: IdentityNS - label: LabelNS - moderation: ModerationNS - repo: RepoNS - server: ServerNS - sync: SyncNS - temp: TempNS + admin: ComAtprotoAdminNS + identity: ComAtprotoIdentityNS + label: ComAtprotoLabelNS + moderation: ComAtprotoModerationNS + repo: ComAtprotoRepoNS + server: ComAtprotoServerNS + sync: ComAtprotoSyncNS + temp: ComAtprotoTempNS constructor(server: Server) { this._server = server - this.admin = new AdminNS(server) - this.identity = new IdentityNS(server) - this.label = new LabelNS(server) - this.moderation = new ModerationNS(server) - this.repo = new RepoNS(server) - this.server = new ServerNS(server) - this.sync = new SyncNS(server) - this.temp = new TempNS(server) + this.admin = new ComAtprotoAdminNS(server) + this.identity = new ComAtprotoIdentityNS(server) + this.label = new ComAtprotoLabelNS(server) + this.moderation = new ComAtprotoModerationNS(server) + this.repo = new ComAtprotoRepoNS(server) + this.server = new ComAtprotoServerNS(server) + this.sync = new ComAtprotoSyncNS(server) + this.temp = new ComAtprotoTempNS(server) } } -export class AdminNS { +export class ComAtprotoAdminNS { _server: Server constructor(server: Server) { @@ -410,7 +411,7 @@ export class AdminNS { } } -export class IdentityNS { +export class ComAtprotoIdentityNS { _server: Server constructor(server: Server) { @@ -440,7 +441,7 @@ export class IdentityNS { } } -export class LabelNS { +export class ComAtprotoLabelNS { _server: Server constructor(server: Server) { @@ -470,7 +471,7 @@ export class LabelNS { } } -export class ModerationNS { +export class ComAtprotoModerationNS { _server: Server constructor(server: Server) { @@ -489,7 +490,7 @@ export class ModerationNS { } } -export class RepoNS { +export class ComAtprotoRepoNS { _server: Server constructor(server: Server) { @@ -585,7 +586,7 @@ export class RepoNS { } } -export class ServerNS { +export class ComAtprotoServerNS { _server: Server constructor(server: Server) { @@ -824,7 +825,7 @@ export class ServerNS { } } -export class SyncNS { +export class ComAtprotoSyncNS { _server: Server constructor(server: Server) { @@ -964,7 +965,7 @@ export class SyncNS { } } -export class TempNS { +export class ComAtprotoTempNS { _server: Server constructor(server: Server) { @@ -1004,6 +1005,17 @@ export class TempNS { return this._server.xrpc.method(nsid, cfg) } + requestPhoneVerification( + cfg: ConfigOf< + AV, + ComAtprotoTempRequestPhoneVerification.Handler>, + ComAtprotoTempRequestPhoneVerification.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.temp.requestPhoneVerification' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + transferAccount( cfg: ConfigOf< AV, @@ -1018,37 +1030,37 @@ export class TempNS { export class AppNS { _server: Server - bsky: BskyNS + bsky: AppBskyNS constructor(server: Server) { this._server = server - this.bsky = new BskyNS(server) + this.bsky = new AppBskyNS(server) } } -export class BskyNS { +export class AppBskyNS { _server: Server - actor: ActorNS - embed: EmbedNS - feed: FeedNS - graph: GraphNS - notification: NotificationNS - richtext: RichtextNS - unspecced: UnspeccedNS + actor: AppBskyActorNS + embed: AppBskyEmbedNS + feed: AppBskyFeedNS + graph: AppBskyGraphNS + notification: AppBskyNotificationNS + richtext: AppBskyRichtextNS + unspecced: AppBskyUnspeccedNS constructor(server: Server) { this._server = server - this.actor = new ActorNS(server) - this.embed = new EmbedNS(server) - this.feed = new FeedNS(server) - this.graph = new GraphNS(server) - this.notification = new NotificationNS(server) - this.richtext = new RichtextNS(server) - this.unspecced = new UnspeccedNS(server) + this.actor = new AppBskyActorNS(server) + this.embed = new AppBskyEmbedNS(server) + this.feed = new AppBskyFeedNS(server) + this.graph = new AppBskyGraphNS(server) + this.notification = new AppBskyNotificationNS(server) + this.richtext = new AppBskyRichtextNS(server) + this.unspecced = new AppBskyUnspeccedNS(server) } } -export class ActorNS { +export class AppBskyActorNS { _server: Server constructor(server: Server) { @@ -1133,7 +1145,7 @@ export class ActorNS { } } -export class EmbedNS { +export class AppBskyEmbedNS { _server: Server constructor(server: Server) { @@ -1141,7 +1153,7 @@ export class EmbedNS { } } -export class FeedNS { +export class AppBskyFeedNS { _server: Server constructor(server: Server) { @@ -1325,7 +1337,7 @@ export class FeedNS { } } -export class GraphNS { +export class AppBskyGraphNS { _server: Server constructor(server: Server) { @@ -1476,7 +1488,7 @@ export class GraphNS { } } -export class NotificationNS { +export class AppBskyNotificationNS { _server: Server constructor(server: Server) { @@ -1528,7 +1540,7 @@ export class NotificationNS { } } -export class RichtextNS { +export class AppBskyRichtextNS { _server: Server constructor(server: Server) { @@ -1536,7 +1548,7 @@ export class RichtextNS { } } -export class UnspeccedNS { +export class AppBskyUnspeccedNS { _server: Server constructor(server: Server) { diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 258d297c69e..1147b9fcb98 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -2664,6 +2664,12 @@ export const schemaDict = { inviteCode: { type: 'string', }, + verificationCode: { + type: 'string', + }, + verificationPhone: { + type: 'string', + }, password: { type: 'string', }, @@ -3068,6 +3074,9 @@ export const schemaDict = { inviteCodeRequired: { type: 'boolean', }, + phoneVerificationRequired: { + type: 'boolean', + }, availableUserDomains: { type: 'array', items: { @@ -4140,6 +4149,29 @@ export const schemaDict = { }, }, }, + ComAtprotoTempRequestPhoneVerification: { + lexicon: 1, + id: 'com.atproto.temp.requestPhoneVerification', + defs: { + main: { + type: 'procedure', + description: + 'Request a verification code to be sent to the supplied phone number', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['phoneNumber'], + properties: { + phoneNumber: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, ComAtprotoTempTransferAccount: { lexicon: 1, id: 'com.atproto.temp.transferAccount', @@ -7992,6 +8024,8 @@ export const ids = { ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels', ComAtprotoTempImportRepo: 'com.atproto.temp.importRepo', ComAtprotoTempPushBlob: 'com.atproto.temp.pushBlob', + ComAtprotoTempRequestPhoneVerification: + 'com.atproto.temp.requestPhoneVerification', ComAtprotoTempTransferAccount: 'com.atproto.temp.transferAccount', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', diff --git a/packages/pds/src/lexicon/types/com/atproto/server/createAccount.ts b/packages/pds/src/lexicon/types/com/atproto/server/createAccount.ts index 109d34cf202..bbf2c009bf5 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/createAccount.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/createAccount.ts @@ -15,6 +15,8 @@ export interface InputSchema { handle: string did?: string inviteCode?: string + verificationCode?: string + verificationPhone?: string password?: string recoveryKey?: string plcOp?: {} diff --git a/packages/pds/src/lexicon/types/com/atproto/server/describeServer.ts b/packages/pds/src/lexicon/types/com/atproto/server/describeServer.ts index bc73d541a04..bb574dba9ff 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/describeServer.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/describeServer.ts @@ -14,6 +14,7 @@ export type InputSchema = undefined export interface OutputSchema { inviteCodeRequired?: boolean + phoneVerificationRequired?: boolean availableUserDomains: string[] links?: Links [k: string]: unknown diff --git a/packages/pds/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts b/packages/pds/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts new file mode 100644 index 00000000000..5a295f701eb --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts @@ -0,0 +1,38 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + phoneNumber: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts new file mode 100644 index 00000000000..6237740ae44 --- /dev/null +++ b/packages/pds/src/twilio.ts @@ -0,0 +1,30 @@ +import twilio from 'twilio' + +type Opts = { + accountSid: string + serviceSid: string + authToken: string +} + +export class TwilioClient { + client: twilio.Twilio + serviceSid: string + + constructor(opts: Opts) { + this.client = twilio(opts.accountSid, opts.authToken) + this.serviceSid = opts.serviceSid + } + + async sendCode(phoneNumber: string) { + await this.client.verify.v2 + .services(this.serviceSid) + .verifications.create({ to: phoneNumber, channel: 'sms' }) + } + + async verifyCode(phoneNumber: string, code: string) { + const res = await this.client.verify.v2 + .services(this.serviceSid) + .verificationChecks.create({ to: phoneNumber, code }) + return res.status === 'approved' + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32c008c2091..dbd04f5c644 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -573,6 +573,9 @@ importers: sharp: specifier: ^0.32.6 version: 0.32.6 + twilio: + specifier: ^4.20.1 + version: 4.20.1 typed-emitter: specifier: ^2.1.0 version: 2.1.0 @@ -5656,7 +5659,6 @@ packages: debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true /agentkeepalive@4.5.0: resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} @@ -5849,6 +5851,16 @@ packages: transitivePeerDependencies: - debug + /axios@1.6.5: + resolution: {integrity: sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==} + dependencies: + follow-redirects: 1.15.5 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /b4a@1.6.4: resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} dev: false @@ -6091,6 +6103,10 @@ packages: node-int64: 0.4.0 dev: true + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true @@ -6485,6 +6501,10 @@ packages: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} dev: true + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: false + /dd-trace@3.13.2: resolution: {integrity: sha512-POO9nEcAufe5pgp2xV1X3PfWip6wh+6TpEcRSlSgZJCIIMvWVCkcIVL/J2a6KAZq6V3Yjbkl8Ktfe+MOzQf5kw==} engines: {node: '>=14'} @@ -6699,6 +6719,12 @@ packages: engines: {node: '>=10'} dev: true + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -7533,6 +7559,16 @@ packages: debug: optional: true + /follow-redirects@1.15.5: + resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -7919,7 +7955,6 @@ packages: debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true /human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} @@ -8804,6 +8839,37 @@ packages: graceful-fs: 4.2.11 dev: true + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.5.4 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /key-encoder@2.0.3: resolution: {integrity: sha512-fgBtpAGIr/Fy5/+ZLQZIPPhsZEcbSlYu/Wu96tNDFNSjSACw5lEIOFeaVdQ/iwrb8oxjlWi6wmWdH76hV6GZjg==} dependencies: @@ -8894,10 +8960,34 @@ packages: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} dev: false + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + /lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} dev: false + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + /lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} dev: false @@ -8906,6 +8996,10 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /lodash.pick@4.4.0: resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} dev: false @@ -9853,6 +9947,10 @@ packages: dependencies: side-channel: 1.0.4 + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -10044,6 +10142,10 @@ packages: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} dev: true + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -10155,6 +10257,10 @@ packages: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} requiresBuild: true + /scmp@2.1.0: + resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==} + dev: false + /secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} dev: true @@ -10812,6 +10918,23 @@ packages: safe-buffer: 5.2.1 dev: false + /twilio@4.20.1: + resolution: {integrity: sha512-raoK6LKBtpaqPpaMamgQkNHnAgReW0rW3PS1Eow177f9yZV7lLw3UyqGctGcREMeWFLyH2kEDpgZG7vT1ezL9Q==} + engines: {node: '>=14.0'} + dependencies: + axios: 1.6.5 + dayjs: 1.11.10 + https-proxy-agent: 5.0.1 + jsonwebtoken: 9.0.2 + qs: 6.11.0 + scmp: 2.1.0 + url-parse: 1.5.10 + xmlbuilder: 13.0.2 + transitivePeerDependencies: + - debug + - supports-color + dev: false + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -11005,6 +11128,13 @@ packages: dependencies: punycode: 2.3.0 + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -11167,6 +11297,11 @@ packages: utf-8-validate: optional: true + /xmlbuilder@13.0.2: + resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==} + engines: {node: '>=6.0'} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} From 3cd2768fb660083026be50fd4c3b9d53e7267a29 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 16 Jan 2024 18:12:52 -0600 Subject: [PATCH 02/23] track verified numbers in db --- .../api/com/atproto/server/createAccount.ts | 13 ++++++++++++ .../atproto/temp/requestPhoneVerification.ts | 20 +++++++++++++++++-- packages/pds/src/config/config.ts | 2 ++ packages/pds/src/config/env.ts | 2 ++ packages/pds/src/db/database-schema.ts | 4 +++- .../20240117T001106576Z-phone-verification.ts | 18 +++++++++++++++++ packages/pds/src/db/migrations/index.ts | 1 + .../pds/src/db/tables/phone-verification.ts | 8 ++++++++ 8 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 packages/pds/src/db/migrations/20240117T001106576Z-phone-verification.ts create mode 100644 packages/pds/src/db/tables/phone-verification.ts diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index e7f8f1890c0..034e31412ac 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -121,6 +121,19 @@ export default function (server: Server, ctx: AppContext) { .execute() } + if ( + ctx.cfg.phoneVerification.required && + input.body.verificationPhone + ) { + await dbTxn.db + .insertInto('phone_verification') + .values({ + did, + phoneNumber: input.body.verificationPhone, + }) + .execute() + } + const { access, refresh } = await ctx.services .auth(dbTxn) .createSession({ diff --git a/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts b/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts index 9170b4c6cea..411d1073e6d 100644 --- a/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts +++ b/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts @@ -2,6 +2,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { InvalidRequestError } from '@atproto/xrpc-server' import { HOUR, MINUTE } from '@atproto/common' +import { countAll } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.com.atproto.temp.requestPhoneVerification({ @@ -16,10 +17,25 @@ export default function (server: Server, ctx: AppContext) { }, ], handler: async ({ input }) => { - if (!ctx.twilio) { + if (!ctx.twilio || !ctx.cfg.phoneVerification.required) { throw new InvalidRequestError('phone verification not enabled') } - await ctx.twilio.sendCode(input.body.phoneNumber) + const accountsPerPhoneNumber = + ctx.cfg.phoneVerification.accountsPerPhoneNumber + const { phoneNumber } = input.body + + const res = await ctx.db.db + .selectFrom('phone_verification') + .select(countAll.as('count')) + .where('phoneNumber', '=', phoneNumber) + .executeTakeFirst() + if (res && res.count >= accountsPerPhoneNumber) { + throw new InvalidRequestError( + `There are too many accounts currently using this phone number. Max: ${accountsPerPhoneNumber}`, + ) + } + + await ctx.twilio.sendCode(phoneNumber) }, }) } diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index 5be9201f26e..6652a2a29eb 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -136,6 +136,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { required: true, twilioAccountSid: env.twilioAccountSid, twilioServiceSid: env.twilioServiceSid, + accountsPerPhoneNumber: env.accountsPerPhoneNumber ?? 3, } } @@ -320,6 +321,7 @@ export type PhoneVerificationConfig = required: true twilioAccountSid: string twilioServiceSid: string + accountsPerPhoneNumber: number } | { required: false diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 1bcfa11a3e7..1dd16a2c2a0 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -51,6 +51,7 @@ export const readEnv = (): ServerEnvironment => { // phone verification phoneVerificationRequired: envBool('PDS_PHONE_VERIFICATION_REQUIRED'), + accountsPerPhoneNumber: envInt('PDS_ACCOUNTS_PER_PHONE_NUMBER'), twilioAccountSid: envStr('PDS_TWILIO_ACCOUNT_SID'), twilioAuthToken: envStr('PDS_TWILIO_AUTH_TOKEN'), twilioServiceSid: envStr('TWILIO_SERVICE_SID'), @@ -164,6 +165,7 @@ export type ServerEnvironment = { // phone verification phoneVerificationRequired?: boolean + accountsPerPhoneNumber?: number twilioAccountSid?: string twilioAuthToken?: string twilioServiceSid?: string diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index 171c24d455d..57599aa12d9 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -18,6 +18,7 @@ import * as moderation from './tables/moderation' import * as repoSeq from './tables/repo-seq' import * as appMigration from './tables/app-migration' import * as runtimeFlag from './tables/runtime-flag' +import * as phoneVerification from './tables/phone-verification' export type DatabaseSchemaType = appMigration.PartialDB & runtimeFlag.PartialDB & @@ -37,7 +38,8 @@ export type DatabaseSchemaType = appMigration.PartialDB & repoBlob.PartialDB & emailToken.PartialDB & moderation.PartialDB & - repoSeq.PartialDB + repoSeq.PartialDB & + phoneVerification.PartialDB export type DatabaseSchema = Kysely diff --git a/packages/pds/src/db/migrations/20240117T001106576Z-phone-verification.ts b/packages/pds/src/db/migrations/20240117T001106576Z-phone-verification.ts new file mode 100644 index 00000000000..3f28552a253 --- /dev/null +++ b/packages/pds/src/db/migrations/20240117T001106576Z-phone-verification.ts @@ -0,0 +1,18 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('phone_verification') + .addColumn('did', 'varchar', (col) => col.primaryKey()) + .addColumn('phoneNumber', 'varchar', (col) => col.notNull()) + .execute() + await db.schema + .createIndex('phone_verification_number_idx') + .on('phone_verification') + .column('phoneNumber') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('phone_verification').execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index 46160dba8fa..8b1e10f89f7 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -8,3 +8,4 @@ export * as _20230926T195532354Z from './20230926T195532354Z-email-tokens' export * as _20230929T213219699Z from './20230929T213219699Z-takedown-id-as-int' export * as _20231011T155513453Z from './20231011T155513453Z-takedown-ref' export * as _20231031T222409283Z from './20231031T222409283Z-user-account-pds' +export * as _20240117T001106576Z from './20240117T001106576Z-phone-verification' diff --git a/packages/pds/src/db/tables/phone-verification.ts b/packages/pds/src/db/tables/phone-verification.ts new file mode 100644 index 00000000000..049ee204bf6 --- /dev/null +++ b/packages/pds/src/db/tables/phone-verification.ts @@ -0,0 +1,8 @@ +export interface PhoneVerification { + did: string + phoneNumber: string +} + +export const tableName = 'phone_verification' + +export type PartialDB = { [tableName]: PhoneVerification } From d0771c2cd2cad452ca26050b1a3ca06ead749d69 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 16 Jan 2024 19:25:25 -0600 Subject: [PATCH 03/23] codegen + tests --- packages/api/src/client/lexicons.ts | 7 +- .../types/com/atproto/server/createAccount.ts | 1 + .../atproto/temp/requestPhoneVerification.ts | 2 +- packages/bsky/src/lexicon/lexicons.ts | 7 +- .../types/com/atproto/server/createAccount.ts | 1 + .../atproto/temp/requestPhoneVerification.ts | 2 +- packages/pds/src/api/com/atproto/index.ts | 2 + packages/pds/src/twilio.ts | 25 +-- packages/pds/tests/phone-verification.test.ts | 156 ++++++++++++++++++ 9 files changed, 187 insertions(+), 16 deletions(-) create mode 100644 packages/pds/tests/phone-verification.test.ts diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 059c99be925..1147b9fcb98 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -2667,6 +2667,9 @@ export const schemaDict = { verificationCode: { type: 'string', }, + verificationPhone: { + type: 'string', + }, password: { type: 'string', }, @@ -4158,9 +4161,9 @@ export const schemaDict = { encoding: 'application/json', schema: { type: 'object', - required: ['phonenumber'], + required: ['phoneNumber'], properties: { - phonenumber: { + phoneNumber: { type: 'string', }, }, diff --git a/packages/api/src/client/types/com/atproto/server/createAccount.ts b/packages/api/src/client/types/com/atproto/server/createAccount.ts index 2e3ec305f2b..b62adf97cb1 100644 --- a/packages/api/src/client/types/com/atproto/server/createAccount.ts +++ b/packages/api/src/client/types/com/atproto/server/createAccount.ts @@ -15,6 +15,7 @@ export interface InputSchema { did?: string inviteCode?: string verificationCode?: string + verificationPhone?: string password?: string recoveryKey?: string plcOp?: {} diff --git a/packages/api/src/client/types/com/atproto/temp/requestPhoneVerification.ts b/packages/api/src/client/types/com/atproto/temp/requestPhoneVerification.ts index 9144fc8b344..06a8972599d 100644 --- a/packages/api/src/client/types/com/atproto/temp/requestPhoneVerification.ts +++ b/packages/api/src/client/types/com/atproto/temp/requestPhoneVerification.ts @@ -10,7 +10,7 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { - phonenumber: string + phoneNumber: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 059c99be925..1147b9fcb98 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -2667,6 +2667,9 @@ export const schemaDict = { verificationCode: { type: 'string', }, + verificationPhone: { + type: 'string', + }, password: { type: 'string', }, @@ -4158,9 +4161,9 @@ export const schemaDict = { encoding: 'application/json', schema: { type: 'object', - required: ['phonenumber'], + required: ['phoneNumber'], properties: { - phonenumber: { + phoneNumber: { type: 'string', }, }, diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts index b7fa352007d..bbf2c009bf5 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts @@ -16,6 +16,7 @@ export interface InputSchema { did?: string inviteCode?: string verificationCode?: string + verificationPhone?: string password?: string recoveryKey?: string plcOp?: {} diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts b/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts index 05aecc1574b..5a295f701eb 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/temp/requestPhoneVerification.ts @@ -11,7 +11,7 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - phonenumber: string + phoneNumber: string [k: string]: unknown } diff --git a/packages/pds/src/api/com/atproto/index.ts b/packages/pds/src/api/com/atproto/index.ts index a5c26c80495..c7d4f217f88 100644 --- a/packages/pds/src/api/com/atproto/index.ts +++ b/packages/pds/src/api/com/atproto/index.ts @@ -6,6 +6,7 @@ import moderation from './moderation' import repo from './repo' import serverMethods from './server' import sync from './sync' +import temp from './temp' export default function (server: Server, ctx: AppContext) { admin(server, ctx) @@ -14,4 +15,5 @@ export default function (server: Server, ctx: AppContext) { repo(server, ctx) serverMethods(server, ctx) sync(server, ctx) + temp(server, ctx) } diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index 6237740ae44..6a534e77cd2 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -6,25 +6,30 @@ type Opts = { authToken: string } +type VerifyClient = ReturnType + export class TwilioClient { - client: twilio.Twilio - serviceSid: string + verifyClient: VerifyClient constructor(opts: Opts) { - this.client = twilio(opts.accountSid, opts.authToken) - this.serviceSid = opts.serviceSid + this.verifyClient = twilio( + opts.accountSid, + opts.authToken, + ).verify.v2.services(opts.serviceSid) } async sendCode(phoneNumber: string) { - await this.client.verify.v2 - .services(this.serviceSid) - .verifications.create({ to: phoneNumber, channel: 'sms' }) + await this.verifyClient.verifications.create({ + to: phoneNumber, + channel: 'sms', + }) } async verifyCode(phoneNumber: string, code: string) { - const res = await this.client.verify.v2 - .services(this.serviceSid) - .verificationChecks.create({ to: phoneNumber, code }) + const res = await this.verifyClient.verificationChecks.create({ + to: phoneNumber, + code, + }) return res.status === 'approved' } } diff --git a/packages/pds/tests/phone-verification.test.ts b/packages/pds/tests/phone-verification.test.ts new file mode 100644 index 00000000000..a4ef1af5d26 --- /dev/null +++ b/packages/pds/tests/phone-verification.test.ts @@ -0,0 +1,156 @@ +import assert from 'assert' +import AtpAgent from '@atproto/api' +import * as crypto from '@atproto/crypto' +import { TestNetworkNoAppView } from '@atproto/dev-env' +import { AppContext } from '../src' + +describe('phone verification', () => { + let network: TestNetworkNoAppView + let ctx: AppContext + let agent: AtpAgent + + let verificationCodes: Record + let sentCodes: { number: string; code: string }[] + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: 'phone_verification', + pds: { + phoneVerificationRequired: true, + twilioAccountSid: 'ACXXXXXXX', + twilioAuthToken: 'AUTH', + twilioServiceSid: 'VAXXXXXXXX', + }, + }) + ctx = network.pds.ctx + assert(ctx.twilio) + verificationCodes = {} + sentCodes = [] + ctx.twilio.sendCode = async (number: string) => { + if (!verificationCodes[number]) { + const code = crypto.randomStr(4, 'base10').slice(0, 6) + verificationCodes[number] = code + } + const code = verificationCodes[number] + sentCodes.push({ code, number }) + } + ctx.twilio.verifyCode = async (number: string, code: string) => { + if (verificationCodes[number] === code) { + delete verificationCodes[number] + return true + } + return false + } + + agent = network.pds.getClient() + }) + + afterAll(async () => { + await network.close() + }) + + const requestCode = async (phoneNumber: string) => { + await agent.api.com.atproto.temp.requestPhoneVerification({ + phoneNumber, + }) + const sent = sentCodes.at(-1) + assert(sent) + assert(sent.number === phoneNumber) + return sent.code + } + + const createAccountWithCode = async (phoneNumber?: string, code?: string) => { + const name = crypto.randomStr(5, 'base32') + const res = await agent.api.com.atproto.server.createAccount({ + email: `${name}@test.com`, + handle: `${name}.test`, + password: name, + verificationPhone: phoneNumber, + verificationCode: code, + }) + return { + ...res.data, + password: name, + } + } + + it('describes the fact that invites are required', async () => { + const res = await agent.api.com.atproto.server.describeServer({}) + expect(res.data.phoneVerificationRequired).toBe(true) + }) + + const aliceNumber = '+11234567890' + let aliceCode: string + let aliceDid: string + + it('requests a phone verification code', async () => { + aliceCode = await requestCode(aliceNumber) + }) + + it('resends a phone verification code', async () => { + const resent = await requestCode(aliceNumber) + expect(resent).toEqual(aliceCode) + }) + + it('allows signup using a valid phone verification code', async () => { + const res = await createAccountWithCode(aliceNumber, aliceCode) + aliceDid = res.did + }) + + it('stores the associated phone number of an account', async () => { + const res = await ctx.db.db + .selectFrom('phone_verification') + .selectAll() + .where('did', '=', aliceDid) + .execute() + expect(res.length).toBe(1) + expect(res[0].phoneNumber).toBe(aliceNumber) + }) + + it('does not allow signup with an already used code', async () => { + const attempt = createAccountWithCode(aliceNumber, aliceCode) + await expect(attempt).rejects.toThrow( + 'Could not verify phone number. Please try again.', + ) + }) + + it('does not allow signup with out a code', async () => { + const attempt = createAccountWithCode() + await expect(attempt).rejects.toThrow( + 'Phone number verification is required on this server and none was provided.', + ) + }) + + it('does not allow signup when missing a code or a phone number', async () => { + const bobNumber = '+1098765432' + const bobCode = await requestCode(bobNumber) + const attempt = createAccountWithCode(undefined, bobCode) + await expect(attempt).rejects.toThrow( + 'Phone number verification is required on this server and none was provided.', + ) + const attempt2 = createAccountWithCode(bobNumber, undefined) + await expect(attempt2).rejects.toThrow( + 'Phone number verification is required on this server and none was provided.', + ) + }) + + it('does not allow signup with a valid code and a mismatched phone number', async () => { + const carolCode = await requestCode('+11111111111') + const attempt = createAccountWithCode('+12222222222', carolCode) + await expect(attempt).rejects.toThrow( + 'Could not verify phone number. Please try again.', + ) + }) + + it('does not allow more than the configured number of signups from the same code', async () => { + const danNumber = '+3333333333' + for (let i = 0; i < 3; i++) { + const danCode = await requestCode(danNumber) + await createAccountWithCode(danNumber, danCode) + } + const attempt = requestCode(danNumber) + await expect(attempt).rejects.toThrow( + `There are too many accounts currently using this phone number. Max: 3`, + ) + }) +}) From 5bc46d8fc8387e4ea5c86d5e0b5e061e49aca1d8 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 16 Jan 2024 19:41:22 -0600 Subject: [PATCH 04/23] add phone verification to dev-env --- packages/dev-env/src/bin.ts | 7 ++++++- packages/dev-env/src/mock/index.ts | 11 +++++++++++ packages/dev-env/src/pds.ts | 2 ++ packages/dev-env/src/util.ts | 22 ++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/dev-env/src/bin.ts b/packages/dev-env/src/bin.ts index 12228579a48..82e768ed1f0 100644 --- a/packages/dev-env/src/bin.ts +++ b/packages/dev-env/src/bin.ts @@ -1,7 +1,7 @@ import './env' import { generateMockSetup } from './mock' import { TestNetwork } from './network' -import { mockMailer } from './util' +import { mockMailer, mockTwilio } from './util' const run = async () => { console.log(` @@ -20,6 +20,10 @@ const run = async () => { hostname: 'localhost', dbPostgresSchema: 'pds', enableDidDocWithSession: true, + phoneVerificationRequired: true, + twilioAccountSid: 'ACXXXXXXX', + twilioAuthToken: 'AUTH', + twilioServiceSid: 'VAXXXXXXXX', }, bsky: { dbPostgresSchema: 'bsky', @@ -27,6 +31,7 @@ const run = async () => { plc: { port: 2582 }, }) mockMailer(network.pds) + mockTwilio(network.pds) await generateMockSetup(network) console.log( diff --git a/packages/dev-env/src/mock/index.ts b/packages/dev-env/src/mock/index.ts index 10f76b1c259..93b2f4717c4 100644 --- a/packages/dev-env/src/mock/index.ts +++ b/packages/dev-env/src/mock/index.ts @@ -76,10 +76,21 @@ export async function generateMockSetup(env: TestNetwork) { let _i = 1 for (const user of users) { + let verificationCode: string | undefined = undefined + let verificationPhone: string | undefined = undefined + if (env.pds.ctx.twilio) { + verificationPhone = `+1111111111${_i}` + await clients.loggedout.api.com.atproto.temp.requestPhoneVerification({ + phoneNumber: verificationPhone, + }) + verificationCode = env.pds.mockedPhoneCodes[verificationPhone] + } const res = await clients.loggedout.api.com.atproto.server.createAccount({ email: user.email, handle: user.handle, password: user.password, + verificationCode, + verificationPhone, }) user.agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`) user.did = res.data.did diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index a4b67166e07..e9b7cd7c692 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -10,6 +10,8 @@ import { uniqueLockId } from './util' import { ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD } from './const' export class TestPds { + mockedPhoneCodes: Record = {} + constructor( public url: string, public port: number, diff --git a/packages/dev-env/src/util.ts b/packages/dev-env/src/util.ts index da4762be0c3..e76a31afada 100644 --- a/packages/dev-env/src/util.ts +++ b/packages/dev-env/src/util.ts @@ -1,4 +1,5 @@ import axios from 'axios' +import * as crypto from '@atproto/crypto' import { IdResolver } from '@atproto/identity' import { TestPds } from './pds' import { TestBsky } from './bsky' @@ -76,3 +77,24 @@ export const uniqueLockId = () => { usedLockIds.add(lockId) return lockId } + +export const mockTwilio = (pds: TestPds) => { + if (!pds.ctx.twilio) return + + pds.ctx.twilio.sendCode = async (number: string) => { + if (!pds.mockedPhoneCodes[number]) { + const code = crypto.randomStr(4, 'base10').slice(0, 6) + pds.mockedPhoneCodes[number] = code + } + const code = pds.mockedPhoneCodes[number] + console.log(`☎️ Phone verification code sent to ${number}: ${code}`) + } + + pds.ctx.twilio.verifyCode = async (number: string, code: string) => { + if (pds.mockedPhoneCodes[number] === code) { + delete pds.mockedPhoneCodes[number] + return true + } + return false + } +} From cd5af038a3d8c589136a9ca2673fb1fd0cf5ede4 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 16 Jan 2024 19:45:57 -0600 Subject: [PATCH 05/23] fix codegen --- packages/api/src/client/index.ts | 102 ++++++++++++++-------------- packages/bsky/src/lexicon/index.ts | 104 ++++++++++++++--------------- packages/pds/src/lexicon/index.ts | 104 ++++++++++++++--------------- 3 files changed, 153 insertions(+), 157 deletions(-) diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index bfb1be016c9..6dfe88b2b1a 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -340,39 +340,39 @@ export class AtpServiceClient { export class ComNS { _service: AtpServiceClient - atproto: ComAtprotoNS + atproto: AtprotoNS constructor(service: AtpServiceClient) { this._service = service - this.atproto = new ComAtprotoNS(service) + this.atproto = new AtprotoNS(service) } } -export class ComAtprotoNS { +export class AtprotoNS { _service: AtpServiceClient - admin: ComAtprotoAdminNS - identity: ComAtprotoIdentityNS - label: ComAtprotoLabelNS - moderation: ComAtprotoModerationNS - repo: ComAtprotoRepoNS - server: ComAtprotoServerNS - sync: ComAtprotoSyncNS - temp: ComAtprotoTempNS + admin: AdminNS + identity: IdentityNS + label: LabelNS + moderation: ModerationNS + repo: RepoNS + server: ServerNS + sync: SyncNS + temp: TempNS constructor(service: AtpServiceClient) { this._service = service - this.admin = new ComAtprotoAdminNS(service) - this.identity = new ComAtprotoIdentityNS(service) - this.label = new ComAtprotoLabelNS(service) - this.moderation = new ComAtprotoModerationNS(service) - this.repo = new ComAtprotoRepoNS(service) - this.server = new ComAtprotoServerNS(service) - this.sync = new ComAtprotoSyncNS(service) - this.temp = new ComAtprotoTempNS(service) + this.admin = new AdminNS(service) + this.identity = new IdentityNS(service) + this.label = new LabelNS(service) + this.moderation = new ModerationNS(service) + this.repo = new RepoNS(service) + this.server = new ServerNS(service) + this.sync = new SyncNS(service) + this.temp = new TempNS(service) } } -export class ComAtprotoAdminNS { +export class AdminNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -594,7 +594,7 @@ export class ComAtprotoAdminNS { } } -export class ComAtprotoIdentityNS { +export class IdentityNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -624,7 +624,7 @@ export class ComAtprotoIdentityNS { } } -export class ComAtprotoLabelNS { +export class LabelNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -643,7 +643,7 @@ export class ComAtprotoLabelNS { } } -export class ComAtprotoModerationNS { +export class ModerationNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -662,7 +662,7 @@ export class ComAtprotoModerationNS { } } -export class ComAtprotoRepoNS { +export class RepoNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -758,7 +758,7 @@ export class ComAtprotoRepoNS { } } -export class ComAtprotoServerNS { +export class ServerNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -997,7 +997,7 @@ export class ComAtprotoServerNS { } } -export class ComAtprotoSyncNS { +export class SyncNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -1126,7 +1126,7 @@ export class ComAtprotoSyncNS { } } -export class ComAtprotoTempNS { +export class TempNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -1191,37 +1191,37 @@ export class ComAtprotoTempNS { export class AppNS { _service: AtpServiceClient - bsky: AppBskyNS + bsky: BskyNS constructor(service: AtpServiceClient) { this._service = service - this.bsky = new AppBskyNS(service) + this.bsky = new BskyNS(service) } } -export class AppBskyNS { +export class BskyNS { _service: AtpServiceClient - actor: AppBskyActorNS - embed: AppBskyEmbedNS - feed: AppBskyFeedNS - graph: AppBskyGraphNS - notification: AppBskyNotificationNS - richtext: AppBskyRichtextNS - unspecced: AppBskyUnspeccedNS + actor: ActorNS + embed: EmbedNS + feed: FeedNS + graph: GraphNS + notification: NotificationNS + richtext: RichtextNS + unspecced: UnspeccedNS constructor(service: AtpServiceClient) { this._service = service - this.actor = new AppBskyActorNS(service) - this.embed = new AppBskyEmbedNS(service) - this.feed = new AppBskyFeedNS(service) - this.graph = new AppBskyGraphNS(service) - this.notification = new AppBskyNotificationNS(service) - this.richtext = new AppBskyRichtextNS(service) - this.unspecced = new AppBskyUnspeccedNS(service) + this.actor = new ActorNS(service) + this.embed = new EmbedNS(service) + this.feed = new FeedNS(service) + this.graph = new GraphNS(service) + this.notification = new NotificationNS(service) + this.richtext = new RichtextNS(service) + this.unspecced = new UnspeccedNS(service) } } -export class AppBskyActorNS { +export class ActorNS { _service: AtpServiceClient profile: ProfileRecord @@ -1369,7 +1369,7 @@ export class ProfileRecord { } } -export class AppBskyEmbedNS { +export class EmbedNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -1377,7 +1377,7 @@ export class AppBskyEmbedNS { } } -export class AppBskyFeedNS { +export class FeedNS { _service: AtpServiceClient generator: GeneratorRecord like: LikeRecord @@ -1880,7 +1880,7 @@ export class ThreadgateRecord { } } -export class AppBskyGraphNS { +export class GraphNS { _service: AtpServiceClient block: BlockRecord follow: FollowRecord @@ -2355,7 +2355,7 @@ export class ListitemRecord { } } -export class AppBskyNotificationNS { +export class NotificationNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -2407,7 +2407,7 @@ export class AppBskyNotificationNS { } } -export class AppBskyRichtextNS { +export class RichtextNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { @@ -2415,7 +2415,7 @@ export class AppBskyRichtextNS { } } -export class AppBskyUnspeccedNS { +export class UnspeccedNS { _service: AtpServiceClient constructor(service: AtpServiceClient) { diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 1dd381f180f..7e49495da5a 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -162,39 +162,39 @@ export class Server { export class ComNS { _server: Server - atproto: ComAtprotoNS + atproto: AtprotoNS constructor(server: Server) { this._server = server - this.atproto = new ComAtprotoNS(server) + this.atproto = new AtprotoNS(server) } } -export class ComAtprotoNS { +export class AtprotoNS { _server: Server - admin: ComAtprotoAdminNS - identity: ComAtprotoIdentityNS - label: ComAtprotoLabelNS - moderation: ComAtprotoModerationNS - repo: ComAtprotoRepoNS - server: ComAtprotoServerNS - sync: ComAtprotoSyncNS - temp: ComAtprotoTempNS + admin: AdminNS + identity: IdentityNS + label: LabelNS + moderation: ModerationNS + repo: RepoNS + server: ServerNS + sync: SyncNS + temp: TempNS constructor(server: Server) { this._server = server - this.admin = new ComAtprotoAdminNS(server) - this.identity = new ComAtprotoIdentityNS(server) - this.label = new ComAtprotoLabelNS(server) - this.moderation = new ComAtprotoModerationNS(server) - this.repo = new ComAtprotoRepoNS(server) - this.server = new ComAtprotoServerNS(server) - this.sync = new ComAtprotoSyncNS(server) - this.temp = new ComAtprotoTempNS(server) + this.admin = new AdminNS(server) + this.identity = new IdentityNS(server) + this.label = new LabelNS(server) + this.moderation = new ModerationNS(server) + this.repo = new RepoNS(server) + this.server = new ServerNS(server) + this.sync = new SyncNS(server) + this.temp = new TempNS(server) } } -export class ComAtprotoAdminNS { +export class AdminNS { _server: Server constructor(server: Server) { @@ -411,7 +411,7 @@ export class ComAtprotoAdminNS { } } -export class ComAtprotoIdentityNS { +export class IdentityNS { _server: Server constructor(server: Server) { @@ -441,7 +441,7 @@ export class ComAtprotoIdentityNS { } } -export class ComAtprotoLabelNS { +export class LabelNS { _server: Server constructor(server: Server) { @@ -471,7 +471,7 @@ export class ComAtprotoLabelNS { } } -export class ComAtprotoModerationNS { +export class ModerationNS { _server: Server constructor(server: Server) { @@ -490,7 +490,7 @@ export class ComAtprotoModerationNS { } } -export class ComAtprotoRepoNS { +export class RepoNS { _server: Server constructor(server: Server) { @@ -586,7 +586,7 @@ export class ComAtprotoRepoNS { } } -export class ComAtprotoServerNS { +export class ServerNS { _server: Server constructor(server: Server) { @@ -825,7 +825,7 @@ export class ComAtprotoServerNS { } } -export class ComAtprotoSyncNS { +export class SyncNS { _server: Server constructor(server: Server) { @@ -965,7 +965,7 @@ export class ComAtprotoSyncNS { } } -export class ComAtprotoTempNS { +export class TempNS { _server: Server constructor(server: Server) { @@ -1030,37 +1030,37 @@ export class ComAtprotoTempNS { export class AppNS { _server: Server - bsky: AppBskyNS + bsky: BskyNS constructor(server: Server) { this._server = server - this.bsky = new AppBskyNS(server) + this.bsky = new BskyNS(server) } } -export class AppBskyNS { +export class BskyNS { _server: Server - actor: AppBskyActorNS - embed: AppBskyEmbedNS - feed: AppBskyFeedNS - graph: AppBskyGraphNS - notification: AppBskyNotificationNS - richtext: AppBskyRichtextNS - unspecced: AppBskyUnspeccedNS + actor: ActorNS + embed: EmbedNS + feed: FeedNS + graph: GraphNS + notification: NotificationNS + richtext: RichtextNS + unspecced: UnspeccedNS constructor(server: Server) { this._server = server - this.actor = new AppBskyActorNS(server) - this.embed = new AppBskyEmbedNS(server) - this.feed = new AppBskyFeedNS(server) - this.graph = new AppBskyGraphNS(server) - this.notification = new AppBskyNotificationNS(server) - this.richtext = new AppBskyRichtextNS(server) - this.unspecced = new AppBskyUnspeccedNS(server) + this.actor = new ActorNS(server) + this.embed = new EmbedNS(server) + this.feed = new FeedNS(server) + this.graph = new GraphNS(server) + this.notification = new NotificationNS(server) + this.richtext = new RichtextNS(server) + this.unspecced = new UnspeccedNS(server) } } -export class AppBskyActorNS { +export class ActorNS { _server: Server constructor(server: Server) { @@ -1145,7 +1145,7 @@ export class AppBskyActorNS { } } -export class AppBskyEmbedNS { +export class EmbedNS { _server: Server constructor(server: Server) { @@ -1153,7 +1153,7 @@ export class AppBskyEmbedNS { } } -export class AppBskyFeedNS { +export class FeedNS { _server: Server constructor(server: Server) { @@ -1337,7 +1337,7 @@ export class AppBskyFeedNS { } } -export class AppBskyGraphNS { +export class GraphNS { _server: Server constructor(server: Server) { @@ -1488,7 +1488,7 @@ export class AppBskyGraphNS { } } -export class AppBskyNotificationNS { +export class NotificationNS { _server: Server constructor(server: Server) { @@ -1540,7 +1540,7 @@ export class AppBskyNotificationNS { } } -export class AppBskyRichtextNS { +export class RichtextNS { _server: Server constructor(server: Server) { @@ -1548,7 +1548,7 @@ export class AppBskyRichtextNS { } } -export class AppBskyUnspeccedNS { +export class UnspeccedNS { _server: Server constructor(server: Server) { @@ -1622,13 +1622,11 @@ type RouteRateLimitOpts = { calcKey?: (ctx: T) => string calcPoints?: (ctx: T) => number } -type HandlerOpts = { blobLimit?: number } type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts type ConfigOf = | Handler | { auth?: Auth - opts?: HandlerOpts rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[] handler: Handler } diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 1dd381f180f..7e49495da5a 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -162,39 +162,39 @@ export class Server { export class ComNS { _server: Server - atproto: ComAtprotoNS + atproto: AtprotoNS constructor(server: Server) { this._server = server - this.atproto = new ComAtprotoNS(server) + this.atproto = new AtprotoNS(server) } } -export class ComAtprotoNS { +export class AtprotoNS { _server: Server - admin: ComAtprotoAdminNS - identity: ComAtprotoIdentityNS - label: ComAtprotoLabelNS - moderation: ComAtprotoModerationNS - repo: ComAtprotoRepoNS - server: ComAtprotoServerNS - sync: ComAtprotoSyncNS - temp: ComAtprotoTempNS + admin: AdminNS + identity: IdentityNS + label: LabelNS + moderation: ModerationNS + repo: RepoNS + server: ServerNS + sync: SyncNS + temp: TempNS constructor(server: Server) { this._server = server - this.admin = new ComAtprotoAdminNS(server) - this.identity = new ComAtprotoIdentityNS(server) - this.label = new ComAtprotoLabelNS(server) - this.moderation = new ComAtprotoModerationNS(server) - this.repo = new ComAtprotoRepoNS(server) - this.server = new ComAtprotoServerNS(server) - this.sync = new ComAtprotoSyncNS(server) - this.temp = new ComAtprotoTempNS(server) + this.admin = new AdminNS(server) + this.identity = new IdentityNS(server) + this.label = new LabelNS(server) + this.moderation = new ModerationNS(server) + this.repo = new RepoNS(server) + this.server = new ServerNS(server) + this.sync = new SyncNS(server) + this.temp = new TempNS(server) } } -export class ComAtprotoAdminNS { +export class AdminNS { _server: Server constructor(server: Server) { @@ -411,7 +411,7 @@ export class ComAtprotoAdminNS { } } -export class ComAtprotoIdentityNS { +export class IdentityNS { _server: Server constructor(server: Server) { @@ -441,7 +441,7 @@ export class ComAtprotoIdentityNS { } } -export class ComAtprotoLabelNS { +export class LabelNS { _server: Server constructor(server: Server) { @@ -471,7 +471,7 @@ export class ComAtprotoLabelNS { } } -export class ComAtprotoModerationNS { +export class ModerationNS { _server: Server constructor(server: Server) { @@ -490,7 +490,7 @@ export class ComAtprotoModerationNS { } } -export class ComAtprotoRepoNS { +export class RepoNS { _server: Server constructor(server: Server) { @@ -586,7 +586,7 @@ export class ComAtprotoRepoNS { } } -export class ComAtprotoServerNS { +export class ServerNS { _server: Server constructor(server: Server) { @@ -825,7 +825,7 @@ export class ComAtprotoServerNS { } } -export class ComAtprotoSyncNS { +export class SyncNS { _server: Server constructor(server: Server) { @@ -965,7 +965,7 @@ export class ComAtprotoSyncNS { } } -export class ComAtprotoTempNS { +export class TempNS { _server: Server constructor(server: Server) { @@ -1030,37 +1030,37 @@ export class ComAtprotoTempNS { export class AppNS { _server: Server - bsky: AppBskyNS + bsky: BskyNS constructor(server: Server) { this._server = server - this.bsky = new AppBskyNS(server) + this.bsky = new BskyNS(server) } } -export class AppBskyNS { +export class BskyNS { _server: Server - actor: AppBskyActorNS - embed: AppBskyEmbedNS - feed: AppBskyFeedNS - graph: AppBskyGraphNS - notification: AppBskyNotificationNS - richtext: AppBskyRichtextNS - unspecced: AppBskyUnspeccedNS + actor: ActorNS + embed: EmbedNS + feed: FeedNS + graph: GraphNS + notification: NotificationNS + richtext: RichtextNS + unspecced: UnspeccedNS constructor(server: Server) { this._server = server - this.actor = new AppBskyActorNS(server) - this.embed = new AppBskyEmbedNS(server) - this.feed = new AppBskyFeedNS(server) - this.graph = new AppBskyGraphNS(server) - this.notification = new AppBskyNotificationNS(server) - this.richtext = new AppBskyRichtextNS(server) - this.unspecced = new AppBskyUnspeccedNS(server) + this.actor = new ActorNS(server) + this.embed = new EmbedNS(server) + this.feed = new FeedNS(server) + this.graph = new GraphNS(server) + this.notification = new NotificationNS(server) + this.richtext = new RichtextNS(server) + this.unspecced = new UnspeccedNS(server) } } -export class AppBskyActorNS { +export class ActorNS { _server: Server constructor(server: Server) { @@ -1145,7 +1145,7 @@ export class AppBskyActorNS { } } -export class AppBskyEmbedNS { +export class EmbedNS { _server: Server constructor(server: Server) { @@ -1153,7 +1153,7 @@ export class AppBskyEmbedNS { } } -export class AppBskyFeedNS { +export class FeedNS { _server: Server constructor(server: Server) { @@ -1337,7 +1337,7 @@ export class AppBskyFeedNS { } } -export class AppBskyGraphNS { +export class GraphNS { _server: Server constructor(server: Server) { @@ -1488,7 +1488,7 @@ export class AppBskyGraphNS { } } -export class AppBskyNotificationNS { +export class NotificationNS { _server: Server constructor(server: Server) { @@ -1540,7 +1540,7 @@ export class AppBskyNotificationNS { } } -export class AppBskyRichtextNS { +export class RichtextNS { _server: Server constructor(server: Server) { @@ -1548,7 +1548,7 @@ export class AppBskyRichtextNS { } } -export class AppBskyUnspeccedNS { +export class UnspeccedNS { _server: Server constructor(server: Server) { @@ -1622,13 +1622,11 @@ type RouteRateLimitOpts = { calcKey?: (ctx: T) => string calcPoints?: (ctx: T) => number } -type HandlerOpts = { blobLimit?: number } type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts type ConfigOf = | Handler | { auth?: Auth - opts?: HandlerOpts rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[] handler: Handler } From e56003988404607cf3b8914753e32ab08a285417 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 18 Jan 2024 10:07:57 -0800 Subject: [PATCH 06/23] Pass createAccount params directly from the generated type --- packages/api/src/agent.ts | 7 +------ packages/api/src/types.ts | 9 +++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/api/src/agent.ts b/packages/api/src/agent.ts index aea3cce9d4b..de80c9de07d 100644 --- a/packages/api/src/agent.ts +++ b/packages/api/src/agent.ts @@ -89,12 +89,7 @@ export class AtpAgent { opts: AtpAgentCreateAccountOpts, ): Promise { try { - const res = await this.api.com.atproto.server.createAccount({ - handle: opts.handle, - password: opts.password, - email: opts.email, - inviteCode: opts.inviteCode, - }) + const res = await this.api.com.atproto.server.createAccount(opts) this.session = { accessJwt: res.data.accessJwt, refreshJwt: res.data.refreshJwt, diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index c0f78bfaafc..b68683681fd 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -1,3 +1,4 @@ +import { ComAtprotoServerCreateAccount } from './client' import { LabelPreference } from './moderation/types' /** @@ -36,12 +37,8 @@ export interface AtpAgentOpts { /** * AtpAgent createAccount() opts */ -export interface AtpAgentCreateAccountOpts { - email: string - password: string - handle: string - inviteCode?: string -} +export type AtpAgentCreateAccountOpts = + ComAtprotoServerCreateAccount.InputSchema /** * AtpAgent login() opts From e62a42cfb4f9f8007b4c02cc146b4eeec1d9e957 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 18 Jan 2024 20:13:54 -0600 Subject: [PATCH 07/23] ensure valid phone number --- packages/pds/src/twilio.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index 6a534e77cd2..868a0c53f81 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -1,3 +1,4 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' import twilio from 'twilio' type Opts = { @@ -18,7 +19,15 @@ export class TwilioClient { ).verify.v2.services(opts.serviceSid) } + ensureValidPhoneNumber(phoneNumber: string) { + const valid = /^\+[1-9]\d{1,14}$/.test(phoneNumber) + if (!valid) { + throw new InvalidRequestError('Invalid phone number') + } + } + async sendCode(phoneNumber: string) { + this.ensureValidPhoneNumber(phoneNumber) await this.verifyClient.verifications.create({ to: phoneNumber, channel: 'sms', @@ -26,6 +35,7 @@ export class TwilioClient { } async verifyCode(phoneNumber: string, code: string) { + this.ensureValidPhoneNumber(phoneNumber) const res = await this.verifyClient.verificationChecks.create({ to: phoneNumber, code, From acd230c04c8b3dd93bc9b69b7e9885d6d36d24d7 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 18 Jan 2024 20:19:51 -0600 Subject: [PATCH 08/23] test for normalization --- packages/pds/tests/phone-verification.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/pds/tests/phone-verification.test.ts b/packages/pds/tests/phone-verification.test.ts index a4ef1af5d26..74efc5cc4dc 100644 --- a/packages/pds/tests/phone-verification.test.ts +++ b/packages/pds/tests/phone-verification.test.ts @@ -143,7 +143,7 @@ describe('phone verification', () => { }) it('does not allow more than the configured number of signups from the same code', async () => { - const danNumber = '+3333333333' + const danNumber = '+13333333333' for (let i = 0; i < 3; i++) { const danCode = await requestCode(danNumber) await createAccountWithCode(danNumber, danCode) @@ -153,4 +153,11 @@ describe('phone verification', () => { `There are too many accounts currently using this phone number. Max: 3`, ) }) + + it('does not allow invalidly formatted phone numbers', async () => { + const eveNumber = '+1-444-444-4444' + expect(() => ctx.twilio?.ensureValidPhoneNumber(eveNumber)).toThrow( + 'Invalid phone number', + ) + }) }) From 08a2e4458c9967f0a82a48b1d8160d0d6a800572 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 18 Jan 2024 20:20:14 -0600 Subject: [PATCH 09/23] comment --- packages/pds/src/twilio.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index 868a0c53f81..667f951a103 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -20,6 +20,7 @@ export class TwilioClient { } ensureValidPhoneNumber(phoneNumber: string) { + // https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164 const valid = /^\+[1-9]\d{1,14}$/.test(phoneNumber) if (!valid) { throw new InvalidRequestError('Invalid phone number') From 7fef18eebda3803a6cb1bf19269c4cf4f51b794e Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 18 Jan 2024 20:21:48 -0600 Subject: [PATCH 10/23] couple more tests --- packages/pds/tests/phone-verification.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/pds/tests/phone-verification.test.ts b/packages/pds/tests/phone-verification.test.ts index 74efc5cc4dc..90d355adc39 100644 --- a/packages/pds/tests/phone-verification.test.ts +++ b/packages/pds/tests/phone-verification.test.ts @@ -155,8 +155,13 @@ describe('phone verification', () => { }) it('does not allow invalidly formatted phone numbers', async () => { - const eveNumber = '+1-444-444-4444' - expect(() => ctx.twilio?.ensureValidPhoneNumber(eveNumber)).toThrow( + expect(() => ctx.twilio?.ensureValidPhoneNumber('+1-444-444-4444')).toThrow( + 'Invalid phone number', + ) + expect(() => ctx.twilio?.ensureValidPhoneNumber('1-444-444-4444')).toThrow( + 'Invalid phone number', + ) + expect(() => ctx.twilio?.ensureValidPhoneNumber('444-444-4444')).toThrow( 'Invalid phone number', ) }) From 04ad9131eb573fb21998a9909c889f08412a8537 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 18 Jan 2024 20:49:15 -0600 Subject: [PATCH 11/23] add some error handling --- packages/pds/src/twilio.ts | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index 667f951a103..661ce360b69 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -1,4 +1,4 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError, UpstreamFailureError } from '@atproto/xrpc-server' import twilio from 'twilio' type Opts = { @@ -29,18 +29,26 @@ export class TwilioClient { async sendCode(phoneNumber: string) { this.ensureValidPhoneNumber(phoneNumber) - await this.verifyClient.verifications.create({ - to: phoneNumber, - channel: 'sms', - }) + try { + await this.verifyClient.verifications.create({ + to: phoneNumber, + channel: 'sms', + }) + } catch (err) { + throw new UpstreamFailureError('Could not send verification text') + } } async verifyCode(phoneNumber: string, code: string) { this.ensureValidPhoneNumber(phoneNumber) - const res = await this.verifyClient.verificationChecks.create({ - to: phoneNumber, - code, - }) - return res.status === 'approved' + try { + const res = await this.verifyClient.verificationChecks.create({ + to: phoneNumber, + code, + }) + return res.status === 'approved' + } catch (err) { + throw new UpstreamFailureError('Could not send verification text') + } } } From ca11d39b6d5963a99597a9a510b76445e959fdca Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 18 Jan 2024 21:14:14 -0600 Subject: [PATCH 12/23] phone number verification --- .../api/com/atproto/server/createAccount.ts | 15 +++++++------ .../atproto/temp/requestPhoneVerification.ts | 4 +++- packages/pds/src/twilio.ts | 15 +++++++++---- packages/pds/tests/phone-verification.test.ts | 22 +++++++++---------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 034e31412ac..5786ff92868 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -40,6 +40,7 @@ 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( @@ -52,9 +53,12 @@ export default function (server: Server, ctx: AppContext) { 'InvalidPhoneVerification', ) } - const verified = await ctx.twilio.verifyCode( + verificationPhone = ctx.twilio.normalizePhoneNumber( input.body.verificationPhone, - input.body.verificationCode, + ) + const verified = await ctx.twilio.verifyCode( + verificationPhone, + input.body.verificationCode.trim(), ) if (!verified) { throw new InvalidRequestError( @@ -121,15 +125,12 @@ export default function (server: Server, ctx: AppContext) { .execute() } - if ( - ctx.cfg.phoneVerification.required && - input.body.verificationPhone - ) { + if (ctx.cfg.phoneVerification.required && verificationPhone) { await dbTxn.db .insertInto('phone_verification') .values({ did, - phoneNumber: input.body.verificationPhone, + phoneNumber: verificationPhone, }) .execute() } diff --git a/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts b/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts index 411d1073e6d..4869d431514 100644 --- a/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts +++ b/packages/pds/src/api/com/atproto/temp/requestPhoneVerification.ts @@ -22,7 +22,9 @@ export default function (server: Server, ctx: AppContext) { } const accountsPerPhoneNumber = ctx.cfg.phoneVerification.accountsPerPhoneNumber - const { phoneNumber } = input.body + const phoneNumber = ctx.twilio.normalizePhoneNumber( + input.body.phoneNumber, + ) const res = await ctx.db.db .selectFrom('phone_verification') diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index 661ce360b69..a2d0846e396 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -19,16 +19,24 @@ export class TwilioClient { ).verify.v2.services(opts.serviceSid) } - ensureValidPhoneNumber(phoneNumber: string) { + normalizePhoneNumber(phoneNumber: string) { + let normalized = phoneNumber.replaceAll(/\(|\)|-| /g, '') + if (!normalized.startsWith('+')) { + if (normalized.length === 10) { + normalized = '+1' + normalized + } else { + normalized = '+' + normalized + } + } // https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164 - const valid = /^\+[1-9]\d{1,14}$/.test(phoneNumber) + const valid = /^\+[1-9]\d{1,14}$/.test(normalized) if (!valid) { throw new InvalidRequestError('Invalid phone number') } + return normalized } async sendCode(phoneNumber: string) { - this.ensureValidPhoneNumber(phoneNumber) try { await this.verifyClient.verifications.create({ to: phoneNumber, @@ -40,7 +48,6 @@ export class TwilioClient { } async verifyCode(phoneNumber: string, code: string) { - this.ensureValidPhoneNumber(phoneNumber) try { const res = await this.verifyClient.verificationChecks.create({ to: phoneNumber, diff --git a/packages/pds/tests/phone-verification.test.ts b/packages/pds/tests/phone-verification.test.ts index 90d355adc39..525845309fb 100644 --- a/packages/pds/tests/phone-verification.test.ts +++ b/packages/pds/tests/phone-verification.test.ts @@ -55,7 +55,6 @@ describe('phone verification', () => { }) const sent = sentCodes.at(-1) assert(sent) - assert(sent.number === phoneNumber) return sent.code } @@ -154,15 +153,16 @@ describe('phone verification', () => { ) }) - it('does not allow invalidly formatted phone numbers', async () => { - expect(() => ctx.twilio?.ensureValidPhoneNumber('+1-444-444-4444')).toThrow( - 'Invalid phone number', - ) - expect(() => ctx.twilio?.ensureValidPhoneNumber('1-444-444-4444')).toThrow( - 'Invalid phone number', - ) - expect(() => ctx.twilio?.ensureValidPhoneNumber('444-444-4444')).toThrow( - 'Invalid phone number', - ) + it('normalizes phone numbers', async () => { + const code1 = await requestCode('+1 (444)444-4444') + expect(verificationCodes['+14444444444']).toEqual(code1) + const code2 = await requestCode('(555)555-5555') + expect(verificationCodes['+15555555555']).toEqual(code2) + const code3 = await requestCode('1(666)666-6666') + expect(verificationCodes['+16666666666']).toEqual(code3) + const attempt1 = requestCode('+1444444444444444') + await expect(attempt1).rejects.toThrow('Invalid phone number') + const attempt2 = requestCode('a44444444') + await expect(attempt2).rejects.toThrow('Invalid phone number') }) }) From 0748b9427d02e447670d897b09a438bbe4820e91 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 22 Jan 2024 21:28:23 -0600 Subject: [PATCH 13/23] build branch --- .github/workflows/build-and-push-pds-aws.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-and-push-pds-aws.yaml b/.github/workflows/build-and-push-pds-aws.yaml index 097f782d88e..7589f56769a 100644 --- a/.github/workflows/build-and-push-pds-aws.yaml +++ b/.github/workflows/build-and-push-pds-aws.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - entryway-twilio env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} From 6884f03c6070592ee99b56268203a9a55ff905d4 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 22 Jan 2024 21:34:19 -0600 Subject: [PATCH 14/23] update env name --- packages/pds/src/config/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 1dd16a2c2a0..1d332f9b8ff 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -54,7 +54,7 @@ export const readEnv = (): ServerEnvironment => { accountsPerPhoneNumber: envInt('PDS_ACCOUNTS_PER_PHONE_NUMBER'), twilioAccountSid: envStr('PDS_TWILIO_ACCOUNT_SID'), twilioAuthToken: envStr('PDS_TWILIO_AUTH_TOKEN'), - twilioServiceSid: envStr('TWILIO_SERVICE_SID'), + twilioServiceSid: envStr('PDS_TWILIO_SERVICE_SID'), // email emailSmtpUrl: envStr('PDS_EMAIL_SMTP_URL'), From a3cf3a3adb03f6fd8c074147a5e508a817a4a983 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 24 Jan 2024 12:46:14 -0600 Subject: [PATCH 15/23] update error language --- packages/pds/src/api/com/atproto/server/createAccount.ts | 4 ++-- packages/pds/tests/phone-verification.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 5786ff92868..1d94f876328 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -44,12 +44,12 @@ export default function (server: Server, ctx: AppContext) { if (ctx.cfg.phoneVerification.required && ctx.twilio) { if (!input.body.verificationPhone) { throw new InvalidRequestError( - 'Phone number verification is required on this server and none was provided.', + `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( - 'Phone number verification is required on this server and none was provided.', + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, 'InvalidPhoneVerification', ) } diff --git a/packages/pds/tests/phone-verification.test.ts b/packages/pds/tests/phone-verification.test.ts index 525845309fb..a203a07ccf9 100644 --- a/packages/pds/tests/phone-verification.test.ts +++ b/packages/pds/tests/phone-verification.test.ts @@ -116,7 +116,7 @@ describe('phone verification', () => { it('does not allow signup with out a code', async () => { const attempt = createAccountWithCode() await expect(attempt).rejects.toThrow( - 'Phone number verification is required on this server and none was provided.', + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, ) }) @@ -125,11 +125,11 @@ describe('phone verification', () => { const bobCode = await requestCode(bobNumber) const attempt = createAccountWithCode(undefined, bobCode) await expect(attempt).rejects.toThrow( - 'Phone number verification is required on this server and none was provided.', + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, ) const attempt2 = createAccountWithCode(bobNumber, undefined) await expect(attempt2).rejects.toThrow( - 'Phone number verification is required on this server and none was provided.', + `Text verification is now required on this server. Please make sure you're using the latest version of the Bluesky app.`, ) }) From b18b766182b2d942b238dc9e85c5022d74d593f8 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 24 Jan 2024 18:10:34 -0600 Subject: [PATCH 16/23] bugfix twilio --- .../api/com/atproto/server/createAccount.ts | 34 ++++++++++++++----- packages/pds/src/logger.ts | 1 + packages/pds/src/twilio.ts | 7 ++-- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 1d94f876328..b78b358fb4c 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -37,6 +37,8 @@ export default function (server: Server, ctx: AppContext) { ? await validateInputsForPdsViaEntryway(ctx, input.body) : await validateInputsForPdsViaUser(ctx, input.body) + await ensureUnusedHandleAndEmail(ctx.db, handle, email) + const now = new Date().toISOString() const passwordScrypt = await scrypt.genSaltAndHash(password) @@ -72,13 +74,6 @@ export default function (server: Server, ctx: AppContext) { const actorTxn = ctx.services.account(dbTxn) const repoTxn = ctx.services.repo(dbTxn) - // it's a bit goofy that we run this logic twice, - // but we run it once for a sanity check before doing scrypt & plc ops - // & a second time for locking + integrity check - if (ctx.cfg.invites.required && inviteCode) { - await ensureCodeIsAvailable(dbTxn, inviteCode, true) - } - // Register user before going out to PLC to get a real did try { await actorTxn.registerUser({ @@ -238,7 +233,8 @@ const validateInputsForPdsViaUser = async ( ctx: AppContext, input: CreateAccountInput, ) => { - const { email, password, inviteCode } = input + const { password, inviteCode } = input + const email = input.email?.toLowerCase() if (input.plcOp) { throw new InvalidRequestError('Unsupported input: "plcOp"') } @@ -269,6 +265,8 @@ const validateInputsForPdsViaUser = async ( did: input.did, }) + await ensureUnusedHandleAndEmail(ctx.db, handle, email) + // check that the invite code still has uses if (ctx.cfg.invites.required && inviteCode) { await ensureCodeIsAvailable(ctx.db, inviteCode) @@ -470,6 +468,26 @@ const reserveSigningKey = async (ctx: AppContext, host: string) => { } } +const ensureUnusedHandleAndEmail = async ( + db: Database, + handle: string, + email: string, +) => { + const res = await db.db + .selectFrom('user_account') + .innerJoin('did_handle', 'did_handle.did', 'user_account.did') + .select(['did_handle.handle', 'user_account.email']) + .where('user_account.email', '=', email) + .where('did_handle.handle', '=', handle) + .executeTakeFirst() + if (!res) return + if (res.email === email) { + throw new InvalidRequestError(`Email already taken: ${email}`) + } else { + throw new InvalidRequestError(`Handle already taken: ${handle}`) + } +} + const randomIndexByWeight = (weights) => { let sum = 0 const cumulative = weights.map((weight) => { diff --git a/packages/pds/src/logger.ts b/packages/pds/src/logger.ts index e8b663b567f..5e93f840fdd 100644 --- a/packages/pds/src/logger.ts +++ b/packages/pds/src/logger.ts @@ -11,6 +11,7 @@ export const seqLogger = subsystemLogger('pds:sequencer') export const mailerLogger = subsystemLogger('pds:mailer') export const labelerLogger = subsystemLogger('pds:labler') export const crawlerLogger = subsystemLogger('pds:crawler') +export const twilioLogger = subsystemLogger('pds:twilio') export const httpLogger = subsystemLogger('pds') export const loggerMiddleware = pinoHttp({ diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index a2d0846e396..113c9e18245 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -1,5 +1,6 @@ import { InvalidRequestError, UpstreamFailureError } from '@atproto/xrpc-server' import twilio from 'twilio' +import { twilioLogger as log } from './logger' type Opts = { accountSid: string @@ -20,7 +21,7 @@ export class TwilioClient { } normalizePhoneNumber(phoneNumber: string) { - let normalized = phoneNumber.replaceAll(/\(|\)|-| /g, '') + let normalized = phoneNumber.trim().replaceAll(/\(|\)|-| /g, '') if (!normalized.startsWith('+')) { if (normalized.length === 10) { normalized = '+1' + normalized @@ -43,6 +44,7 @@ export class TwilioClient { channel: 'sms', }) } catch (err) { + log.error({ err, phoneNumber }, 'error sending twilio code') throw new UpstreamFailureError('Could not send verification text') } } @@ -55,7 +57,8 @@ export class TwilioClient { }) return res.status === 'approved' } catch (err) { - throw new UpstreamFailureError('Could not send verification text') + log.error({ err, phoneNumber, code }, 'error verifying twilio code') + throw new UpstreamFailureError('Could not verify code. Please try again') } } } From f9e51aa418301afaf5f7e1de13b3189393893175 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 24 Jan 2024 18:19:57 -0600 Subject: [PATCH 17/23] patch up some tests --- packages/pds/tests/account.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/pds/tests/account.test.ts b/packages/pds/tests/account.test.ts index f157380a1c1..c6e127f7162 100644 --- a/packages/pds/tests/account.test.ts +++ b/packages/pds/tests/account.test.ts @@ -169,7 +169,7 @@ describe('account', () => { const userKey = await crypto.Secp256k1Keypair.create() const baseDidInfo = { signingKey: ctx.repoSigningKey.did(), - handle: 'byo-did.test', + handle: 'byo-did2.test', rotationKeys: [ userKey.did(), ctx.cfg.identity.recoveryDidKey ?? '', @@ -179,9 +179,9 @@ describe('account', () => { signer: userKey, } const baseAccntInfo = { - email: 'byo-did@test.com', - handle: 'byo-did.test', - password: 'byo-did-pass', + email: 'byo-did2@test.com', + handle: 'byo-did2.test', + password: 'byo-did2-pass', } const did1 = await ctx.plcClient.createDid({ @@ -304,7 +304,7 @@ describe('account', () => { handle: 'carol.test', password, }), - ).rejects.toThrow('Email already taken: BOB@TEST.COM') + ).rejects.toThrow('Email already taken: bob@test.com') await expect( agent.api.com.atproto.server.createAccount({ From c7ba6778f8634b7d1470da9050438b6bedfc962c Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 24 Jan 2024 18:24:50 -0600 Subject: [PATCH 18/23] patch more tests --- packages/pds/src/api/com/atproto/server/createAccount.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index b78b358fb4c..8b58f3dc6b9 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -110,6 +110,7 @@ export default function (server: Server, ctx: AppContext) { // insert invite code use if (ctx.cfg.invites.required && inviteCode) { + await ensureCodeIsAvailable(dbTxn, inviteCode, true) await dbTxn.db .insertInto('invite_code_use') .values({ From 9a1fe0ae340e38a9bf34a4fa942679492356d4f1 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 24 Jan 2024 18:37:02 -0600 Subject: [PATCH 19/23] fix query --- packages/pds/src/api/com/atproto/server/createAccount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 8b58f3dc6b9..929654e33b0 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -479,7 +479,7 @@ const ensureUnusedHandleAndEmail = async ( .innerJoin('did_handle', 'did_handle.did', 'user_account.did') .select(['did_handle.handle', 'user_account.email']) .where('user_account.email', '=', email) - .where('did_handle.handle', '=', handle) + .orWhere('did_handle.handle', '=', handle) .executeTakeFirst() if (!res) return if (res.email === email) { From 5d5d7bce7db1e53c447b1b3acfca08519ca1abef Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 25 Jan 2024 11:15:43 -0600 Subject: [PATCH 20/23] tidy account existence check --- .../api/com/atproto/server/createAccount.ts | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 929654e33b0..1fda28e0266 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -11,7 +11,10 @@ import * as scrypt from '../../../../db/scrypt' import { Server } from '../../../../lexicon' import { InputSchema as CreateAccountInput } from '../../../../lexicon/types/com/atproto/server/createAccount' import { countAll } from '../../../../db/util' -import { UserAlreadyExistsError } from '../../../../services/account' +import { + AccountService, + UserAlreadyExistsError, +} from '../../../../services/account' import AppContext from '../../../../context' import Database from '../../../../db' import { didDocForSession } from './util' @@ -37,8 +40,6 @@ export default function (server: Server, ctx: AppContext) { ? await validateInputsForPdsViaEntryway(ctx, input.body) : await validateInputsForPdsViaUser(ctx, input.body) - await ensureUnusedHandleAndEmail(ctx.db, handle, email) - const now = new Date().toISOString() const passwordScrypt = await scrypt.genSaltAndHash(password) @@ -266,7 +267,7 @@ const validateInputsForPdsViaUser = async ( did: input.did, }) - await ensureUnusedHandleAndEmail(ctx.db, handle, email) + await ensureUnusedHandleAndEmail(ctx.services.account(ctx.db), handle, email) // check that the invite code still has uses if (ctx.cfg.invites.required && inviteCode) { @@ -470,21 +471,17 @@ const reserveSigningKey = async (ctx: AppContext, host: string) => { } const ensureUnusedHandleAndEmail = async ( - db: Database, + accountSrvc: AccountService, handle: string, email: string, ) => { - const res = await db.db - .selectFrom('user_account') - .innerJoin('did_handle', 'did_handle.did', 'user_account.did') - .select(['did_handle.handle', 'user_account.email']) - .where('user_account.email', '=', email) - .orWhere('did_handle.handle', '=', handle) - .executeTakeFirst() - if (!res) return - if (res.email === email) { + const [byHandle, byEmail] = await Promise.all([ + accountSrvc.getAccount(handle, true), + accountSrvc.getAccountByEmail(email, true), + ]) + if (byEmail) { throw new InvalidRequestError(`Email already taken: ${email}`) - } else { + } else if (byHandle) { throw new InvalidRequestError(`Handle already taken: ${handle}`) } } From e8b47b5356483bdb6e4c013dd9db77ec77c390db Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 25 Jan 2024 13:18:41 -0600 Subject: [PATCH 21/23] improve error handling --- packages/pds/src/twilio.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index 113c9e18245..3fb5f77ce7e 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -45,7 +45,18 @@ export class TwilioClient { }) } catch (err) { log.error({ err, phoneNumber }, 'error sending twilio code') - throw new UpstreamFailureError('Could not send verification text') + const code = typeof err === 'object' ? err?.['code'] : undefined + if (code === 60200) { + throw new InvalidRequestError( + 'Could not send verification text: invalid phone number', + ) + } else if (code === 60220) { + throw new InvalidRequestError( + `We're sorry, we're not currently able to send verification messages to China. We're working with our providers to solve this as quickly as possible.`, + ) + } else { + throw new UpstreamFailureError('Could not send verification text') + } } } From 021fa9adb5f30a33b37a6effb220ef7fefdb665c Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 25 Jan 2024 13:47:35 -0600 Subject: [PATCH 22/23] handle one more error --- packages/pds/src/twilio.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pds/src/twilio.ts b/packages/pds/src/twilio.ts index 3fb5f77ce7e..eec698865a3 100644 --- a/packages/pds/src/twilio.ts +++ b/packages/pds/src/twilio.ts @@ -50,9 +50,9 @@ export class TwilioClient { throw new InvalidRequestError( 'Could not send verification text: invalid phone number', ) - } else if (code === 60220) { + } else if (code === 60605 || code === 60220) { throw new InvalidRequestError( - `We're sorry, we're not currently able to send verification messages to China. We're working with our providers to solve this as quickly as possible.`, + `We're sorry, we're not currently able to send verification messages to your country. We're working with our providers to solve this as quickly as possible.`, ) } else { throw new UpstreamFailureError('Could not send verification text') From 49ced7cf20973390a2d2c91396b429102fb7037d Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 29 Jan 2024 16:35:14 -0600 Subject: [PATCH 23/23] dont build branch --- .github/workflows/build-and-push-pds-aws.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-and-push-pds-aws.yaml b/.github/workflows/build-and-push-pds-aws.yaml index 7589f56769a..097f782d88e 100644 --- a/.github/workflows/build-and-push-pds-aws.yaml +++ b/.github/workflows/build-and-push-pds-aws.yaml @@ -3,7 +3,6 @@ on: push: branches: - main - - entryway-twilio env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}