+35
-9
.zed/settings.json
+35
-9
.zed/settings.json
···
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
1
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
+
},
2
21
"languages": {
22
+
"JavaScript": {
23
+
"language_servers": [
24
+
"deno",
25
+
"!vtsls",
26
+
"!eslint",
27
+
"..."
28
+
]
29
+
},
3
30
"TypeScript": {
4
31
"language_servers": [
5
-
"wakatime",
6
32
"deno",
7
33
"!typescript-language-server",
8
34
"!vtsls",
9
-
"!eslint"
10
-
],
11
-
"formatter": "language_server"
35
+
"!eslint",
36
+
"..."
37
+
]
12
38
},
13
39
"TSX": {
14
40
"language_servers": [
15
-
"wakatime",
16
41
"deno",
17
42
"!typescript-language-server",
18
43
"!vtsls",
19
-
"!eslint"
20
-
],
21
-
"formatter": "language_server"
44
+
"!eslint",
45
+
"..."
46
+
]
22
47
}
23
-
}
48
+
},
49
+
"formatter": "language_server"
24
50
}
+155
-66
islands/MigrationProgress.tsx
+155
-66
islands/MigrationProgress.tsx
···
40
40
*/
41
41
export default function MigrationProgress(props: MigrationProgressProps) {
42
42
const [token, setToken] = useState("");
43
-
const [migrationState, setMigrationState] = useState<MigrationStateInfo | null>(null);
43
+
const [migrationState, setMigrationState] = useState<
44
+
MigrationStateInfo | null
45
+
>(null);
44
46
45
47
const [steps, setSteps] = useState<MigrationStep[]>([
46
48
{ name: "Create Account", status: "pending" },
···
139
141
const getStepDisplayName = (step: MigrationStep, index: number) => {
140
142
if (step.status === "completed") {
141
143
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";
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";
146
152
}
147
153
}
148
154
149
155
if (step.status === "in-progress") {
150
156
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
+
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...";
157
168
}
158
169
}
159
170
160
171
if (step.status === "verifying") {
161
172
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...";
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...";
166
181
}
167
182
}
168
183
···
268
283
console.error("Blob migration: Error response:", json);
269
284
throw new Error(json.message || "Failed to migrate blobs");
270
285
} catch {
271
-
console.error("Blob migration: Non-JSON error response:", blobsText);
286
+
console.error(
287
+
"Blob migration: Non-JSON error response:",
288
+
blobsText,
289
+
);
272
290
throw new Error(blobsText || "Failed to migrate blobs");
273
291
}
274
292
}
···
290
308
console.error("Preferences migration: Error response:", json);
291
309
throw new Error(json.message || "Failed to migrate preferences");
292
310
} catch {
293
-
console.error("Preferences migration: Non-JSON error response:", prefsText);
311
+
console.error(
312
+
"Preferences migration: Non-JSON error response:",
313
+
prefsText,
314
+
);
294
315
throw new Error(prefsText || "Failed to migrate preferences");
295
316
}
296
317
}
···
329
350
if (!requestRes.ok) {
330
351
try {
331
352
const json = JSON.parse(requestText);
332
-
throw new Error(json.message || "Failed to request identity migration");
353
+
throw new Error(
354
+
json.message || "Failed to request identity migration",
355
+
);
333
356
} catch {
334
-
throw new Error(requestText || "Failed to request identity migration");
357
+
throw new Error(
358
+
requestText || "Failed to request identity migration",
359
+
);
335
360
}
336
361
}
337
362
···
345
370
console.log("Identity migration requested successfully");
346
371
347
372
// Update step name to prompt for token
348
-
setSteps(prevSteps =>
373
+
setSteps((prevSteps) =>
349
374
prevSteps.map((step, i) =>
350
375
i === 2
351
-
? { ...step, name: "Enter the token sent to your email to complete identity migration" }
376
+
? {
377
+
...step,
378
+
name:
379
+
"Enter the token sent to your email to complete identity migration",
380
+
}
352
381
: step
353
382
)
354
383
);
···
389
418
if (!identityRes.ok) {
390
419
try {
391
420
const json = JSON.parse(identityData);
392
-
throw new Error(json.message || "Failed to complete identity migration");
421
+
throw new Error(
422
+
json.message || "Failed to complete identity migration",
423
+
);
393
424
} catch {
394
-
throw new Error(identityData || "Failed to complete identity migration");
425
+
throw new Error(
426
+
identityData || "Failed to complete identity migration",
427
+
);
395
428
}
396
429
}
397
430
···
404
437
} catch {
405
438
throw new Error("Invalid response from server");
406
439
}
407
-
408
440
409
441
updateStepStatus(2, "verifying");
410
442
const verified = await verifyStep(2);
···
554
586
updateStepStatus(stepNum, "completed");
555
587
return true;
556
588
} else {
557
-
console.log(`Verification: Step ${stepNum + 1} is not ready:`, data.reason);
589
+
console.log(
590
+
`Verification: Step ${stepNum + 1} is not ready:`,
591
+
data.reason,
592
+
);
558
593
const statusDetails = {
559
594
activated: data.activated,
560
595
validDid: data.validDid,
···
565
600
indexedRecords: data.indexedRecords,
566
601
privateStateValues: data.privateStateValues,
567
602
expectedBlobs: data.expectedBlobs,
568
-
importedBlobs: data.importedBlobs
603
+
importedBlobs: data.importedBlobs,
569
604
};
570
-
console.log(`Verification: Step ${stepNum + 1} status details:`, statusDetails);
571
-
const errorMessage = `${data.reason || "Verification failed"}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
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)}`;
572
612
updateStepStatus(stepNum, "error", errorMessage);
573
613
return false;
574
614
}
575
615
} catch (e) {
576
616
console.error(`Verification: Error in step ${stepNum + 1}:`, e);
577
-
updateStepStatus(stepNum, "error", e instanceof Error ? e.message : String(e));
617
+
updateStepStatus(
618
+
stepNum,
619
+
"error",
620
+
e instanceof Error ? e.message : String(e),
621
+
);
578
622
return false;
579
623
}
580
624
};
···
583
627
<div class="space-y-8">
584
628
{/* Migration state alert */}
585
629
{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
-
}`}>
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
+
>
591
637
<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
-
}`}>
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
+
>
595
645
{migrationState.state === "maintenance" ? "⚠️" : "🚫"}
596
646
</div>
597
647
<div>
598
648
<h3 class="font-semibold mb-1">
599
-
{migrationState.state === "maintenance" ? "Maintenance Mode" : "Service Unavailable"}
649
+
{migrationState.state === "maintenance"
650
+
? "Maintenance Mode"
651
+
: "Service Unavailable"}
600
652
</h3>
601
653
<p class="text-sm">{migrationState.message}</p>
602
654
</div>
···
635
687
</p>
636
688
)}
637
689
{index === 2 && step.status === "in-progress" &&
638
-
step.name === "Enter the token sent to your email to complete identity migration" && (
690
+
step.name ===
691
+
"Enter the token sent to your email to complete identity migration" &&
692
+
(
639
693
<div class="mt-4 space-y-4">
640
694
<p class="text-sm text-blue-800 dark:text-blue-200">
641
-
Please check your email for the migration token and enter it below:
695
+
Please check your email for the migration token and enter
696
+
it below:
642
697
</p>
643
698
<div class="flex space-x-2">
644
699
<input
···
657
712
</button>
658
713
</div>
659
714
</div>
660
-
)
661
-
}
715
+
)}
662
716
</div>
663
717
</div>
664
718
))}
···
666
720
667
721
{steps[3].status === "completed" && (
668
722
<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.
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.
671
728
</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");
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);
682
744
}
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>
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>
695
784
</div>
696
785
)}
697
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
···
28
28
case "maintenance":
29
29
return {
30
30
state: "maintenance",
31
-
message: "Migration services are temporarily unavailable for scheduled maintenance. Please try again later.",
31
+
message: "Migration services are temporarily unavailable for maintenance. Please try again later.",
32
32
allowMigration: false,
33
33
};
34
34
+31
-10
lib/sessions.ts
+31
-10
lib/sessions.ts
···
1
1
import { Agent } from "npm:@atproto/api";
2
-
import { OauthSession, CredentialSession } from "./types.ts";
3
-
import { getCredentialSession, getCredentialSessionAgent } from "./cred/sessions.ts";
2
+
import { CredentialSession, OauthSession } from "./types.ts";
3
+
import {
4
+
getCredentialSession,
5
+
getCredentialSessionAgent,
6
+
} from "./cred/sessions.ts";
4
7
import { getOauthSession, getOauthSessionAgent } from "./oauth/sessions.ts";
5
8
import { IronSession } from "npm:iron-session";
6
9
···
14
17
export async function getSession(
15
18
req: Request,
16
19
res: Response = new Response(),
17
-
isMigration: boolean = false
20
+
isMigration: boolean = false,
18
21
): Promise<IronSession<OauthSession | CredentialSession>> {
19
22
if (isMigration) {
20
23
return await getCredentialSession(req, res, true);
···
23
26
const credentialSession = await getCredentialSession(req, res);
24
27
25
28
if (oauthSession.did) {
26
-
console.log("Oauth session found")
29
+
console.log("Oauth session found");
27
30
return oauthSession;
28
31
}
29
32
if (credentialSession.did) {
···
43
46
export async function getSessionAgent(
44
47
req: Request,
45
48
res: Response = new Response(),
46
-
isMigration: boolean = false
49
+
isMigration: boolean = false,
47
50
): Promise<Agent | null> {
48
51
if (isMigration) {
49
52
return await getCredentialSessionAgent(req, res, isMigration);
50
53
}
51
54
52
55
const oauthAgent = await getOauthSessionAgent(req);
53
-
const credentialAgent = await getCredentialSessionAgent(req, res, isMigration);
56
+
const credentialAgent = await getCredentialSessionAgent(
57
+
req,
58
+
res,
59
+
isMigration,
60
+
);
54
61
55
62
if (oauthAgent) {
56
63
return oauthAgent;
···
66
73
/**
67
74
* Destroy all sessions for the given request.
68
75
* @param req - The request object
76
+
* @param res - The response object
69
77
*/
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);
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
+
);
74
90
75
91
if (oauthSession.did) {
76
92
oauthSession.destroy();
···
79
95
credentialSession.destroy();
80
96
}
81
97
if (migrationSession.did) {
98
+
console.log("DESTROYING MIGRATION SESSION", migrationSession);
82
99
migrationSession.destroy();
100
+
} else {
101
+
console.log("MIGRATION SESSION NOT FOUND", migrationSession);
83
102
}
103
+
104
+
return response;
84
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";
1
+
import { destroyAllSessions, getSession } from "../../lib/sessions.ts";
2
2
import { oauthClient } from "../../lib/oauth/client.ts";
3
3
import { define } from "../../utils.ts";
4
4
···
13
13
if (session.did) {
14
14
// Try to revoke both types of sessions - the one that doesn't exist will just no-op
15
15
await Promise.all([
16
-
oauthClient.revoke(session.did).catch(console.error)
16
+
oauthClient.revoke(session.did).catch(console.error),
17
17
]);
18
18
// Then destroy the iron session
19
19
session.destroy();
20
20
}
21
21
22
22
// Destroy all sessions including migration session
23
-
await destroyAllSessions(req);
23
+
const result = await destroyAllSessions(req, response);
24
24
25
-
return response;
25
+
return result;
26
26
} catch (error: unknown) {
27
27
const err = error instanceof Error ? error : new Error(String(error));
28
28
console.error("Logout failed:", err.message);
+2
-2
routes/api/migrate/create.ts
+2
-2
routes/api/migrate/create.ts
···
45
45
return new Response("Could not create new agent", { status: 400 });
46
46
}
47
47
48
-
console.log("getting did")
48
+
console.log("getting did");
49
49
const session = await oldAgent.com.atproto.server.getSession();
50
50
const accountDid = session.data.did;
51
-
console.log("got did")
51
+
console.log("got did");
52
52
const describeRes = await newAgent.com.atproto.server.describeServer();
53
53
const newServerDid = describeRes.data.did;
54
54
const inviteRequired = describeRes.data.inviteCodeRequired ?? false;
+178
-44
routes/api/migrate/data/blobs.ts
+178
-44
routes/api/migrate/data/blobs.ts
···
1
1
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
2
3
import { define } from "../../../../utils.ts";
3
4
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
4
5
···
41
42
);
42
43
}
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
+
44
60
// Migrate blobs
45
61
const migrationLogs: string[] = [];
46
62
const migratedBlobs: string[] = [];
···
52
68
53
69
const startTime = Date.now();
54
70
console.log(`[${new Date().toISOString()}] Starting blob migration...`);
55
-
migrationLogs.push(`[${new Date().toISOString()}] Starting blob migration...`);
71
+
migrationLogs.push(
72
+
`[${new Date().toISOString()}] Starting blob migration...`,
73
+
);
56
74
57
75
// First count total blobs
58
76
console.log(`[${new Date().toISOString()}] Starting blob count...`);
59
-
migrationLogs.push(`[${new Date().toISOString()}] Starting blob count...`);
77
+
migrationLogs.push(
78
+
`[${new Date().toISOString()}] Starting blob count...`,
79
+
);
60
80
61
81
const session = await oldAgent.com.atproto.server.getSession();
62
82
const accountDid = session.data.did;
63
83
64
84
do {
65
85
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}...`);
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
+
);
68
96
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
69
97
did: accountDid,
70
98
cursor: blobCursor,
···
74
102
totalBlobs += newBlobs;
75
103
const pageTime = Date.now() - pageStartTime;
76
104
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}`);
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
+
);
79
115
80
116
pageCount++;
81
117
blobCursor = listedBlobs.data.cursor;
82
118
} while (blobCursor);
83
119
84
-
console.log(`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`);
85
-
migrationLogs.push(`[${new Date().toISOString()}] Total blobs to migrate: ${totalBlobs}`);
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
+
);
86
126
87
127
// Reset cursor for actual migration
88
128
blobCursor = undefined;
···
91
131
92
132
do {
93
133
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}...`);
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
+
);
96
144
97
145
const listedBlobs = await oldAgent.com.atproto.sync.listBlobs({
98
146
did: accountDid,
···
100
148
});
101
149
102
150
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`);
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
+
);
105
165
106
166
blobCursor = listedBlobs.data.cursor;
107
167
108
168
for (const cid of listedBlobs.data.cids) {
109
169
try {
110
170
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})...`);
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
+
);
113
185
114
186
const blobRes = await oldAgent.com.atproto.sync.getBlob({
115
187
did: accountDid,
···
123
195
124
196
const size = parseInt(contentLength, 10);
125
197
if (isNaN(size)) {
126
-
throw new Error(`Blob ${cid} has invalid content length: ${contentLength}`);
198
+
throw new Error(
199
+
`Blob ${cid} has invalid content length: ${contentLength}`,
200
+
);
127
201
}
128
202
129
203
const MAX_SIZE = 200 * 1024 * 1024; // 200MB
130
204
if (size > MAX_SIZE) {
131
-
throw new Error(`Blob ${cid} exceeds maximum size limit (${size} bytes)`);
205
+
throw new Error(
206
+
`Blob ${cid} exceeds maximum size limit (${size} bytes)`,
207
+
);
132
208
}
133
209
134
-
console.log(`[${new Date().toISOString()}] Downloading blob ${cid} (${size} bytes)...`);
135
-
migrationLogs.push(`[${new Date().toISOString()}] Downloading blob ${cid} (${size} bytes)...`);
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
+
);
136
220
137
221
if (!blobRes.data) {
138
-
throw new Error(`Failed to download blob ${cid}: No data received`);
222
+
throw new Error(
223
+
`Failed to download blob ${cid}: No data received`,
224
+
);
139
225
}
140
226
141
-
console.log(`[${new Date().toISOString()}] Uploading blob ${cid} to new account...`);
142
-
migrationLogs.push(`[${new Date().toISOString()}] Uploading blob ${cid} to new account...`);
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
+
);
143
237
144
238
try {
145
239
await newAgent.com.atproto.repo.uploadBlob(blobRes.data);
146
240
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`);
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
+
);
149
255
migratedBlobs.push(cid);
150
256
} 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)}`);
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
+
);
153
268
}
154
269
} catch (error) {
155
-
const errorMessage = error instanceof Error ? error.message : String(error);
156
-
const detailedError = `[${new Date().toISOString()}] Failed to migrate blob ${cid}: ${errorMessage}`;
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}`;
157
276
console.error(detailedError);
158
-
console.error('Full error details:', error);
277
+
console.error("Full error details:", error);
159
278
migrationLogs.push(detailedError);
160
279
failedBlobs.push(cid);
161
280
}
162
281
163
282
processedBlobs++;
164
-
const progressLog = `[${new Date().toISOString()}] Progress: ${processedBlobs}/${totalBlobs} blobs processed (${Math.round((processedBlobs/totalBlobs)*100)}%)`;
283
+
const progressLog = `[${
284
+
new Date().toISOString()
285
+
}] Progress: ${processedBlobs}/${totalBlobs} blobs processed (${
286
+
Math.round((processedBlobs / totalBlobs) * 100)
287
+
}%)`;
165
288
console.log(progressLog);
166
289
migrationLogs.push(progressLog);
167
290
}
···
169
292
} while (blobCursor);
170
293
171
294
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)`;
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)`;
173
302
console.log(completionMessage);
174
303
migrationLogs.push(completionMessage);
175
304
···
187
316
totalBlobs,
188
317
logs: migrationLogs,
189
318
timing: {
190
-
totalTime: totalTime/1000
191
-
}
319
+
totalTime: totalTime / 1000,
320
+
},
192
321
}),
193
322
{
194
323
status: 200,
195
324
headers: {
196
325
"Content-Type": "application/json",
197
326
...Object.fromEntries(res.headers),
198
-
}
199
-
}
327
+
},
328
+
},
200
329
);
201
330
} catch (error) {
202
331
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);
332
+
console.error(
333
+
`[${new Date().toISOString()}] Blob migration error:`,
334
+
message,
335
+
);
336
+
console.error("Full error details:", error);
205
337
return new Response(
206
338
JSON.stringify({
207
339
success: false,
208
340
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)
341
+
error: error instanceof Error
342
+
? {
343
+
name: error.name,
344
+
message: error.message,
345
+
stack: error.stack,
346
+
}
347
+
: String(error),
214
348
}),
215
349
{
216
350
status: 500,
217
351
headers: {
218
352
"Content-Type": "application/json",
219
353
...Object.fromEntries(res.headers),
220
-
}
221
-
}
354
+
},
355
+
},
222
356
);
223
357
}
224
-
}
358
+
},
225
359
});
+92
-34
routes/api/migrate/data/prefs.ts
+92
-34
routes/api/migrate/data/prefs.ts
···
1
1
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
2
3
import { define } from "../../../../utils.ts";
3
4
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
4
5
···
17
18
console.log("Preferences migration: Got new agent:", !!newAgent);
18
19
19
20
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
-
});
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
+
);
27
46
}
28
47
29
48
// Migrate preferences
30
49
const migrationLogs: string[] = [];
31
50
const startTime = Date.now();
32
-
console.log(`[${new Date().toISOString()}] Starting preferences migration...`);
33
-
migrationLogs.push(`[${new Date().toISOString()}] Starting preferences migration...`);
51
+
console.log(
52
+
`[${new Date().toISOString()}] Starting preferences migration...`,
53
+
);
54
+
migrationLogs.push(
55
+
`[${new Date().toISOString()}] Starting preferences migration...`,
56
+
);
34
57
35
58
// Fetch preferences
36
-
console.log(`[${new Date().toISOString()}] Fetching preferences from old account...`);
37
-
migrationLogs.push(`[${new Date().toISOString()}] Fetching preferences from old account...`);
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
+
);
38
69
39
70
const fetchStartTime = Date.now();
40
71
const prefs = await oldAgent.app.bsky.actor.getPreferences();
41
72
const fetchTime = Date.now() - fetchStartTime;
42
73
43
-
console.log(`[${new Date().toISOString()}] Preferences fetched in ${fetchTime/1000} seconds`);
44
-
migrationLogs.push(`[${new Date().toISOString()}] Preferences fetched in ${fetchTime/1000} seconds`);
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
+
);
45
84
46
85
// Update preferences
47
-
console.log(`[${new Date().toISOString()}] Updating preferences on new account...`);
48
-
migrationLogs.push(`[${new Date().toISOString()}] Updating preferences on new account...`);
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
+
);
49
92
50
93
const updateStartTime = Date.now();
51
94
await newAgent.app.bsky.actor.putPreferences(prefs.data);
52
95
const updateTime = Date.now() - updateStartTime;
53
96
54
-
console.log(`[${new Date().toISOString()}] Preferences updated in ${updateTime/1000} seconds`);
55
-
migrationLogs.push(`[${new Date().toISOString()}] Preferences updated in ${updateTime/1000} seconds`);
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
+
);
56
107
57
108
const totalTime = Date.now() - startTime;
58
-
const completionMessage = `[${new Date().toISOString()}] Preferences migration completed in ${totalTime/1000} seconds total`;
109
+
const completionMessage = `[${
110
+
new Date().toISOString()
111
+
}] Preferences migration completed in ${totalTime / 1000} seconds total`;
59
112
console.log(completionMessage);
60
113
migrationLogs.push(completionMessage);
61
114
···
65
118
message: "Preferences migration completed successfully",
66
119
logs: migrationLogs,
67
120
timing: {
68
-
fetchTime: fetchTime/1000,
69
-
updateTime: updateTime/1000,
70
-
totalTime: totalTime/1000
71
-
}
121
+
fetchTime: fetchTime / 1000,
122
+
updateTime: updateTime / 1000,
123
+
totalTime: totalTime / 1000,
124
+
},
72
125
}),
73
126
{
74
127
status: 200,
75
128
headers: {
76
129
"Content-Type": "application/json",
77
130
...Object.fromEntries(res.headers),
78
-
}
79
-
}
131
+
},
132
+
},
80
133
);
81
134
} catch (error) {
82
135
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);
136
+
console.error(
137
+
`[${new Date().toISOString()}] Preferences migration error:`,
138
+
message,
139
+
);
140
+
console.error("Full error details:", error);
85
141
return new Response(
86
142
JSON.stringify({
87
143
success: false,
88
144
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)
145
+
error: error instanceof Error
146
+
? {
147
+
name: error.name,
148
+
message: error.message,
149
+
stack: error.stack,
150
+
}
151
+
: String(error),
94
152
}),
95
153
{
96
154
status: 500,
97
155
headers: {
98
156
"Content-Type": "application/json",
99
157
...Object.fromEntries(res.headers),
100
-
}
101
-
}
158
+
},
159
+
},
102
160
);
103
161
}
104
-
}
162
+
},
105
163
});
+86
-35
routes/api/migrate/data/repo.ts
+86
-35
routes/api/migrate/data/repo.ts
···
1
1
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
2
3
import { define } from "../../../../utils.ts";
3
4
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
4
5
···
13
14
const oldAgent = await getSessionAgent(ctx.req);
14
15
console.log("Repo migration: Got old agent:", !!oldAgent);
15
16
16
-
17
17
const newAgent = await getSessionAgent(ctx.req, res, true);
18
18
console.log("Repo migration: Got new agent:", !!newAgent);
19
19
20
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
-
});
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
+
);
28
46
}
29
47
30
48
const session = await oldAgent.com.atproto.server.getSession();
···
33
51
const migrationLogs: string[] = [];
34
52
const startTime = Date.now();
35
53
console.log(`[${new Date().toISOString()}] Starting repo migration...`);
36
-
migrationLogs.push(`[${new Date().toISOString()}] Starting repo migration...`);
54
+
migrationLogs.push(
55
+
`[${new Date().toISOString()}] Starting repo migration...`,
56
+
);
37
57
38
58
// 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...`);
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
+
);
41
65
42
66
const fetchStartTime = Date.now();
43
67
const repoData = await oldAgent.com.atproto.sync.getRepo({
···
45
69
});
46
70
const fetchTime = Date.now() - fetchStartTime;
47
71
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`);
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
+
);
50
82
51
-
console.log(`[${new Date().toISOString()}] Importing repo data to new account...`);
52
-
migrationLogs.push(`[${new Date().toISOString()}] Importing repo data to new account...`);
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
+
);
53
89
54
90
// Import repo data to new account
55
91
const importStartTime = Date.now();
56
92
await newAgent.com.atproto.repo.importRepo(repoData.data, {
57
-
encoding: "application/vnd.ipld.car"
93
+
encoding: "application/vnd.ipld.car",
58
94
});
59
95
const importTime = Date.now() - importStartTime;
60
96
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`);
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
+
);
63
107
64
108
const totalTime = Date.now() - startTime;
65
-
const completionMessage = `[${new Date().toISOString()}] Repo migration completed in ${totalTime/1000} seconds total`;
109
+
const completionMessage = `[${
110
+
new Date().toISOString()
111
+
}] Repo migration completed in ${totalTime / 1000} seconds total`;
66
112
console.log(completionMessage);
67
113
migrationLogs.push(completionMessage);
68
114
···
72
118
message: "Repo migration completed successfully",
73
119
logs: migrationLogs,
74
120
timing: {
75
-
fetchTime: fetchTime/1000,
76
-
importTime: importTime/1000,
77
-
totalTime: totalTime/1000
78
-
}
121
+
fetchTime: fetchTime / 1000,
122
+
importTime: importTime / 1000,
123
+
totalTime: totalTime / 1000,
124
+
},
79
125
}),
80
126
{
81
127
status: 200,
82
128
headers: {
83
129
"Content-Type": "application/json",
84
130
...Object.fromEntries(res.headers),
85
-
}
86
-
}
131
+
},
132
+
},
87
133
);
88
134
} catch (error) {
89
135
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);
136
+
console.error(
137
+
`[${new Date().toISOString()}] Repo migration error:`,
138
+
message,
139
+
);
140
+
console.error("Full error details:", error);
92
141
return new Response(
93
142
JSON.stringify({
94
143
success: false,
95
144
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)
145
+
error: error instanceof Error
146
+
? {
147
+
name: error.name,
148
+
message: error.message,
149
+
stack: error.stack,
150
+
}
151
+
: String(error),
101
152
}),
102
153
{
103
154
status: 500,
104
155
headers: {
105
156
"Content-Type": "application/json",
106
157
...Object.fromEntries(res.headers),
107
-
}
108
-
}
158
+
},
159
+
},
109
160
);
110
161
}
111
-
}
162
+
},
112
163
});
+13
routes/api/migrate/finalize.ts
+13
routes/api/migrate/finalize.ts
···
1
1
import { getSessionAgent } from "../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../lib/check-dids.ts";
2
3
import { define } from "../../../utils.ts";
3
4
import { assertMigrationAllowed } from "../../../lib/migration-state.ts";
4
5
···
17
18
return new Response("Migration session not found or invalid", {
18
19
status: 400,
19
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
+
);
20
33
}
21
34
22
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";
1
+
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
4
3
import { define } from "../../../../utils.ts";
5
4
import { assertMigrationAllowed } from "../../../../lib/migration-state.ts";
6
5
···
56
55
);
57
56
}
58
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
+
59
73
// Request the signature
60
74
console.log("Requesting PLC operation signature...");
61
75
try {
···
65
79
console.error("Error requesting PLC operation signature:", {
66
80
name: error instanceof Error ? error.name : "Unknown",
67
81
message: error instanceof Error ? error.message : String(error),
68
-
status: 400
82
+
status: 400,
69
83
});
70
84
throw error;
71
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";
1
+
import { getSessionAgent } from "../../../../lib/sessions.ts";
2
+
import { checkDidsMatch } from "../../../../lib/check-dids.ts";
4
3
import { Secp256k1Keypair } from "npm:@atproto/crypto";
5
4
import * as ui8 from "npm:uint8arrays";
6
5
import { define } from "../../../../utils.ts";
···
55
54
JSON.stringify({
56
55
success: false,
57
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",
58
72
}),
59
73
{
60
74
status: 400,
+2
-2
routes/api/migrate/next-step.ts
+2
-2
routes/api/migrate/next-step.ts
···
17
17
// Check conditions in sequence to determine the next step
18
18
if (!newStatus.data) {
19
19
nextStep = 1;
20
-
} else if (!(newStatus.data.repoCommit &&
20
+
} else if (!(newStatus.data.repoCommit &&
21
21
newStatus.data.indexedRecords === oldStatus.data.indexedRecords &&
22
22
newStatus.data.privateStateValues === oldStatus.data.privateStateValues &&
23
23
newStatus.data.expectedBlobs === newStatus.data.importedBlobs &&
···
42
42
}
43
43
});
44
44
}
45
-
})
45
+
})
+130
-104
routes/api/migrate/status.ts
+130
-104
routes/api/migrate/status.ts
···
1
+
import { checkDidsMatch } from "../../../lib/check-dids.ts";
1
2
import { getSessionAgent } from "../../../lib/sessions.ts";
2
3
import { define } from "../../../utils.ts";
3
4
4
5
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);
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);
11
12
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
-
}
13
+
console.log("Status check: Getting agents");
14
+
const oldAgent = await getSessionAgent(ctx.req);
15
+
const newAgent = await getSessionAgent(ctx.req, new Response(), true);
23
16
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
-
}
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
+
}
35
24
36
-
console.log("Status check: Account statuses", {
37
-
old: oldStatus.data,
38
-
new: newStatus.data
39
-
});
25
+
const didsMatch = await checkDidsMatch(ctx.req);
40
26
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;
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();
59
30
60
-
if (isReady) {
61
-
console.log("Status check: Step 2 ready");
62
-
return { ready: true };
63
-
}
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
+
}
64
38
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.");
39
+
console.log("Status check: Account statuses", {
40
+
old: oldStatus.data,
41
+
new: newStatus.data,
42
+
});
75
43
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 };
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 };
99
115
}
116
+
console.log(
117
+
"Status check: Step 4 not ready - Account not activated",
118
+
);
119
+
return { ready: false, reason: "Account not activated" };
120
+
}
100
121
}
122
+
} else {
123
+
console.log("Status check: No step specified, returning ready");
124
+
return { ready: true };
125
+
}
126
+
};
101
127
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
-
}
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
+
};
115
141
116
-
console.log("Status check: Complete", status);
117
-
return Response.json(status);
118
-
}
119
-
})
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
10
11
11
if (!service || !handle || !email || !password) {
12
12
return (
13
-
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-4">
13
+
<div class="bg-gray-50 dark:bg-gray-900 p-4">
14
14
<div class="max-w-2xl mx-auto">
15
15
<div class="bg-red-50 dark:bg-red-900 p-4 rounded-lg">
16
16
<p class="text-red-800 dark:text-red-200">
···
24
24
}
25
25
26
26
return (
27
-
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-4">
27
+
<div class="bg-gray-50 dark:bg-gray-900 p-4">
28
28
<div class="max-w-2xl mx-auto">
29
29
<h1 class="font-mono text-3xl font-bold text-gray-900 dark:text-white mb-8">
30
30
Migration Progress