Skip to content

Commit 5b7dbf3

Browse files
committed
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ws8U3Nx9CBQKWPPg1u2Yy4
1 parent bf41c5d commit 5b7dbf3

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
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.

apps/webapp/app/components/primitives/ClipboardField.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,43 @@ const variants = {
6060
},
6161
};
6262

63+
const SECURE_MASK = "••••••••••••••••";
64+
65+
/**
66+
* Builds the masked display string, optionally revealing the first/last few
67+
* characters in cleartext so users can confirm a copied value. A custom mask
68+
* string (when `secure` is a string) is always shown as-is.
69+
*/
70+
function maskValue(
71+
value: string,
72+
secure: boolean | string,
73+
revealStart: number,
74+
revealEnd: number
75+
) {
76+
if (typeof secure === "string") {
77+
return secure;
78+
}
79+
80+
const start = Math.max(0, revealStart);
81+
const end = Math.max(0, revealEnd);
82+
83+
// Nothing to reveal, or revealing would leak the whole value: fully mask.
84+
if ((start === 0 && end === 0) || start + end >= value.length) {
85+
return SECURE_MASK;
86+
}
87+
88+
const revealedStart = start > 0 ? value.slice(0, start) : "";
89+
const revealedEnd = end > 0 ? value.slice(-end) : "";
90+
return `${revealedStart}${SECURE_MASK}${revealedEnd}`;
91+
}
92+
6393
type ClipboardFieldProps = {
6494
value: string;
6595
secure?: boolean | string;
96+
/** When masked, reveal this many of the first characters in cleartext. */
97+
secureRevealStart?: number;
98+
/** When masked, reveal this many of the last characters in cleartext. */
99+
secureRevealEnd?: number;
66100
variant: keyof typeof variants;
67101
className?: string;
68102
icon?: React.ReactNode;
@@ -73,6 +107,8 @@ type ClipboardFieldProps = {
73107
export function ClipboardField({
74108
value,
75109
secure = false,
110+
secureRevealStart = 0,
111+
secureRevealEnd = 0,
76112
variant,
77113
className,
78114
icon,
@@ -87,6 +123,8 @@ export function ClipboardField({
87123
setIsSecure(secure !== undefined && secure);
88124
}, [secure]);
89125

126+
const maskedValue = maskValue(value, secure, secureRevealStart, secureRevealEnd);
127+
90128
return (
91129
<span className={cn(container, fullWidth ? "w-full" : "max-w-fit", className)}>
92130
{icon && (
@@ -100,7 +138,7 @@ export function ClipboardField({
100138
<input
101139
type="text"
102140
ref={inputIcon}
103-
value={isSecure ? (typeof secure === "string" ? secure : "••••••••••••••••") : value}
141+
value={isSecure ? maskedValue : value}
104142
readOnly={true}
105143
className={cn(
106144
"shrink grow select-all overflow-x-auto",

apps/webapp/app/routes/account.tokens/route.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,9 @@ function CreatePersonalAccessToken({
372372
</Callout>
373373
<ClipboardField
374374
secure
375+
// 7-char "tr_pat_" prefix + 4 token chars, matching the tokens list display
376+
secureRevealStart={11}
377+
secureRevealEnd={4}
375378
value={token.token}
376379
variant={"secondary/medium"}
377380
icon={<ShieldExclamationIcon className="size-5 text-success" />}

0 commit comments

Comments
 (0)