feat: mask provider api key input (#772)

This commit is contained in:
Kevin Codex
2026-04-20 08:25:22 +08:00
committed by GitHub
parent f828171ef1
commit 13e9f22a83
6 changed files with 57 additions and 32 deletions

View File

@@ -148,6 +148,42 @@ type Position = {
column: number
}
export function maskTextWithVisibleEdges(
value: string,
mask: string,
visiblePrefix = 3,
visibleSuffix = 3,
): string {
if (!mask || !value) return value
const graphemes = Array.from(getGraphemeSegmenter().segment(value))
const secretGraphemeCount = graphemes.filter(
({ segment }) => segment !== '\n',
).length
const visibleCount = visiblePrefix + visibleSuffix
if (secretGraphemeCount <= visibleCount) {
return graphemes
.map(({ segment }) => (segment === '\n' ? segment : mask))
.join('')
}
let secretIndex = 0
return graphemes
.map(({ segment }) => {
if (segment === '\n') return segment
const nextSegment =
secretIndex < visiblePrefix ||
secretIndex >= secretGraphemeCount - visibleSuffix
? segment
: mask
secretIndex += 1
return nextSegment
})
.join('')
}
export class Cursor {
readonly offset: number
constructor(
@@ -208,7 +244,12 @@ export class Cursor {
maxVisibleLines?: number,
) {
const { line, column } = this.getPosition()
const allLines = this.measuredText.getWrappedText()
const allLines = mask
? new MeasuredText(
maskTextWithVisibleEdges(this.text, mask),
this.measuredText.columns,
).getWrappedText()
: this.measuredText.getWrappedText()
const startLine = this.getViewportStartLine(maxVisibleLines)
const endLine =
@@ -221,23 +262,6 @@ export class Cursor {
.map((text, i) => {
const currentLine = i + startLine
let displayText = text
if (mask) {
const graphemes = Array.from(getGraphemeSegmenter().segment(text))
if (currentLine === allLines.length - 1) {
// Last line: mask all but the trailing 6 chars so the user can
// confirm they pasted the right thing without exposing the full token
const visibleCount = Math.min(6, graphemes.length)
const maskCount = graphemes.length - visibleCount
const splitOffset =
graphemes.length > visibleCount ? graphemes[maskCount]!.index : 0
displayText = mask.repeat(maskCount) + text.slice(splitOffset)
} else {
// Earlier wrapped lines: fully mask. Previously only the last line
// was masked, leaking the start of the token on narrow terminals
// where the pasted OAuth code wraps across multiple lines.
displayText = mask.repeat(graphemes.length)
}
}
// looking for the line with the cursor
if (line !== currentLine) return displayText.trimEnd()

View File

@@ -621,8 +621,8 @@ test('buildStartupEnvFromProfile treats explicit falsey provider flags as user i
})
test('maskSecretForDisplay preserves only a short prefix and suffix', () => {
assert.equal(maskSecretForDisplay('sk-secret-12345678'), 'sk-...5678')
assert.equal(maskSecretForDisplay('AIzaSecret12345678'), 'AIza...5678')
assert.equal(maskSecretForDisplay('sk-secret-12345678'), 'sk-...678')
assert.equal(maskSecretForDisplay('AIzaSecret12345678'), 'AIz...678')
})
test('redactSecretValueForDisplay masks poisoned display fields that equal configured secrets', () => {
@@ -630,7 +630,7 @@ test('redactSecretValueForDisplay masks poisoned display fields that equal confi
assert.equal(
redactSecretValueForDisplay(apiKey, { OPENAI_API_KEY: apiKey }),
'sk-...5678',
'sk-...678',
)
assert.equal(
redactSecretValueForDisplay('gpt-4o', { OPENAI_API_KEY: apiKey }),

View File

@@ -61,15 +61,7 @@ export function maskSecretForDisplay(
return 'configured'
}
if (sanitized.startsWith('sk-')) {
return `${sanitized.slice(0, 3)}...${sanitized.slice(-4)}`
}
if (sanitized.startsWith('AIza')) {
return `${sanitized.slice(0, 4)}...${sanitized.slice(-4)}`
}
return `${sanitized.slice(0, 2)}...${sanitized.slice(-4)}`
return `${sanitized.slice(0, 3)}...${sanitized.slice(-3)}`
}
export function redactSecretValueForDisplay(