this repo has no description
1import type {
2 InboundMigrationState,
3 InboundStep,
4 MigrationProgress,
5 OutboundMigrationState,
6 OutboundStep,
7 ServerDescription,
8 StoredMigrationState,
9} from "./types";
10import {
11 AtprotoClient,
12 createLocalClient,
13 resolvePdsUrl,
14} from "./atproto-client";
15import {
16 clearMigrationState,
17 loadMigrationState,
18 saveMigrationState,
19 updateProgress,
20 updateStep,
21} from "./storage";
22
23function migrationLog(stage: string, data?: Record<string, unknown>) {
24 const timestamp = new Date().toISOString();
25 const msg = `[MIGRATION ${timestamp}] ${stage}`;
26 if (data) {
27 console.log(msg, JSON.stringify(data, null, 2));
28 } else {
29 console.log(msg);
30 }
31}
32
33function createInitialProgress(): MigrationProgress {
34 return {
35 repoExported: false,
36 repoImported: false,
37 blobsTotal: 0,
38 blobsMigrated: 0,
39 blobsFailed: [],
40 prefsMigrated: false,
41 plcSigned: false,
42 activated: false,
43 deactivated: false,
44 currentOperation: "",
45 };
46}
47
48export function createInboundMigrationFlow() {
49 let state = $state<InboundMigrationState>({
50 direction: "inbound",
51 step: "welcome",
52 sourcePdsUrl: "",
53 sourceDid: "",
54 sourceHandle: "",
55 targetHandle: "",
56 targetEmail: "",
57 targetPassword: "",
58 inviteCode: "",
59 sourceAccessToken: null,
60 sourceRefreshToken: null,
61 serviceAuthToken: null,
62 emailVerifyToken: "",
63 plcToken: "",
64 progress: createInitialProgress(),
65 error: null,
66 requires2FA: false,
67 twoFactorCode: "",
68 });
69
70 let sourceClient: AtprotoClient | null = null;
71 let localClient: AtprotoClient | null = null;
72 let localServerInfo: ServerDescription | null = null;
73
74 function setStep(step: InboundStep) {
75 state.step = step;
76 state.error = null;
77 saveMigrationState(state);
78 updateStep(step);
79 }
80
81 function setError(error: string) {
82 state.error = error;
83 saveMigrationState(state);
84 }
85
86 function setProgress(updates: Partial<MigrationProgress>) {
87 state.progress = { ...state.progress, ...updates };
88 updateProgress(updates);
89 }
90
91 async function loadLocalServerInfo(): Promise<ServerDescription> {
92 if (!localClient) {
93 localClient = createLocalClient();
94 }
95 if (!localServerInfo) {
96 localServerInfo = await localClient.describeServer();
97 }
98 return localServerInfo;
99 }
100
101 async function resolveSourcePds(handle: string): Promise<void> {
102 try {
103 const { did, pdsUrl } = await resolvePdsUrl(handle);
104 state.sourcePdsUrl = pdsUrl;
105 state.sourceDid = did;
106 state.sourceHandle = handle;
107 sourceClient = new AtprotoClient(pdsUrl);
108 } catch (e) {
109 throw new Error(`Could not resolve handle: ${(e as Error).message}`);
110 }
111 }
112
113 async function loginToSource(
114 handle: string,
115 password: string,
116 twoFactorCode?: string,
117 ): Promise<void> {
118 migrationLog("loginToSource START", { handle, has2FA: !!twoFactorCode });
119
120 if (!state.sourcePdsUrl) {
121 await resolveSourcePds(handle);
122 }
123
124 if (!sourceClient) {
125 sourceClient = new AtprotoClient(state.sourcePdsUrl);
126 }
127
128 try {
129 migrationLog("loginToSource: Calling createSession on OLD PDS", {
130 pdsUrl: state.sourcePdsUrl,
131 });
132 const session = await sourceClient.login(handle, password, twoFactorCode);
133 migrationLog("loginToSource SUCCESS", {
134 did: session.did,
135 handle: session.handle,
136 pdsUrl: state.sourcePdsUrl,
137 });
138 state.sourceAccessToken = session.accessJwt;
139 state.sourceRefreshToken = session.refreshJwt;
140 state.sourceDid = session.did;
141 state.sourceHandle = session.handle;
142 state.requires2FA = false;
143 saveMigrationState(state);
144 } catch (e) {
145 const err = e as Error & { error?: string };
146 migrationLog("loginToSource FAILED", {
147 error: err.message,
148 errorCode: err.error,
149 });
150 if (err.error === "AuthFactorTokenRequired") {
151 state.requires2FA = true;
152 throw new Error(
153 "Two-factor authentication required. Please enter the code sent to your email.",
154 );
155 }
156 throw e;
157 }
158 }
159
160 async function checkHandleAvailability(handle: string): Promise<boolean> {
161 if (!localClient) {
162 localClient = createLocalClient();
163 }
164 try {
165 await localClient.resolveHandle(handle);
166 return false;
167 } catch {
168 return true;
169 }
170 }
171
172 async function authenticateToLocal(
173 email: string,
174 password: string,
175 ): Promise<void> {
176 if (!localClient) {
177 localClient = createLocalClient();
178 }
179 await localClient.loginDeactivated(email, password);
180 }
181
182 async function startMigration(): Promise<void> {
183 migrationLog("startMigration START", {
184 sourceDid: state.sourceDid,
185 sourceHandle: state.sourceHandle,
186 targetHandle: state.targetHandle,
187 sourcePdsUrl: state.sourcePdsUrl,
188 });
189
190 if (!sourceClient || !state.sourceAccessToken) {
191 migrationLog("startMigration ERROR: Not logged in to source PDS");
192 throw new Error("Not logged in to source PDS");
193 }
194
195 if (!localClient) {
196 localClient = createLocalClient();
197 }
198
199 setStep("migrating");
200 setProgress({ currentOperation: "Getting service auth token..." });
201
202 try {
203 migrationLog("startMigration: Loading local server info");
204 const serverInfo = await loadLocalServerInfo();
205 migrationLog("startMigration: Got server info", {
206 serverDid: serverInfo.did,
207 });
208
209 migrationLog("startMigration: Getting service auth token from OLD PDS");
210 const { token } = await sourceClient.getServiceAuth(
211 serverInfo.did,
212 "com.atproto.server.createAccount",
213 );
214 migrationLog("startMigration: Got service auth token");
215 state.serviceAuthToken = token;
216
217 setProgress({ currentOperation: "Creating account on new PDS..." });
218
219 const accountParams = {
220 did: state.sourceDid,
221 handle: state.targetHandle,
222 email: state.targetEmail,
223 password: state.targetPassword,
224 inviteCode: state.inviteCode || undefined,
225 };
226
227 migrationLog("startMigration: Creating account on NEW PDS", {
228 did: accountParams.did,
229 handle: accountParams.handle,
230 });
231 const session = await localClient.createAccount(accountParams, token);
232 migrationLog("startMigration: Account created on NEW PDS", {
233 did: session.did,
234 });
235 localClient.setAccessToken(session.accessJwt);
236
237 setProgress({ currentOperation: "Exporting repository..." });
238 migrationLog("startMigration: Exporting repo from OLD PDS");
239 const exportStart = Date.now();
240 const car = await sourceClient.getRepo(state.sourceDid);
241 migrationLog("startMigration: Repo exported", {
242 durationMs: Date.now() - exportStart,
243 sizeBytes: car.byteLength,
244 });
245 setProgress({
246 repoExported: true,
247 currentOperation: "Importing repository...",
248 });
249
250 migrationLog("startMigration: Importing repo to NEW PDS");
251 const importStart = Date.now();
252 await localClient.importRepo(car);
253 migrationLog("startMigration: Repo imported", {
254 durationMs: Date.now() - importStart,
255 });
256 setProgress({
257 repoImported: true,
258 currentOperation: "Counting blobs...",
259 });
260
261 const accountStatus = await localClient.checkAccountStatus();
262 migrationLog("startMigration: Account status", {
263 expectedBlobs: accountStatus.expectedBlobs,
264 importedBlobs: accountStatus.importedBlobs,
265 });
266 setProgress({
267 blobsTotal: accountStatus.expectedBlobs,
268 currentOperation: "Migrating blobs...",
269 });
270
271 await migrateBlobs();
272
273 setProgress({ currentOperation: "Migrating preferences..." });
274 await migratePreferences();
275
276 migrationLog(
277 "startMigration: Initial migration complete, waiting for email verification",
278 );
279 setStep("email-verify");
280 } catch (e) {
281 const err = e as Error & { error?: string; status?: number };
282 const message = err.message || err.error ||
283 `Unknown error (status ${err.status || "unknown"})`;
284 migrationLog("startMigration FAILED", {
285 error: message,
286 errorCode: err.error,
287 status: err.status,
288 stack: err.stack,
289 });
290 setError(message);
291 setStep("error");
292 }
293 }
294
295 async function migrateBlobs(): Promise<void> {
296 if (!sourceClient || !localClient) return;
297
298 let cursor: string | undefined;
299 let migrated = 0;
300
301 do {
302 const { blobs, cursor: nextCursor } = await localClient.listMissingBlobs(
303 cursor,
304 100,
305 );
306
307 for (const blob of blobs) {
308 try {
309 setProgress({
310 currentOperation: `Migrating blob ${
311 migrated + 1
312 }/${state.progress.blobsTotal}...`,
313 });
314
315 const blobData = await sourceClient.getBlob(
316 state.sourceDid,
317 blob.cid,
318 );
319 await localClient.uploadBlob(blobData, "application/octet-stream");
320 migrated++;
321 setProgress({ blobsMigrated: migrated });
322 } catch (e) {
323 state.progress.blobsFailed.push(blob.cid);
324 }
325 }
326
327 cursor = nextCursor;
328 } while (cursor);
329 }
330
331 async function migratePreferences(): Promise<void> {
332 if (!sourceClient || !localClient) return;
333
334 try {
335 const prefs = await sourceClient.getPreferences();
336 await localClient.putPreferences(prefs);
337 setProgress({ prefsMigrated: true });
338 } catch {
339 }
340 }
341
342 async function submitEmailVerifyToken(
343 token: string,
344 localPassword?: string,
345 ): Promise<void> {
346 if (!localClient) {
347 localClient = createLocalClient();
348 }
349
350 state.emailVerifyToken = token;
351 setError(null);
352
353 try {
354 await localClient.verifyToken(token, state.targetEmail);
355
356 if (!sourceClient) {
357 setStep("source-login");
358 setError(
359 "Email verified! Please log in to your old account again to complete the migration.",
360 );
361 return;
362 }
363
364 if (localPassword) {
365 setProgress({ currentOperation: "Authenticating to new PDS..." });
366 await localClient.loginDeactivated(state.targetEmail, localPassword);
367 }
368
369 if (!localClient.getAccessToken()) {
370 setError("Email verified! Please enter your password to continue.");
371 return;
372 }
373
374 setProgress({ currentOperation: "Requesting PLC operation token..." });
375 await sourceClient.requestPlcOperationSignature();
376 setStep("plc-token");
377 } catch (e) {
378 const err = e as Error & { error?: string; status?: number };
379 const message = err.message || err.error ||
380 `Unknown error (status ${err.status || "unknown"})`;
381 setError(message);
382 }
383 }
384
385 async function resendEmailVerification(): Promise<void> {
386 if (!localClient) {
387 localClient = createLocalClient();
388 }
389 await localClient.resendMigrationVerification();
390 }
391
392 let checkingEmailVerification = false;
393
394 async function checkEmailVerifiedAndProceed(): Promise<boolean> {
395 if (checkingEmailVerification) return false;
396 if (!sourceClient || !localClient) return false;
397
398 checkingEmailVerification = true;
399 try {
400 await localClient.loginDeactivated(
401 state.targetEmail,
402 state.targetPassword,
403 );
404 await sourceClient.requestPlcOperationSignature();
405 setStep("plc-token");
406 return true;
407 } catch (e) {
408 const err = e as Error & { error?: string };
409 if (err.error === "AccountNotVerified") {
410 return false;
411 }
412 return false;
413 } finally {
414 checkingEmailVerification = false;
415 }
416 }
417
418 async function submitPlcToken(token: string): Promise<void> {
419 migrationLog("submitPlcToken START", {
420 sourceDid: state.sourceDid,
421 sourceHandle: state.sourceHandle,
422 targetHandle: state.targetHandle,
423 sourcePdsUrl: state.sourcePdsUrl,
424 });
425
426 if (!sourceClient || !localClient) {
427 migrationLog("submitPlcToken ERROR: Not connected to PDSes", {
428 hasSourceClient: !!sourceClient,
429 hasLocalClient: !!localClient,
430 });
431 throw new Error("Not connected to PDSes");
432 }
433
434 state.plcToken = token;
435 setStep("finalizing");
436 setProgress({ currentOperation: "Signing PLC operation..." });
437
438 try {
439 migrationLog("Step 1: Getting recommended DID credentials from NEW PDS");
440 const credentials = await localClient.getRecommendedDidCredentials();
441 migrationLog("Step 1 COMPLETE: Got credentials", {
442 rotationKeys: credentials.rotationKeys,
443 alsoKnownAs: credentials.alsoKnownAs,
444 verificationMethods: credentials.verificationMethods,
445 services: credentials.services,
446 });
447
448 migrationLog("Step 2: Signing PLC operation on OLD PDS", {
449 sourcePdsUrl: state.sourcePdsUrl,
450 });
451 const signStart = Date.now();
452 const { operation } = await sourceClient.signPlcOperation({
453 token,
454 ...credentials,
455 });
456 migrationLog("Step 2 COMPLETE: PLC operation signed", {
457 durationMs: Date.now() - signStart,
458 operationType: operation.type,
459 operationPrev: operation.prev,
460 });
461
462 setProgress({
463 plcSigned: true,
464 currentOperation: "Submitting PLC operation...",
465 });
466 migrationLog("Step 3: Submitting PLC operation to NEW PDS");
467 const submitStart = Date.now();
468 await localClient.submitPlcOperation(operation);
469 migrationLog("Step 3 COMPLETE: PLC operation submitted", {
470 durationMs: Date.now() - submitStart,
471 });
472
473 setProgress({
474 currentOperation: "Activating account (waiting for DID propagation)...",
475 });
476 migrationLog("Step 4: Activating account on NEW PDS");
477 const activateStart = Date.now();
478 await localClient.activateAccount();
479 migrationLog("Step 4 COMPLETE: Account activated on NEW PDS", {
480 durationMs: Date.now() - activateStart,
481 });
482 setProgress({ activated: true });
483
484 setProgress({ currentOperation: "Deactivating old account..." });
485 migrationLog("Step 5: Deactivating account on OLD PDS", {
486 sourcePdsUrl: state.sourcePdsUrl,
487 });
488 const deactivateStart = Date.now();
489 try {
490 await sourceClient.deactivateAccount();
491 migrationLog("Step 5 COMPLETE: Account deactivated on OLD PDS", {
492 durationMs: Date.now() - deactivateStart,
493 success: true,
494 });
495 setProgress({ deactivated: true });
496 } catch (deactivateErr) {
497 const err = deactivateErr as Error & {
498 error?: string;
499 status?: number;
500 };
501 migrationLog("Step 5 FAILED: Could not deactivate on OLD PDS", {
502 durationMs: Date.now() - deactivateStart,
503 error: err.message,
504 errorCode: err.error,
505 status: err.status,
506 });
507 }
508
509 migrationLog("submitPlcToken SUCCESS: Migration complete", {
510 sourceDid: state.sourceDid,
511 newHandle: state.targetHandle,
512 });
513 setStep("success");
514 clearMigrationState();
515 } catch (e) {
516 const err = e as Error & { error?: string; status?: number };
517 const message = err.message || err.error ||
518 `Unknown error (status ${err.status || "unknown"})`;
519 migrationLog("submitPlcToken FAILED", {
520 error: message,
521 errorCode: err.error,
522 status: err.status,
523 stack: err.stack,
524 });
525 state.step = "plc-token";
526 state.error = message;
527 saveMigrationState(state);
528 }
529 }
530
531 async function requestPlcToken(): Promise<void> {
532 if (!sourceClient) {
533 throw new Error("Not connected to source PDS");
534 }
535 setProgress({ currentOperation: "Requesting PLC operation token..." });
536 await sourceClient.requestPlcOperationSignature();
537 }
538
539 async function resendPlcToken(): Promise<void> {
540 if (!sourceClient) {
541 throw new Error("Not connected to source PDS");
542 }
543 await sourceClient.requestPlcOperationSignature();
544 }
545
546 function reset(): void {
547 state = {
548 direction: "inbound",
549 step: "welcome",
550 sourcePdsUrl: "",
551 sourceDid: "",
552 sourceHandle: "",
553 targetHandle: "",
554 targetEmail: "",
555 targetPassword: "",
556 inviteCode: "",
557 sourceAccessToken: null,
558 sourceRefreshToken: null,
559 serviceAuthToken: null,
560 emailVerifyToken: "",
561 plcToken: "",
562 progress: createInitialProgress(),
563 error: null,
564 requires2FA: false,
565 twoFactorCode: "",
566 };
567 sourceClient = null;
568 clearMigrationState();
569 }
570
571 async function resumeFromState(stored: StoredMigrationState): Promise<void> {
572 if (stored.direction !== "inbound") return;
573
574 state.sourcePdsUrl = stored.sourcePdsUrl;
575 state.sourceDid = stored.sourceDid;
576 state.sourceHandle = stored.sourceHandle;
577 state.targetHandle = stored.targetHandle;
578 state.targetEmail = stored.targetEmail;
579 state.progress = {
580 ...createInitialProgress(),
581 ...stored.progress,
582 };
583
584 state.step = "source-login";
585 }
586
587 function getLocalSession():
588 | { accessJwt: string; did: string; handle: string }
589 | null {
590 if (!localClient) return null;
591 const token = localClient.getAccessToken();
592 if (!token) return null;
593 return {
594 accessJwt: token,
595 did: state.sourceDid,
596 handle: state.targetHandle,
597 };
598 }
599
600 return {
601 get state() {
602 return state;
603 },
604 setStep,
605 setError,
606 loadLocalServerInfo,
607 loginToSource,
608 authenticateToLocal,
609 checkHandleAvailability,
610 startMigration,
611 submitEmailVerifyToken,
612 resendEmailVerification,
613 checkEmailVerifiedAndProceed,
614 requestPlcToken,
615 submitPlcToken,
616 resendPlcToken,
617 reset,
618 resumeFromState,
619 getLocalSession,
620
621 updateField<K extends keyof InboundMigrationState>(
622 field: K,
623 value: InboundMigrationState[K],
624 ) {
625 state[field] = value;
626 },
627 };
628}
629
630export function createOutboundMigrationFlow() {
631 let state = $state<OutboundMigrationState>({
632 direction: "outbound",
633 step: "welcome",
634 localDid: "",
635 localHandle: "",
636 targetPdsUrl: "",
637 targetPdsDid: "",
638 targetHandle: "",
639 targetEmail: "",
640 targetPassword: "",
641 inviteCode: "",
642 targetAccessToken: null,
643 targetRefreshToken: null,
644 serviceAuthToken: null,
645 plcToken: "",
646 progress: createInitialProgress(),
647 error: null,
648 targetServerInfo: null,
649 });
650
651 let localClient: AtprotoClient | null = null;
652 let targetClient: AtprotoClient | null = null;
653
654 function setStep(step: OutboundStep) {
655 state.step = step;
656 state.error = null;
657 saveMigrationState(state);
658 updateStep(step);
659 }
660
661 function setError(error: string) {
662 state.error = error;
663 saveMigrationState(state);
664 }
665
666 function setProgress(updates: Partial<MigrationProgress>) {
667 state.progress = { ...state.progress, ...updates };
668 updateProgress(updates);
669 }
670
671 async function validateTargetPds(url: string): Promise<ServerDescription> {
672 const normalizedUrl = url.replace(/\/$/, "");
673 targetClient = new AtprotoClient(normalizedUrl);
674
675 try {
676 const serverInfo = await targetClient.describeServer();
677 state.targetPdsUrl = normalizedUrl;
678 state.targetPdsDid = serverInfo.did;
679 state.targetServerInfo = serverInfo;
680 return serverInfo;
681 } catch (e) {
682 throw new Error(`Could not connect to PDS: ${(e as Error).message}`);
683 }
684 }
685
686 function initLocalClient(
687 accessToken: string,
688 did?: string,
689 handle?: string,
690 ): void {
691 localClient = createLocalClient();
692 localClient.setAccessToken(accessToken);
693 if (did) {
694 state.localDid = did;
695 }
696 if (handle) {
697 state.localHandle = handle;
698 }
699 }
700
701 async function startMigration(currentDid: string): Promise<void> {
702 if (!localClient || !targetClient) {
703 throw new Error("Not connected to PDSes");
704 }
705
706 setStep("migrating");
707 setProgress({ currentOperation: "Getting service auth token..." });
708
709 try {
710 const { token } = await localClient.getServiceAuth(
711 state.targetPdsDid,
712 "com.atproto.server.createAccount",
713 );
714 state.serviceAuthToken = token;
715
716 setProgress({ currentOperation: "Creating account on new PDS..." });
717
718 const accountParams = {
719 did: currentDid,
720 handle: state.targetHandle,
721 email: state.targetEmail,
722 password: state.targetPassword,
723 inviteCode: state.inviteCode || undefined,
724 };
725
726 const session = await targetClient.createAccount(accountParams, token);
727 state.targetAccessToken = session.accessJwt;
728 state.targetRefreshToken = session.refreshJwt;
729 targetClient.setAccessToken(session.accessJwt);
730
731 setProgress({ currentOperation: "Exporting repository..." });
732
733 const car = await localClient.getRepo(currentDid);
734 setProgress({
735 repoExported: true,
736 currentOperation: "Importing repository...",
737 });
738
739 await targetClient.importRepo(car);
740 setProgress({
741 repoImported: true,
742 currentOperation: "Counting blobs...",
743 });
744
745 const accountStatus = await targetClient.checkAccountStatus();
746 setProgress({
747 blobsTotal: accountStatus.expectedBlobs,
748 currentOperation: "Migrating blobs...",
749 });
750
751 await migrateBlobs(currentDid);
752
753 setProgress({ currentOperation: "Migrating preferences..." });
754 await migratePreferences();
755
756 setProgress({ currentOperation: "Requesting PLC operation token..." });
757 await localClient.requestPlcOperationSignature();
758
759 setStep("plc-token");
760 } catch (e) {
761 const err = e as Error & { error?: string; status?: number };
762 const message = err.message || err.error ||
763 `Unknown error (status ${err.status || "unknown"})`;
764 setError(message);
765 setStep("error");
766 }
767 }
768
769 async function migrateBlobs(did: string): Promise<void> {
770 if (!localClient || !targetClient) return;
771
772 let cursor: string | undefined;
773 let migrated = 0;
774
775 do {
776 const { blobs, cursor: nextCursor } = await targetClient.listMissingBlobs(
777 cursor,
778 100,
779 );
780
781 for (const blob of blobs) {
782 try {
783 setProgress({
784 currentOperation: `Migrating blob ${
785 migrated + 1
786 }/${state.progress.blobsTotal}...`,
787 });
788
789 const blobData = await localClient.getBlob(did, blob.cid);
790 await targetClient.uploadBlob(blobData, "application/octet-stream");
791 migrated++;
792 setProgress({ blobsMigrated: migrated });
793 } catch (e) {
794 state.progress.blobsFailed.push(blob.cid);
795 }
796 }
797
798 cursor = nextCursor;
799 } while (cursor);
800 }
801
802 async function migratePreferences(): Promise<void> {
803 if (!localClient || !targetClient) return;
804
805 try {
806 const prefs = await localClient.getPreferences();
807 await targetClient.putPreferences(prefs);
808 setProgress({ prefsMigrated: true });
809 } catch {
810 }
811 }
812
813 async function submitPlcToken(token: string): Promise<void> {
814 if (!localClient || !targetClient) {
815 throw new Error("Not connected to PDSes");
816 }
817
818 state.plcToken = token;
819 setStep("finalizing");
820 setProgress({ currentOperation: "Signing PLC operation..." });
821
822 try {
823 const credentials = await targetClient.getRecommendedDidCredentials();
824
825 const { operation } = await localClient.signPlcOperation({
826 token,
827 ...credentials,
828 });
829
830 setProgress({
831 plcSigned: true,
832 currentOperation: "Submitting PLC operation...",
833 });
834
835 await targetClient.submitPlcOperation(operation);
836
837 setProgress({ currentOperation: "Activating account on new PDS..." });
838 await targetClient.activateAccount();
839 setProgress({ activated: true });
840
841 setProgress({ currentOperation: "Deactivating old account..." });
842 try {
843 await localClient.deactivateAccount();
844 setProgress({ deactivated: true });
845 } catch {
846 }
847
848 if (state.localDid.startsWith("did:web:")) {
849 setProgress({
850 currentOperation: "Updating DID document forwarding...",
851 });
852 try {
853 await localClient.updateMigrationForwarding(state.targetPdsUrl);
854 } catch (e) {
855 console.warn("Failed to update migration forwarding:", e);
856 }
857 }
858
859 setStep("success");
860 clearMigrationState();
861 } catch (e) {
862 const err = e as Error & { error?: string; status?: number };
863 const message = err.message || err.error ||
864 `Unknown error (status ${err.status || "unknown"})`;
865 setError(message);
866 setStep("plc-token");
867 }
868 }
869
870 async function resendPlcToken(): Promise<void> {
871 if (!localClient) {
872 throw new Error("Not connected to local PDS");
873 }
874 await localClient.requestPlcOperationSignature();
875 }
876
877 function reset(): void {
878 state = {
879 direction: "outbound",
880 step: "welcome",
881 localDid: "",
882 localHandle: "",
883 targetPdsUrl: "",
884 targetPdsDid: "",
885 targetHandle: "",
886 targetEmail: "",
887 targetPassword: "",
888 inviteCode: "",
889 targetAccessToken: null,
890 targetRefreshToken: null,
891 serviceAuthToken: null,
892 plcToken: "",
893 progress: createInitialProgress(),
894 error: null,
895 targetServerInfo: null,
896 };
897 localClient = null;
898 targetClient = null;
899 clearMigrationState();
900 }
901
902 return {
903 get state() {
904 return state;
905 },
906 setStep,
907 setError,
908 validateTargetPds,
909 initLocalClient,
910 startMigration,
911 submitPlcToken,
912 resendPlcToken,
913 reset,
914
915 updateField<K extends keyof OutboundMigrationState>(
916 field: K,
917 value: OutboundMigrationState[K],
918 ) {
919 state[field] = value;
920 },
921 };
922}
923
924export type InboundMigrationFlow = ReturnType<
925 typeof createInboundMigrationFlow
926>;
927export type OutboundMigrationFlow = ReturnType<
928 typeof createOutboundMigrationFlow
929>;