this repo has no description
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>;