From 92cc5f71fb690ac3387e94658c456c250b185666 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 5 May 2026 17:17:26 -0400 Subject: [PATCH 1/5] feat(dashnote): accept WIF private key login with auto-detected secret shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Login form now accepts either a BIP39 mnemonic or a WIF private key in a single field, dispatching by whitespace presence. WIF flow looks up the identity via byPublicKeyHash, validates the matched key is an AUTHENTICATION key at HIGH or CRITICAL level (rejecting MASTER which can't sign documents), and rejects disabled keys. Failed logins no longer clobber prior session state — they restore the prior status, or fall back to browsing if a remembered identity exists. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../dashnote/src/components/LoginModal.tsx | 65 ++-- .../dashnote/src/dash/loginWithPrivateKey.ts | 265 ++++++++++++++ example-apps/dashnote/src/dash/types.ts | 3 + .../dashnote/src/lib/detectSecretShape.ts | 12 + .../dashnote/src/session/SessionContext.tsx | 68 +++- .../dashnote/src/session/keyManagerFromKey.ts | 19 + .../dashnote/test/LoginModal.test.tsx | 47 ++- .../dashnote/test/SessionContext.test.tsx | 169 ++++++++- .../dashnote/test/detectSecretShape.test.ts | 48 +++ .../dashnote/test/loginWithPrivateKey.test.ts | 345 ++++++++++++++++++ 10 files changed, 993 insertions(+), 48 deletions(-) create mode 100644 example-apps/dashnote/src/dash/loginWithPrivateKey.ts create mode 100644 example-apps/dashnote/src/lib/detectSecretShape.ts create mode 100644 example-apps/dashnote/src/session/keyManagerFromKey.ts create mode 100644 example-apps/dashnote/test/detectSecretShape.test.ts create mode 100644 example-apps/dashnote/test/loginWithPrivateKey.test.ts diff --git a/example-apps/dashnote/src/components/LoginModal.tsx b/example-apps/dashnote/src/components/LoginModal.tsx index cb6e2e2..7e33c0c 100644 --- a/example-apps/dashnote/src/components/LoginModal.tsx +++ b/example-apps/dashnote/src/components/LoginModal.tsx @@ -1,6 +1,7 @@ -import { useEffect, useState, type FormEvent } from "react"; +import { useEffect, useMemo, useState, type FormEvent } from "react"; import { registerContract } from "../dash/contract"; +import { detectSecretShape } from "../lib/detectSecretShape"; import { errorMessage } from "../lib/logger"; import { useSession } from "../session/useSession"; import { Modal } from "./Modal"; @@ -13,7 +14,7 @@ export interface LoginModalProps { export function LoginModal({ open, onClose }: LoginModalProps) { const session = useSession(); - const [mnemonic, setMnemonic] = useState(""); + const [secret, setSecret] = useState(""); const [identityIndex, setIdentityIndex] = useState("0"); const [contractInput, setContractInput] = useState(session.contractId ?? ""); const [error, setError] = useState(null); @@ -26,6 +27,13 @@ export function LoginModal({ open, onClose }: LoginModalProps) { const showRememberedPanel = Boolean( session.rememberedIdentityId && !useDifferentIdentity, ); + // Detect what the user pasted so we can hide the identity-index field for + // single-key WIF input (where DIP-13 derivation doesn't apply). + const secretShape = useMemo( + () => (secret.trim() ? detectSecretShape(secret) : null), + [secret], + ); + const isWifInput = secretShape === "wif"; useEffect(() => { setContractInput(session.contractId ?? ""); @@ -36,7 +44,7 @@ export function LoginModal({ open, onClose }: LoginModalProps) { setRememberMe(true); setUseDifferentIdentity(false); setError(null); - setMnemonic(""); + setSecret(""); } }, [open]); @@ -69,11 +77,11 @@ export function LoginModal({ open, onClose }: LoginModalProps) { setSubmitting(true); try { const index = Number.parseInt(identityIndex, 10); - await session.login(mnemonic, { + await session.login(secret, { identityIndex: Number.isNaN(index) ? 0 : index, rememberMe, }); - setMnemonic(""); + setSecret(""); onClose(); } catch (err) { setError(errorMessage(err)); @@ -240,11 +248,12 @@ export function LoginModal({ open, onClose }: LoginModalProps) {