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