import { useState } from "preact/hooks"; import { Link } from "../components/Link.tsx"; interface PlcUpdateStep { name: string; status: "pending" | "in-progress" | "verifying" | "completed" | "error"; error?: string; } // Content chunks for the description const contentChunks = [ { title: "Welcome to Key Management", subtitle: "BOARDING PASS - SECTION A", content: ( <>
GATE: KEY-01 • SEAT: DID-1A

This tool helps you add a new rotation key to your{" "} PLC (Public Ledger of Credentials) . Having control of a rotation key gives you sovereignty over your DID (Decentralized Identifier).

), }, { title: "Key Benefits", subtitle: "BOARDING PASS - SECTION B", content: ( <>
GATE: KEY-02 • SEAT: DID-1B

PROVIDER MOBILITY ✈️

Change your PDS without losing your identity, protecting you if your provider becomes hostile.

IDENTITY CONTROL ✨

Modify your DID document independently of your provider.

💡 It's good practice to have a rotation key so you can move to a different provider if you need to.

), }, { title: "⚠️ CRITICAL SECURITY WARNING", subtitle: "BOARDING PASS - SECTION C", content: ( <>
GATE: KEY-03 • SEAT: DID-1C
⚠️

NON-REVOCABLE KEY WARNING

This rotation key CANNOT BE DISABLED OR DELETED once added:

💡 We recommend adding a custom rotation key but recommend{" "} against{" "} having more than one custom rotation key, as more than one increases risk.

), }, { title: "Technical Overview", subtitle: "BOARDING PASS - SECTION C", content: ( <>
GATE: KEY-03 • SEAT: DID-1C
📝

TECHNICAL DETAILS

The rotation key is a did:key that will be added to your PLC document's rotationKeys array. This process uses the AT Protocol's PLC operations to update your DID document. Learn more about did:plc

), }, ]; export default function PlcUpdateProgress() { const [hasStarted, setHasStarted] = useState(false); const [currentChunkIndex, setCurrentChunkIndex] = useState(0); const [steps, setSteps] = useState([ { name: "Generate Rotation Key", status: "pending" }, { name: "Start PLC update", status: "pending" }, { name: "Complete PLC update", status: "pending" }, ]); const [generatedKey, setGeneratedKey] = useState(""); const [keyJson, setKeyJson] = useState(null); const [emailToken, setEmailToken] = useState(""); const [hasDownloadedKey, setHasDownloadedKey] = useState(false); const [downloadedKeyId, setDownloadedKeyId] = useState(null); const [hasContinuedPastDownload, setHasContinuedPastDownload] = useState( false, ); const updateStepStatus = ( index: number, status: PlcUpdateStep["status"], error?: string, ) => { console.log( `Updating step ${index} to ${status}${ error ? ` with error: ${error}` : "" }`, ); setSteps((prevSteps) => prevSteps.map((step, i) => i === index ? { ...step, status, error } : i > index ? { ...step, status: "pending", error: undefined } : step ) ); }; const handleStart = () => { setHasStarted(true); // Automatically start the first step setTimeout(() => { handleGenerateKey(); }, 100); }; const getStepDisplayName = (step: PlcUpdateStep, index: number) => { if (step.status === "completed") { switch (index) { case 0: return "Rotation Key Generated"; case 1: return "PLC Operation Requested"; case 2: return "PLC Update Completed"; } } if (step.status === "in-progress") { switch (index) { case 0: return "Generating Rotation Key..."; case 1: return "Requesting PLC Operation Token..."; case 2: return step.name === "Enter the code sent to your email to complete PLC update" ? step.name : "Completing PLC Update..."; } } if (step.status === "verifying") { switch (index) { case 0: return "Verifying Rotation Key Generation..."; case 1: return "Verifying PLC Operation Token Request..."; case 2: return "Verifying PLC Update Completion..."; } } return step.name; }; const handleStartPlcUpdate = async (keyToUse?: string) => { const key = keyToUse || generatedKey; // Debug logging console.log("=== PLC Update Debug ==="); console.log("Current state:", { keyToUse, generatedKey, key, hasKeyJson: !!keyJson, keyJsonId: keyJson?.publicKeyDid, hasDownloadedKey, downloadedKeyId, steps: steps.map((s) => ({ name: s.name, status: s.status })), }); if (!key) { console.log("No key generated yet"); updateStepStatus(1, "error", "No key generated yet"); return; } if (!keyJson || keyJson.publicKeyDid !== key) { console.log("Key mismatch or missing:", { hasKeyJson: !!keyJson, keyJsonId: keyJson?.publicKeyDid, expectedKey: key, }); updateStepStatus( 1, "error", "Please ensure you have the correct key loaded", ); return; } updateStepStatus(1, "in-progress"); try { // First request the token console.log("Requesting PLC token..."); const tokenRes = await fetch("/api/plc/token", { method: "GET", }); const tokenText = await tokenRes.text(); console.log("Token response:", tokenText); if (!tokenRes.ok) { try { const json = JSON.parse(tokenText); throw new Error(json.message || "Failed to request PLC token"); } catch { throw new Error(tokenText || "Failed to request PLC token"); } } let data; try { data = JSON.parse(tokenText); if (!data.success) { throw new Error(data.message || "Failed to request token"); } } catch { throw new Error("Invalid response from server"); } console.log("Token request successful, updating UI..."); // Update step name to prompt for token setSteps((prevSteps) => prevSteps.map((step, i) => i === 1 ? { ...step, name: "Enter the code sent to your email to complete PLC update", status: "in-progress", } : step ) ); } catch (error) { console.error("Token request failed:", error); updateStepStatus( 1, "error", error instanceof Error ? error.message : String(error), ); } }; const handleTokenSubmit = async () => { console.log("=== Token Submit Debug ==="); console.log("Current state:", { emailToken, generatedKey, keyJsonId: keyJson?.publicKeyDid, steps: steps.map((s) => ({ name: s.name, status: s.status })), }); if (!emailToken) { console.log("No token provided"); updateStepStatus(1, "error", "Please enter the email token"); return; } if (!keyJson || !keyJson.publicKeyDid) { console.log("Missing key data"); updateStepStatus(1, "error", "Key data is missing, please try again"); return; } // Prevent duplicate submissions if (steps[1].status === "completed" || steps[2].status === "completed") { console.log("Update already completed, preventing duplicate submission"); return; } updateStepStatus(1, "completed"); try { updateStepStatus(2, "in-progress"); console.log("Submitting update request with token..."); // Send the update request with both key and token const res = await fetch("/api/plc/update", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: keyJson.publicKeyDid, token: emailToken, }), }); const text = await res.text(); console.log("Update response:", text); let data; try { data = JSON.parse(text); } catch { throw new Error("Invalid response from server"); } // Check for error responses if (!res.ok || !data.success) { const errorMessage = data.message || "Failed to complete PLC update"; console.error("Update failed:", errorMessage); throw new Error(errorMessage); } // Only proceed if we have a successful response console.log("Update completed successfully!"); // Add a delay before marking steps as completed for better UX updateStepStatus(2, "verifying"); const verifyRes = await fetch("/api/plc/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: keyJson.publicKeyDid, }), }); const verifyText = await verifyRes.text(); console.log("Verification response:", verifyText); let verifyData; try { verifyData = JSON.parse(verifyText); } catch { throw new Error("Invalid verification response from server"); } if (!verifyRes.ok || !verifyData.success) { const errorMessage = verifyData.message || "Failed to verify PLC update"; console.error("Verification failed:", errorMessage); throw new Error(errorMessage); } console.log("Verification successful, marking steps as completed"); updateStepStatus(2, "completed"); } catch (error) { console.error("Update failed:", error); // Reset the steps to error state updateStepStatus( 1, "error", error instanceof Error ? error.message : String(error), ); updateStepStatus(2, "pending"); // Reset the final step // If token is invalid, we should clear it so user can try again if ( error instanceof Error && error.message.toLowerCase().includes("token is invalid") ) { setEmailToken(""); } } }; const handleDownload = () => { console.log("=== Download Debug ==="); console.log("Download started with:", { hasKeyJson: !!keyJson, keyJsonId: keyJson?.publicKeyDid, }); if (!keyJson) { console.error("No key JSON to download"); return; } try { const jsonString = JSON.stringify(keyJson, null, 2); const filename = `plc-key-${keyJson.publicKeyDid || "unknown"}.json`; // Create data URL const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(jsonString); // Create download link const downloadAnchorNode = document.createElement("a"); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", filename); // For Chrome/Firefox compatibility downloadAnchorNode.style.display = "none"; document.body.appendChild(downloadAnchorNode); // Trigger download downloadAnchorNode.click(); // Cleanup document.body.removeChild(downloadAnchorNode); console.log("Download completed, showing continue button..."); setHasDownloadedKey(true); setDownloadedKeyId(keyJson.publicKeyDid); // Keep step 0 in completed state but don't auto-proceed } catch (error) { console.error("Download failed:", error); } }; const handleGenerateKey = async () => { console.log("=== Generate Key Debug ==="); updateStepStatus(0, "in-progress"); setKeyJson(null); setGeneratedKey(""); setHasDownloadedKey(false); setDownloadedKeyId(null); try { console.log("Requesting new key..."); const res = await fetch("/api/plc/keys"); const text = await res.text(); console.log("Key generation response:", text); if (!res.ok) { try { const json = JSON.parse(text); throw new Error(json.message || "Failed to generate key"); } catch { throw new Error(text || "Failed to generate key"); } } let data; try { data = JSON.parse(text); } catch { throw new Error("Invalid response from /api/plc/keys"); } if (!data.publicKeyDid || !data.privateKeyHex) { throw new Error("Key generation failed: missing key data"); } console.log("Key generated successfully:", { keyId: data.publicKeyDid, }); setGeneratedKey(data.publicKeyDid); setKeyJson(data); updateStepStatus(0, "completed"); } catch (error) { console.error("Key generation failed:", error); updateStepStatus( 0, "error", error instanceof Error ? error.message : String(error), ); } }; const getStepIcon = (status: PlcUpdateStep["status"]) => { switch (status) { case "pending": return (
); case "in-progress": return (
); case "verifying": return (
); case "completed": return (
); case "error": return (
); } }; const getStepClasses = (status: PlcUpdateStep["status"]) => { const baseClasses = "flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200"; switch (status) { case "pending": return `${baseClasses} bg-gray-50 dark:bg-gray-800`; case "in-progress": return `${baseClasses} bg-blue-50 dark:bg-blue-900`; case "verifying": return `${baseClasses} bg-yellow-50 dark:bg-yellow-900`; case "completed": return `${baseClasses} bg-green-50 dark:bg-green-900`; case "error": return `${baseClasses} bg-red-50 dark:bg-red-900`; } }; const requestNewToken = async () => { try { console.log("Requesting new token..."); const res = await fetch("/api/plc/token", { method: "GET", }); const text = await res.text(); console.log("Token request response:", text); if (!res.ok) { throw new Error(text || "Failed to request new token"); } let data; try { data = JSON.parse(text); if (!data.success) { throw new Error(data.message || "Failed to request token"); } } catch { throw new Error("Invalid response from server"); } // Clear any existing error and token setEmailToken(""); updateStepStatus(1, "in-progress"); updateStepStatus(2, "pending"); } catch (error) { console.error("Failed to request new token:", error); updateStepStatus( 1, "error", error instanceof Error ? error.message : String(error), ); } }; if (!hasStarted) { return (
{contentChunks[currentChunkIndex].subtitle}

{contentChunks[currentChunkIndex].title}

{/* Main Description */}
{contentChunks[currentChunkIndex].content}
{/* Navigation */}
{currentChunkIndex === contentChunks.length - 1 ? ( ) : ( )}
{/* Progress Dots */}
{contentChunks.map((_, index) => (
))}
); } return (
{/* Progress Steps */}

Key Generation Progress

{/* Add a help tooltip */}
{/* Steps with enhanced visual hierarchy */} {steps.map((step, index) => (
{getStepIcon(step.status)}

{getStepDisplayName(step, index)}

{/* Add step number */} Step {index + 1} of {steps.length}
{step.error && (

{(() => { try { const err = JSON.parse(step.error); return err.message || step.error; } catch { return step.error; } })()}

)} {/* Key Download Warning */} {index === 0 && step.status === "completed" && !hasContinuedPastDownload && (

Critical Security Step

Your rotation key grants control over your identity:

  • Store Securely:{" "} Use a password manager
  • Keep Private:{" "} Never share with anyone
  • Backup: Keep a secure backup copy
  • Required:{" "} Needed for future DID modifications
{hasDownloadedKey && ( )}
{!hasDownloadedKey && (
Download required to proceed
)}
)} {/* Email Code Input */} {index === 1 && (step.status === "in-progress" || step.status === "verifying") && step.name === "Enter the code sent to your email to complete PLC update" && (

Check your email for the verification code to complete the PLC update:

setEmailToken(e.currentTarget.value)} placeholder="Enter verification code" class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-blue-400 dark:focus:ring-blue-400" />
{step.error && (

{step.error}

{step.error .toLowerCase() .includes("token is invalid") && (

The verification code may have expired. Request a new code to try again.

)}
)}
)}
))}
{/* Success Message */} {steps[2].status === "completed" && (

PLC Update Successful!

Your rotation key has been successfully added to your PLC record. You can now use this key for future DID modifications.

Support Us
)}
); }