;
e.stopPropagation(); // Ensure event doesn't bubble up
const { type, data } = e.detail;
const descriptor = data && "descriptor" in data
? data.descriptor
: undefined;
try {
switch (type) {
case "passkey-register":
await this.handlePasskeyRegister();
break;
case "passkey-authenticate":
await this.handlePasskeyAuthenticate(descriptor);
break;
case "passphrase-generate":
await this.handlePassphraseGenerate();
break;
case "passphrase-authenticate":
if (!data || !("mnemonic" in data) || !data.mnemonic) {
throw new Error("Invalid mnemonic.");
}
await this.handlePassphraseAuthenticate(data.mnemonic);
break;
case "clear-stored-credential":
this.handleClearStoredCredential();
break;
}
} catch (error) {
console.error("[LoginView] Auth event error:", error);
this.error = error instanceof Error
? error.message
: "Authentication failed";
}
};
// Auth event handlers
private async handlePasskeyRegister() {
this.isProcessing = true;
this.error = null;
try {
const passkey = await PassKey.create(
"Common Tools User",
"commontoolsuser",
);
const identity = await passkey.createRootKey();
// Save identity to keyStore
const keyStore = this.getKeyStore();
if (keyStore) {
await keyStore.set(ROOT_KEY, identity);
}
// Send identity to root
this.command({ type: "set-identity", identity });
this.registrationSuccess = true;
} catch (e) {
console.error("[LoginView] Passkey register error:", e);
this.error = e instanceof Error
? e.message
: "Passkey registration failed";
this.flow = null;
this.method = null;
} finally {
this.isProcessing = false;
}
}
private async handlePasskeyAuthenticate(
descriptor?: PublicKeyCredentialDescriptor,
) {
this.isProcessing = true;
this.error = null;
try {
const passkey = await PassKey.get({
allowCredentials: descriptor ? [descriptor] : [],
});
const identity = await passkey.createRootKey();
// Save identity to keyStore
const keyStore = this.getKeyStore();
if (keyStore) {
await keyStore.set(ROOT_KEY, identity);
}
// Store credential info for future logins
const credential = createPasskeyCredential(passkey.id());
saveCredential(credential);
this.storedCredential = credential;
// Send identity to root
this.command({ type: "set-identity", identity });
} catch (e) {
console.error("[LoginView] Passkey authenticate error:", e);
this.error = e instanceof Error
? e.message
: "Passkey authentication failed";
} finally {
this.isProcessing = false;
}
}
private async handlePassphraseGenerate() {
this.isProcessing = true;
this.error = null;
try {
const [, mnemonic] = await Identity.generateMnemonic();
this.mnemonic = mnemonic;
} catch (e) {
console.error("[LoginView] Passphrase generate error:", e);
this.error = e instanceof Error
? e.message
: "Failed to generate passphrase";
} finally {
this.isProcessing = false;
}
}
private async handlePassphraseAuthenticate(mnemonic: string) {
this.isProcessing = true;
this.error = null;
try {
const identity = await Identity.fromMnemonic(mnemonic);
// Save identity to keyStore
const keyStore = this.getKeyStore();
if (keyStore) {
await keyStore.set(ROOT_KEY, identity);
}
// Store credential indicator if not already stored
if (!this.storedCredential) {
const credential = createPassphraseCredential();
saveCredential(credential);
this.storedCredential = credential;
}
// Send identity to root
this.command({ type: "set-identity", identity });
} catch (e) {
console.error("[LoginView] Passphrase authenticate error:", e);
this.error = e instanceof Error ? e.message : "Invalid passphrase";
} finally {
this.isProcessing = false;
}
}
private handleClearStoredCredential() {
clearStoredCredential();
this.storedCredential = null;
}
private handleRegister() {
if (this.method === AUTH_METHOD_PASSKEY) {
this.dispatchAuthEvent("passkey-register");
} else {
this.dispatchAuthEvent("passphrase-generate");
}
}
private handleLogin(passphrase?: string) {
console.log("[LoginView] Handling login:", {
method: this.method,
hasPassphrase: !!passphrase,
hasStoredCredential: !!this.storedCredential,
});
if (this.method === AUTH_METHOD_PASSKEY) {
const descriptor = getPublicKeyCredentialDescriptor(
this.storedCredential,
);
this.dispatchAuthEvent("passkey-authenticate", { descriptor });
} else if (passphrase) {
this.dispatchAuthEvent("passphrase-authenticate", {
mnemonic: passphrase,
});
}
}
private async copyToClipboard() {
if (this.mnemonic) {
await navigator.clipboard.writeText(this.mnemonic);
this.copied = true;
setTimeout(() => this.copied = false, 2000);
}
}
private renderInitial() {
if (this.storedCredential) {
const isPassphrase =
this.storedCredential.method === AUTH_METHOD_PASSPHRASE;
const isPasskeyAvailable = this.availableMethods.includes(
AUTH_METHOD_PASSKEY,
);
return html`
${!isPassphrase
? html`
this.handleQuickUnlock()}"
>
🔒 Login with Passkey (${this.storedCredential.id.slice(-4)})
{
this.dispatchAuthEvent("clear-stored-credential");
}}"
title="Remove saved credential"
>
🗑️
`
: html`
this.handleQuickUnlock()}"
>
🔒 Login with Passphrase
`} ${isPassphrase && isPasskeyAvailable
? html`
{
this.flow = "login";
this.method = AUTH_METHOD_PASSKEY;
this.handleLogin();
}}">
" 🔑 Login w/ Passkey
`
: !isPassphrase
? html`
{
this.flow = "login";
this.method = AUTH_METHOD_PASSPHRASE;
}}">
🔑 Login w/ Passphrase
`
: null}
this.flow = "register"}">
➕ Register New Key
`;
}
return html`
this.flow = "register"}">
➕ Register
this.flow = "login"}">
🔒 Login
`;
}
private handleQuickUnlock() {
if (!this.storedCredential) return;
this.method = this.storedCredential.method;
this.flow = "login";
if (this.storedCredential.method === AUTH_METHOD_PASSKEY) {
this.handleLogin();
}
}
private renderMethodSelection() {
return html`
${this.flow === "login" ? "Login with" : "Register with"}
${this.availableMethods.map((method) =>
html`
this.handleMethodSelect(method)}">
${method === AUTH_METHOD_PASSKEY
? "🔑 Use Passkey"
: "📝 Use Passphrase"}
`
)}
{
this.flow = null;
this.method = null;
}}">
← Back
`;
}
private handleMethodSelect(method: AuthMethod) {
this.method = method;
if (this.flow === "register") {
this.handleRegister();
} else if (this.flow === "login" && method === AUTH_METHOD_PASSKEY) {
this.handleLogin();
}
}
private renderPassphraseAuth() {
if (this.flow === "register") {
if (this.mnemonic) {
return this.renderMnemonicDisplay();
}
return html`
this.handleRegister()}">
🔑 Generate Passphrase
{
this.flow = null;
this.method = null;
}}">
← Back
`;
}
return html`
{
this.flow = null;
this.method = null;
}}">
← Back
`;
}
private handlePassphraseLogin = (e: Event) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
const passphrase = formData.get("passphrase") as string;
this.handleLogin(passphrase);
};
private renderMnemonicDisplay() {
return html`
Your Secret Recovery Phrase:
${this.copied ? "✓ Copied" : "📋 Copy"}
⚠️ Keep this secret, it's your password.
{
// User has saved the mnemonic, now authenticate with it
if (this.mnemonic) {
this.handleLogin(this.mnemonic);
}
this.mnemonic = null;
}}"
>
🔒 I've Saved It - Continue
`;
}
private renderSuccess() {
return html`
✓ ${this.method === AUTH_METHOD_PASSKEY
? "Passkey"
: "Passphrase"} successfully registered!
{
this.registrationSuccess = false;
this.flow = "login";
}}"
>
🔒 Continue to Login
`;
}
override render() {
return html`
${this.error
? html`
${this.error}
`
: ""} ${this.isProcessing
? html`
Please follow the browser's prompts to continue...
`
: this.mnemonic
? this.renderMnemonicDisplay()
: this.registrationSuccess
? this.renderSuccess()
: this.flow === null
? this.renderInitial()
: this.method === null
? this.renderMethodSelection()
: this.method === AUTH_METHOD_PASSPHRASE
? this.renderPassphraseAuth()
: this.method === AUTH_METHOD_PASSKEY
? html`
Please follow the browser's prompts to continue...
`
: ""}
`;
}
}
globalThis.customElements.define("x-login-view", XLoginView);