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