From 5b7dbf3a87a719381520c1d61c08a8a11e512dc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 11:27:47 +0000 Subject: [PATCH] Reveal first/last characters of a newly created Personal Access Token The token shown after creating a Personal Access Token was fully masked, so users had no way to confirm they had copied the right value. The field now reveals the first and last few characters in cleartext (the middle stays masked), matching the partial display already used in the tokens list. Adds general `secureRevealStart` / `secureRevealEnd` props to ClipboardField (default 0/0, so all other usages remain fully masked) and enables 4/4 in the token creation dialog. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Ws8U3Nx9CBQKWPPg1u2Yy4 --- .../pat-token-reveal-last-chars.md | 6 +++ .../components/primitives/ClipboardField.tsx | 40 ++++++++++++++++++- .../app/routes/account.tokens/route.tsx | 3 ++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .server-changes/pat-token-reveal-last-chars.md diff --git a/.server-changes/pat-token-reveal-last-chars.md b/.server-changes/pat-token-reveal-last-chars.md new file mode 100644 index 00000000000..29797f73d34 --- /dev/null +++ b/.server-changes/pat-token-reveal-last-chars.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +When you create a Personal Access Token, the generated token now shows its first and last few characters instead of being fully hidden, so you can confirm you copied the right value. diff --git a/apps/webapp/app/components/primitives/ClipboardField.tsx b/apps/webapp/app/components/primitives/ClipboardField.tsx index 0304e5bca6b..7774f301218 100644 --- a/apps/webapp/app/components/primitives/ClipboardField.tsx +++ b/apps/webapp/app/components/primitives/ClipboardField.tsx @@ -60,9 +60,43 @@ const variants = { }, }; +const SECURE_MASK = "••••••••••••••••"; + +/** + * Builds the masked display string, optionally revealing the first/last few + * characters in cleartext so users can confirm a copied value. A custom mask + * string (when `secure` is a string) is always shown as-is. + */ +function maskValue( + value: string, + secure: boolean | string, + revealStart: number, + revealEnd: number +) { + if (typeof secure === "string") { + return secure; + } + + const start = Math.max(0, revealStart); + const end = Math.max(0, revealEnd); + + // Nothing to reveal, or revealing would leak the whole value: fully mask. + if ((start === 0 && end === 0) || start + end >= value.length) { + return SECURE_MASK; + } + + const revealedStart = start > 0 ? value.slice(0, start) : ""; + const revealedEnd = end > 0 ? value.slice(-end) : ""; + return `${revealedStart}${SECURE_MASK}${revealedEnd}`; +} + type ClipboardFieldProps = { value: string; secure?: boolean | string; + /** When masked, reveal this many of the first characters in cleartext. */ + secureRevealStart?: number; + /** When masked, reveal this many of the last characters in cleartext. */ + secureRevealEnd?: number; variant: keyof typeof variants; className?: string; icon?: React.ReactNode; @@ -73,6 +107,8 @@ type ClipboardFieldProps = { export function ClipboardField({ value, secure = false, + secureRevealStart = 0, + secureRevealEnd = 0, variant, className, icon, @@ -87,6 +123,8 @@ export function ClipboardField({ setIsSecure(secure !== undefined && secure); }, [secure]); + const maskedValue = maskValue(value, secure, secureRevealStart, secureRevealEnd); + return ( {icon && ( @@ -100,7 +138,7 @@ export function ClipboardField({ }