fix(qa): expose credential fingerprints in admin list

This commit is contained in:
Vincent Koc
2026-05-28 12:04:20 +02:00
parent 93577ad587
commit 1bc32e53ab
5 changed files with 46 additions and 16 deletions
+7 -2
View File
@@ -521,21 +521,26 @@ function printQaCredentialListTable(credentials: QaCredentialRecord[]) {
}
const rows = credentials.map((credential) => ({
credentialId: credential.credentialId,
fingerprint: credential.credentialFingerprint ?? "",
kind: credential.kind,
status: credential.status,
leased: formatQaCredentialLeaseState(credential),
note: credential.note ?? "",
}));
const idWidth = Math.max("credentialId".length, ...rows.map((row) => row.credentialId.length));
const fingerprintWidth = Math.max(
"fingerprint".length,
...rows.map((row) => row.fingerprint.length),
);
const kindWidth = Math.max("kind".length, ...rows.map((row) => row.kind.length));
const statusWidth = Math.max("status".length, ...rows.map((row) => row.status.length));
const leaseWidth = Math.max("leased".length, ...rows.map((row) => row.leased.length));
process.stdout.write(
`${"credentialId".padEnd(idWidth)} ${"kind".padEnd(kindWidth)} ${"status".padEnd(statusWidth)} ${"leased".padEnd(leaseWidth)} note\n`,
`${"credentialId".padEnd(idWidth)} ${"fingerprint".padEnd(fingerprintWidth)} ${"kind".padEnd(kindWidth)} ${"status".padEnd(statusWidth)} ${"leased".padEnd(leaseWidth)} note\n`,
);
for (const row of rows) {
process.stdout.write(
`${row.credentialId.padEnd(idWidth)} ${row.kind.padEnd(kindWidth)} ${row.status.padEnd(statusWidth)} ${row.leased.padEnd(leaseWidth)} ${row.note}\n`,
`${row.credentialId.padEnd(idWidth)} ${row.fingerprint.padEnd(fingerprintWidth)} ${row.kind.padEnd(kindWidth)} ${row.status.padEnd(statusWidth)} ${row.leased.padEnd(leaseWidth)} ${row.note}\n`,
);
}
}
@@ -1,5 +1,5 @@
import { execFile } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
@@ -15,6 +15,7 @@ import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/strin
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { z } from "zod";
import { startQaGatewayChild } from "../../gateway-child.js";
import { fingerprintQaCredentialId } from "../../qa-credentials-fingerprint.runtime.js";
import { DEFAULT_QA_LIVE_PROVIDER_MODE } from "../../providers/index.js";
import {
defaultQaModelForMode,
@@ -736,14 +737,6 @@ function renderWhatsAppQaMarkdown(params: {
return lines.join("\n");
}
function fingerprintWhatsAppCredentialId(credentialId: string | undefined) {
if (!credentialId) {
return undefined;
}
const digest = createHash("sha256").update(credentialId).digest("hex").slice(0, 16);
return `sha256:${digest}`;
}
function createMissingGroupJidScenarioResult(params: {
explicitScenarioSelection: boolean;
scenario: WhatsAppQaScenarioDefinition;
@@ -983,7 +976,7 @@ export async function runWhatsAppQaLive(params: {
const passed = scenarioResults.filter((entry) => entry.status === "pass").length;
const failed = scenarioResults.filter((entry) => entry.status === "fail").length;
const skipped = scenarioResults.filter((entry) => entry.status === "skip").length;
const credentialFingerprint = fingerprintWhatsAppCredentialId(credentialLease?.credentialId);
const credentialFingerprint = fingerprintQaCredentialId(credentialLease?.credentialId);
const summary: WhatsAppQaSummary = {
credentials: credentialLease
? {
@@ -1057,7 +1050,7 @@ export const testing = {
buildWhatsAppQaConfig,
createMissingGroupJidScenarioResult,
findScenarios,
fingerprintWhatsAppCredentialId,
fingerprintWhatsAppCredentialId: fingerprintQaCredentialId,
isTransientWhatsAppQaDriverError,
parseWhatsAppQaCredentialPayload,
renderWhatsAppQaMarkdown,
@@ -85,6 +85,7 @@ describe("qa credential admin runtime", () => {
});
expect(result.credential.credentialId).toBe("cred-1");
expect(result.credential.credentialFingerprint).toMatch(/^sha256:[0-9a-f]{16}$/);
expect(requireFirstFetchInput(fetchImpl)).toBe(
"https://first-schnauzer-821.convex.site/qa-credentials/v1/admin/add",
);
@@ -225,6 +226,7 @@ describe("qa credential admin runtime", () => {
});
expect(result.credentials).toHaveLength(1);
expect(result.credentials[0]?.credentialFingerprint).toMatch(/^sha256:[0-9a-f]{16}$/);
const bodyText = requireFirstFetchInit(fetchImpl).body;
expect(typeof bodyText).toBe("string");
const body = JSON.parse(bodyText as string) as Record<string, unknown>;
@@ -8,6 +8,7 @@ import {
parseQaCredentialPositiveIntegerEnv,
QA_CREDENTIALS_DEFAULT_ENDPOINT_PREFIX,
} from "./qa-credentials-common.runtime.js";
import { fingerprintQaCredentialId } from "./qa-credentials-fingerprint.runtime.js";
const DEFAULT_ENDPOINT_PREFIX = QA_CREDENTIALS_DEFAULT_ENDPOINT_PREFIX;
const DEFAULT_HTTP_TIMEOUT_MS = 15_000;
@@ -32,6 +33,7 @@ const credentialLeaseSchema = z.object({
const credentialRecordSchema = z.object({
credentialId: z.string().min(1),
credentialFingerprint: z.string().optional(),
kind: z.string().min(1),
status: credentialStatusSchema,
createdAtMs: z.number().int(),
@@ -442,10 +444,17 @@ function normalizeLimit(value: number | undefined) {
return value;
}
function withQaCredentialFingerprint(credential: QaCredentialRecord): QaCredentialRecord {
return {
...credential,
credentialFingerprint: fingerprintQaCredentialId(credential.credentialId),
};
}
export async function addQaCredentialSet(options: AddQaCredentialSetOptions) {
const config = resolveAdminConfig(options);
const fetchImpl = options.fetchImpl ?? fetch;
return await postJson({
const result = await postJson({
fetchImpl,
authToken: config.authToken,
httpTimeoutMs: config.httpTimeoutMs,
@@ -459,12 +468,16 @@ export async function addQaCredentialSet(options: AddQaCredentialSetOptions) {
actorId: config.actorId,
},
});
return {
...result,
credential: withQaCredentialFingerprint(result.credential),
};
}
export async function removeQaCredentialSet(options: RemoveQaCredentialSetOptions) {
const config = resolveAdminConfig(options);
const fetchImpl = options.fetchImpl ?? fetch;
return await postJson({
const result = await postJson({
fetchImpl,
authToken: config.authToken,
httpTimeoutMs: config.httpTimeoutMs,
@@ -475,6 +488,10 @@ export async function removeQaCredentialSet(options: RemoveQaCredentialSetOption
actorId: config.actorId,
},
});
return {
...result,
credential: withQaCredentialFingerprint(result.credential),
};
}
export async function listQaCredentialSets(options: ListQaCredentialSetsOptions) {
@@ -482,7 +499,7 @@ export async function listQaCredentialSets(options: ListQaCredentialSetsOptions)
const fetchImpl = options.fetchImpl ?? fetch;
const status = normalizeStatus(options.status);
const limit = normalizeLimit(options.limit);
return await postJson({
const result = await postJson({
fetchImpl,
authToken: config.authToken,
httpTimeoutMs: config.httpTimeoutMs,
@@ -495,4 +512,8 @@ export async function listQaCredentialSets(options: ListQaCredentialSetsOptions)
...(limit !== undefined ? { limit } : {}),
},
});
return {
...result,
credentials: result.credentials.map((credential) => withQaCredentialFingerprint(credential)),
};
}
@@ -0,0 +1,9 @@
import { createHash } from "node:crypto";
export function fingerprintQaCredentialId(credentialId: string | undefined) {
if (!credentialId) {
return undefined;
}
const digest = createHash("sha256").update(credentialId).digest("hex").slice(0, 16);
return `sha256:${digest}`;
}