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); + }