diff --git a/frontend/src/components/migration/InboundWizard.svelte b/frontend/src/components/migration/InboundWizard.svelte
index 84a8f75..21421f6 100644
--- a/frontend/src/components/migration/InboundWizard.svelte
+++ b/frontend/src/components/migration/InboundWizard.svelte
@@ -22,6 +22,7 @@
let checkingHandle = $state(false)
const isResumedMigration = $derived(flow.state.progress.repoImported)
+ const isDidWeb = $derived(flow.state.sourceDid.startsWith("did:web:"))
$effect(() => {
if (flow.state.step === 'welcome' || flow.state.step === 'choose-handle') {
@@ -187,7 +188,20 @@
}
}
- const steps = ['Login', 'Handle', 'Review', 'Transfer', 'Verify Email', 'Verify PLC', 'Complete']
+ async function completeDidWeb() {
+ loading = true
+ try {
+ await flow.completeDidWebMigration()
+ } catch (err) {
+ flow.setError((err as Error).message)
+ } finally {
+ loading = false
+ }
+ }
+
+ const steps = $derived(isDidWeb
+ ? ['Login', 'Handle', 'Review', 'Transfer', 'Verify Email', 'Update DID', 'Complete']
+ : ['Login', 'Handle', 'Review', 'Transfer', 'Verify Email', 'Verify PLC', 'Complete'])
function getCurrentStepIndex(): number {
switch (flow.state.step) {
case 'welcome':
@@ -197,6 +211,7 @@
case 'migrating': return 3
case 'email-verify': return 4
case 'plc-token':
+ case 'did-web-update':
case 'finalizing': return 5
case 'success': return 6
default: return 0
@@ -589,6 +604,46 @@
+ {:else if flow.state.step === 'did-web-update'}
+
+
{$_('migration.inbound.didWebUpdate.title')}
+
{$_('migration.inbound.didWebUpdate.desc')}
+
+
+
+ {$_('migration.inbound.didWebUpdate.yourDid')} {flow.state.sourceDid}
+
+
+ {$_('migration.inbound.didWebUpdate.updateInstructions')}
+
+
+
+
+
{`{
+ "id": "${flow.state.sourceDid}",
+ "service": [
+ {
+ "id": "#atproto_pds",
+ "type": "AtprotoPersonalDataServer",
+ "serviceEndpoint": "${window.location.origin}"
+ }
+ ]
+}`}
+
+
+
+ {$_('migration.inbound.didWebUpdate.important')} {$_('migration.inbound.didWebUpdate.verifyFirst')}
+ {$_('migration.inbound.didWebUpdate.fileLocation')} https://{flow.state.sourceDid.replace('did:web:', '')}/.well-known/did.json
+
+
+
+
+
+
+
+
{:else if flow.state.step === 'finalizing'}
Finalizing Migration
@@ -1021,4 +1076,29 @@
border-radius: var(--radius-lg);
margin-bottom: var(--space-5);
}
+
+ .code-block {
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-4);
+ margin-bottom: var(--space-5);
+ overflow-x: auto;
+ }
+
+ .code-block pre {
+ margin: 0;
+ font-family: var(--font-mono);
+ font-size: var(--text-sm);
+ white-space: pre-wrap;
+ word-break: break-all;
+ }
+
+ code {
+ font-family: var(--font-mono);
+ background: var(--bg-primary);
+ padding: 2px 6px;
+ border-radius: var(--radius-sm);
+ font-size: 0.9em;
+ }
diff --git a/frontend/src/lib/migration/flow.svelte.ts b/frontend/src/lib/migration/flow.svelte.ts
index 5346d3b..5bc9c96 100644
--- a/frontend/src/lib/migration/flow.svelte.ts
+++ b/frontend/src/lib/migration/flow.svelte.ts
@@ -371,9 +371,13 @@ export function createInboundMigrationFlow() {
return;
}
- setProgress({ currentOperation: "Requesting PLC operation token..." });
- await sourceClient.requestPlcOperationSignature();
- setStep("plc-token");
+ if (state.sourceDid.startsWith("did:web:")) {
+ setStep("did-web-update");
+ } else {
+ setProgress({ currentOperation: "Requesting PLC operation token..." });
+ await sourceClient.requestPlcOperationSignature();
+ setStep("plc-token");
+ }
} catch (e) {
const err = e as Error & { error?: string; status?: number };
const message = err.message || err.error ||
@@ -401,8 +405,12 @@ export function createInboundMigrationFlow() {
state.targetEmail,
state.targetPassword,
);
- await sourceClient.requestPlcOperationSignature();
- setStep("plc-token");
+ if (state.sourceDid.startsWith("did:web:")) {
+ setStep("did-web-update");
+ } else {
+ await sourceClient.requestPlcOperationSignature();
+ setStep("plc-token");
+ }
return true;
} catch (e) {
const err = e as Error & { error?: string };
@@ -543,6 +551,55 @@ export function createInboundMigrationFlow() {
await sourceClient.requestPlcOperationSignature();
}
+ async function completeDidWebMigration(): Promise {
+ migrationLog("completeDidWebMigration START", {
+ sourceDid: state.sourceDid,
+ sourceHandle: state.sourceHandle,
+ targetHandle: state.targetHandle,
+ });
+
+ if (!sourceClient || !localClient) {
+ migrationLog("completeDidWebMigration ERROR: Not connected to PDSes");
+ throw new Error("Not connected to PDSes");
+ }
+
+ setStep("finalizing");
+ setProgress({ currentOperation: "Activating account..." });
+
+ try {
+ migrationLog("Activating account on NEW PDS");
+ const activateStart = Date.now();
+ await localClient.activateAccount();
+ migrationLog("Account activated", { durationMs: Date.now() - activateStart });
+ setProgress({ activated: true });
+
+ setProgress({ currentOperation: "Deactivating old account..." });
+ migrationLog("Deactivating account on OLD PDS");
+ const deactivateStart = Date.now();
+ try {
+ await sourceClient.deactivateAccount();
+ migrationLog("Account deactivated on OLD PDS", {
+ durationMs: Date.now() - deactivateStart,
+ });
+ setProgress({ deactivated: true });
+ } catch (deactivateErr) {
+ const err = deactivateErr as Error & { error?: string };
+ migrationLog("Could not deactivate on OLD PDS", { error: err.message });
+ }
+
+ migrationLog("completeDidWebMigration SUCCESS");
+ setStep("success");
+ clearMigrationState();
+ } catch (e) {
+ const err = e as Error & { error?: string; status?: number };
+ const message = err.message || err.error ||
+ `Unknown error (status ${err.status || "unknown"})`;
+ migrationLog("completeDidWebMigration FAILED", { error: message });
+ setError(message);
+ setStep("did-web-update");
+ }
+ }
+
function reset(): void {
state = {
direction: "inbound",
@@ -614,6 +671,7 @@ export function createInboundMigrationFlow() {
requestPlcToken,
submitPlcToken,
resendPlcToken,
+ completeDidWebMigration,
reset,
resumeFromState,
getLocalSession,
diff --git a/frontend/src/lib/migration/types.ts b/frontend/src/lib/migration/types.ts
index 7bc7728..57242e9 100644
--- a/frontend/src/lib/migration/types.ts
+++ b/frontend/src/lib/migration/types.ts
@@ -6,6 +6,7 @@ export type InboundStep =
| "migrating"
| "email-verify"
| "plc-token"
+ | "did-web-update"
| "finalizing"
| "success"
| "error";
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 826b3e8..c5994d8 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -1106,6 +1106,17 @@
"resend": "Resend Token",
"resending": "Resending..."
},
+ "didWebUpdate": {
+ "title": "Update Your DID Document",
+ "desc": "Since you're using a did:web identity, you need to update your DID document to point to this PDS.",
+ "yourDid": "Your DID is:",
+ "updateInstructions": "Update the did.json file at your domain to point the atproto_pds service endpoint to this PDS:",
+ "important": "Important:",
+ "verifyFirst": "Make sure your DID document is updated and publicly accessible before completing the migration.",
+ "fileLocation": "The file should be at:",
+ "complete": "Complete Migration",
+ "completing": "Completing..."
+ },
"finalizing": {
"title": "Finalizing Migration",
"desc": "Please wait while we complete the migration...",
diff --git a/frontend/src/locales/fi.json b/frontend/src/locales/fi.json
index b3bd448..e258758 100644
--- a/frontend/src/locales/fi.json
+++ b/frontend/src/locales/fi.json
@@ -1106,6 +1106,17 @@
"resend": "Lähetä uudelleen",
"resending": "Lähetetään..."
},
+ "didWebUpdate": {
+ "title": "Päivitä DID-dokumenttisi",
+ "desc": "Koska käytät did:web-identiteettiä, sinun täytyy päivittää DID-dokumenttisi osoittamaan tähän PDS:ään.",
+ "yourDid": "DID:si on:",
+ "updateInstructions": "Päivitä verkkotunnuksesi did.json-tiedosto niin, että atproto_pds-palvelun päätepiste osoittaa tähän PDS:ään:",
+ "important": "Tärkeää:",
+ "verifyFirst": "Varmista, että DID-dokumenttisi on päivitetty ja julkisesti saatavilla ennen siirron viimeistelyä.",
+ "fileLocation": "Tiedoston tulee sijaita:",
+ "complete": "Viimeistele siirto",
+ "completing": "Viimeistellään..."
+ },
"finalizing": {
"title": "Viimeistellään siirtoa",
"desc": "Odota, kun viimeistelemme siirtoa...",
diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json
index 7f9cc1e..cf62d21 100644
--- a/frontend/src/locales/ja.json
+++ b/frontend/src/locales/ja.json
@@ -1106,6 +1106,17 @@
"resend": "再送信",
"resending": "送信中..."
},
+ "didWebUpdate": {
+ "title": "DIDドキュメントを更新",
+ "desc": "did:webアイデンティティを使用しているため、DIDドキュメントを更新してこのPDSを指すようにする必要があります。",
+ "yourDid": "あなたのDID:",
+ "updateInstructions": "ドメインのdid.jsonファイルを更新して、atproto_pdsサービスエンドポイントをこのPDSに向けてください:",
+ "important": "重要:",
+ "verifyFirst": "移行を完了する前に、DIDドキュメントが更新され、公開アクセス可能であることを確認してください。",
+ "fileLocation": "ファイルの場所:",
+ "complete": "移行を完了",
+ "completing": "完了中..."
+ },
"finalizing": {
"title": "移行を完了中",
"desc": "移行を完了しています...",
diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json
index 0e45776..a5d5ef0 100644
--- a/frontend/src/locales/ko.json
+++ b/frontend/src/locales/ko.json
@@ -1106,6 +1106,17 @@
"resend": "재전송",
"resending": "전송 중..."
},
+ "didWebUpdate": {
+ "title": "DID 문서 업데이트",
+ "desc": "did:web 아이덴티티를 사용하고 있으므로 DID 문서를 이 PDS를 가리키도록 업데이트해야 합니다.",
+ "yourDid": "당신의 DID:",
+ "updateInstructions": "도메인의 did.json 파일을 업데이트하여 atproto_pds 서비스 엔드포인트가 이 PDS를 가리키도록 하세요:",
+ "important": "중요:",
+ "verifyFirst": "마이그레이션을 완료하기 전에 DID 문서가 업데이트되고 공개적으로 접근 가능한지 확인하세요.",
+ "fileLocation": "파일 위치:",
+ "complete": "마이그레이션 완료",
+ "completing": "완료 중..."
+ },
"finalizing": {
"title": "마이그레이션 완료 중",
"desc": "마이그레이션을 완료하는 중입니다...",
diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json
index 8a8c8ac..db5cfa5 100644
--- a/frontend/src/locales/sv.json
+++ b/frontend/src/locales/sv.json
@@ -1106,6 +1106,17 @@
"resend": "Skicka igen",
"resending": "Skickar..."
},
+ "didWebUpdate": {
+ "title": "Uppdatera ditt DID-dokument",
+ "desc": "Eftersom du använder en did:web-identitet måste du uppdatera ditt DID-dokument för att peka på denna PDS.",
+ "yourDid": "Ditt DID är:",
+ "updateInstructions": "Uppdatera did.json-filen på din domän så att atproto_pds-tjänstens slutpunkt pekar på denna PDS:",
+ "important": "Viktigt:",
+ "verifyFirst": "Se till att ditt DID-dokument är uppdaterat och offentligt tillgängligt innan du slutför flytten.",
+ "fileLocation": "Filen ska finnas på:",
+ "complete": "Slutför flytt",
+ "completing": "Slutför..."
+ },
"finalizing": {
"title": "Slutför flytt",
"desc": "Vänta medan vi slutför flytten...",
diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json
index 3d7cc98..1161cb4 100644
--- a/frontend/src/locales/zh.json
+++ b/frontend/src/locales/zh.json
@@ -1106,6 +1106,17 @@
"resend": "重新发送",
"resending": "发送中..."
},
+ "didWebUpdate": {
+ "title": "更新您的DID文档",
+ "desc": "由于您使用的是did:web身份,您需要更新DID文档以指向此PDS。",
+ "yourDid": "您的DID是:",
+ "updateInstructions": "更新您域名上的did.json文件,将atproto_pds服务端点指向此PDS:",
+ "important": "重要提示:",
+ "verifyFirst": "在完成迁移之前,请确保您的DID文档已更新并可公开访问。",
+ "fileLocation": "文件应位于:",
+ "complete": "完成迁移",
+ "completing": "完成中..."
+ },
"finalizing": {
"title": "正在完成迁移",
"desc": "请稍候,正在完成迁移...",
diff --git a/frontend/src/routes/RepoExplorer.svelte b/frontend/src/routes/RepoExplorer.svelte
index acddf80..ff3ea5d 100644
--- a/frontend/src/routes/RepoExplorer.svelte
+++ b/frontend/src/routes/RepoExplorer.svelte
@@ -495,10 +495,20 @@
.back {
color: var(--text-secondary);
text-decoration: none;
+ padding: var(--space-1) var(--space-2);
+ margin: calc(-1 * var(--space-1)) calc(-1 * var(--space-2));
+ border-radius: var(--radius-sm);
+ transition: background var(--transition-fast), color var(--transition-fast);
}
.back:hover {
color: var(--accent);
+ background: var(--accent-muted);
+ }
+
+ .back:focus {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
}
.sep {
@@ -508,16 +518,25 @@
.breadcrumb-link {
background: none;
border: none;
- padding: 0;
+ padding: var(--space-1) var(--space-2);
+ margin: calc(-1 * var(--space-1)) calc(-1 * var(--space-2));
color: var(--accent);
cursor: pointer;
font-size: inherit;
+ border-radius: var(--radius-sm);
+ transition: background var(--transition-fast);
}
.breadcrumb-link:hover {
+ background: var(--accent-muted);
text-decoration: underline;
}
+ .breadcrumb-link:focus {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+
.current {
color: var(--text-secondary);
}
@@ -683,19 +702,29 @@
align-items: center;
width: 100%;
padding: var(--space-3);
- background: var(--bg-card);
+ background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
text-align: left;
color: var(--text-primary);
- transition: border-color var(--transition-fast);
+ transition: background var(--transition-fast), border-color var(--transition-fast);
}
.collection-link:hover {
+ background: var(--bg-secondary);
border-color: var(--accent);
}
+ .collection-link:focus {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+
+ .collection-link:active {
+ background: var(--bg-tertiary);
+ }
+
.nsid {
font-weight: var(--font-medium);
color: var(--accent);
@@ -705,6 +734,10 @@
color: var(--text-muted);
}
+ .collection-link:hover .arrow {
+ color: var(--accent);
+ }
+
.record-list {
list-style: none;
padding: 0;
@@ -718,19 +751,29 @@
display: block;
width: 100%;
padding: var(--space-4);
- background: var(--bg-card);
+ background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
cursor: pointer;
text-align: left;
color: var(--text-primary);
- transition: border-color var(--transition-fast);
+ transition: background var(--transition-fast), border-color var(--transition-fast);
}
.record-item:hover {
+ background: var(--bg-secondary);
border-color: var(--accent);
}
+ .record-item:focus {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+
+ .record-item:active {
+ background: var(--bg-tertiary);
+ }
+
.record-info {
display: flex;
justify-content: space-between;
@@ -927,4 +970,14 @@
padding: var(--space-6);
border-radius: var(--radius-xl);
}
+
+ .page ::selection {
+ background: var(--accent);
+ color: var(--text-inverse);
+ }
+
+ .page ::-moz-selection {
+ background: var(--accent);
+ color: var(--text-inverse);
+ }