From 109e245d56e7f001c6ebd8a3ea54a8c31d03bc51 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:55:31 +0100 Subject: [PATCH] feat(webapp): show the first and last characters of a new PAT (#4363) --- .../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 000000000..29797f73d --- /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 0304e5bca..7774f3012 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({ }