tangled
alpha
login
or
join now
mary.my.id
/
boat
22
fork
atom
handy online tools for AT Protocol
boat.kelinci.net
atproto
bluesky
atcute
typescript
solidjs
22
fork
atom
overview
issues
pulls
pipelines
feat: more work into migration
mary.my.id
3 months ago
a463ad3c
c7e2b3c9
verified
This commit was signed with the committer's
known signature
.
mary.my.id
SSH Key Fingerprint:
SHA256:ZlTP/auFSGpGnaoDg4mCTG1g9OZvXp62jWR4c6H4O3c=
+178
-75
3 changed files
expand all
collapse all
unified
split
src
views
account
account-migrate
sections
blobs.tsx
identity.tsx
repository.tsx
+17
-16
src/views/account/account-migrate/sections/blobs.tsx
···
172
172
try {
173
173
const data = await entry.bytes();
174
174
await destClient.post('com.atproto.repo.uploadBlob', {
175
175
-
encoding: 'application/octet-stream',
176
175
input: data,
176
176
+
headers: {
177
177
+
'content-type': 'application/octet-stream',
178
178
+
},
177
179
});
178
180
uploaded++;
179
181
} catch (err) {
···
228
230
const contentType = response.headers.get('content-type') || 'application/octet-stream';
229
231
230
232
await destClient.post('com.atproto.repo.uploadBlob', {
231
231
-
encoding: contentType,
232
233
input: response.data,
234
234
+
headers: {
235
235
+
'content-type': contentType,
236
236
+
},
233
237
});
234
238
235
239
uploaded++;
···
297
301
const sourceData = importFromSourceMutation.data;
298
302
299
303
if (fileData && !fileData.cancelled) {
300
300
-
return `Uploaded ${fileData.uploaded} blobs` + (fileData.failed > 0 ? ` (${fileData.failed} failed)` : '');
304
304
+
return (
305
305
+
`Uploaded ${fileData.uploaded} blobs` + (fileData.failed > 0 ? ` (${fileData.failed} failed)` : '')
306
306
+
);
301
307
}
302
308
if (sourceData) {
303
309
if (sourceData.uploaded === 0 && sourceData.failed === 0) return 'No missing blobs';
304
304
-
return `Uploaded ${sourceData.uploaded} blobs` + (sourceData.failed > 0 ? ` (${sourceData.failed} failed)` : '');
310
310
+
return (
311
311
+
`Uploaded ${sourceData.uploaded} blobs` +
312
312
+
(sourceData.failed > 0 ? ` (${sourceData.failed} failed)` : '')
313
313
+
);
305
314
}
306
315
return importProgress();
307
316
};
···
311
320
return (
312
321
<Accordion title="Blobs">
313
322
<Subsection title="Export from source">
314
314
-
<p class="text-sm text-gray-600">
315
315
-
Download all blobs as a tarball for backup or manual import.
316
316
-
</p>
323
323
+
<p class="text-sm text-gray-600">Download all blobs as a tarball for backup or manual import.</p>
317
324
318
325
<Show when={source()} fallback={<p class="text-sm text-gray-500">Resolve source account first.</p>}>
319
326
{(src) => (
···
339
346
</Subsection>
340
347
341
348
<Subsection title="Import to destination">
342
342
-
<p class="text-sm text-gray-600">
343
343
-
Upload blobs from a tarball or transfer directly from source.
344
344
-
</p>
349
349
+
<p class="text-sm text-gray-600">Upload blobs from a tarball or transfer directly from source.</p>
345
350
346
351
<Show
347
352
when={destination()?.manager}
···
376
381
{(text) => <span class="text-sm text-gray-600">{text()}</span>}
377
382
</Show>
378
383
379
379
-
<Show when={getImportError()}>
380
380
-
{(err) => <p class="text-sm text-red-600">{`${err()}`}</p>}
381
381
-
</Show>
384
384
+
<Show when={getImportError()}>{(err) => <p class="text-sm text-red-600">{`${err()}`}</p>}</Show>
382
385
</>
383
386
)}
384
387
</Show>
···
403
406
<Show when={checkStatusMutation.data}>
404
407
{(status) => (
405
408
<span class="text-sm">
406
406
-
<StatusBadge
407
407
-
variant={status().imported === status().expected ? 'success' : 'pending'}
408
408
-
>
409
409
+
<StatusBadge variant={status().imported === status().expected ? 'success' : 'pending'}>
409
410
{status().imported}/{status().expected} blobs
410
411
</StatusBadge>
411
412
</span>
+152
-50
src/views/account/account-migrate/sections/identity.tsx
···
2
2
3
3
import { Client, ClientResponseError, type CredentialManager, ok } from '@atcute/client';
4
4
import { type DidKeyString, Secp256k1PrivateKeyExportable } from '@atcute/crypto';
5
5
+
import type { Did } from '@atcute/lexicons/syntax';
5
6
7
7
+
import { getPlcAuditLogs } from '~/api/queries/plc';
6
8
import { formatTotpCode, TOTP_RE } from '~/api/utils/auth';
7
9
8
10
import { createMutation } from '~/lib/utils/mutation';
···
11
13
import Button from '~/components/inputs/button';
12
14
import TextInput from '~/components/inputs/text-input';
13
15
import ToggleInput from '~/components/inputs/toggle-input';
16
16
+
17
17
+
import { getPlcPayload } from '~/views/identity/plc-applicator/plc-utils';
14
18
15
19
import { useMigration } from '../context';
16
20
···
48
52
const loadCredentialsMutation = createMutation({
49
53
async mutationFn({ manager }: { manager: CredentialManager }) {
50
54
const client = new Client({ handler: manager });
51
51
-
return (await ok(client.get('com.atproto.identity.getRecommendedDidCredentials', {}))) as RecommendedCredentials;
55
55
+
return (await ok(
56
56
+
client.get('com.atproto.identity.getRecommendedDidCredentials', {}),
57
57
+
)) as RecommendedCredentials;
58
58
+
},
59
59
+
onError(err) {
60
60
+
console.error(err);
61
61
+
},
62
62
+
});
63
63
+
64
64
+
// Analyze current rotation keys to find user-controlled keys that should be preserved
65
65
+
const analyzeRotationKeysMutation = createMutation({
66
66
+
async mutationFn({ did, sourceManager }: { did: Did<'plc'>; sourceManager: CredentialManager }, signal) {
67
67
+
// Get current rotation keys from PLC audit log
68
68
+
const auditLogs = await getPlcAuditLogs({ did, signal });
69
69
+
const latestEntry = auditLogs[auditLogs.length - 1];
70
70
+
const currentPayload = getPlcPayload(latestEntry);
71
71
+
const currentRotationKeys = currentPayload.rotationKeys ?? [];
72
72
+
73
73
+
// Get source PDS's recommended credentials to identify PDS-controlled keys
74
74
+
const sourceClient = new Client({ handler: sourceManager });
75
75
+
const sourcePdsCredentials = (await ok(
76
76
+
sourceClient.get('com.atproto.identity.getRecommendedDidCredentials', {}),
77
77
+
)) as RecommendedCredentials;
78
78
+
const sourcePdsKeys = new Set(sourcePdsCredentials.rotationKeys ?? []);
79
79
+
80
80
+
// Keys in current doc that aren't from source PDS are user-controlled
81
81
+
const userControlledKeys = currentRotationKeys.filter((key) => !sourcePdsKeys.has(key));
82
82
+
83
83
+
return {
84
84
+
currentRotationKeys,
85
85
+
sourcePdsKeys: sourcePdsCredentials.rotationKeys ?? [],
86
86
+
userControlledKeys,
87
87
+
};
88
88
+
},
89
89
+
onSuccess(data) {
90
90
+
// Pre-populate custom keys with user-controlled keys
91
91
+
if (data.userControlledKeys.length > 0) {
92
92
+
setCustomKeys(data.userControlledKeys);
93
93
+
}
52
94
},
53
95
onError(err) {
54
96
console.error(err);
···
199
241
</p>
200
242
</div>
201
243
202
202
-
<Subsection title="1. Request operation signature">
203
203
-
<p class="text-sm text-gray-600">Request a confirmation token via email from your source PDS.</p>
204
204
-
205
205
-
<Show
206
206
-
when={source()?.manager}
207
207
-
fallback={<p class="text-sm text-gray-500">Sign in to source account first.</p>}
208
208
-
>
209
209
-
{(manager) => (
210
210
-
<>
211
211
-
<div class="flex items-center gap-3">
212
212
-
<Button
213
213
-
onClick={() => requestTokenMutation.mutate({ manager: manager() })}
214
214
-
disabled={requestTokenMutation.isPending}
215
215
-
>
216
216
-
{requestTokenMutation.isPending ? 'Requesting...' : 'Request token'}
217
217
-
</Button>
218
218
-
219
219
-
<Show when={requestTokenMutation.isSuccess}>
220
220
-
<StatusBadge variant="success">Email sent</StatusBadge>
221
221
-
</Show>
222
222
-
</div>
223
223
-
224
224
-
<Show when={requestTokenMutation.isError}>
225
225
-
<p class="text-sm text-red-600">{`${requestTokenMutation.error}`}</p>
226
226
-
</Show>
227
227
-
228
228
-
<Show when={requestTokenMutation.isSuccess}>
229
229
-
<p class="text-sm text-gray-600">Check your email inbox for the confirmation code.</p>
230
230
-
</Show>
231
231
-
</>
232
232
-
)}
233
233
-
</Show>
234
234
-
</Subsection>
235
235
-
236
236
-
<Subsection title="2. Preview new credentials">
244
244
+
<Subsection title="1. Preview new credentials">
237
245
<p class="text-sm text-gray-600">View what your DID document will look like after the migration.</p>
238
246
239
247
<Show
···
265
273
<>
266
274
<div class="mt-2 text-sm">
267
275
<p class="text-gray-500">
268
268
-
PDS rotation keys ({creds().rotationKeys?.length ?? 0}/5):
276
276
+
Destination PDS rotation keys ({creds().rotationKeys?.length ?? 0}/5):
269
277
</p>
270
278
<div class="mt-1 flex flex-col gap-1">
271
279
<For each={creds().rotationKeys ?? []}>
272
272
-
{(key) => (
273
273
-
<code class="block truncate text-xs text-gray-700">{key}</code>
274
274
-
)}
280
280
+
{(key) => <code class="block truncate text-xs text-gray-700">{key}</code>}
275
281
</For>
276
282
</div>
277
283
</div>
278
284
285
285
+
<Show when={source()?.manager && source()}>
286
286
+
{(src) => (
287
287
+
<div class="mt-3 rounded border border-blue-200 bg-blue-50 p-3">
288
288
+
<div class="flex items-center justify-between">
289
289
+
<p class="text-sm font-medium text-blue-800">Analyze existing rotation keys</p>
290
290
+
<Button
291
291
+
variant="outline"
292
292
+
onClick={() =>
293
293
+
analyzeRotationKeysMutation.mutate({
294
294
+
did: src().did as Did<'plc'>,
295
295
+
sourceManager: src().manager!,
296
296
+
})
297
297
+
}
298
298
+
disabled={analyzeRotationKeysMutation.isPending}
299
299
+
>
300
300
+
{analyzeRotationKeysMutation.isPending ? 'Analyzing...' : 'Analyze'}
301
301
+
</Button>
302
302
+
</div>
303
303
+
<p class="mt-1 text-xs text-blue-600">
304
304
+
Check if you have any user-controlled rotation keys that should be preserved
305
305
+
during migration.
306
306
+
</p>
307
307
+
308
308
+
<Show when={analyzeRotationKeysMutation.error}>
309
309
+
<p class="mt-2 text-sm text-red-600">{`${analyzeRotationKeysMutation.error}`}</p>
310
310
+
</Show>
311
311
+
312
312
+
<Show when={analyzeRotationKeysMutation.data}>
313
313
+
{(analysis) => (
314
314
+
<div class="mt-2 text-sm">
315
315
+
<Show
316
316
+
when={analysis().userControlledKeys.length > 0}
317
317
+
fallback={
318
318
+
<p class="text-blue-700">
319
319
+
No user-controlled rotation keys found. Your current keys are all
320
320
+
managed by your source PDS.
321
321
+
</p>
322
322
+
}
323
323
+
>
324
324
+
<p class="font-medium text-blue-800">
325
325
+
Found {analysis().userControlledKeys.length} user-controlled key(s) to
326
326
+
preserve:
327
327
+
</p>
328
328
+
<div class="mt-1 flex flex-col gap-1">
329
329
+
<For each={analysis().userControlledKeys}>
330
330
+
{(key) => (
331
331
+
<code class="block truncate text-xs text-blue-700">{key}</code>
332
332
+
)}
333
333
+
</For>
334
334
+
</div>
335
335
+
<p class="mt-2 text-xs text-blue-600">
336
336
+
These keys have been added to the custom keys section below.
337
337
+
</p>
338
338
+
</Show>
339
339
+
</div>
340
340
+
)}
341
341
+
</Show>
342
342
+
</div>
343
343
+
)}
344
344
+
</Show>
345
345
+
279
346
<details class="mt-2">
280
280
-
<summary class="cursor-pointer text-sm text-gray-600">
281
281
-
View full credentials
282
282
-
</summary>
347
347
+
<summary class="cursor-pointer text-sm text-gray-600">View full credentials</summary>
283
348
<pre class="mt-2 max-h-48 overflow-auto rounded border border-gray-200 bg-gray-50 p-2 font-mono text-xs">
284
349
{JSON.stringify(creds(), null, 2)}
285
350
</pre>
···
292
357
</Show>
293
358
</Subsection>
294
359
295
295
-
<Subsection title="3. Rotation keys (optional)">
360
360
+
<Subsection title="2. Rotation keys (optional)">
296
361
<p class="text-sm text-gray-600">
297
297
-
Add a rotation key to recover your account if your new PDS goes rogue. This will be prepended to
298
298
-
the PDS rotation keys shown above.
362
362
+
Add a rotation key to recover your account if your new PDS goes rogue. This will be prepended to the
363
363
+
PDS rotation keys shown above.
299
364
</p>
300
365
301
366
<ToggleInput
···
323
388
<div class="rounded border border-green-300 bg-green-50 p-3">
324
389
<p class="mb-2 text-sm font-semibold text-green-800">Save your rotation key private key!</p>
325
390
<p class="mb-3 text-xs text-green-700">
326
326
-
Store this securely. You'll need it to recover your account if your PDS becomes
327
327
-
unavailable or malicious.
391
391
+
Store this securely. You'll need it to recover your account if your PDS becomes unavailable or
392
392
+
malicious.
328
393
</p>
329
394
330
395
<div class="flex flex-col gap-2 text-sm">
···
345
410
)}
346
411
</Show>
347
412
348
348
-
<div class="mt-4 border-t border-gray-200 pt-4">
413
413
+
<div class="rounded border border-gray-200 bg-gray-50 p-3">
349
414
<p class="mb-2 text-sm font-medium text-gray-700">Custom rotation keys</p>
350
415
<p class="mb-3 text-xs text-gray-500">
351
416
Add existing rotation keys (did:key format) you already control.
···
394
459
</div>
395
460
</Subsection>
396
461
462
462
+
<Subsection title="3. Request operation signature">
463
463
+
<p class="text-sm text-gray-600">Request a confirmation token via email from your source PDS.</p>
464
464
+
465
465
+
<Show
466
466
+
when={source()?.manager}
467
467
+
fallback={<p class="text-sm text-gray-500">Sign in to source account first.</p>}
468
468
+
>
469
469
+
{(manager) => (
470
470
+
<>
471
471
+
<div class="flex items-center gap-3">
472
472
+
<Button
473
473
+
onClick={() => requestTokenMutation.mutate({ manager: manager() })}
474
474
+
disabled={requestTokenMutation.isPending}
475
475
+
>
476
476
+
{requestTokenMutation.isPending ? 'Requesting...' : 'Request token'}
477
477
+
</Button>
478
478
+
479
479
+
<Show when={requestTokenMutation.isSuccess}>
480
480
+
<StatusBadge variant="success">Email sent</StatusBadge>
481
481
+
</Show>
482
482
+
</div>
483
483
+
484
484
+
<Show when={requestTokenMutation.isError}>
485
485
+
<p class="text-sm text-red-600">{`${requestTokenMutation.error}`}</p>
486
486
+
</Show>
487
487
+
488
488
+
<Show when={requestTokenMutation.isSuccess}>
489
489
+
<p class="text-sm text-gray-600">Check your email inbox for the confirmation code.</p>
490
490
+
</Show>
491
491
+
</>
492
492
+
)}
493
493
+
</Show>
494
494
+
</Subsection>
495
495
+
397
496
<Subsection title="4. Sign and submit">
398
497
<p class="text-sm text-gray-600">Enter the confirmation code and submit the PLC operation.</p>
399
498
···
422
521
/>
423
522
424
523
<div class="flex items-center gap-3">
425
425
-
<Button onClick={handleSignAndSubmit} disabled={signAndSubmitMutation.isPending || !canSignAndSubmit()}>
524
524
+
<Button
525
525
+
onClick={handleSignAndSubmit}
526
526
+
disabled={signAndSubmitMutation.isPending || !canSignAndSubmit()}
527
527
+
>
426
528
{signAndSubmitMutation.isPending ? 'Submitting...' : 'Sign and submit'}
427
529
</Button>
428
530
+9
-9
src/views/account/account-migrate/sections/repository.tsx
···
109
109
return null;
110
110
}
111
111
112
112
-
setImportStatus('Reading file...');
113
112
const file = await fd.getFile();
114
114
-
const data = new Uint8Array(await file.arrayBuffer());
115
113
116
116
-
setImportStatus(`Uploading repository (${formatBytes(data.length)})...`);
114
114
+
setImportStatus(`Uploading repository (${formatBytes(file.size)})...`);
117
115
118
116
const destClient = new Client({ handler: manager });
119
117
const importResp = await destClient.post('com.atproto.repo.importRepo', {
120
118
as: null,
121
121
-
encoding: 'application/vnd.ipld.car',
122
122
-
input: data,
119
119
+
input: file,
120
120
+
headers: {
121
121
+
'content-type': 'application/vnd.ipld.car',
122
122
+
},
123
123
});
124
124
125
125
if (!importResp.ok) {
···
170
170
const destClient = new Client({ handler: destManager });
171
171
const importResp = await destClient.post('com.atproto.repo.importRepo', {
172
172
as: null,
173
173
-
encoding: 'application/vnd.ipld.car',
174
173
input: response.data,
174
174
+
headers: {
175
175
+
'content-type': 'application/vnd.ipld.car',
176
176
+
},
175
177
});
176
178
177
179
if (!importResp.ok) {
···
229
231
</Subsection>
230
232
231
233
<Subsection title="Import to destination">
232
232
-
<p class="text-sm text-gray-600">
233
233
-
Upload a repository CAR file or transfer directly from source.
234
234
-
</p>
234
234
+
<p class="text-sm text-gray-600">Upload a repository CAR file or transfer directly from source.</p>
235
235
236
236
<Show
237
237
when={destination()?.manager}