feat(secrets): track key names so list/describe work for backend-stored secrets
Some checks failed
Some checks failed
Post-migration, every Secret on a non-plaintext backend had an empty `data`
column (values live in the backend; only externalRef on the row). The CLI's
\`get secrets\` showed \`KEYS: -\` and \`describe secret\` showed \`(empty)\` for
all 9 migrated secrets — useless without --show-values.
Fix: dedicated \`keyNames Json\` column on Secret that stores the sorted key
list independently from the values. Populated on every write path, lazily
backfilled on first read for pre-existing rows that pre-date the column.
Schema default \`[]\` keeps prisma db push self-healing on rolling upgrades.
- src/db/prisma/schema.prisma: add Secret.keyNames Json @default("[]")
- src/mcpd/src/repositories/secret.repository.ts: pipe keyNames through create
+ update
- src/mcpd/src/services/secret.service.ts:
- create/update populate keyNames = sorted Object.keys(data)
- getById lazy-backfills empty keyNames (cheap: derives from data for
plaintext, single backend read for openbao)
- src/mcpd/src/services/secret-migrate.service.ts: migrate writes keyNames
alongside the new backendId so freshly-migrated rows are populated without
a follow-up read
- src/cli/src/commands/get.ts: KEYS column reads keyNames first, falls back
to Object.keys(data) for older rows
- src/cli/src/commands/describe.ts: shows the Data section keys whenever
keyNames OR data has entries (so backend-stored secrets render their key
list); --show-values still resolves through the backend
After deploy, the 9 already-migrated secrets backfill their keyNames on the
next describe-by-id, with no operator action needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -207,12 +207,23 @@ function formatSecretDetail(secret: Record<string, unknown>, showValues: boolean
|
||||
lines.push(`${pad('Name:')}${secret.name}`);
|
||||
|
||||
const data = secret.data as Record<string, string> | undefined;
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
const keyNames = Array.isArray(secret.keyNames) ? secret.keyNames as string[] : [];
|
||||
// The data row carries the actual values for plaintext-backed secrets and
|
||||
// is empty for backend-stored ones — fall back to keyNames so we can still
|
||||
// show what KEYS exist without their values.
|
||||
const knownKeys = (data && Object.keys(data).length > 0)
|
||||
? Object.keys(data)
|
||||
: keyNames;
|
||||
|
||||
if (knownKeys.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('Data:');
|
||||
const keyW = Math.max(4, ...Object.keys(data).map((k) => k.length)) + 2;
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const display = showValues ? value : '***';
|
||||
const keyW = Math.max(4, ...knownKeys.map((k) => k.length)) + 2;
|
||||
for (const key of knownKeys) {
|
||||
const value = data?.[key];
|
||||
const display = showValues
|
||||
? (value ?? '(reveal failed — backend unreachable?)')
|
||||
: '***';
|
||||
lines.push(` ${key.padEnd(keyW)}${display}`);
|
||||
}
|
||||
if (!showValues) {
|
||||
|
||||
@@ -33,6 +33,10 @@ interface SecretRow {
|
||||
id: string;
|
||||
name: string;
|
||||
data: Record<string, string>;
|
||||
/** Sorted list of key names — populated by mcpd whether the values live in
|
||||
* the row (plaintext) or in a remote backend (openbao). Falls back to
|
||||
* Object.keys(data) for older rows that pre-date the column. */
|
||||
keyNames?: string[];
|
||||
}
|
||||
|
||||
interface TemplateRow {
|
||||
@@ -179,7 +183,19 @@ const mcpTokenColumns: Column<McpTokenRow>[] = [
|
||||
|
||||
const secretColumns: Column<SecretRow>[] = [
|
||||
{ header: 'NAME', key: 'name' },
|
||||
{ header: 'KEYS', key: (r) => Object.keys(r.data).join(', ') || '-', width: 40 },
|
||||
{
|
||||
header: 'KEYS',
|
||||
key: (r) => {
|
||||
// Prefer the dedicated column. It's populated whether the secret lives
|
||||
// in the row (plaintext) or behind a backend (openbao); the row's `data`
|
||||
// is empty in the latter case.
|
||||
const tracked = Array.isArray(r.keyNames) && r.keyNames.length > 0 ? r.keyNames : null;
|
||||
const fallback = Object.keys(r.data ?? {});
|
||||
const keys = tracked ?? fallback;
|
||||
return keys.length > 0 ? keys.join(', ') : '-';
|
||||
},
|
||||
width: 40,
|
||||
},
|
||||
{ header: 'ID', key: 'id' },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user