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