diff --git a/.env.example b/.env.example index 4187515..a2e05ad 100644 --- a/.env.example +++ b/.env.example @@ -106,6 +106,10 @@ AWS_SECRET_ACCESS_KEY=minioadmin # INVITE_CODE_REQUIRED=false # Comma-separated list of available user domains # AVAILABLE_USER_DOMAINS=example.com +# Enable self-hosted did:web identities (default: true) +# Hosting did:web requires a long-term commitment to serve DID documents. +# Set to false if you don't want to offer this option. +# ENABLE_SELF_HOSTED_DID_WEB=true # ============================================================================= # Server Metadata (returned by describeServer) # ============================================================================= diff --git a/.sqlx/query-c36e3ae06df1d0f795771b2452df4cb3d78b00fdb7ed44b9adbc105cd2cb2782.json b/.sqlx/query-0d0622d485361d9f4d4f85e1ba23a331f16155c08208b65b448e1e4659a070b8.json similarity index 81% rename from .sqlx/query-c36e3ae06df1d0f795771b2452df4cb3d78b00fdb7ed44b9adbc105cd2cb2782.json rename to .sqlx/query-0d0622d485361d9f4d4f85e1ba23a331f16155c08208b65b448e1e4659a070b8.json index fd29910..e0e7996 100644 --- a/.sqlx/query-c36e3ae06df1d0f795771b2452df4cb3d78b00fdb7ed44b9adbc105cd2cb2782.json +++ b/.sqlx/query-0d0622d485361d9f4d4f85e1ba23a331f16155c08208b65b448e1e4659a070b8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n handle, email, email_verified, is_admin, deactivated_at, takedown_ref, preferred_locale,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1", + "query": "SELECT\n handle, email, email_verified, is_admin, deactivated_at, takedown_ref, preferred_locale,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified, migrated_to_pds, migrated_at\n FROM users WHERE did = $1", "describe": { "columns": [ { @@ -69,6 +69,16 @@ "ordinal": 10, "name": "signal_verified", "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "migrated_to_pds", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "migrated_at", + "type_info": "Timestamptz" } ], "parameters": { @@ -87,8 +97,10 @@ false, false, false, - false + false, + true, + true ] }, - "hash": "c36e3ae06df1d0f795771b2452df4cb3d78b00fdb7ed44b9adbc105cd2cb2782" + "hash": "0d0622d485361d9f4d4f85e1ba23a331f16155c08208b65b448e1e4659a070b8" } diff --git a/.sqlx/query-0d6565c792bb9c2845d03ac1cb984658d77a26f90df511686e47b358c79a8ebe.json b/.sqlx/query-0d6565c792bb9c2845d03ac1cb984658d77a26f90df511686e47b358c79a8ebe.json new file mode 100644 index 0000000..5c8966b --- /dev/null +++ b/.sqlx/query-0d6565c792bb9c2845d03ac1cb984658d77a26f90df511686e47b358c79a8ebe.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET deactivated_at = NOW(), delete_after = $2, migrated_to_pds = $3, migrated_at = NOW() WHERE did = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Timestamptz", + "Text" + ] + }, + "nullable": [] + }, + "hash": "0d6565c792bb9c2845d03ac1cb984658d77a26f90df511686e47b358c79a8ebe" +} diff --git a/.sqlx/query-36f84d8aa41bb289d09fda5e26a91543b1bfd100c659db47bef97954f4c25580.json b/.sqlx/query-36f84d8aa41bb289d09fda5e26a91543b1bfd100c659db47bef97954f4c25580.json new file mode 100644 index 0000000..7380479 --- /dev/null +++ b/.sqlx/query-36f84d8aa41bb289d09fda5e26a91543b1bfd100c659db47bef97954f4c25580.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, handle, migrated_to_pds FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "handle", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "migrated_to_pds", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true + ] + }, + "hash": "36f84d8aa41bb289d09fda5e26a91543b1bfd100c659db47bef97954f4c25580" +} diff --git a/.sqlx/query-63cfbd8c2fda2c01cb9a97fc2768b60cafecaa4fa3006c2db9848e852d867073.json b/.sqlx/query-63cfbd8c2fda2c01cb9a97fc2768b60cafecaa4fa3006c2db9848e852d867073.json new file mode 100644 index 0000000..d7aaa1e --- /dev/null +++ b/.sqlx/query-63cfbd8c2fda2c01cb9a97fc2768b60cafecaa4fa3006c2db9848e852d867073.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, migrated_to_pds, handle FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "migrated_to_pds", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "handle", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + true, + false + ] + }, + "hash": "63cfbd8c2fda2c01cb9a97fc2768b60cafecaa4fa3006c2db9848e852d867073" +} diff --git a/.sqlx/query-7f8bc1ef416b851704ccc5232abd65a71e624aab95ae44f57448a88bef78e2a3.json b/.sqlx/query-7f8bc1ef416b851704ccc5232abd65a71e624aab95ae44f57448a88bef78e2a3.json new file mode 100644 index 0000000..bac9712 --- /dev/null +++ b/.sqlx/query-7f8bc1ef416b851704ccc5232abd65a71e624aab95ae44f57448a88bef78e2a3.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO did_web_overrides (user_id, verification_methods, also_known_as, updated_at)\n VALUES ($1, COALESCE($2, '[]'::jsonb), COALESCE($3, '{}'::text[]), $4)\n ON CONFLICT (user_id) DO UPDATE SET\n verification_methods = CASE WHEN $2 IS NOT NULL THEN $2 ELSE did_web_overrides.verification_methods END,\n also_known_as = CASE WHEN $3 IS NOT NULL THEN $3 ELSE did_web_overrides.also_known_as END,\n updated_at = $4\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Jsonb", + "TextArray", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "7f8bc1ef416b851704ccc5232abd65a71e624aab95ae44f57448a88bef78e2a3" +} diff --git a/.sqlx/query-d91efadf413a90b907727cffd1ce13eb68c3ef017a34e69629df1255de312317.json b/.sqlx/query-d91efadf413a90b907727cffd1ce13eb68c3ef017a34e69629df1255de312317.json new file mode 100644 index 0000000..5cd61a2 --- /dev/null +++ b/.sqlx/query-d91efadf413a90b907727cffd1ce13eb68c3ef017a34e69629df1255de312317.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT (migrated_to_pds IS NOT NULL AND deactivated_at IS NOT NULL) as \"migrated!: bool\" FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "migrated!: bool", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "d91efadf413a90b907727cffd1ce13eb68c3ef017a34e69629df1255de312317" +} diff --git a/.sqlx/query-e6aa80223281c7d4303122f92a9cf0e8718ca331412fb48c4bd24a5c5acb4492.json b/.sqlx/query-e6aa80223281c7d4303122f92a9cf0e8718ca331412fb48c4bd24a5c5acb4492.json new file mode 100644 index 0000000..d5a8a86 --- /dev/null +++ b/.sqlx/query-e6aa80223281c7d4303122f92a9cf0e8718ca331412fb48c4bd24a5c5acb4492.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT verification_methods, also_known_as FROM did_web_overrides WHERE user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "verification_methods", + "type_info": "Jsonb" + }, + { + "ordinal": 1, + "name": "also_known_as", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "e6aa80223281c7d4303122f92a9cf0e8718ca331412fb48c4bd24a5c5acb4492" +} diff --git a/.sqlx/query-1901ab0945813eee128c0f5de066c61ef13f671243add1d1c4d722e4f8b5c1ce.json b/.sqlx/query-f06ceae0d1567cab89c48516544879b7ee5a0e9e07afeca837cd49ddd54c129d.json similarity index 79% rename from .sqlx/query-1901ab0945813eee128c0f5de066c61ef13f671243add1d1c4d722e4f8b5c1ce.json rename to .sqlx/query-f06ceae0d1567cab89c48516544879b7ee5a0e9e07afeca837cd49ddd54c129d.json index d7fdd62..a21de0d 100644 --- a/.sqlx/query-1901ab0945813eee128c0f5de066c61ef13f671243add1d1c4d722e4f8b5c1ce.json +++ b/.sqlx/query-f06ceae0d1567cab89c48516544879b7ee5a0e9e07afeca837cd49ddd54c129d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n u.id, u.did, u.handle, u.password_hash, u.email, u.deactivated_at, u.takedown_ref,\n u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,\n u.allow_legacy_login,\n u.preferred_comms_channel as \"preferred_comms_channel: crate::comms::CommsChannel\",\n k.key_bytes, k.encryption_version,\n (SELECT verified FROM user_totp WHERE did = u.did) as totp_enabled\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.email = $1 OR u.did = $1", + "query": "SELECT\n u.id, u.did, u.handle, u.password_hash, u.email, u.deactivated_at, u.takedown_ref,\n u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,\n u.allow_legacy_login, u.migrated_to_pds,\n u.preferred_comms_channel as \"preferred_comms_channel: crate::comms::CommsChannel\",\n k.key_bytes, k.encryption_version,\n (SELECT verified FROM user_totp WHERE did = u.did) as totp_enabled\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.email = $1 OR u.did = $1", "describe": { "columns": [ { @@ -65,6 +65,11 @@ }, { "ordinal": 12, + "name": "migrated_to_pds", + "type_info": "Text" + }, + { + "ordinal": 13, "name": "preferred_comms_channel: crate::comms::CommsChannel", "type_info": { "Custom": { @@ -81,17 +86,17 @@ } }, { - "ordinal": 13, + "ordinal": 14, "name": "key_bytes", "type_info": "Bytea" }, { - "ordinal": 14, + "ordinal": 15, "name": "encryption_version", "type_info": "Int4" }, { - "ordinal": 15, + "ordinal": 16, "name": "totp_enabled", "type_info": "Bool" } @@ -114,11 +119,12 @@ false, false, false, + true, false, false, true, null ] }, - "hash": "1901ab0945813eee128c0f5de066c61ef13f671243add1d1c4d722e4f8b5c1ce" + "hash": "f06ceae0d1567cab89c48516544879b7ee5a0e9e07afeca837cd49ddd54c129d" } diff --git a/TODO.md b/TODO.md index dd77b6f..17cd6f0 100644 --- a/TODO.md +++ b/TODO.md @@ -5,14 +5,14 @@ ### Migration tool Seamless account migration built into the UI, inspired by pdsmoover. Users shouldn't need external tools or brain surgery on half-done account states. -- [ ] Add `migratingTo` parameter to `deactivateAccount` endpoint -- [ ] For self-hosted did:web users: set `migrated_to_pds`, update DID doc serviceEndpoint -- [ ] "Migrated" account state for self-hosted did:web: can authenticate but no repo operations -- [ ] Migrated did:web user UI: minimal dashboard with "update forwarding PDS" setting, or full migration wizard to handle PDS 2 -> PDS 3 moves automatically -- [ ] Outbound UI wizard: new PDS URL -> export repo -> guide account creation -> complete migration -- [ ] Inbound UI wizard: login to old PDS -> choose handle -> import -> PLC token flow -- [ ] Support `createAccount` with existing DID + service auth token -- [ ] Progress tracking with resume capability +- [x] Add `migratingTo` parameter to `deactivateAccount` endpoint +- [x] For self-hosted did:web users: set `migrated_to_pds`, update DID doc serviceEndpoint +- [x] "Migrated" account state for self-hosted did:web: can authenticate but no repo operations +- [x] Migrated did:web user UI: minimal dashboard with "update forwarding PDS" setting, or full migration wizard to handle PDS 2 -> PDS 3 moves automatically +- [x] Outbound UI wizard: new PDS URL -> export repo -> guide account creation -> complete migration +- [x] Inbound UI wizard: login to old PDS -> choose handle -> import -> PLC token flow +- [x] Support `createAccount` with existing DID + service auth token +- [x] Progress tracking with resume capability - [ ] Scheduled automatic backups (CAR export) - [ ] One-click restore from backup diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index c0ab1cd..2d3d629 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -33,6 +33,7 @@ import DelegationAudit from './routes/DelegationAudit.svelte' import ActAs from './routes/ActAs.svelte' import Migration from './routes/Migration.svelte' + import DidDocumentEditor from './routes/DidDocumentEditor.svelte' import Home from './routes/Home.svelte' initI18n() @@ -116,6 +117,8 @@ return ActAs case '/migrate': return Migration + case '/did-document': + return DidDocumentEditor default: return Home } diff --git a/frontend/src/components/migration/InboundWizard.svelte b/frontend/src/components/migration/InboundWizard.svelte index dddcfa0..95cb853 100644 --- a/frontend/src/components/migration/InboundWizard.svelte +++ b/frontend/src/components/migration/InboundWizard.svelte @@ -620,7 +620,15 @@
{`{
+  "@context": [
+    "https://www.w3.org/ns/did/v1",
+    "https://w3id.org/security/multikey/v1",
+    "https://w3id.org/security/suites/secp256k1-2019/v1"
+  ],
   "id": "${flow.state.sourceDid}",
+  "alsoKnownAs": [
+    "at://${flow.state.targetHandle || '...'}"
+  ],
   "verificationMethod": [
     {
       "id": "${flow.state.sourceDid}#atproto",
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 406ab63..af68482 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -86,11 +86,36 @@ export interface Session {
   preferredChannelVerified?: boolean;
   isAdmin?: boolean;
   active?: boolean;
-  status?: "active" | "deactivated";
+  status?: "active" | "deactivated" | "migrated";
+  migratedToPds?: string;
+  migratedAt?: string;
   accessJwt: string;
   refreshJwt: string;
 }
 
+export interface VerificationMethod {
+  id: string;
+  type: string;
+  publicKeyMultibase: string;
+}
+
+export interface DidDocument {
+  "@context": string[];
+  id: string;
+  alsoKnownAs: string[];
+  verificationMethod: Array<{
+    id: string;
+    type: string;
+    controller: string;
+    publicKeyMultibase: string;
+  }>;
+  service: Array<{
+    id: string;
+    type: string;
+    serviceEndpoint: string;
+  }>;
+}
+
 export interface AppPassword {
   name: string;
   createdAt: string;
@@ -330,6 +355,7 @@ export const api = {
     links?: { privacyPolicy?: string; termsOfService?: string };
     version?: string;
     availableCommsChannels?: string[];
+    selfHostedDidWebEnabled?: boolean;
   }> {
     return xrpc("com.atproto.server.describeServer");
   },
@@ -1057,4 +1083,61 @@ export const api = {
       token: accessToken,
     });
   },
+
+  async getDidDocument(token: string): Promise {
+    return xrpc("com.tranquil.account.getDidDocument", { token });
+  },
+
+  async updateDidDocument(
+    token: string,
+    params: {
+      verificationMethods?: VerificationMethod[];
+      alsoKnownAs?: string[];
+      serviceEndpoint?: string;
+    },
+  ): Promise<{ success: boolean }> {
+    return xrpc("com.tranquil.account.updateDidDocument", {
+      method: "POST",
+      token,
+      body: params,
+    });
+  },
+
+  async deactivateAccount(
+    token: string,
+    deleteAfter?: string,
+    migratingTo?: string,
+  ): Promise {
+    await xrpc("com.atproto.server.deactivateAccount", {
+      method: "POST",
+      token,
+      body: { deleteAfter, migratingTo },
+    });
+  },
+
+  async getMigrationStatus(token: string): Promise<{
+    migratedToPds?: string;
+    migratedAt?: string;
+    forwardingEnabled: boolean;
+  }> {
+    return xrpc("com.tranquil.account.getMigrationStatus", { token });
+  },
+
+  async updateMigrationForwarding(
+    token: string,
+    forwardingPds?: string,
+  ): Promise<{ success: boolean }> {
+    return xrpc("com.tranquil.account.updateMigrationForwarding", {
+      method: "POST",
+      token,
+      body: { forwardingPds },
+    });
+  },
+
+  async clearMigrationForwarding(token: string): Promise<{ success: boolean }> {
+    return xrpc("com.tranquil.account.clearMigrationForwarding", {
+      method: "POST",
+      token,
+    });
+  },
 };
diff --git a/frontend/src/lib/migration/atproto-client.ts b/frontend/src/lib/migration/atproto-client.ts
index 15e3025..e2f2104 100644
--- a/frontend/src/lib/migration/atproto-client.ts
+++ b/frontend/src/lib/migration/atproto-client.ts
@@ -327,12 +327,19 @@ export class AtprotoClient {
     );
   }
 
-  async deactivateAccount(): Promise {
-    apiLog("POST", `${this.baseUrl}/xrpc/com.atproto.server.deactivateAccount`);
+  async deactivateAccount(migratingTo?: string): Promise {
+    apiLog("POST", `${this.baseUrl}/xrpc/com.atproto.server.deactivateAccount`, {
+      migratingTo,
+    });
     const start = Date.now();
     try {
+      const body: { migratingTo?: string } = {};
+      if (migratingTo) {
+        body.migratingTo = migratingTo;
+      }
       await this.xrpc("com.atproto.server.deactivateAccount", {
         httpMethod: "POST",
+        body,
       });
       apiLog(
         "POST",
@@ -340,6 +347,7 @@ export class AtprotoClient {
         {
           durationMs: Date.now() - start,
           success: true,
+          migratingTo,
         },
       );
     } catch (e) {
@@ -352,6 +360,7 @@ export class AtprotoClient {
           error: err.message,
           errorCode: err.error,
           status: err.status,
+          migratingTo,
         },
       );
       throw e;
diff --git a/frontend/src/lib/migration/flow.svelte.ts b/frontend/src/lib/migration/flow.svelte.ts
index 73a2fc4..c21ea80 100644
--- a/frontend/src/lib/migration/flow.svelte.ts
+++ b/frontend/src/lib/migration/flow.svelte.ts
@@ -906,22 +906,11 @@ export function createOutboundMigrationFlow() {
 
       setProgress({ currentOperation: "Deactivating old account..." });
       try {
-        await localClient.deactivateAccount();
+        await localClient.deactivateAccount(state.targetPdsUrl);
         setProgress({ deactivated: true });
       } catch {
       }
 
-      if (state.localDid.startsWith("did:web:")) {
-        setProgress({
-          currentOperation: "Updating DID document forwarding...",
-        });
-        try {
-          await localClient.updateMigrationForwarding(state.targetPdsUrl);
-        } catch (e) {
-          console.warn("Failed to update migration forwarding:", e);
-        }
-      }
-
       setStep("success");
       clearMigrationState();
     } catch (e) {
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index c5994d8..806ea9a 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -87,6 +87,7 @@
     "didPlcHint": "Portable identity managed by PLC Directory",
     "didWeb": "did:web",
     "didWebHint": "Identity hosted on this PDS (read warning below)",
+    "didWebDisabledHint": "Not available on this PDS - use did:plc or bring your own did:web",
     "didWebBYOD": "did:web (BYOD)",
     "didWebBYODHint": "Bring your own domain",
     "didWebWarningTitle": "Important: Understand the trade-offs",
@@ -175,7 +176,46 @@
     "navDelegation": "Delegation",
     "navDelegationDesc": "Manage account controllers and delegated accounts",
     "navAdmin": "Admin Panel",
-    "navAdminDesc": "Server stats and admin operations"
+    "navAdminDesc": "Server stats and admin operations",
+    "navDidDocument": "DID Document",
+    "navDidDocumentDesc": "Manage your DID document for external migrations",
+    "migrated": "Migrated",
+    "migratedTitle": "Account Migrated",
+    "migratedMessage": "Your account has migrated to {pds}. Your DID document is still hosted here, and you can update it for future migrations.",
+    "navMigrateAgain": "Migrate Again",
+    "navMigrateAgainDesc": "Move to another PDS and update your DID document"
+  },
+  "didEditor": {
+    "title": "DID Document Editor",
+    "preview": "Current DID Document",
+    "verificationMethods": "Verification Methods",
+    "verificationMethodsDesc": "Signing keys that can act on behalf of your DID. When you migrate to a new PDS, add their signing key here.",
+    "addKey": "Add Key",
+    "removeKey": "Remove",
+    "keyId": "Key ID",
+    "keyIdPlaceholder": "#atproto",
+    "publicKey": "Public Key (Multibase)",
+    "publicKeyPlaceholder": "zQ3sh...",
+    "noKeys": "No verification methods configured. Using the local PDS key.",
+    "alsoKnownAs": "Also Known As",
+    "alsoKnownAsDesc": "Handles that point to your DID. Update this when your handle changes on a new PDS.",
+    "addHandle": "Add Handle",
+    "removeHandle": "Remove",
+    "handle": "Handle",
+    "handlePlaceholder": "at://handle.newpds.com",
+    "noHandles": "No handles configured. Using the local handle.",
+    "serviceEndpoint": "Service Endpoint",
+    "serviceEndpointDesc": "The PDS that currently hosts your account data. Update this when migrating.",
+    "currentPds": "Current PDS URL",
+    "save": "Save Changes",
+    "saving": "Saving...",
+    "success": "DID document updated successfully",
+    "saveFailed": "Failed to save DID document",
+    "loadFailed": "Failed to load DID document",
+    "invalidMultibase": "Public key must be a valid multibase string starting with 'z'",
+    "invalidHandle": "Handle must be an at:// URI (e.g., at://handle.example.com)",
+    "helpTitle": "What is this?",
+    "helpText": "When you migrate to another PDS, that PDS generates new signing keys. Update your DID document here so it points to your new keys and location. This enables multi-hop migrations (PDS 1 → PDS 2 → PDS 3)."
   },
   "settings": {
     "title": "Account Settings",
@@ -792,6 +832,7 @@
     "didPlcHint": "Portable identity managed by PLC Directory",
     "didWeb": "did:web",
     "didWebHint": "Identity hosted on this PDS (read warning below)",
+    "didWebDisabledHint": "Not available on this PDS - use did:plc or bring your own did:web",
     "didWebBYOD": "did:web (BYOD)",
     "didWebBYODHint": "Bring your own domain",
     "didWebWarningTitle": "Important: Understand the trade-offs",
diff --git a/frontend/src/locales/fi.json b/frontend/src/locales/fi.json
index e258758..42e6adf 100644
--- a/frontend/src/locales/fi.json
+++ b/frontend/src/locales/fi.json
@@ -87,6 +87,7 @@
     "didPlcHint": "Siirrettävä identiteetti, jota hallinnoi PLC Directory",
     "didWeb": "did:web",
     "didWebHint": "Identiteetti isännöidään tällä PDS:llä (lue alla oleva varoitus)",
+    "didWebDisabledHint": "Ei saatavilla tällä PDS:llä - käytä did:plc:tä tai tuo oma did:web",
     "didWebBYOD": "did:web (oma verkkotunnus)",
     "didWebBYODHint": "Käytä omaa verkkotunnustasi",
     "didWebWarningTitle": "Tärkeää: Ymmärrä kompromissit",
@@ -175,7 +176,46 @@
     "navDelegation": "Delegointi",
     "navDelegationDesc": "Hallitse tilin ohjaajia ja delegoituja tilejä",
     "navAdmin": "Ylläpitopaneeli",
-    "navAdminDesc": "Palvelintilastot ja ylläpitotoiminnot"
+    "navAdminDesc": "Palvelintilastot ja ylläpitotoiminnot",
+    "navDidDocument": "DID-dokumentti",
+    "navDidDocumentDesc": "Hallitse DID-dokumenttiasi ulkoisia siirtoja varten",
+    "migrated": "Siirretty",
+    "migratedTitle": "Tili siirretty",
+    "migratedMessage": "Tilisi on siirretty palvelimelle {pds}. DID-dokumenttisi isännöidään edelleen täällä, ja voit päivittää sen tulevia siirtoja varten.",
+    "navMigrateAgain": "Siirrä uudelleen",
+    "navMigrateAgainDesc": "Siirrä toiseen PDS:ään ja päivitä DID-dokumenttisi"
+  },
+  "didEditor": {
+    "title": "DID-dokumentin muokkain",
+    "preview": "Nykyinen DID-dokumentti",
+    "verificationMethods": "Vahvistusmenetelmät",
+    "verificationMethodsDesc": "Allekirjoitusavaimet, jotka voivat toimia DID:si puolesta. Kun siirryt uuteen PDS:ään, lisää niiden allekirjoitusavain tähän.",
+    "addKey": "Lisää avain",
+    "removeKey": "Poista",
+    "keyId": "Avaimen tunnus",
+    "keyIdPlaceholder": "#atproto",
+    "publicKey": "Julkinen avain (Multibase)",
+    "publicKeyPlaceholder": "zQ3sh...",
+    "noKeys": "Ei vahvistusmenetelmiä määritetty. Käytetään paikallista PDS-avainta.",
+    "alsoKnownAs": "Tunnetaan myös nimellä",
+    "alsoKnownAsDesc": "Kahvat, jotka osoittavat DID:iisi. Päivitä tämä, kun kahvasi muuttuu uudessa PDS:ssä.",
+    "addHandle": "Lisää kahva",
+    "removeHandle": "Poista",
+    "handle": "Kahva",
+    "handlePlaceholder": "at://kahva.uusipds.com",
+    "noHandles": "Ei kahvoja määritetty. Käytetään paikallista kahvaa.",
+    "serviceEndpoint": "Palvelupäätepiste",
+    "serviceEndpointDesc": "PDS, joka tällä hetkellä isännöi tilitietojasi. Päivitä tämä siirron yhteydessä.",
+    "currentPds": "Nykyinen PDS-URL",
+    "save": "Tallenna muutokset",
+    "saving": "Tallennetaan...",
+    "success": "DID-dokumentti päivitetty onnistuneesti",
+    "saveFailed": "DID-dokumentin tallennus epäonnistui",
+    "loadFailed": "DID-dokumentin lataus epäonnistui",
+    "invalidMultibase": "Julkisen avaimen on oltava kelvollinen multibase-merkkijono, joka alkaa 'z':llä",
+    "invalidHandle": "Kahvan on oltava at://-URI (esim. at://kahva.esimerkki.com)",
+    "helpTitle": "Mikä tämä on?",
+    "helpText": "Kun siirryt toiseen PDS:ään, se luo uudet allekirjoitusavaimet. Päivitä DID-dokumenttisi tässä osoittamaan uusiin avaimiin ja sijaintiin. Tämä mahdollistaa monivaiheiset siirrot (PDS 1 → PDS 2 → PDS 3)."
   },
   "settings": {
     "title": "Tilin asetukset",
@@ -817,6 +857,7 @@
     "didPlcHint": "Siirrettävä identiteetti, jota hallinnoi PLC Directory",
     "didWeb": "did:web",
     "didWebHint": "Tällä PDS:llä isännöity identiteetti (lue varoitus alla)",
+    "didWebDisabledHint": "Ei saatavilla tällä PDS:llä - käytä did:plc:tä tai tuo oma did:web",
     "didWebBYOD": "did:web (BYOD)",
     "didWebBYODHint": "Tuo oma verkkotunnuksesi",
     "didWebWarningTitle": "Tärkeää: Ymmärrä kompromissit",
diff --git a/frontend/src/locales/ja.json b/frontend/src/locales/ja.json
index cf62d21..6f55e68 100644
--- a/frontend/src/locales/ja.json
+++ b/frontend/src/locales/ja.json
@@ -87,6 +87,7 @@
     "didPlcHint": "PLC ディレクトリで管理されるポータブルアイデンティティ",
     "didWeb": "did:web",
     "didWebHint": "この PDS でホストされるアイデンティティ(下記の警告をお読みください)",
+    "didWebDisabledHint": "この PDS では利用できません - did:plc を使用するか、独自の did:web を持ち込んでください",
     "didWebBYOD": "did:web (自前ドメイン)",
     "didWebBYODHint": "独自ドメインを持ち込む",
     "didWebWarningTitle": "重要: トレードオフをご理解ください",
@@ -175,7 +176,34 @@
     "navDelegation": "委任",
     "navDelegationDesc": "アカウントコントローラーと委任アカウントを管理",
     "navAdmin": "管理パネル",
-    "navAdminDesc": "サーバー統計と管理操作"
+    "navAdminDesc": "サーバー統計と管理操作",
+    "navDidDocument": "DID ドキュメント",
+    "navDidDocumentDesc": "DID ドキュメントとキーを管理",
+    "migrated": "移行済み",
+    "migratedTitle": "アカウント移行済み",
+    "migratedMessage": "アカウントは {pds} に移行されました。DID ドキュメントは引き続きここでホストされています。",
+    "navMigrateAgain": "再移行",
+    "navMigrateAgainDesc": "別の PDS に移行して DID ドキュメントを更新"
+  },
+  "didEditor": {
+    "title": "DID ドキュメントエディター",
+    "preview": "現在の DID ドキュメント",
+    "verificationMethods": "検証方法(署名キー)",
+    "addKey": "キーを追加",
+    "removeKey": "削除",
+    "keyId": "キー ID",
+    "keyIdPlaceholder": "#atproto",
+    "publicKey": "公開キー(Multibase)",
+    "publicKeyPlaceholder": "zQ3sh...",
+    "alsoKnownAs": "別名(ハンドル)",
+    "addHandle": "ハンドルを追加",
+    "handlePlaceholder": "at://handle.pds.com",
+    "serviceEndpoint": "サービスエンドポイント(現在の PDS)",
+    "save": "変更を保存",
+    "saving": "保存中...",
+    "success": "DID ドキュメントを更新しました",
+    "helpTitle": "これは何ですか?",
+    "helpText": "別の PDS に移行すると、その PDS が新しい署名キーを生成します。ここで DID ドキュメントを更新して、新しいキーと場所を指すようにしてください。"
   },
   "settings": {
     "title": "アカウント設定",
@@ -817,6 +845,7 @@
     "didPlcHint": "PLC Directoryで管理されるポータブルなアイデンティティ",
     "didWeb": "did:web",
     "didWebHint": "このPDSでホストされるアイデンティティ(以下の警告を参照)",
+    "didWebDisabledHint": "この PDS では利用できません - did:plc を使用するか、独自の did:web を持ち込んでください",
     "didWebBYOD": "did:web(BYOD)",
     "didWebBYODHint": "独自ドメインを持ち込む",
     "didWebWarningTitle": "重要:トレードオフを理解する",
diff --git a/frontend/src/locales/ko.json b/frontend/src/locales/ko.json
index a5d5ef0..b4ec653 100644
--- a/frontend/src/locales/ko.json
+++ b/frontend/src/locales/ko.json
@@ -87,6 +87,7 @@
     "didPlcHint": "PLC 디렉토리에서 관리하는 이동 가능한 ID",
     "didWeb": "did:web",
     "didWebHint": "이 PDS에서 호스팅되는 ID (아래 경고 참조)",
+    "didWebDisabledHint": "이 PDS에서 사용할 수 없음 - did:plc를 사용하거나 자체 did:web을 가져오세요",
     "didWebBYOD": "did:web (자체 도메인)",
     "didWebBYODHint": "자체 도메인 사용",
     "didWebWarningTitle": "중요: 장단점을 이해하세요",
@@ -175,7 +176,34 @@
     "navDelegation": "위임",
     "navDelegationDesc": "계정 컨트롤러 및 위임된 계정 관리",
     "navAdmin": "관리 패널",
-    "navAdminDesc": "서버 통계 및 관리 작업"
+    "navAdminDesc": "서버 통계 및 관리 작업",
+    "navDidDocument": "DID 문서",
+    "navDidDocumentDesc": "DID 문서 및 키 관리",
+    "migrated": "마이그레이션됨",
+    "migratedTitle": "계정 마이그레이션됨",
+    "migratedMessage": "계정이 {pds}로 마이그레이션되었습니다. DID 문서는 여전히 여기에서 호스팅됩니다.",
+    "navMigrateAgain": "다시 마이그레이션",
+    "navMigrateAgainDesc": "다른 PDS로 이동하고 DID 문서 업데이트"
+  },
+  "didEditor": {
+    "title": "DID 문서 편집기",
+    "preview": "현재 DID 문서",
+    "verificationMethods": "검증 방법 (서명 키)",
+    "addKey": "키 추가",
+    "removeKey": "삭제",
+    "keyId": "키 ID",
+    "keyIdPlaceholder": "#atproto",
+    "publicKey": "공개 키 (Multibase)",
+    "publicKeyPlaceholder": "zQ3sh...",
+    "alsoKnownAs": "다른 이름 (핸들)",
+    "addHandle": "핸들 추가",
+    "handlePlaceholder": "at://handle.pds.com",
+    "serviceEndpoint": "서비스 엔드포인트 (현재 PDS)",
+    "save": "변경사항 저장",
+    "saving": "저장 중...",
+    "success": "DID 문서가 업데이트되었습니다",
+    "helpTitle": "이것은 무엇인가요?",
+    "helpText": "다른 PDS로 마이그레이션하면 해당 PDS가 새 서명 키를 생성합니다. 여기에서 DID 문서를 업데이트하여 새 키와 위치를 가리키도록 하세요."
   },
   "settings": {
     "title": "계정 설정",
@@ -817,6 +845,7 @@
     "didPlcHint": "PLC Directory에서 관리하는 이동 가능한 아이덴티티",
     "didWeb": "did:web",
     "didWebHint": "이 PDS에서 호스팅되는 아이덴티티 (아래 경고 읽기)",
+    "didWebDisabledHint": "이 PDS에서 사용할 수 없음 - did:plc를 사용하거나 자체 did:web을 가져오세요",
     "didWebBYOD": "did:web (BYOD)",
     "didWebBYODHint": "자체 도메인 사용",
     "didWebWarningTitle": "중요: 장단점 이해하기",
diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json
index db5cfa5..f605bc7 100644
--- a/frontend/src/locales/sv.json
+++ b/frontend/src/locales/sv.json
@@ -87,6 +87,7 @@
     "didPlcHint": "Portabel identitet hanterad av PLC Directory",
     "didWeb": "did:web",
     "didWebHint": "Identitet lagrad på denna PDS (läs varningen nedan)",
+    "didWebDisabledHint": "Inte tillgänglig på denna PDS - använd did:plc eller ta med din egen did:web",
     "didWebBYOD": "did:web (egen domän)",
     "didWebBYODHint": "Använd din egen domän",
     "didWebWarningTitle": "Viktigt: Förstå avvägningarna",
@@ -175,7 +176,34 @@
     "navDelegation": "Delegering",
     "navDelegationDesc": "Hantera kontokontrollanter och delegerade konton",
     "navAdmin": "Adminpanel",
-    "navAdminDesc": "Serverstatistik och administratörsoperationer"
+    "navAdminDesc": "Serverstatistik och administratörsoperationer",
+    "navDidDocument": "DID-dokument",
+    "navDidDocumentDesc": "Hantera ditt DID-dokument och nycklar",
+    "migrated": "Flyttad",
+    "migratedTitle": "Konto flyttat",
+    "migratedMessage": "Ditt konto har flyttats till {pds}. Ditt DID-dokument finns fortfarande här.",
+    "navMigrateAgain": "Flytta igen",
+    "navMigrateAgainDesc": "Flytta till en annan PDS och uppdatera ditt DID-dokument"
+  },
+  "didEditor": {
+    "title": "DID-dokumentredigerare",
+    "preview": "Nuvarande DID-dokument",
+    "verificationMethods": "Verifieringsmetoder (signeringsnycklar)",
+    "addKey": "Lägg till nyckel",
+    "removeKey": "Ta bort",
+    "keyId": "Nyckel-ID",
+    "keyIdPlaceholder": "#atproto",
+    "publicKey": "Publik nyckel (Multibase)",
+    "publicKeyPlaceholder": "zQ3sh...",
+    "alsoKnownAs": "Även känd som (användarnamn)",
+    "addHandle": "Lägg till användarnamn",
+    "handlePlaceholder": "at://handle.pds.com",
+    "serviceEndpoint": "Tjänstslutpunkt (nuvarande PDS)",
+    "save": "Spara ändringar",
+    "saving": "Sparar...",
+    "success": "DID-dokumentet har uppdaterats",
+    "helpTitle": "Vad är detta?",
+    "helpText": "När du flyttar till en annan PDS genererar den PDS nya signeringsnycklar. Uppdatera ditt DID-dokument här så att det pekar på dina nya nycklar och plats."
   },
   "settings": {
     "title": "Kontoinställningar",
@@ -817,6 +845,7 @@
     "didPlcHint": "Portabel identitet som hanteras av PLC Directory",
     "didWeb": "did:web",
     "didWebHint": "Identitet som lagras på denna PDS (läs varningen nedan)",
+    "didWebDisabledHint": "Inte tillgänglig på denna PDS - använd did:plc eller ta med din egen did:web",
     "didWebBYOD": "did:web (BYOD)",
     "didWebBYODHint": "Ta med din egen domän",
     "didWebWarningTitle": "Viktigt: Förstå kompromisserna",
diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json
index 1161cb4..49fa3bc 100644
--- a/frontend/src/locales/zh.json
+++ b/frontend/src/locales/zh.json
@@ -87,6 +87,7 @@
     "didPlcHint": "由 PLC 目录管理的可迁移身份",
     "didWeb": "did:web",
     "didWebHint": "托管在此 PDS 上的身份(请阅读下方警告)",
+    "didWebDisabledHint": "此 PDS 不可用 - 请使用 did:plc 或携带自己的 did:web",
     "didWebBYOD": "did:web(自带域名)",
     "didWebBYODHint": "使用您自己的域名",
     "didWebWarningTitle": "重要提示:了解利弊",
@@ -175,7 +176,34 @@
     "navDelegation": "账户委托",
     "navDelegationDesc": "管理控制者和委托账户",
     "navAdmin": "管理后台",
-    "navAdminDesc": "服务器统计和管理操作"
+    "navAdminDesc": "服务器统计和管理操作",
+    "navDidDocument": "DID 文档",
+    "navDidDocumentDesc": "管理您的 DID 文档和密钥",
+    "migrated": "已迁移",
+    "migratedTitle": "账户已迁移",
+    "migratedMessage": "您的账户已迁移到 {pds}。您的 DID 文档仍在此处托管。",
+    "navMigrateAgain": "再次迁移",
+    "navMigrateAgainDesc": "迁移到另一个 PDS 并更新您的 DID 文档"
+  },
+  "didEditor": {
+    "title": "DID 文档编辑器",
+    "preview": "当前 DID 文档",
+    "verificationMethods": "验证方法(签名密钥)",
+    "addKey": "添加密钥",
+    "removeKey": "删除",
+    "keyId": "密钥 ID",
+    "keyIdPlaceholder": "#atproto",
+    "publicKey": "公钥(Multibase)",
+    "publicKeyPlaceholder": "zQ3sh...",
+    "alsoKnownAs": "别名(用户名)",
+    "addHandle": "添加用户名",
+    "handlePlaceholder": "at://handle.pds.com",
+    "serviceEndpoint": "服务端点(当前 PDS)",
+    "save": "保存更改",
+    "saving": "保存中...",
+    "success": "DID 文档已更新",
+    "helpTitle": "这是什么?",
+    "helpText": "当您迁移到另一个 PDS 时,该 PDS 会生成新的签名密钥。在此处更新您的 DID 文档,使其指向您的新密钥和位置。"
   },
   "settings": {
     "title": "账户设置",
@@ -792,6 +820,7 @@
     "didPlcHint": "由 PLC 目录管理的可迁移身份",
     "didWeb": "did:web",
     "didWebHint": "托管在此 PDS 上的身份(请阅读下方警告)",
+    "didWebDisabledHint": "此 PDS 不可用 - 请使用 did:plc 或携带自己的 did:web",
     "didWebBYOD": "did:web(自带域名)",
     "didWebBYODHint": "使用您自己的域名",
     "didWebWarningTitle": "重要:了解利弊",
diff --git a/frontend/src/routes/Dashboard.svelte b/frontend/src/routes/Dashboard.svelte
index eaad876..0aca5b8 100644
--- a/frontend/src/routes/Dashboard.svelte
+++ b/frontend/src/routes/Dashboard.svelte
@@ -99,7 +99,12 @@
       
- {#if auth.session.status === 'deactivated' || auth.session.active === false} + {#if auth.session.status === 'migrated'} +
+ {$_('dashboard.migratedTitle')} +

{$_('dashboard.migratedMessage', { values: { pds: auth.session.migratedToPds || 'another PDS' } })}

+
+ {:else if auth.session.status === 'deactivated' || auth.session.active === false}
{$_('dashboard.deactivatedTitle')}

{$_('dashboard.deactivatedMessage')}

@@ -115,7 +120,9 @@ {#if auth.session.isAdmin} {$_('dashboard.admin')} {/if} - {#if auth.session.status === 'deactivated' || auth.session.active === false} + {#if auth.session.status === 'migrated'} + {$_('dashboard.migrated')} + {:else if auth.session.status === 'deactivated' || auth.session.active === false} {$_('dashboard.deactivated')} {/if} @@ -156,49 +163,68 @@
@@ -374,6 +400,12 @@ border: 1px solid var(--warning-border); } + .badge.migrated { + background: var(--info-bg, #e0f2fe); + color: var(--info-text, #0369a1); + border: 1px solid var(--info-border, #7dd3fc); + } + .nav-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); @@ -440,4 +472,36 @@ color: var(--warning-text); font-size: var(--text-sm); } + + .migrated-banner { + background: var(--info-bg, #e0f2fe); + border: 1px solid var(--info-border, #7dd3fc); + border-radius: var(--radius-xl); + padding: var(--space-5) var(--space-6); + margin-bottom: var(--space-7); + } + + .migrated-banner strong { + color: var(--info-text, #0369a1); + font-size: var(--text-base); + } + + .migrated-banner p { + margin: var(--space-3) 0 0 0; + color: var(--info-text, #0369a1); + font-size: var(--text-sm); + } + + .nav-card.migrated-card { + border-color: var(--info-border, #7dd3fc); + background: linear-gradient(135deg, var(--bg-card) 0%, var(--info-bg, #e0f2fe) 100%); + } + + .nav-card.migrated-card:hover { + box-shadow: 0 2px 12px var(--info-bg, #e0f2fe); + } + + .nav-card.migrated-card h3 { + color: var(--info-text, #0369a1); + } diff --git a/frontend/src/routes/DidDocumentEditor.svelte b/frontend/src/routes/DidDocumentEditor.svelte new file mode 100644 index 0000000..0a2cf34 --- /dev/null +++ b/frontend/src/routes/DidDocumentEditor.svelte @@ -0,0 +1,457 @@ + + +
+
+ {$_('common.backToDashboard')} +

{$_('didEditor.title')}

+
+ + {#if message} +
{message.text}
+ {/if} + + {#if loading} +
{$_('common.loading')}
+ {:else} +
+

{$_('didEditor.helpTitle')}

+

{$_('didEditor.helpText')}

+
+ +
+

{$_('didEditor.preview')}

+
{JSON.stringify(didDocument, null, 2)}
+
+ +
+

{$_('didEditor.verificationMethods')}

+

{$_('didEditor.verificationMethodsDesc')}

+ + {#if verificationMethods.length > 0} + + {:else} +

{$_('didEditor.noKeys')}

+ {/if} + +
+

{$_('didEditor.addKey')}

+
+
+ + +
+
+ + +
+ +
+
+
+ +
+

{$_('didEditor.alsoKnownAs')}

+

{$_('didEditor.alsoKnownAsDesc')}

+ + {#if alsoKnownAs.length > 0} + + {:else} +

{$_('didEditor.noHandles')}

+ {/if} + +
+
+
+ + +
+ +
+
+
+ +
+

{$_('didEditor.serviceEndpoint')}

+

{$_('didEditor.serviceEndpointDesc')}

+
+ + +
+
+ +
+ +
+ {/if} +
+ + diff --git a/frontend/src/routes/Register.svelte b/frontend/src/routes/Register.svelte index 76f3654..4839921 100644 --- a/frontend/src/routes/Register.svelte +++ b/frontend/src/routes/Register.svelte @@ -13,6 +13,7 @@ availableUserDomains: string[] inviteCodeRequired: boolean availableCommsChannels?: string[] + selfHostedDidWebEnabled?: boolean } | null>(null) let loadingServerInfo = $state(true) let serverInfoLoaded = false @@ -237,11 +238,15 @@ - -