fix(desktop): show saved key previews without the backend's redaction sentinel

Fixes #124378

Co-authored-by: Adolan <94890352+Adolanium@users.noreply.github.com>
This commit is contained in:
Brooklyn Nicholson
2026-09-27 12:57:32 -05:00
committed by brooklyn!
parent b07a3b46ed
commit a776efb06a
4 changed files with 28 additions and 3 deletions

View File

@@ -38,6 +38,7 @@ import { useRouteEnumParam } from '../hooks/use-route-enum-param'
import { DetailColumn, ListColumn, MasterDetail } from '../master-detail'
import { PageSearchShell } from '../page-search-shell'
import { CREDENTIAL_CONTROL_CLASS } from '../settings/credential-key-ui'
import { credentialPreview } from '../settings/helpers'
import { ListRow } from '../settings/primitives'
import { SettingsProfileScope } from '../settings/profile-scope'
import type { SetStatusbarItemGroup } from '../shell/statusbar-controls'
@@ -998,7 +999,7 @@ function MessagingField({
className={CREDENTIAL_CONTROL_CLASS}
id={fieldId}
onChange={event => onEdit(field.key, event.target.value)}
placeholder={field.is_set ? field.redacted_value || m.replaceValue : copy.placeholder}
placeholder={field.is_set ? credentialPreview(field.redacted_value) || m.replaceValue : copy.placeholder}
type={field.is_password ? 'password' : 'text'}
value={edits[field.key] || ''}
/>

View File

@@ -9,7 +9,7 @@ import { cn } from '@/lib/utils'
import type { EnvVarInfo } from '@/types/hermes'
import { CONTROL_TEXT } from './constants'
import { prettyName, withoutKey } from './helpers'
import { credentialPreview, prettyName, withoutKey } from './helpers'
import { LIST_ROW_COLUMNS, ListRow } from './primitives'
import type { EnvRowProps } from './types'
@@ -68,7 +68,7 @@ export function KeyField({
const draft = edits[editKey] ?? ''
const dirty = draft.trim().length > 0
const busy = saving === varKey
const masked = info.redacted_value ?? '••••••••'
const masked = credentialPreview(info.redacted_value) ?? '••••••••'
const startEdit = () => setEdits(c => ({ ...c, [editKey]: '' }))
const cancel = () => setEdits(c => withoutKey(c, editKey))
const update = (e: ChangeEvent<HTMLInputElement>) => setEdits(c => ({ ...c, [editKey]: e.target.value }))

View File

@@ -6,6 +6,7 @@ import { BUILTIN_PERSONALITIES } from './constants'
import { defineFieldCopy, fieldCopyForSchemaKey, schemaKeyToFieldCopyKey } from './field-copy'
import {
clearsEnabledToolsets,
credentialPreview,
diffConfig,
enumOptionsFor,
getNested,
@@ -445,3 +446,13 @@ describe('settings helpers', () => {
})
})
})
describe('credentialPreview', () => {
it('unwraps the backend preview sentinel and masks label-less forms', () => {
expect(credentialPreview('«redacted:sk-h...JPJ8»')).toBe('sk-h...JPJ8')
expect(credentialPreview('«redacted-secret»')).toBe('••••••••')
expect(credentialPreview('«redacted-vault-secret»')).toBe('••••••••')
expect(credentialPreview('sk-h...JPJ8')).toBe('sk-h...JPJ8')
expect(credentialPreview(null)).toBeNull()
})
})

View File

@@ -26,6 +26,19 @@ export const withoutKey = <T>(record: Record<string, T>, key: string) => {
export const redactedValue = (v: string) => (v.length <= 8 ? '••••' : `${v.slice(0, 4)}...${v.slice(-4)}`)
// The backend wraps stored-key previews in a write-guard sentinel
// (hermes_cli/web_routers/_common.redacted_credential_preview): show the inner
// preview, and a plain mask for the label-less forms.
export const credentialPreview = (value: null | string | undefined): null | string => {
if (!value?.startsWith('«redacted')) {
return value || null
}
const inner = /^«redacted:(.+)»$/.exec(value)?.[1]?.trim()
return inner || '••••••••'
}
// Longest-prefix match so a more specific group like ``MINIMAX_CN_`` is
// chosen over its shorter parent ``MINIMAX_``. Falls back to the bucket
// "Other" used by the Keys settings view for un-grouped env vars.