+35
-9
.zed/settings.json
+35
-9
.zed/settings.json
···
1
{
2
"languages": {
3
"TypeScript": {
4
"language_servers": [
5
-
"wakatime",
6
"deno",
7
"!typescript-language-server",
8
"!vtsls",
9
-
"!eslint"
10
-
],
11
-
"formatter": "language_server"
12
},
13
"TSX": {
14
"language_servers": [
15
-
"wakatime",
16
"deno",
17
"!typescript-language-server",
18
"!vtsls",
19
-
"!eslint"
20
-
],
21
-
"formatter": "language_server"
22
}
23
-
}
24
}
···
1
+
// Folder-specific settings
2
+
//
3
+
// For a full list of overridable settings, and general information on folder-specific settings,
4
+
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
5
{
6
+
"lsp": {
7
+
"deno": {
8
+
"settings": {
9
+
"deno": {
10
+
"enable": true,
11
+
"cacheOnSave": true,
12
+
"suggest": {
13
+
"imports": {
14
+
"autoDiscover": true
15
+
}
16
+
}
17
+
}
18
+
}
19
+
}
20
+
},
21
"languages": {
22
+
"JavaScript": {
23
+
"language_servers": [
24
+
"deno",
25
+
"!vtsls",
26
+
"!eslint",
27
+
"..."
28
+
]
29
+
},
30
"TypeScript": {
31
"language_servers": [
32
"deno",
33
"!typescript-language-server",
34
"!vtsls",
35
+
"!eslint",
36
+
"..."
37
+
]
38
},
39
"TSX": {
40
"language_servers": [
41
"deno",
42
"!typescript-language-server",
43
"!vtsls",
44
+
"!eslint",
45
+
"..."
46
+
]
47
}
48
+
},
49
+
"formatter": "language_server"
50
}
+155
-66
islands/MigrationProgress.tsx
+155
-66
islands/MigrationProgress.tsx
···
40
*/
41
export default function MigrationProgress(props: MigrationProgressProps) {
42
const [token, setToken] = useState("");
43
-
const [migrationState, setMigrationState] = useState<MigrationStateInfo | null>(null);
44
45
const [steps, setSteps] = useState<MigrationStep[]>([
46
{ name: "Create Account", status: "pending" },
···
139
const getStepDisplayName = (step: MigrationStep, index: number) => {
140
if (step.status === "completed") {
141
switch (index) {
142
-
case 0: return "Account Created";
143
-
case 1: return "Data Migrated";
144
-
case 2: return "Identity Migrated";
145
-
case 3: return "Migration Finalized";
146
}
147
}
148
149
if (step.status === "in-progress") {
150
switch (index) {
151
-
case 0: return "Creating your new account...";
152
-
case 1: return "Migrating your data...";
153
-
case 2: return step.name === "Enter the token sent to your email to complete identity migration"
154
-
? step.name
155
-
: "Migrating your identity...";
156
-
case 3: return "Finalizing migration...";
157
}
158
}
159
160
if (step.status === "verifying") {
161
switch (index) {
162
-
case 0: return "Verifying account creation...";
163
-
case 1: return "Verifying data migration...";
164
-
case 2: return "Verifying identity migration...";
165
-
case 3: return "Verifying migration completion...";
166
}
167
}
168
···
268
console.error("Blob migration: Error response:", json);
269
throw new Error(json.message || "Failed to migrate blobs");
270
} catch {
271
-
console.error("Blob migration: Non-JSON error response:", blobsText);
272
throw new Error(blobsText || "Failed to migrate blobs");
273
}
274
}
···
290
console.error("Preferences migration: Error response:", json);
291
throw new Error(json.message || "Failed to migrate preferences");
292
} catch {
293
-
console.error("Preferences migration: Non-JSON error response:", prefsText);
294
throw new Error(prefsText || "Failed to migrate preferences");
295
}
296
}
···
329
if (!requestRes.ok) {
330
try {
331
const json = JSON.parse(requestText);
332
-
throw new Error(json.message || "Failed to request identity migration");
333
} catch {
334
-
throw new Error(requestText || "Failed to request identity migration");
335
}
336
}
337
···
345
console.log("Identity migration requested successfully");
346
347
// Update step name to prompt for token
348
-
setSteps(prevSteps =>
349
prevSteps.map((step, i) =>
350
i === 2
351
-
? { ...step, name: "Enter the token sent to your email to complete identity migration" }
352
: step
353
)
354
);
···
389
if (!identityRes.ok) {
390
try {
391
const json = JSON.parse(identityData);
392
-
throw new Error(json.message || "Failed to complete identity migration");
393
} catch {
394
-
throw new Error(identityData || "Failed to complete identity migration");
395
}
396
}
397
···
404
} catch {
405
throw new Error("Invalid response from server");
406
}
407
-
408
409
updateStepStatus(2, "verifying");
410
const verified = await verifyStep(2);
···
554
updateStepStatus(stepNum, "completed");
555
return true;
556
} else {
557
-
console.log(`Verification: Step ${stepNum + 1} is not ready:`, data.reason);
558
const statusDetails = {
559
activated: data.activated,
560
validDid: data.validDid,
···
565
indexedRecords: data.indexedRecords,
566
privateStateValues: data.privateStateValues,
567
expectedBlobs: data.expectedBlobs,
568
-
importedBlobs: data.importedBlobs
569
};
570
-
console.log(`Verification: Step ${stepNum + 1} status details:`, statusDetails);
571
-
const errorMessage = `${data.reason || "Verification failed"}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
572
updateStepStatus(stepNum, "error", errorMessage);
573
return false;
574
}
575
} catch (e) {
576
console.error(`Verification: Error in step ${stepNum + 1}:`, e);
577
-
updateStepStatus(stepNum, "error", e instanceof Error ? e.message : String(e));
578
return false;
579
}
580
};
···
583
<div class="space-y-8">
584
{/* Migration state alert */}
585
{migrationState && !migrationState.allowMigration && (
586
-
<div class={`p-4 rounded-lg border ${
587
-
migrationState.state === "maintenance"
588
-
? "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200"
589
-
: "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200"
590
-
}`}>
591
<div class="flex items-center">
592
-
<div class={`mr-3 ${
593
-
migrationState.state === "maintenance" ? "text-yellow-600 dark:text-yellow-400" : "text-red-600 dark:text-red-400"
594
-
}`}>
595
{migrationState.state === "maintenance" ? "⚠️" : "🚫"}
596
</div>
597
<div>
598
<h3 class="font-semibold mb-1">
599
-
{migrationState.state === "maintenance" ? "Maintenance Mode" : "Service Unavailable"}
600
</h3>
601
<p class="text-sm">{migrationState.message}</p>
602
</div>
···
635
</p>
636
)}
637
{index === 2 && step.status === "in-progress" &&
638
-
step.name === "Enter the token sent to your email to complete identity migration" && (
639
<div class="mt-4 space-y-4">
640
<p class="text-sm text-blue-800 dark:text-blue-200">
641
-
Please check your email for the migration token and enter it below:
642
</p>
643
<div class="flex space-x-2">
644
<input
···
657
</button>
658
</div>
659
</div>
660
-
)
661
-
}
662
</div>
663
</div>
664
))}
···
666
667
{steps[3].status === "completed" && (
668
<div class="p-4 bg-green-50 dark:bg-green-900 rounded-lg border-2 border-green-200 dark:border-green-800">
669
-
<p class="text-sm text-green-800 dark:text-green-200">
670
-
Migration completed successfully! You can now close this page.
671
</p>
672
-
<button
673
-
type="button"
674
-
onClick={async () => {
675
-
try {
676
-
const response = await fetch("/api/logout", {
677
-
method: "POST",
678
-
credentials: "include",
679
-
});
680
-
if (!response.ok) {
681
-
throw new Error("Logout failed");
682
}
683
-
globalThis.location.href = "/";
684
-
} catch (error) {
685
-
console.error("Failed to logout:", error);
686
-
}
687
-
}}
688
-
class="mt-4 mr-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200"
689
-
>
690
-
Sign Out
691
-
</button>
692
-
<a href="https://ko-fi.com/knotbin" target="_blank" class="mt-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200">
693
-
Donate
694
-
</a>
695
</div>
696
)}
697
</div>
···
40
*/
41
export default function MigrationProgress(props: MigrationProgressProps) {
42
const [token, setToken] = useState("");
43
+
const [migrationState, setMigrationState] = useState<
44
+
MigrationStateInfo | null
45
+
>(null);
46
47
const [steps, setSteps] = useState<MigrationStep[]>([
48
{ name: "Create Account", status: "pending" },
···
141
const getStepDisplayName = (step: MigrationStep, index: number) => {
142
if (step.status === "completed") {
143
switch (index) {
144
+
case 0:
145
+
return "Account Created";
146
+
case 1:
147
+
return "Data Migrated";
148
+
case 2:
149
+
return "Identity Migrated";
150
+
case 3:
151
+
return "Migration Finalized";
152
}
153
}
154
155
if (step.status === "in-progress") {
156
switch (index) {
157
+
case 0:
158
+
return "Creating your new account...";
159
+
case 1:
160
+
return "Migrating your data...";
161
+
case 2:
162
+
return step.name ===
163
+
"Enter the token sent to your email to complete identity migration"
164
+
? step.name
165
+
: "Migrating your identity...";
166
+
case 3:
167
+
return "Finalizing migration...";
168
}
169
}
170
171
if (step.status === "verifying") {
172
switch (index) {
173
+
case 0:
174
+
return "Verifying account creation...";
175
+
case 1:
176
+
return "Verifying data migration...";
177
+
case 2:
178
+
return "Verifying identity migration...";
179
+
case 3:
180
+
return "Verifying migration completion...";
181
}
182
}
183
···
283
console.error("Blob migration: Error response:", json);
284
throw new Error(json.message || "Failed to migrate blobs");
285
} catch {
286
+
console.error(
287
+
"Blob migration: Non-JSON error response:",
288
+
blobsText,
289
+
);
290
throw new Error(blobsText || "Failed to migrate blobs");
291
}
292
}
···
308
console.error("Preferences migration: Error response:", json);
309
throw new Error(json.message || "Failed to migrate preferences");
310
} catch {
311
+
console.error(
312
+
"Preferences migration: Non-JSON error response:",
313
+
prefsText,
314
+
);
315
throw new Error(prefsText || "Failed to migrate preferences");
316
}
317
}
···
350
if (!requestRes.ok) {
351
try {
352
const json = JSON.parse(requestText);
353
+
throw new Error(
354
+
json.message || "Failed to request identity migration",
355
+
);
356
} catch {
357
+
throw new Error(
358
+
requestText || "Failed to request identity migration",
359
+
);
360
}
361
}
362
···
370
console.log("Identity migration requested successfully");
371
372
// Update step name to prompt for token
373
+
setSteps((prevSteps) =>
374
prevSteps.map((step, i) =>
375
i === 2
376
+
? {
377
+
...step,
378
+
name:
379
+
"Enter the token sent to your email to complete identity migration",
380
+
}
381
: step
382
)
383
);
···
418
if (!identityRes.ok) {
419
try {
420
const json = JSON.parse(identityData);
421
+
throw new Error(
422
+
json.message || "Failed to complete identity migration",
423
+
);
424
} catch {
425
+
throw new Error(
426
+
identityData || "Failed to complete identity migration",
427
+
);
428
}
429
}
430
···
437
} catch {
438
throw new Error("Invalid response from server");
439
}
440
441
updateStepStatus(2, "verifying");
442
const verified = await verifyStep(2);
···
586
updateStepStatus(stepNum, "completed");
587
return true;
588
} else {
589
+
console.log(
590
+
`Verification: Step ${stepNum + 1} is not ready:`,
591
+
data.reason,
592
+
);
593
const statusDetails = {
594
activated: data.activated,
595
validDid: data.validDid,
···
600
indexedRecords: data.indexedRecords,
601
privateStateValues: data.privateStateValues,
602
expectedBlobs: data.expectedBlobs,
603
+
importedBlobs: data.importedBlobs,
604
};
605
+
console.log(
606
+
`Verification: Step ${stepNum + 1} status details:`,
607
+
statusDetails,
608
+
);
609
+
const errorMessage = `${
610
+
data.reason || "Verification failed"
611
+
}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
612
updateStepStatus(stepNum, "error", errorMessage);
613
return false;
614
}
615
} catch (e) {
616
console.error(`Verification: Error in step ${stepNum + 1}:`, e);
617
+
updateStepStatus(
618
+
stepNum,
619
+
"error",
620
+
e instanceof Error ? e.message : String(e),
621
+
);
622
return false;
623
}
624
};
···
627
<div class="space-y-8">
628
{/* Migration state alert */}
629
{migrationState && !migrationState.allowMigration && (
630
+
<div
631
+
class={`p-4 rounded-lg border ${
632
+
migrationState.state === "maintenance"
633
+
? "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-200"
634
+
: "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200"
635
+
}`}
636
+
>
637
<div class="flex items-center">
638
+
<div
639
+
class={`mr-3 ${
640
+
migrationState.state === "maintenance"
641
+
? "text-yellow-600 dark:text-yellow-400"
642
+
: "text-red-600 dark:text-red-400"
643
+
}`}
644
+
>
645
{migrationState.state === "maintenance" ? "⚠️" : "🚫"}
646
</div>
647
<div>
648
<h3 class="font-semibold mb-1">
649
+
{migrationState.state === "maintenance"
650
+
? "Maintenance Mode"
651
+
: "Service Unavailable"}
652
</h3>
653
<p class="text-sm">{migrationState.message}</p>
654
</div>
···
687
</p>
688
)}
689
{index === 2 && step.status === "in-progress" &&
690
+
step.name ===
691
+
"Enter the token sent to your email to complete identity migration" &&
692
+
(
693
<div class="mt-4 space-y-4">
694
<p class="text-sm text-blue-800 dark:text-blue-200">
695
+
Please check your email for the migration token and enter
696
+
it below:
697
</p>
698
<div class="flex space-x-2">
699
<input
···
712
</button>
713
</div>
714
</div>
715
+
)}
716
</div>
717
</div>
718
))}
···
720
721
{steps[3].status === "completed" && (
722
<div class="p-4 bg-green-50 dark:bg-green-900 rounded-lg border-2 border-green-200 dark:border-green-800">
723
+
<p class="text-sm text-green-800 dark:text-green-200 pb-2">
724
+
Migration completed successfully! Sign out to finish the process and
725
+
return home.<br />
726
+
Please consider donating to Airport to support server and
727
+
development costs.
728
</p>
729
+
<div class="flex space-x-4">
730
+
<button
731
+
type="button"
732
+
onClick={async () => {
733
+
try {
734
+
const response = await fetch("/api/logout", {
735
+
method: "POST",
736
+
credentials: "include",
737
+
});
738
+
if (!response.ok) {
739
+
throw new Error("Logout failed");
740
+
}
741
+
globalThis.location.href = "/";
742
+
} catch (error) {
743
+
console.error("Failed to logout:", error);
744
}
745
+
}}
746
+
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2"
747
+
>
748
+
<svg
749
+
class="w-5 h-5"
750
+
fill="none"
751
+
stroke="currentColor"
752
+
viewBox="0 0 24 24"
753
+
>
754
+
<path
755
+
stroke-linecap="round"
756
+
stroke-linejoin="round"
757
+
stroke-width="2"
758
+
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
759
+
/>
760
+
</svg>
761
+
<span>Sign Out</span>
762
+
</button>
763
+
<a
764
+
href="https://ko-fi.com/knotbin"
765
+
target="_blank"
766
+
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2"
767
+
>
768
+
<svg
769
+
class="w-5 h-5"
770
+
fill="none"
771
+
stroke="currentColor"
772
+
viewBox="0 0 24 24"
773
+
>
774
+
<path
775
+
stroke-linecap="round"
776
+
stroke-linejoin="round"
777
+
stroke-width="2"
778
+
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
779
+
/>
780
+
</svg>
781
+
<span>Support Us</span>
782
+
</a>
783
+
</div>
784
</div>
785
)}
786
</div>
+7
lib/check-dids.ts
+7
lib/check-dids.ts
···
···
1
+
import { getSession } from "./sessions.ts";
2
+
3
+
export async function checkDidsMatch(req: Request): Promise<boolean> {
4
+
const oldSession = await getSession(req, undefined, false);
5
+
const newSession = await getSession(req, undefined, true);
6
+
return oldSession.did === newSession.did;
7
+
}
+1
-1
lib/migration-state.ts
+1
-1
lib/migration-state.ts
+31
-10
lib/sessions.ts
+31
-10
lib/sessions.ts
···
1
import { Agent } from "npm:@atproto/api";
2
-
import { OauthSession, CredentialSession } from "./types.ts";
3
-
import { getCredentialSession, getCredentialSessionAgent } from "./cred/sessions.ts";
4
import { getOauthSession, getOauthSessionAgent } from "./oauth/sessions.ts";
5
import { IronSession } from "npm:iron-session";
6
···
14
export async function getSession(
15
req: Request,
16
res: Response = new Response(),
17
-
isMigration: boolean = false
18
): Promise<IronSession<OauthSession | CredentialSession>> {
19
if (isMigration) {
20
return await getCredentialSession(req, res, true);
···
23
const credentialSession = await getCredentialSession(req, res);
24
25
if (oauthSession.did) {
26
-
console.log("Oauth session found")
27
return oauthSession;
28
}
29
if (credentialSession.did) {
···
43
export async function getSessionAgent(
44
req: Request,
45
res: Response = new Response(),
46
-
isMigration: boolean = false
47
): Promise<Agent | null> {
48
if (isMigration) {
49
return await getCredentialSessionAgent(req, res, isMigration);
50
}
51
52
const oauthAgent = await getOauthSessionAgent(req);
53
-
const credentialAgent = await getCredentialSessionAgent(req, res, isMigration);
54
55
if (oauthAgent) {
56
return oauthAgent;
···
66
/**
67
* Destroy all sessions for the given request.
68
* @param req - The request object
69
*/
70
-
export async function destroyAllSessions(req: Request) {
71
-
const oauthSession = await getOauthSession(req);
72
-
const credentialSession = await getCredentialSession(req);
73
-
const migrationSession = await getCredentialSession(req, new Response(), true);
74
75
if (oauthSession.did) {
76
oauthSession.destroy();
···
79
credentialSession.destroy();
80
}
81
if (migrationSession.did) {
82
migrationSession.destroy();
83
}
84
}
···
1
import { Agent } from "npm:@atproto/api";
2
+
import { CredentialSession, OauthSession } from "./types.ts";
3
+
import {
4
+
getCredentialSession,
5
+
getCredentialSessionAgent,
6
+
} from "./cred/sessions.ts";
7
import { getOauthSession, getOauthSessionAgent } from "./oauth/sessions.ts";
8
import { IronSession } from "npm:iron-session";
9
···
17
export async function getSession(
18
req: Request,
19
res: Response = new Response(),
20
+
isMigration: boolean = false,
21
): Promise<IronSession<OauthSession | CredentialSession>> {
22
if (isMigration) {
23
return await getCredentialSession(req, res, true);
···
26
const credentialSession = await getCredentialSession(req, res);
27
28
if (oauthSession.did) {
29
+
console.log("Oauth session found");
30
return oauthSession;
31
}
32
if (credentialSession.did) {
···
46
export async function getSessionAgent(
47
req: Request,
48
res: Response = new Response(),
49
+
isMigration: boolean = false,
50
): Promise<Agent | null> {
51
if (isMigration) {
52
return await getCredentialSessionAgent(req, res, isMigration);
53
}
54
55
const oauthAgent = await getOauthSessionAgent(req);
56
+
const credentialAgent = await getCredentialSessionAgent(
57
+
req,
58
+
res,
59
+
isMigration,
60
+
);
61
62
if (oauthAgent) {
63
return oauthAgent;
···
73
/**
74
* Destroy all sessions for the given request.
75
* @param req - The request object
76
+
* @param res - The response object
77
*/
78
+
export async function destroyAllSessions(
79
+
req: Request,
80
+
res?: Response,
81
+
): Promise<Response> {
82
+
const response = res || new Response();
83
+
const oauthSession = await getOauthSession(req, response);
84
+
const credentialSession = await getCredentialSession(req, res);
85
+
const migrationSession = await getCredentialSession(
86
+
req,
87
+
res,
88
+
true,
89
+
);
90
91
if (oauthSession.did) {
92
oauthSession.destroy();
···
95
credentialSession.destroy();
96
}
97
if (migrationSession.did) {
98
+
console.log("DESTROYING MIGRATION SESSION", migrationSession);
99
migrationSession.destroy();
100
+
} else {
101
+
console.log("MIGRATION SESSION NOT FOUND", migrationSession);
102
}
103
+
104
+
return response;
105
}
+1
-1
lib/storage.ts
+1
-1
lib/storage.ts
+4
-4
routes/api/logout.ts
+4
-4
routes/api/logout.ts
···
1
-
import { getSession, destroyAllSessions } from "../../lib/sessions.ts";
2
import { oauthClient } from "../../lib/oauth/client.ts";
3
import { define } from "../../utils.ts";
4
···
13
if (session.did) {
14
// Try to revoke both types of sessions - the one that doesn't exist will just no-op
15
await Promise.all([
16
-
oauthClient.revoke(session.did).catch(console.error)
17
]);
18
// Then destroy the iron session
19
session.destroy();
20
}
21
22
// Destroy all sessions including migration session
23
-
await destroyAllSessions(req);
24
25
-
return response;
26
} catch (error: unknown) {
27
const err = error instanceof Error ? error : new Error(String(error));
28
console.error("Logout failed:", err.message);
···
1
+
import { destroyAllSessions, getSession } from "../../lib/sessions.ts";
2
import { oauthClient } from "../../lib/oauth/client.ts";
3
import { define } from "../../utils.ts";
4
···
13
if (session.did) {
14
// Try to revoke both types of sessions - the one that doesn't exist will just no-op
15
await Promise.all([
16
+
oauthClient.revoke(session.did).catch(console.error),
17
]);
18
// Then destroy the iron session
19
session.destroy();
20
}
21
22
// Destroy all sessions including migration session
23
+
const result = await destroyAllSessions(req, response);
24
25
+
return result;
26
} catch (error: unknown) {
27
const err = error instanceof Error ? error : new Error(String(error));
28
console.error("Logout failed:", err.message);
+2
-2
routes/api/migrate/create.ts
+2
-2
routes/api/migrate/create.ts
···
45
return new Response("Could not create new agent", { status: 400 });
46
}
47
48
-
console.log("getting did")
49
const session = await oldAgent.com.atproto.server.getSession();
50
const accountDid = session.data.did;
51
-
console.log("got did")
52
const describeRes = await newAgent.com.atproto.server.describeServer();
53
const newServerDid = describeRes.data.did;
54
const inviteRequired = describeRes.data.inviteCodeRequired ?? false;
···
45
return new Response("Could not create new agent", { status: 400 });
46
}
47
48
+
console.log("getting did");
49
const session = await oldAgent.com.atproto.server.getSession();
50
const accountDid = session.data.did;
51
+
console.log("got did");
52
const describeRes = await newAgent.com.atproto.server.describeServer();
53
const newServerDid = describeRes.data.did;
54
const inviteRequired = describeRes.data.inviteCodeRequired ?? false;
+178
-44
routes/api/migrate/data/blobs.ts
+178
-44
routes/api/migrate/data/blobs.ts
···
1
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
import { define } from "../../../../utils.ts";
3
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
4
···
41
);
42
}
43
44
// Migrate blobs
45
const migrationLogs: string[] = [];
46
const migratedBlobs: string[] = [];
···
52
53
const startTime = Date.now();
54
console.log(`[${new Date().toISOString()}] Starting blob migration...`);
55
-
migrationLogs.push(`[${new Date().toISOString()}] Starting blob migration...`);
56
57
// First count total blobs
58
console.log(`[${new Date().toISOString()}] Starting blob count...`);
59
-
migrationLogs.push(`[${new Date().toISOString()}] Starting blob count...`);
60
61
const session = await oldAgent.com.atproto.server.getSession();
62
const accountDid = session.data.did;
63
64
do {
65
const pageStartTime = Date.now();
66
-
console.log(`[${new Date().toISOString()}] Counting blobs on page ${pageCount + 1}...`);
67
-
migrationLogs.push(`[${new Date().toISOString()}] Counting blobs on page ${pageCount + 1}...`);
68
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
69
did: accountDid,
70
cursor: blobCursor,
···
74
totalBlobs += newBlobs;
75
const pageTime = Date.now() - pageStartTime;
76
77
-
console.log(`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds, total so far: ${totalBlobs}`);
78
-
migrationLogs.push(`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds, total so far: ${totalBlobs}`);
79
80
pageCount++;
81
blobCursor = listedBlobs.data.cursor;
82
} while (blobCursor);
83
84
-
console.log(`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`);
85
-
migrationLogs.push(`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`);
86
87
// Reset cursor for actual migration
88
blobCursor = undefined;
···
91
92
do {
93
const pageStartTime = Date.now();
94
-
console.log(`[${new Date().toISOString()}] Fetching blob list page ${pageCount + 1}...`);
95
-
migrationLogs.push(`[${new Date().toISOString()}] Fetching blob list page ${pageCount + 1}...`);
96
97
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
98
did: accountDid,
···
100
});
101
102
const pageTime = Date.now() - pageStartTime;
103
-
console.log(`[${new Date().toISOString()}] Found ${listedBlobs.data.cids.length} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds`);
104
-
migrationLogs.push(`[${new Date().toISOString()}] Found ${listedBlobs.data.cids.length} blobs on page ${pageCount + 1} in ${pageTime/1000} seconds`);
105
106
blobCursor = listedBlobs.data.cursor;
107
108
for (const cid of listedBlobs.data.cids) {
109
try {
110
const blobStartTime = Date.now();
111
-
console.log(`[${new Date().toISOString()}] Starting migration for blob ${cid} (${processedBlobs + 1} of ${totalBlobs})...`);
112
-
migrationLogs.push(`[${new Date().toISOString()}] Starting migration for blob ${cid} (${processedBlobs + 1} of ${totalBlobs})...`);
113
114
const blobRes = await oldAgent.com.atproto.sync.getBlob({
115
did: accountDid,
···
123
124
const size = parseInt(contentLength, 10);
125
if (isNaN(size)) {
126
-
throw new Error(`Blob ${cid} has invalid content length: ${contentLength}`);
127
}
128
129
const MAX_SIZE = 200 * 1024 * 1024; // 200MB
130
if (size > MAX_SIZE) {
131
-
throw new Error(`Blob ${cid} exceeds maximum size limit (${size} bytes)`);
132
}
133
134
-
console.log(`[${new Date().toISOString()}] Downloading blob ${cid} (${size} bytes)...`);
135
-
migrationLogs.push(`[${new Date().toISOString()}] Downloading blob ${cid} (${size} bytes)...`);
136
137
if (!blobRes.data) {
138
-
throw new Error(`Failed to download blob ${cid}: No data received`);
139
}
140
141
-
console.log(`[${new Date().toISOString()}] Uploading blob ${cid} to new account...`);
142
-
migrationLogs.push(`[${new Date().toISOString()}] Uploading blob ${cid} to new account...`);
143
144
try {
145
await newAgent.com.atproto.repo.uploadBlob(blobRes.data);
146
const blobTime = Date.now() - blobStartTime;
147
-
console.log(`[${new Date().toISOString()}] Successfully migrated blob ${cid} in ${blobTime/1000} seconds`);
148
-
migrationLogs.push(`[${new Date().toISOString()}] Successfully migrated blob ${cid} in ${blobTime/1000} seconds`);
149
migratedBlobs.push(cid);
150
} catch (uploadError) {
151
-
console.error(`[${new Date().toISOString()}] Failed to upload blob ${cid}:`, uploadError);
152
-
throw new Error(`Upload failed: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`);
153
}
154
} catch (error) {
155
-
const errorMessage = error instanceof Error ? error.message : String(error);
156
-
const detailedError = `[${new Date().toISOString()}] Failed to migrate blob ${cid}: ${errorMessage}`;
157
console.error(detailedError);
158
-
console.error('Full error details:', error);
159
migrationLogs.push(detailedError);
160
failedBlobs.push(cid);
161
}
162
163
processedBlobs++;
164
-
const progressLog = `[${new Date().toISOString()}] Progress: ${processedBlobs}/${totalBlobs} blobs processed (${Math.round((processedBlobs/totalBlobs)*100)}%)`;
165
console.log(progressLog);
166
migrationLogs.push(progressLog);
167
}
···
169
} while (blobCursor);
170
171
const totalTime = Date.now() - startTime;
172
-
const completionMessage = `[${new Date().toISOString()}] Blob migration completed in ${totalTime/1000} seconds: ${migratedBlobs.length} blobs migrated${failedBlobs.length > 0 ? `, ${failedBlobs.length} failed` : ''} (${pageCount} pages processed)`;
173
console.log(completionMessage);
174
migrationLogs.push(completionMessage);
175
···
187
totalBlobs,
188
logs: migrationLogs,
189
timing: {
190
-
totalTime: totalTime/1000
191
-
}
192
}),
193
{
194
status: 200,
195
headers: {
196
"Content-Type": "application/json",
197
...Object.fromEntries(res.headers),
198
-
}
199
-
}
200
);
201
} catch (error) {
202
const message = error instanceof Error ? error.message : String(error);
203
-
console.error(`[${new Date().toISOString()}] Blob migration error:`, message);
204
-
console.error('Full error details:', error);
205
return new Response(
206
JSON.stringify({
207
success: false,
208
message: `Blob migration failed: ${message}`,
209
-
error: error instanceof Error ? {
210
-
name: error.name,
211
-
message: error.message,
212
-
stack: error.stack,
213
-
} : String(error)
214
}),
215
{
216
status: 500,
217
headers: {
218
"Content-Type": "application/json",
219
...Object.fromEntries(res.headers),
220
-
}
221
-
}
222
);
223
}
224
-
}
225
});
···
1
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
3
import { define } from "../../../../utils.ts";
4
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
5
···
42
);
43
}
44
45
+
// Verify DIDs match between sessions
46
+
const didsMatch = await checkDidsMatch(ctx.req);
47
+
if (!didsMatch) {
48
+
return new Response(
49
+
JSON.stringify({
50
+
success: false,
51
+
message: "Invalid state, original and target DIDs do not match",
52
+
}),
53
+
{
54
+
status: 400,
55
+
headers: { "Content-Type": "application/json" },
56
+
},
57
+
);
58
+
}
59
+
60
// Migrate blobs
61
const migrationLogs: string[] = [];
62
const migratedBlobs: string[] = [];
···
68
69
const startTime = Date.now();
70
console.log(`[${new Date().toISOString()}] Starting blob migration...`);
71
+
migrationLogs.push(
72
+
`[${new Date().toISOString()}] Starting blob migration...`,
73
+
);
74
75
// First count total blobs
76
console.log(`[${new Date().toISOString()}] Starting blob count...`);
77
+
migrationLogs.push(
78
+
`[${new Date().toISOString()}] Starting blob count...`,
79
+
);
80
81
const session = await oldAgent.com.atproto.server.getSession();
82
const accountDid = session.data.did;
83
84
do {
85
const pageStartTime = Date.now();
86
+
console.log(
87
+
`[${new Date().toISOString()}] Counting blobs on page ${
88
+
pageCount + 1
89
+
}...`,
90
+
);
91
+
migrationLogs.push(
92
+
`[${new Date().toISOString()}] Counting blobs on page ${
93
+
pageCount + 1
94
+
}...`,
95
+
);
96
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
97
did: accountDid,
98
cursor: blobCursor,
···
102
totalBlobs += newBlobs;
103
const pageTime = Date.now() - pageStartTime;
104
105
+
console.log(
106
+
`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${
107
+
pageCount + 1
108
+
} in ${pageTime / 1000} seconds, total so far: ${totalBlobs}`,
109
+
);
110
+
migrationLogs.push(
111
+
`[${new Date().toISOString()}] Found ${newBlobs} blobs on page ${
112
+
pageCount + 1
113
+
} in ${pageTime / 1000} seconds, total so far: ${totalBlobs}`,
114
+
);
115
116
pageCount++;
117
blobCursor = listedBlobs.data.cursor;
118
} while (blobCursor);
119
120
+
console.log(
121
+
`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`,
122
+
);
123
+
migrationLogs.push(
124
+
`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`,
125
+
);
126
127
// Reset cursor for actual migration
128
blobCursor = undefined;
···
131
132
do {
133
const pageStartTime = Date.now();
134
+
console.log(
135
+
`[${new Date().toISOString()}] Fetching blob list page ${
136
+
pageCount + 1
137
+
}...`,
138
+
);
139
+
migrationLogs.push(
140
+
`[${new Date().toISOString()}] Fetching blob list page ${
141
+
pageCount + 1
142
+
}...`,
143
+
);
144
145
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
146
did: accountDid,
···
148
});
149
150
const pageTime = Date.now() - pageStartTime;
151
+
console.log(
152
+
`[${
153
+
new Date().toISOString()
154
+
}] Found ${listedBlobs.data.cids.length} blobs on page ${
155
+
pageCount + 1
156
+
} in ${pageTime / 1000} seconds`,
157
+
);
158
+
migrationLogs.push(
159
+
`[${
160
+
new Date().toISOString()
161
+
}] Found ${listedBlobs.data.cids.length} blobs on page ${
162
+
pageCount + 1
163
+
} in ${pageTime / 1000} seconds`,
164
+
);
165
166
blobCursor = listedBlobs.data.cursor;
167
168
for (const cid of listedBlobs.data.cids) {
169
try {
170
const blobStartTime = Date.now();
171
+
console.log(
172
+
`[${
173
+
new Date().toISOString()
174
+
}] Starting migration for blob ${cid} (${
175
+
processedBlobs + 1
176
+
} of ${totalBlobs})...`,
177
+
);
178
+
migrationLogs.push(
179
+
`[${
180
+
new Date().toISOString()
181
+
}] Starting migration for blob ${cid} (${
182
+
processedBlobs + 1
183
+
} of ${totalBlobs})...`,
184
+
);
185
186
const blobRes = await oldAgent.com.atproto.sync.getBlob({
187
did: accountDid,
···
195
196
const size = parseInt(contentLength, 10);
197
if (isNaN(size)) {
198
+
throw new Error(
199
+
`Blob ${cid} has invalid content length: ${contentLength}`,
200
+
);
201
}
202
203
const MAX_SIZE = 200 * 1024 * 1024; // 200MB
204
if (size > MAX_SIZE) {
205
+
throw new Error(
206
+
`Blob ${cid} exceeds maximum size limit (${size} bytes)`,
207
+
);
208
}
209
210
+
console.log(
211
+
`[${
212
+
new Date().toISOString()
213
+
}] Downloading blob ${cid} (${size} bytes)...`,
214
+
);
215
+
migrationLogs.push(
216
+
`[${
217
+
new Date().toISOString()
218
+
}] Downloading blob ${cid} (${size} bytes)...`,
219
+
);
220
221
if (!blobRes.data) {
222
+
throw new Error(
223
+
`Failed to download blob ${cid}: No data received`,
224
+
);
225
}
226
227
+
console.log(
228
+
`[${
229
+
new Date().toISOString()
230
+
}] Uploading blob ${cid} to new account...`,
231
+
);
232
+
migrationLogs.push(
233
+
`[${
234
+
new Date().toISOString()
235
+
}] Uploading blob ${cid} to new account...`,
236
+
);
237
238
try {
239
await newAgent.com.atproto.repo.uploadBlob(blobRes.data);
240
const blobTime = Date.now() - blobStartTime;
241
+
console.log(
242
+
`[${
243
+
new Date().toISOString()
244
+
}] Successfully migrated blob ${cid} in ${
245
+
blobTime / 1000
246
+
} seconds`,
247
+
);
248
+
migrationLogs.push(
249
+
`[${
250
+
new Date().toISOString()
251
+
}] Successfully migrated blob ${cid} in ${
252
+
blobTime / 1000
253
+
} seconds`,
254
+
);
255
migratedBlobs.push(cid);
256
} catch (uploadError) {
257
+
console.error(
258
+
`[${new Date().toISOString()}] Failed to upload blob ${cid}:`,
259
+
uploadError,
260
+
);
261
+
throw new Error(
262
+
`Upload failed: ${
263
+
uploadError instanceof Error
264
+
? uploadError.message
265
+
: String(uploadError)
266
+
}`,
267
+
);
268
}
269
} catch (error) {
270
+
const errorMessage = error instanceof Error
271
+
? error.message
272
+
: String(error);
273
+
const detailedError = `[${
274
+
new Date().toISOString()
275
+
}] Failed to migrate blob ${cid}: ${errorMessage}`;
276
console.error(detailedError);
277
+
console.error("Full error details:", error);
278
migrationLogs.push(detailedError);
279
failedBlobs.push(cid);
280
}
281
282
processedBlobs++;
283
+
const progressLog = `[${
284
+
new Date().toISOString()
285
+
}] Progress: ${processedBlobs}/${totalBlobs} blobs processed (${
286
+
Math.round((processedBlobs / totalBlobs) * 100)
287
+
}%)`;
288
console.log(progressLog);
289
migrationLogs.push(progressLog);
290
}
···
292
} while (blobCursor);
293
294
const totalTime = Date.now() - startTime;
295
+
const completionMessage = `[${
296
+
new Date().toISOString()
297
+
}] Blob migration completed in ${
298
+
totalTime / 1000
299
+
} seconds: ${migratedBlobs.length} blobs migrated${
300
+
failedBlobs.length > 0 ? `, ${failedBlobs.length} failed` : ""
301
+
} (${pageCount} pages processed)`;
302
console.log(completionMessage);
303
migrationLogs.push(completionMessage);
304
···
316
totalBlobs,
317
logs: migrationLogs,
318
timing: {
319
+
totalTime: totalTime / 1000,
320
+
},
321
}),
322
{
323
status: 200,
324
headers: {
325
"Content-Type": "application/json",
326
...Object.fromEntries(res.headers),
327
+
},
328
+
},
329
);
330
} catch (error) {
331
const message = error instanceof Error ? error.message : String(error);
332
+
console.error(
333
+
`[${new Date().toISOString()}] Blob migration error:`,
334
+
message,
335
+
);
336
+
console.error("Full error details:", error);
337
return new Response(
338
JSON.stringify({
339
success: false,
340
message: `Blob migration failed: ${message}`,
341
+
error: error instanceof Error
342
+
? {
343
+
name: error.name,
344
+
message: error.message,
345
+
stack: error.stack,
346
+
}
347
+
: String(error),
348
}),
349
{
350
status: 500,
351
headers: {
352
"Content-Type": "application/json",
353
...Object.fromEntries(res.headers),
354
+
},
355
+
},
356
);
357
}
358
+
},
359
});
+92
-34
routes/api/migrate/data/prefs.ts
+92
-34
routes/api/migrate/data/prefs.ts
···
1
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
import { define } from "../../../../utils.ts";
3
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
4
···
17
console.log("Preferences migration: Got new agent:", !!newAgent);
18
19
if (!oldAgent || !newAgent) {
20
-
return new Response(JSON.stringify({
21
-
success: false,
22
-
message: "Not authenticated"
23
-
}), {
24
-
status: 401,
25
-
headers: { "Content-Type": "application/json" }
26
-
});
27
}
28
29
// Migrate preferences
30
const migrationLogs: string[] = [];
31
const startTime = Date.now();
32
-
console.log(`[${new Date().toISOString()}] Starting preferences migration...`);
33
-
migrationLogs.push(`[${new Date().toISOString()}] Starting preferences migration...`);
34
35
// Fetch preferences
36
-
console.log(`[${new Date().toISOString()}] Fetching preferences from old account...`);
37
-
migrationLogs.push(`[${new Date().toISOString()}] Fetching preferences from old account...`);
38
39
const fetchStartTime = Date.now();
40
const prefs = await oldAgent.app.bsky.actor.getPreferences();
41
const fetchTime = Date.now() - fetchStartTime;
42
43
-
console.log(`[${new Date().toISOString()}] Preferences fetched in ${fetchTime/1000} seconds`);
44
-
migrationLogs.push(`[${new Date().toISOString()}] Preferences fetched in ${fetchTime/1000} seconds`);
45
46
// Update preferences
47
-
console.log(`[${new Date().toISOString()}] Updating preferences on new account...`);
48
-
migrationLogs.push(`[${new Date().toISOString()}] Updating preferences on new account...`);
49
50
const updateStartTime = Date.now();
51
await newAgent.app.bsky.actor.putPreferences(prefs.data);
52
const updateTime = Date.now() - updateStartTime;
53
54
-
console.log(`[${new Date().toISOString()}] Preferences updated in ${updateTime/1000} seconds`);
55
-
migrationLogs.push(`[${new Date().toISOString()}] Preferences updated in ${updateTime/1000} seconds`);
56
57
const totalTime = Date.now() - startTime;
58
-
const completionMessage = `[${new Date().toISOString()}] Preferences migration completed in ${totalTime/1000} seconds total`;
59
console.log(completionMessage);
60
migrationLogs.push(completionMessage);
61
···
65
message: "Preferences migration completed successfully",
66
logs: migrationLogs,
67
timing: {
68
-
fetchTime: fetchTime/1000,
69
-
updateTime: updateTime/1000,
70
-
totalTime: totalTime/1000
71
-
}
72
}),
73
{
74
status: 200,
75
headers: {
76
"Content-Type": "application/json",
77
...Object.fromEntries(res.headers),
78
-
}
79
-
}
80
);
81
} catch (error) {
82
const message = error instanceof Error ? error.message : String(error);
83
-
console.error(`[${new Date().toISOString()}] Preferences migration error:`, message);
84
-
console.error('Full error details:', error);
85
return new Response(
86
JSON.stringify({
87
success: false,
88
message: `Preferences migration failed: ${message}`,
89
-
error: error instanceof Error ? {
90
-
name: error.name,
91
-
message: error.message,
92
-
stack: error.stack,
93
-
} : String(error)
94
}),
95
{
96
status: 500,
97
headers: {
98
"Content-Type": "application/json",
99
...Object.fromEntries(res.headers),
100
-
}
101
-
}
102
);
103
}
104
-
}
105
});
···
1
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
3
import { define } from "../../../../utils.ts";
4
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
5
···
18
console.log("Preferences migration: Got new agent:", !!newAgent);
19
20
if (!oldAgent || !newAgent) {
21
+
return new Response(
22
+
JSON.stringify({
23
+
success: false,
24
+
message: "Not authenticated",
25
+
}),
26
+
{
27
+
status: 401,
28
+
headers: { "Content-Type": "application/json" },
29
+
},
30
+
);
31
+
}
32
+
33
+
// Verify DIDs match between sessions
34
+
const didsMatch = await checkDidsMatch(ctx.req);
35
+
if (!didsMatch) {
36
+
return new Response(
37
+
JSON.stringify({
38
+
success: false,
39
+
message: "Invalid state, original and target DIDs do not match",
40
+
}),
41
+
{
42
+
status: 400,
43
+
headers: { "Content-Type": "application/json" },
44
+
},
45
+
);
46
}
47
48
// Migrate preferences
49
const migrationLogs: string[] = [];
50
const startTime = Date.now();
51
+
console.log(
52
+
`[${new Date().toISOString()}] Starting preferences migration...`,
53
+
);
54
+
migrationLogs.push(
55
+
`[${new Date().toISOString()}] Starting preferences migration...`,
56
+
);
57
58
// Fetch preferences
59
+
console.log(
60
+
`[${
61
+
new Date().toISOString()
62
+
}] Fetching preferences from old account...`,
63
+
);
64
+
migrationLogs.push(
65
+
`[${
66
+
new Date().toISOString()
67
+
}] Fetching preferences from old account...`,
68
+
);
69
70
const fetchStartTime = Date.now();
71
const prefs = await oldAgent.app.bsky.actor.getPreferences();
72
const fetchTime = Date.now() - fetchStartTime;
73
74
+
console.log(
75
+
`[${new Date().toISOString()}] Preferences fetched in ${
76
+
fetchTime / 1000
77
+
} seconds`,
78
+
);
79
+
migrationLogs.push(
80
+
`[${new Date().toISOString()}] Preferences fetched in ${
81
+
fetchTime / 1000
82
+
} seconds`,
83
+
);
84
85
// Update preferences
86
+
console.log(
87
+
`[${new Date().toISOString()}] Updating preferences on new account...`,
88
+
);
89
+
migrationLogs.push(
90
+
`[${new Date().toISOString()}] Updating preferences on new account...`,
91
+
);
92
93
const updateStartTime = Date.now();
94
await newAgent.app.bsky.actor.putPreferences(prefs.data);
95
const updateTime = Date.now() - updateStartTime;
96
97
+
console.log(
98
+
`[${new Date().toISOString()}] Preferences updated in ${
99
+
updateTime / 1000
100
+
} seconds`,
101
+
);
102
+
migrationLogs.push(
103
+
`[${new Date().toISOString()}] Preferences updated in ${
104
+
updateTime / 1000
105
+
} seconds`,
106
+
);
107
108
const totalTime = Date.now() - startTime;
109
+
const completionMessage = `[${
110
+
new Date().toISOString()
111
+
}] Preferences migration completed in ${totalTime / 1000} seconds total`;
112
console.log(completionMessage);
113
migrationLogs.push(completionMessage);
114
···
118
message: "Preferences migration completed successfully",
119
logs: migrationLogs,
120
timing: {
121
+
fetchTime: fetchTime / 1000,
122
+
updateTime: updateTime / 1000,
123
+
totalTime: totalTime / 1000,
124
+
},
125
}),
126
{
127
status: 200,
128
headers: {
129
"Content-Type": "application/json",
130
...Object.fromEntries(res.headers),
131
+
},
132
+
},
133
);
134
} catch (error) {
135
const message = error instanceof Error ? error.message : String(error);
136
+
console.error(
137
+
`[${new Date().toISOString()}] Preferences migration error:`,
138
+
message,
139
+
);
140
+
console.error("Full error details:", error);
141
return new Response(
142
JSON.stringify({
143
success: false,
144
message: `Preferences migration failed: ${message}`,
145
+
error: error instanceof Error
146
+
? {
147
+
name: error.name,
148
+
message: error.message,
149
+
stack: error.stack,
150
+
}
151
+
: String(error),
152
}),
153
{
154
status: 500,
155
headers: {
156
"Content-Type": "application/json",
157
...Object.fromEntries(res.headers),
158
+
},
159
+
},
160
);
161
}
162
+
},
163
});
+86
-35
routes/api/migrate/data/repo.ts
+86
-35
routes/api/migrate/data/repo.ts
···
1
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
import { define } from "../../../../utils.ts";
3
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
4
···
13
const oldAgent = await getSessionAgent(ctx.req);
14
console.log("Repo migration: Got old agent:", !!oldAgent);
15
16
-
17
const newAgent = await getSessionAgent(ctx.req, res, true);
18
console.log("Repo migration: Got new agent:", !!newAgent);
19
20
if (!oldAgent || !newAgent) {
21
-
return new Response(JSON.stringify({
22
-
success: false,
23
-
message: "Not authenticated"
24
-
}), {
25
-
status: 401,
26
-
headers: { "Content-Type": "application/json" }
27
-
});
28
}
29
30
const session = await oldAgent.com.atproto.server.getSession();
···
33
const migrationLogs: string[] = [];
34
const startTime = Date.now();
35
console.log(`[${new Date().toISOString()}] Starting repo migration...`);
36
-
migrationLogs.push(`[${new Date().toISOString()}] Starting repo migration...`);
37
38
// Get repo data from old account
39
-
console.log(`[${new Date().toISOString()}] Fetching repo data from old account...`);
40
-
migrationLogs.push(`[${new Date().toISOString()}] Fetching repo data from old account...`);
41
42
const fetchStartTime = Date.now();
43
const repoData = await oldAgent.com.atproto.sync.getRepo({
···
45
});
46
const fetchTime = Date.now() - fetchStartTime;
47
48
-
console.log(`[${new Date().toISOString()}] Repo data fetched in ${fetchTime/1000} seconds`);
49
-
migrationLogs.push(`[${new Date().toISOString()}] Repo data fetched in ${fetchTime/1000} seconds`);
50
51
-
console.log(`[${new Date().toISOString()}] Importing repo data to new account...`);
52
-
migrationLogs.push(`[${new Date().toISOString()}] Importing repo data to new account...`);
53
54
// Import repo data to new account
55
const importStartTime = Date.now();
56
await newAgent.com.atproto.repo.importRepo(repoData.data, {
57
-
encoding: "application/vnd.ipld.car"
58
});
59
const importTime = Date.now() - importStartTime;
60
61
-
console.log(`[${new Date().toISOString()}] Repo data imported in ${importTime/1000} seconds`);
62
-
migrationLogs.push(`[${new Date().toISOString()}] Repo data imported in ${importTime/1000} seconds`);
63
64
const totalTime = Date.now() - startTime;
65
-
const completionMessage = `[${new Date().toISOString()}] Repo migration completed in ${totalTime/1000} seconds total`;
66
console.log(completionMessage);
67
migrationLogs.push(completionMessage);
68
···
72
message: "Repo migration completed successfully",
73
logs: migrationLogs,
74
timing: {
75
-
fetchTime: fetchTime/1000,
76
-
importTime: importTime/1000,
77
-
totalTime: totalTime/1000
78
-
}
79
}),
80
{
81
status: 200,
82
headers: {
83
"Content-Type": "application/json",
84
...Object.fromEntries(res.headers),
85
-
}
86
-
}
87
);
88
} catch (error) {
89
const message = error instanceof Error ? error.message : String(error);
90
-
console.error(`[${new Date().toISOString()}] Repo migration error:`, message);
91
-
console.error('Full error details:', error);
92
return new Response(
93
JSON.stringify({
94
success: false,
95
message: `Repo migration failed: ${message}`,
96
-
error: error instanceof Error ? {
97
-
name: error.name,
98
-
message: error.message,
99
-
stack: error.stack,
100
-
} : String(error)
101
}),
102
{
103
status: 500,
104
headers: {
105
"Content-Type": "application/json",
106
...Object.fromEntries(res.headers),
107
-
}
108
-
}
109
);
110
}
111
-
}
112
});
···
1
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
3
import { define } from "../../../../utils.ts";
4
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
5
···
14
const oldAgent = await getSessionAgent(ctx.req);
15
console.log("Repo migration: Got old agent:", !!oldAgent);
16
17
const newAgent = await getSessionAgent(ctx.req, res, true);
18
console.log("Repo migration: Got new agent:", !!newAgent);
19
20
if (!oldAgent || !newAgent) {
21
+
return new Response(
22
+
JSON.stringify({
23
+
success: false,
24
+
message: "Not authenticated",
25
+
}),
26
+
{
27
+
status: 401,
28
+
headers: { "Content-Type": "application/json" },
29
+
},
30
+
);
31
+
}
32
+
33
+
// Verify DIDs match between sessions
34
+
const didsMatch = await checkDidsMatch(ctx.req);
35
+
if (!didsMatch) {
36
+
return new Response(
37
+
JSON.stringify({
38
+
success: false,
39
+
message: "Invalid state, original and target DIDs do not match",
40
+
}),
41
+
{
42
+
status: 400,
43
+
headers: { "Content-Type": "application/json" },
44
+
},
45
+
);
46
}
47
48
const session = await oldAgent.com.atproto.server.getSession();
···
51
const migrationLogs: string[] = [];
52
const startTime = Date.now();
53
console.log(`[${new Date().toISOString()}] Starting repo migration...`);
54
+
migrationLogs.push(
55
+
`[${new Date().toISOString()}] Starting repo migration...`,
56
+
);
57
58
// Get repo data from old account
59
+
console.log(
60
+
`[${new Date().toISOString()}] Fetching repo data from old account...`,
61
+
);
62
+
migrationLogs.push(
63
+
`[${new Date().toISOString()}] Fetching repo data from old account...`,
64
+
);
65
66
const fetchStartTime = Date.now();
67
const repoData = await oldAgent.com.atproto.sync.getRepo({
···
69
});
70
const fetchTime = Date.now() - fetchStartTime;
71
72
+
console.log(
73
+
`[${new Date().toISOString()}] Repo data fetched in ${
74
+
fetchTime / 1000
75
+
} seconds`,
76
+
);
77
+
migrationLogs.push(
78
+
`[${new Date().toISOString()}] Repo data fetched in ${
79
+
fetchTime / 1000
80
+
} seconds`,
81
+
);
82
83
+
console.log(
84
+
`[${new Date().toISOString()}] Importing repo data to new account...`,
85
+
);
86
+
migrationLogs.push(
87
+
`[${new Date().toISOString()}] Importing repo data to new account...`,
88
+
);
89
90
// Import repo data to new account
91
const importStartTime = Date.now();
92
await newAgent.com.atproto.repo.importRepo(repoData.data, {
93
+
encoding: "application/vnd.ipld.car",
94
});
95
const importTime = Date.now() - importStartTime;
96
97
+
console.log(
98
+
`[${new Date().toISOString()}] Repo data imported in ${
99
+
importTime / 1000
100
+
} seconds`,
101
+
);
102
+
migrationLogs.push(
103
+
`[${new Date().toISOString()}] Repo data imported in ${
104
+
importTime / 1000
105
+
} seconds`,
106
+
);
107
108
const totalTime = Date.now() - startTime;
109
+
const completionMessage = `[${
110
+
new Date().toISOString()
111
+
}] Repo migration completed in ${totalTime / 1000} seconds total`;
112
console.log(completionMessage);
113
migrationLogs.push(completionMessage);
114
···
118
message: "Repo migration completed successfully",
119
logs: migrationLogs,
120
timing: {
121
+
fetchTime: fetchTime / 1000,
122
+
importTime: importTime / 1000,
123
+
totalTime: totalTime / 1000,
124
+
},
125
}),
126
{
127
status: 200,
128
headers: {
129
"Content-Type": "application/json",
130
...Object.fromEntries(res.headers),
131
+
},
132
+
},
133
);
134
} catch (error) {
135
const message = error instanceof Error ? error.message : String(error);
136
+
console.error(
137
+
`[${new Date().toISOString()}] Repo migration error:`,
138
+
message,
139
+
);
140
+
console.error("Full error details:", error);
141
return new Response(
142
JSON.stringify({
143
success: false,
144
message: `Repo migration failed: ${message}`,
145
+
error: error instanceof Error
146
+
? {
147
+
name: error.name,
148
+
message: error.message,
149
+
stack: error.stack,
150
+
}
151
+
: String(error),
152
}),
153
{
154
status: 500,
155
headers: {
156
"Content-Type": "application/json",
157
...Object.fromEntries(res.headers),
158
+
},
159
+
},
160
);
161
}
162
+
},
163
});
+13
routes/api/migrate/finalize.ts
+13
routes/api/migrate/finalize.ts
···
1
import { getSessionAgent } from "../../../lib/sessions.ts";
2
import { define } from "../../../utils.ts";
3
import { assertMigrationAllowed } from "../../../lib/migration-state.ts";
4
···
17
return new Response("Migration session not found or invalid", {
18
status: 400,
19
});
20
}
21
22
// Activate new account and deactivate old account
···
1
import { getSessionAgent } from "../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../lib/check-dids.ts";
3
import { define } from "../../../utils.ts";
4
import { assertMigrationAllowed } from "../../../lib/migration-state.ts";
5
···
18
return new Response("Migration session not found or invalid", {
19
status: 400,
20
});
21
+
}
22
+
23
+
// Verify DIDs match between sessions
24
+
const didsMatch = await checkDidsMatch(ctx.req);
25
+
if (!didsMatch) {
26
+
return new Response(
27
+
JSON.stringify({
28
+
success: false,
29
+
message: "Invalid state, original and target DIDs do not match",
30
+
}),
31
+
{ status: 400, headers: { "Content-Type": "application/json" } },
32
+
);
33
}
34
35
// Activate new account and deactivate old account
+18
-4
routes/api/migrate/identity/request.ts
+18
-4
routes/api/migrate/identity/request.ts
···
1
-
import {
2
-
getSessionAgent,
3
-
} from "../../../../lib/sessions.ts";
4
import { define } from "../../../../utils.ts";
5
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
6
···
56
);
57
}
58
59
// Request the signature
60
console.log("Requesting PLC operation signature...");
61
try {
···
65
console.error("Error requesting PLC operation signature:", {
66
name: error instanceof Error ? error.name : "Unknown",
67
message: error instanceof Error ? error.message : String(error),
68
-
status: 400
69
});
70
throw error;
71
}
···
1
+
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
3
import { define } from "../../../../utils.ts";
4
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
5
···
55
);
56
}
57
58
+
// Verify DIDs match between sessions
59
+
const didsMatch = await checkDidsMatch(ctx.req);
60
+
if (!didsMatch) {
61
+
return new Response(
62
+
JSON.stringify({
63
+
success: false,
64
+
message: "Invalid state, original and target DIDs do not match",
65
+
}),
66
+
{
67
+
status: 400,
68
+
headers: { "Content-Type": "application/json" },
69
+
},
70
+
);
71
+
}
72
+
73
// Request the signature
74
console.log("Requesting PLC operation signature...");
75
try {
···
79
console.error("Error requesting PLC operation signature:", {
80
name: error instanceof Error ? error.name : "Unknown",
81
message: error instanceof Error ? error.message : String(error),
82
+
status: 400,
83
});
84
throw error;
85
}
+17
-3
routes/api/migrate/identity/sign.ts
+17
-3
routes/api/migrate/identity/sign.ts
···
1
-
import {
2
-
getSessionAgent,
3
-
} from "../../../../lib/sessions.ts";
4
import { Secp256k1Keypair } from "npm:@atproto/crypto";
5
import * as ui8 from "npm:uint8arrays";
6
import { define } from "../../../../utils.ts";
···
55
JSON.stringify({
56
success: false,
57
message: "Migration session not found or invalid",
58
}),
59
{
60
status: 400,
···
1
+
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
3
import { Secp256k1Keypair } from "npm:@atproto/crypto";
4
import * as ui8 from "npm:uint8arrays";
5
import { define } from "../../../../utils.ts";
···
54
JSON.stringify({
55
success: false,
56
message: "Migration session not found or invalid",
57
+
}),
58
+
{
59
+
status: 400,
60
+
headers: { "Content-Type": "application/json" },
61
+
},
62
+
);
63
+
}
64
+
65
+
// Verify DIDs match between sessions
66
+
const didsMatch = await checkDidsMatch(ctx.req);
67
+
if (!didsMatch) {
68
+
return new Response(
69
+
JSON.stringify({
70
+
success: false,
71
+
message: "Invalid state, original and target DIDs do not match",
72
}),
73
{
74
status: 400,
+2
-2
routes/api/migrate/next-step.ts
+2
-2
routes/api/migrate/next-step.ts
···
17
// Check conditions in sequence to determine the next step
18
if (!newStatus.data) {
19
nextStep = 1;
20
-
} else if (!(newStatus.data.repoCommit &&
21
newStatus.data.indexedRecords === oldStatus.data.indexedRecords &&
22
newStatus.data.privateStateValues === oldStatus.data.privateStateValues &&
23
newStatus.data.expectedBlobs === newStatus.data.importedBlobs &&
···
42
}
43
});
44
}
45
-
})
···
17
// Check conditions in sequence to determine the next step
18
if (!newStatus.data) {
19
nextStep = 1;
20
+
} else if (!(newStatus.data.repoCommit &&
21
newStatus.data.indexedRecords === oldStatus.data.indexedRecords &&
22
newStatus.data.privateStateValues === oldStatus.data.privateStateValues &&
23
newStatus.data.expectedBlobs === newStatus.data.importedBlobs &&
···
42
}
43
});
44
}
45
+
})
+130
-104
routes/api/migrate/status.ts
+130
-104
routes/api/migrate/status.ts
···
1
import { getSessionAgent } from "../../../lib/sessions.ts";
2
import { define } from "../../../utils.ts";
3
4
export const handler = define.handlers({
5
-
async GET(ctx) {
6
-
console.log("Status check: Starting");
7
-
const url = new URL(ctx.req.url);
8
-
const params = new URLSearchParams(url.search);
9
-
const step = params.get("step");
10
-
console.log("Status check: Step", step);
11
12
-
console.log("Status check: Getting agents");
13
-
const oldAgent = await getSessionAgent(ctx.req);
14
-
const newAgent = await getSessionAgent(ctx.req, new Response(), true);
15
-
16
-
if (!oldAgent || !newAgent) {
17
-
console.log("Status check: Unauthorized - missing agents", {
18
-
hasOldAgent: !!oldAgent,
19
-
hasNewAgent: !!newAgent
20
-
});
21
-
return new Response("Unauthorized", { status: 401 });
22
-
}
23
24
-
console.log("Status check: Fetching account statuses");
25
-
const oldStatus = await oldAgent.com.atproto.server.checkAccountStatus();
26
-
const newStatus = await newAgent.com.atproto.server.checkAccountStatus();
27
-
28
-
if (!oldStatus.data || !newStatus.data) {
29
-
console.error("Status check: Failed to verify status", {
30
-
hasOldStatus: !!oldStatus.data,
31
-
hasNewStatus: !!newStatus.data
32
-
});
33
-
return new Response("Could not verify status", { status: 500 });
34
-
}
35
36
-
console.log("Status check: Account statuses", {
37
-
old: oldStatus.data,
38
-
new: newStatus.data
39
-
});
40
41
-
const readyToContinue = () => {
42
-
if (step) {
43
-
console.log("Status check: Evaluating step", step);
44
-
switch (step) {
45
-
case "1": {
46
-
if (newStatus.data) {
47
-
console.log("Status check: Step 1 ready");
48
-
return { ready: true };
49
-
}
50
-
console.log("Status check: Step 1 not ready - new account status not available");
51
-
return { ready: false, reason: "New account status not available" };
52
-
}
53
-
case "2": {
54
-
const isReady = newStatus.data.repoCommit &&
55
-
newStatus.data.indexedRecords === oldStatus.data.indexedRecords &&
56
-
newStatus.data.privateStateValues === oldStatus.data.privateStateValues &&
57
-
newStatus.data.expectedBlobs === newStatus.data.importedBlobs &&
58
-
newStatus.data.importedBlobs === oldStatus.data.importedBlobs;
59
60
-
if (isReady) {
61
-
console.log("Status check: Step 2 ready");
62
-
return { ready: true };
63
-
}
64
65
-
const reasons = [];
66
-
if (!newStatus.data.repoCommit) reasons.push("Repository not imported.");
67
-
if (newStatus.data.indexedRecords < oldStatus.data.indexedRecords)
68
-
reasons.push("Not all records imported.");
69
-
if (newStatus.data.privateStateValues < oldStatus.data.privateStateValues)
70
-
reasons.push("Not all private state values imported.");
71
-
if (newStatus.data.expectedBlobs !== newStatus.data.importedBlobs)
72
-
reasons.push("Expected blobs not fully imported.");
73
-
if (newStatus.data.importedBlobs < oldStatus.data.importedBlobs)
74
-
reasons.push("Not all blobs imported.");
75
76
-
console.log("Status check: Step 2 not ready", { reasons });
77
-
return { ready: false, reason: reasons.join(", ") };
78
-
}
79
-
case "3": {
80
-
if (newStatus.data.validDid) {
81
-
console.log("Status check: Step 3 ready");
82
-
return { ready: true };
83
-
}
84
-
console.log("Status check: Step 3 not ready - DID not valid");
85
-
return { ready: false, reason: "DID not valid" };
86
-
}
87
-
case "4": {
88
-
if (newStatus.data.activated === true && oldStatus.data.activated === false) {
89
-
console.log("Status check: Step 4 ready");
90
-
return { ready: true };
91
-
}
92
-
console.log("Status check: Step 4 not ready - Account not activated");
93
-
return { ready: false, reason: "Account not activated" };
94
-
}
95
-
}
96
-
} else {
97
-
console.log("Status check: No step specified, returning ready");
98
-
return { ready: true };
99
}
100
}
101
102
-
const status = {
103
-
activated: newStatus.data.activated,
104
-
validDid: newStatus.data.validDid,
105
-
repoCommit: newStatus.data.repoCommit,
106
-
repoRev: newStatus.data.repoRev,
107
-
repoBlocks: newStatus.data.repoBlocks,
108
-
expectedRecords: oldStatus.data.indexedRecords,
109
-
indexedRecords: newStatus.data.indexedRecords,
110
-
privateStateValues: newStatus.data.privateStateValues,
111
-
expectedBlobs: newStatus.data.expectedBlobs,
112
-
importedBlobs: newStatus.data.importedBlobs,
113
-
...readyToContinue()
114
-
}
115
116
-
console.log("Status check: Complete", status);
117
-
return Response.json(status);
118
-
}
119
-
})
···
1
+
import { checkDidsMatch } from "../../../lib/check-dids.ts";
2
import { getSessionAgent } from "../../../lib/sessions.ts";
3
import { define } from "../../../utils.ts";
4
5
export const handler = define.handlers({
6
+
async GET(ctx) {
7
+
console.log("Status check: Starting");
8
+
const url = new URL(ctx.req.url);
9
+
const params = new URLSearchParams(url.search);
10
+
const step = params.get("step");
11
+
console.log("Status check: Step", step);
12
13
+
console.log("Status check: Getting agents");
14
+
const oldAgent = await getSessionAgent(ctx.req);
15
+
const newAgent = await getSessionAgent(ctx.req, new Response(), true);
16
17
+
if (!oldAgent || !newAgent) {
18
+
console.log("Status check: Unauthorized - missing agents", {
19
+
hasOldAgent: !!oldAgent,
20
+
hasNewAgent: !!newAgent,
21
+
});
22
+
return new Response("Unauthorized", { status: 401 });
23
+
}
24
25
+
const didsMatch = await checkDidsMatch(ctx.req);
26
27
+
console.log("Status check: Fetching account statuses");
28
+
const oldStatus = await oldAgent.com.atproto.server.checkAccountStatus();
29
+
const newStatus = await newAgent.com.atproto.server.checkAccountStatus();
30
31
+
if (!oldStatus.data || !newStatus.data) {
32
+
console.error("Status check: Failed to verify status", {
33
+
hasOldStatus: !!oldStatus.data,
34
+
hasNewStatus: !!newStatus.data,
35
+
});
36
+
return new Response("Could not verify status", { status: 500 });
37
+
}
38
39
+
console.log("Status check: Account statuses", {
40
+
old: oldStatus.data,
41
+
new: newStatus.data,
42
+
});
43
44
+
const readyToContinue = () => {
45
+
if (!didsMatch) {
46
+
return {
47
+
ready: false,
48
+
reason: "Invalid state, original and target DIDs do not match",
49
+
};
50
+
}
51
+
if (step) {
52
+
console.log("Status check: Evaluating step", step);
53
+
switch (step) {
54
+
case "1": {
55
+
if (newStatus.data) {
56
+
console.log("Status check: Step 1 ready");
57
+
return { ready: true };
58
+
}
59
+
console.log(
60
+
"Status check: Step 1 not ready - new account status not available",
61
+
);
62
+
return { ready: false, reason: "New account status not available" };
63
+
}
64
+
case "2": {
65
+
const isReady = newStatus.data.repoCommit &&
66
+
newStatus.data.indexedRecords === oldStatus.data.indexedRecords &&
67
+
newStatus.data.privateStateValues ===
68
+
oldStatus.data.privateStateValues &&
69
+
newStatus.data.expectedBlobs === newStatus.data.importedBlobs &&
70
+
newStatus.data.importedBlobs === oldStatus.data.importedBlobs;
71
+
72
+
if (isReady) {
73
+
console.log("Status check: Step 2 ready");
74
+
return { ready: true };
75
+
}
76
+
77
+
const reasons = [];
78
+
if (!newStatus.data.repoCommit) {
79
+
reasons.push("Repository not imported.");
80
+
}
81
+
if (newStatus.data.indexedRecords < oldStatus.data.indexedRecords) {
82
+
reasons.push("Not all records imported.");
83
+
}
84
+
if (
85
+
newStatus.data.privateStateValues <
86
+
oldStatus.data.privateStateValues
87
+
) {
88
+
reasons.push("Not all private state values imported.");
89
+
}
90
+
if (newStatus.data.expectedBlobs !== newStatus.data.importedBlobs) {
91
+
reasons.push("Expected blobs not fully imported.");
92
+
}
93
+
if (newStatus.data.importedBlobs < oldStatus.data.importedBlobs) {
94
+
reasons.push("Not all blobs imported.");
95
+
}
96
+
97
+
console.log("Status check: Step 2 not ready", { reasons });
98
+
return { ready: false, reason: reasons.join(", ") };
99
+
}
100
+
case "3": {
101
+
if (newStatus.data.validDid) {
102
+
console.log("Status check: Step 3 ready");
103
+
return { ready: true };
104
+
}
105
+
console.log("Status check: Step 3 not ready - DID not valid");
106
+
return { ready: false, reason: "DID not valid" };
107
+
}
108
+
case "4": {
109
+
if (
110
+
newStatus.data.activated === true &&
111
+
oldStatus.data.activated === false
112
+
) {
113
+
console.log("Status check: Step 4 ready");
114
+
return { ready: true };
115
}
116
+
console.log(
117
+
"Status check: Step 4 not ready - Account not activated",
118
+
);
119
+
return { ready: false, reason: "Account not activated" };
120
+
}
121
}
122
+
} else {
123
+
console.log("Status check: No step specified, returning ready");
124
+
return { ready: true };
125
+
}
126
+
};
127
128
+
const status = {
129
+
activated: newStatus.data.activated,
130
+
validDid: newStatus.data.validDid,
131
+
repoCommit: newStatus.data.repoCommit,
132
+
repoRev: newStatus.data.repoRev,
133
+
repoBlocks: newStatus.data.repoBlocks,
134
+
expectedRecords: oldStatus.data.indexedRecords,
135
+
indexedRecords: newStatus.data.indexedRecords,
136
+
privateStateValues: newStatus.data.privateStateValues,
137
+
expectedBlobs: newStatus.data.expectedBlobs,
138
+
importedBlobs: newStatus.data.importedBlobs,
139
+
...readyToContinue(),
140
+
};
141
142
+
console.log("Status check: Complete", status);
143
+
return Response.json(status);
144
+
},
145
+
});
+2
-2
routes/migrate/progress.tsx
+2
-2
routes/migrate/progress.tsx
···
10
11
if (!service || !handle || !email || !password) {
12
return (
13
-
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-4">
14
<div class="max-w-2xl mx-auto">
15
<div class="bg-red-50 dark:bg-red-900 p-4 rounded-lg">
16
<p class="text-red-800 dark:text-red-200">
···
24
}
25
26
return (
27
-
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-4">
28
<div class="max-w-2xl mx-auto">
29
<h1 class="font-mono text-3xl font-bold text-gray-900 dark:text-white mb-8">
30
Migration Progress
···
10
11
if (!service || !handle || !email || !password) {
12
return (
13
+
<div class="bg-gray-50 dark:bg-gray-900 p-4">
14
<div class="max-w-2xl mx-auto">
15
<div class="bg-red-50 dark:bg-red-900 p-4 rounded-lg">
16
<p class="text-red-800 dark:text-red-200">
···
24
}
25
26
return (
27
+
<div class="bg-gray-50 dark:bg-gray-900 p-4">
28
<div class="max-w-2xl mx-auto">
29
<h1 class="font-mono text-3xl font-bold text-gray-900 dark:text-white mb-8">
30
Migration Progress