this repo has no description
1import { api, ApiError } from '../api' 2import { generateKeypair, createServiceJwt, generateDidDocument } from '../crypto' 3import type { 4 RegistrationMode, 5 RegistrationStep, 6 RegistrationInfo, 7 ExternalDidWebState, 8 AccountResult, 9 SessionState, 10} from './types' 11 12export interface RegistrationFlowState { 13 mode: RegistrationMode 14 step: RegistrationStep 15 info: RegistrationInfo 16 externalDidWeb: ExternalDidWebState 17 account: AccountResult | null 18 session: SessionState | null 19 error: string | null 20 submitting: boolean 21 pdsHostname: string 22} 23 24export function createRegistrationFlow(mode: RegistrationMode, pdsHostname: string) { 25 let state = $state<RegistrationFlowState>({ 26 mode, 27 step: 'info', 28 info: { 29 handle: '', 30 email: '', 31 password: '', 32 inviteCode: '', 33 didType: 'plc', 34 externalDid: '', 35 verificationChannel: 'email', 36 discordId: '', 37 telegramUsername: '', 38 signalNumber: '', 39 }, 40 externalDidWeb: { 41 keyMode: 'reserved', 42 }, 43 account: null, 44 session: null, 45 error: null, 46 submitting: false, 47 pdsHostname, 48 }) 49 50 function getPdsEndpoint(): string { 51 return `https://${state.pdsHostname}` 52 } 53 54 function getPdsDid(): string { 55 return `did:web:${state.pdsHostname}` 56 } 57 58 function getFullHandle(): string { 59 return `${state.info.handle.trim()}.${state.pdsHostname}` 60 } 61 62 function extractDomain(did: string): string { 63 return did.replace('did:web:', '').replace(/%3A/g, ':') 64 } 65 66 function setError(err: unknown) { 67 if (err instanceof ApiError) { 68 state.error = err.message || 'An error occurred' 69 } else if (err instanceof Error) { 70 state.error = err.message || 'An error occurred' 71 } else { 72 state.error = 'An error occurred' 73 } 74 } 75 76 async function proceedFromInfo() { 77 state.error = null 78 if (state.info.didType === 'web-external') { 79 state.step = 'key-choice' 80 } else { 81 state.step = 'creating' 82 } 83 } 84 85 async function selectKeyMode(keyMode: 'reserved' | 'byod') { 86 state.submitting = true 87 state.error = null 88 state.externalDidWeb.keyMode = keyMode 89 90 try { 91 let publicKeyMultibase: string 92 93 if (keyMode === 'reserved') { 94 const result = await api.reserveSigningKey(state.info.externalDid!.trim()) 95 state.externalDidWeb.reservedSigningKey = result.signingKey 96 publicKeyMultibase = result.signingKey.replace('did:key:', '') 97 } else { 98 const keypair = await generateKeypair() 99 state.externalDidWeb.byodPrivateKey = keypair.privateKey 100 state.externalDidWeb.byodPublicKeyMultibase = keypair.publicKeyMultibase 101 publicKeyMultibase = keypair.publicKeyMultibase 102 } 103 104 const didDoc = generateDidDocument( 105 state.info.externalDid!.trim(), 106 publicKeyMultibase, 107 getFullHandle(), 108 getPdsEndpoint() 109 ) 110 state.externalDidWeb.initialDidDocument = JSON.stringify(didDoc, null, '\t') 111 state.step = 'initial-did-doc' 112 } catch (err) { 113 setError(err) 114 } finally { 115 state.submitting = false 116 } 117 } 118 119 async function confirmInitialDidDoc() { 120 state.step = 'creating' 121 } 122 123 async function createPasswordAccount() { 124 state.submitting = true 125 state.error = null 126 127 try { 128 let byodToken: string | undefined 129 130 if (state.info.didType === 'web-external' && state.externalDidWeb.keyMode === 'byod' && state.externalDidWeb.byodPrivateKey) { 131 byodToken = await createServiceJwt( 132 state.externalDidWeb.byodPrivateKey, 133 state.info.externalDid!.trim(), 134 getPdsDid(), 135 'com.atproto.server.createAccount' 136 ) 137 } 138 139 const result = await api.createAccount({ 140 handle: state.info.handle.trim(), 141 email: state.info.email.trim(), 142 password: state.info.password!, 143 inviteCode: state.info.inviteCode?.trim() || undefined, 144 didType: state.info.didType, 145 did: state.info.didType === 'web-external' ? state.info.externalDid!.trim() : undefined, 146 signingKey: state.info.didType === 'web-external' && state.externalDidWeb.keyMode === 'reserved' 147 ? state.externalDidWeb.reservedSigningKey 148 : undefined, 149 verificationChannel: state.info.verificationChannel, 150 discordId: state.info.discordId?.trim() || undefined, 151 telegramUsername: state.info.telegramUsername?.trim() || undefined, 152 signalNumber: state.info.signalNumber?.trim() || undefined, 153 }, byodToken) 154 155 state.account = { 156 did: result.did, 157 handle: result.handle, 158 } 159 state.step = 'verify' 160 } catch (err) { 161 setError(err) 162 } finally { 163 state.submitting = false 164 } 165 } 166 167 async function createPasskeyAccount() { 168 state.submitting = true 169 state.error = null 170 171 try { 172 let byodToken: string | undefined 173 174 if (state.info.didType === 'web-external' && state.externalDidWeb.keyMode === 'byod' && state.externalDidWeb.byodPrivateKey) { 175 byodToken = await createServiceJwt( 176 state.externalDidWeb.byodPrivateKey, 177 state.info.externalDid!.trim(), 178 getPdsDid(), 179 'com.atproto.server.createAccount' 180 ) 181 } 182 183 const result = await api.createPasskeyAccount({ 184 handle: state.info.handle.trim(), 185 email: state.info.email?.trim() || undefined, 186 inviteCode: state.info.inviteCode?.trim() || undefined, 187 didType: state.info.didType, 188 did: state.info.didType === 'web-external' ? state.info.externalDid!.trim() : undefined, 189 signingKey: state.info.didType === 'web-external' && state.externalDidWeb.keyMode === 'reserved' 190 ? state.externalDidWeb.reservedSigningKey 191 : undefined, 192 verificationChannel: state.info.verificationChannel, 193 discordId: state.info.discordId?.trim() || undefined, 194 telegramUsername: state.info.telegramUsername?.trim() || undefined, 195 signalNumber: state.info.signalNumber?.trim() || undefined, 196 }, byodToken) 197 198 state.account = { 199 did: result.did, 200 handle: result.handle, 201 setupToken: result.setupToken, 202 } 203 state.step = 'passkey' 204 } catch (err) { 205 setError(err) 206 } finally { 207 state.submitting = false 208 } 209 } 210 211 function setPasskeyComplete(appPassword: string, appPasswordName: string) { 212 if (state.account) { 213 state.account.appPassword = appPassword 214 state.account.appPasswordName = appPasswordName 215 } 216 state.step = 'app-password' 217 } 218 219 function proceedFromAppPassword() { 220 state.step = 'verify' 221 } 222 223 async function verifyAccount(code: string) { 224 state.submitting = true 225 state.error = null 226 227 try { 228 const confirmResult = await api.confirmSignup(state.account!.did, code.trim()) 229 230 if (state.info.didType === 'web-external') { 231 const password = state.mode === 'passkey' ? state.account!.appPassword! : state.info.password! 232 const session = await api.createSession(state.account!.did, password) 233 state.session = { 234 accessJwt: session.accessJwt, 235 refreshJwt: session.refreshJwt, 236 } 237 238 if (state.externalDidWeb.keyMode === 'byod') { 239 const credentials = await api.getRecommendedDidCredentials(session.accessJwt) 240 const newPublicKeyMultibase = credentials.verificationMethods?.atproto?.replace('did:key:', '') || '' 241 242 const didDoc = generateDidDocument( 243 state.info.externalDid!.trim(), 244 newPublicKeyMultibase, 245 state.account!.handle, 246 getPdsEndpoint() 247 ) 248 state.externalDidWeb.updatedDidDocument = JSON.stringify(didDoc, null, '\t') 249 state.step = 'updated-did-doc' 250 } else { 251 await api.activateAccount(session.accessJwt) 252 await finalizeSession() 253 state.step = 'redirect-to-dashboard' 254 } 255 } else { 256 state.session = { 257 accessJwt: confirmResult.accessJwt, 258 refreshJwt: confirmResult.refreshJwt, 259 } 260 await finalizeSession() 261 state.step = 'redirect-to-dashboard' 262 } 263 } catch (err) { 264 setError(err) 265 } finally { 266 state.submitting = false 267 } 268 } 269 270 async function activateAccount() { 271 state.submitting = true 272 state.error = null 273 274 try { 275 await api.activateAccount(state.session!.accessJwt) 276 await finalizeSession() 277 state.step = 'redirect-to-dashboard' 278 } catch (err) { 279 setError(err) 280 } finally { 281 state.submitting = false 282 } 283 } 284 285 function goBack() { 286 switch (state.step) { 287 case 'key-choice': 288 state.step = 'info' 289 break 290 case 'initial-did-doc': 291 state.step = 'key-choice' 292 break 293 case 'passkey': 294 state.step = state.info.didType === 'web-external' ? 'initial-did-doc' : 'info' 295 break 296 } 297 } 298 299 async function finalizeSession() { 300 if (!state.session || !state.account) return 301 const { setSession } = await import('../auth.svelte') 302 setSession({ 303 did: state.account.did, 304 handle: state.account.handle, 305 accessJwt: state.session.accessJwt, 306 refreshJwt: state.session.refreshJwt, 307 }) 308 } 309 310 return { 311 get state() { return state }, 312 get info() { return state.info }, 313 get externalDidWeb() { return state.externalDidWeb }, 314 get account() { return state.account }, 315 get session() { return state.session }, 316 317 getPdsEndpoint, 318 getPdsDid, 319 getFullHandle, 320 extractDomain, 321 322 proceedFromInfo, 323 selectKeyMode, 324 confirmInitialDidDoc, 325 createPasswordAccount, 326 createPasskeyAccount, 327 setPasskeyComplete, 328 proceedFromAppPassword, 329 verifyAccount, 330 activateAccount, 331 finalizeSession, 332 goBack, 333 334 setError(msg: string) { state.error = msg }, 335 clearError() { state.error = null }, 336 setSubmitting(val: boolean) { state.submitting = val }, 337 } 338} 339 340export type RegistrationFlow = ReturnType<typeof createRegistrationFlow>