feat: add typed user query CLI
This commit is contained in:
@@ -2114,6 +2114,52 @@ test("hwlab-cli client request covers arbitrary Cloud Web same-origin API routes
|
||||
assert.equal(result.payload.body.fullBodyAvailable, true);
|
||||
});
|
||||
|
||||
test("hwlab-cli client user query wrappers expose typed redacted summaries", async () => {
|
||||
const calls: any[] = [];
|
||||
const fetchImpl = async (url: string | URL, init: any = {}) => {
|
||||
calls.push({ url: String(url), init });
|
||||
if (String(url).endsWith("/v1/users/me")) {
|
||||
return new Response(JSON.stringify({ ok: true, authenticated: true, actor: { id: "usr_test", username: "inner-test", role: "user", status: "active" }, authMethod: "user-billing-api-key", session: { id: "key_test", keyPrefix: "hwl_live_Zci", key: "hwl_live_should_not_print" } }), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/access/status")) {
|
||||
return new Response(JSON.stringify({ ok: true, authenticated: true, actor: { id: "usr_test", username: "inner-test", role: "user" }, authMethod: "user-billing-api-key", permissions: ["code_agent"], routes: { currentUser: "/v1/users/me" } }), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/api-keys")) {
|
||||
return new Response(JSON.stringify({ ok: true, count: 1, items: [{ id: "key_test", name: "fixed", keyPrefix: "hwl_live_Zci", key: "hwl_live_raw_secret", scopes: ["api"], status: "active", valuesRedacted: true }] }), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/billing/summary?limit=7")) {
|
||||
return new Response(JSON.stringify({ ok: true, credits: { balance: 99, reserved: 0 }, ledger: { count: 1, items: [{ id: "led_test", kind: "usage", reason: "hwlab-code-agent", deltaCredits: -1, metadata: { traceId: "trc_test", sessionId: "ses_test" } }] }, usage: { recordCount: 1, byService: [{ service: "hwlab-code-agent", credits: 1 }] } }), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/usage/summary")) {
|
||||
return new Response(JSON.stringify({ ok: true, usage: { recordCount: 1, byService: [{ service: "hwlab-code-agent", credits: 1 }], lastUsedAt: "2026-06-15T00:00:00Z" }, records: [{ id: "usage_test", service: "hwlab-code-agent", credits: 1, metadata: { traceId: "trc_test" } }] }), { status: 200 });
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: false, error: { code: "unexpected_test_route" } }), { status: 404 });
|
||||
};
|
||||
const env = { HWLAB_API_KEY: "hwl_live_user_test_key" };
|
||||
|
||||
const account = await runHwlabCli(["client", "account", "status", "--base-url", "http://web.test"], { env, fetchImpl });
|
||||
const keys = await runHwlabCli(["client", "api-keys", "list", "--base-url", "http://web.test"], { env, fetchImpl });
|
||||
const billing = await runHwlabCli(["client", "billing", "summary", "--limit", "7", "--base-url", "http://web.test"], { env, fetchImpl });
|
||||
const usage = await runHwlabCli(["client", "usage", "summary", "--base-url", "http://web.test"], { env, fetchImpl });
|
||||
|
||||
assert.equal(account.exitCode, 0);
|
||||
assert.equal(account.payload.action, "client.account.status");
|
||||
assert.equal(account.payload.identity.actor.username, "inner-test");
|
||||
assert.equal(account.payload.access.permissions[0], "code_agent");
|
||||
assert.equal(keys.exitCode, 0);
|
||||
assert.equal(keys.payload.action, "client.api-keys.list");
|
||||
assert.equal(keys.payload.body.items[0].prefix, "hwl_live_Zci");
|
||||
assert.equal(billing.exitCode, 0);
|
||||
assert.equal(billing.payload.route.path, "/v1/billing/summary?limit=7");
|
||||
assert.equal(billing.payload.body.ledger.items[0].traceId, "trc_test");
|
||||
assert.equal(usage.exitCode, 0);
|
||||
assert.equal(usage.payload.body.usage.recordCount, 1);
|
||||
assert.equal(calls.every((call) => call.init.headers.authorization === "Bearer hwl_live_user_test_key"), true);
|
||||
assert.equal(calls.every((call) => call.init.headers.cookie === undefined), true);
|
||||
assert.equal(JSON.stringify([account.payload, keys.payload, billing.payload, usage.payload]).includes("hwl_live_raw_secret"), false);
|
||||
assert.equal(JSON.stringify([account.payload, keys.payload, billing.payload, usage.payload]).includes("hwl_live_should_not_print"), false);
|
||||
});
|
||||
|
||||
test("hwlab-cli client request compacts array responses by default", async () => {
|
||||
const result = await runHwlabCli([
|
||||
"client",
|
||||
|
||||
@@ -74,6 +74,10 @@ async function clientCommand(context: any) {
|
||||
if (groupHelp) return groupHelp;
|
||||
}
|
||||
if (group === "auth") return authCommand(next);
|
||||
if (group === "account" || group === "user") return accountCommand(next);
|
||||
if (group === "api-keys" || group === "apikeys" || group === "keys") return apiKeysCommand(next);
|
||||
if (group === "billing") return billingCommand(next);
|
||||
if (group === "usage") return usageCommand(next);
|
||||
if (group === "access") return accessCommand(next);
|
||||
if (group === "provider-profiles" || group === "provider-profile" || group === "providers") return providerProfilesCommand(next);
|
||||
if (group === "runtime") return runtimeCommand(next);
|
||||
@@ -89,6 +93,10 @@ async function clientCommand(context: any) {
|
||||
|
||||
function clientSubcommandHelp(group: string) {
|
||||
if (group === "auth") return authHelp();
|
||||
if (group === "account" || group === "user") return accountHelp();
|
||||
if (group === "api-keys" || group === "apikeys" || group === "keys") return apiKeysHelp();
|
||||
if (group === "billing") return billingHelp();
|
||||
if (group === "usage") return usageHelp();
|
||||
if (group === "access") return accessHelp();
|
||||
if (group === "provider-profiles" || group === "provider-profile" || group === "providers") return providerProfilesHelp();
|
||||
if (group === "runtime") return runtimeHelp();
|
||||
@@ -120,6 +128,10 @@ function help() {
|
||||
"HWLAB_API_KEY=hwl_live_... HWLAB_RUNTIME_NAMESPACE=hwlab-v02 hwlab-cli client auth whoami",
|
||||
"HWLAB_API_KEY=hwl_live_... HWLAB_RUNTIME_NAMESPACE=hwlab-v02 hwlab-cli client request GET /v1/users/me",
|
||||
"hwlab-cli client auth status",
|
||||
"hwlab-cli client account status",
|
||||
"hwlab-cli client api-keys list",
|
||||
"hwlab-cli client billing summary --limit 5",
|
||||
"hwlab-cli client usage summary",
|
||||
"hwlab-cli client auth session --web-session",
|
||||
"hwlab-cli client auth profiles",
|
||||
"hwlab-cli client access summary",
|
||||
@@ -218,6 +230,117 @@ function authHelp() {
|
||||
});
|
||||
}
|
||||
|
||||
async function accountCommand(context: any) {
|
||||
const subcommand = context.rest[0] || "status";
|
||||
if (wantsHelp(context)) return accountHelp();
|
||||
if (subcommand !== "status" && subcommand !== "summary") throw cliError("unsupported_account_command", `unsupported account command: ${subcommand}`, { subcommand });
|
||||
const identityPath = "/v1/users/me";
|
||||
const accessPath = "/v1/access/status";
|
||||
const [identity, access] = await Promise.all([
|
||||
requestJson({ ...context, method: "GET", path: identityPath }),
|
||||
requestJson({ ...context, method: "GET", path: accessPath })
|
||||
]);
|
||||
const success = responseSucceeded(identity) && responseSucceeded(access);
|
||||
const endpoint = identity.runtimeEndpoint ?? access.runtimeEndpoint ?? runtimeEndpoint(context.parsed, context.env, context.endpointKind ?? "web");
|
||||
return ok("client.account.status", {
|
||||
baseUrl: endpoint.baseUrl,
|
||||
runtimeEndpoint: runtimeEndpointVisibility(endpoint),
|
||||
routes: {
|
||||
identity: route("GET", identityPath),
|
||||
access: route("GET", accessPath)
|
||||
},
|
||||
requests: {
|
||||
identity: requestVisibility(identity),
|
||||
access: requestVisibility(access)
|
||||
},
|
||||
auth: identity.auth ?? access.auth,
|
||||
authDiagnosis: identity.authDiagnosis ?? access.authDiagnosis ?? null,
|
||||
identity: identityBodyForCli(identity.body, context.parsed),
|
||||
access: accessStatusBodyForCli(access.body, context.parsed),
|
||||
body: {
|
||||
identity: identityBodyForCli(identity.body, context.parsed),
|
||||
access: accessStatusBodyForCli(access.body, context.parsed)
|
||||
}
|
||||
}, success ? "succeeded" : "failed");
|
||||
}
|
||||
|
||||
function accountHelp() {
|
||||
return ok("client.account.help", {
|
||||
serviceRuntime: false,
|
||||
imagePublished: false,
|
||||
sameOrigin: true,
|
||||
valuesPrinted: false,
|
||||
commands: [
|
||||
"status"
|
||||
],
|
||||
routes: {
|
||||
identity: route("GET", "/v1/users/me"),
|
||||
access: route("GET", "/v1/access/status")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function apiKeysCommand(context: any) {
|
||||
const subcommand = context.rest[0] || "list";
|
||||
if (wantsHelp(context)) return apiKeysHelp();
|
||||
if (subcommand !== "list" && subcommand !== "summary") throw cliError("unsupported_api_keys_command", `unsupported api-keys command: ${subcommand}`, { subcommand });
|
||||
const pathName = "/v1/api-keys";
|
||||
const response = await requestJson({ ...context, method: "GET", path: pathName });
|
||||
return responsePayload("client.api-keys.list", response, context, { route: route("GET", pathName), body: apiKeysBodyForCli(response.body, context.parsed), valuesPrinted: false });
|
||||
}
|
||||
|
||||
function apiKeysHelp() {
|
||||
return ok("client.api-keys.help", {
|
||||
serviceRuntime: false,
|
||||
imagePublished: false,
|
||||
sameOrigin: true,
|
||||
valuesPrinted: false,
|
||||
commands: ["list"],
|
||||
routes: { list: route("GET", "/v1/api-keys") }
|
||||
});
|
||||
}
|
||||
|
||||
async function billingCommand(context: any) {
|
||||
const subcommand = context.rest[0] || "summary";
|
||||
if (wantsHelp(context)) return billingHelp();
|
||||
if (subcommand !== "summary") throw cliError("unsupported_billing_command", `unsupported billing command: ${subcommand}`, { subcommand });
|
||||
const limit = boundedInteger(context.parsed.limit, 5, 1, 100);
|
||||
const pathName = `/v1/billing/summary?limit=${encodeURIComponent(String(limit))}`;
|
||||
const response = await requestJson({ ...context, method: "GET", path: pathName });
|
||||
return responsePayload("client.billing.summary", response, context, { route: route("GET", pathName), limit, availability: selfServiceAvailability(response, "billing-summary"), body: billingSummaryBodyForCli(response.body, context.parsed), valuesPrinted: false });
|
||||
}
|
||||
|
||||
function billingHelp() {
|
||||
return ok("client.billing.help", {
|
||||
serviceRuntime: false,
|
||||
imagePublished: false,
|
||||
sameOrigin: true,
|
||||
valuesPrinted: false,
|
||||
commands: ["summary [--limit N]"],
|
||||
routes: { summary: route("GET", "/v1/billing/summary?limit=N") }
|
||||
});
|
||||
}
|
||||
|
||||
async function usageCommand(context: any) {
|
||||
const subcommand = context.rest[0] || "summary";
|
||||
if (wantsHelp(context)) return usageHelp();
|
||||
if (subcommand !== "summary") throw cliError("unsupported_usage_command", `unsupported usage command: ${subcommand}`, { subcommand });
|
||||
const pathName = "/v1/usage/summary";
|
||||
const response = await requestJson({ ...context, method: "GET", path: pathName });
|
||||
return responsePayload("client.usage.summary", response, context, { route: route("GET", pathName), availability: selfServiceAvailability(response, "usage-summary"), body: usageSummaryBodyForCli(response.body, context.parsed), valuesPrinted: false });
|
||||
}
|
||||
|
||||
function usageHelp() {
|
||||
return ok("client.usage.help", {
|
||||
serviceRuntime: false,
|
||||
imagePublished: false,
|
||||
sameOrigin: true,
|
||||
valuesPrinted: false,
|
||||
commands: ["summary"],
|
||||
routes: { summary: route("GET", "/v1/usage/summary") }
|
||||
});
|
||||
}
|
||||
|
||||
async function accessCommand(context: any) {
|
||||
const subcommand = context.rest[0] || "summary";
|
||||
if (wantsHelp(context)) return accessHelp();
|
||||
@@ -2934,6 +3057,243 @@ function responseBodyForCli(body: any, parsed: ParsedArgs) {
|
||||
return parsed.full === true ? body : compactApiBody(body);
|
||||
}
|
||||
|
||||
function identityBodyForCli(body: any, parsed: ParsedArgs) {
|
||||
if (parsed.full === true) return redactUserQueryBody(body);
|
||||
const source = body?.data && typeof body.data === "object" ? body.data : body;
|
||||
return pruneUndefined({
|
||||
ok: body?.ok,
|
||||
authenticated: source?.authenticated ?? body?.authenticated,
|
||||
actor: actorForCli(source?.actor ?? body?.actor),
|
||||
user: actorForCli(source?.user ?? body?.user),
|
||||
authMethod: text(source?.authMethod ?? body?.authMethod) || undefined,
|
||||
session: apiKeyMetadataForCli(source?.session ?? body?.session ?? source?.apiKey ?? body?.apiKey),
|
||||
roles: arrayOfText(source?.roles ?? body?.roles),
|
||||
permissions: arrayOfText(source?.permissions ?? body?.permissions),
|
||||
scopes: arrayOfText(source?.scopes ?? body?.scopes),
|
||||
error: errorForCli(body?.error),
|
||||
valuesPrinted: false,
|
||||
fullBodyAvailable: true
|
||||
});
|
||||
}
|
||||
|
||||
function accessStatusBodyForCli(body: any, parsed: ParsedArgs) {
|
||||
if (parsed.full === true) return redactUserQueryBody(body);
|
||||
const source = body?.data && typeof body.data === "object" ? body.data : body;
|
||||
const routes = source?.routes && typeof source.routes === "object" ? Object.keys(source.routes).sort() : undefined;
|
||||
return pruneUndefined({
|
||||
ok: body?.ok,
|
||||
authenticated: source?.authenticated ?? body?.authenticated,
|
||||
actor: actorForCli(source?.actor ?? body?.actor),
|
||||
user: actorForCli(source?.user ?? body?.user),
|
||||
role: text(source?.role ?? body?.role) || undefined,
|
||||
authMethod: text(source?.authMethod ?? body?.authMethod) || undefined,
|
||||
permissions: arrayOfText(source?.permissions ?? body?.permissions),
|
||||
scopes: arrayOfText(source?.scopes ?? body?.scopes),
|
||||
roles: arrayOfText(source?.roles ?? body?.roles),
|
||||
tools: source?.tools,
|
||||
routes,
|
||||
routeCount: routes?.length,
|
||||
key: apiKeyMetadataForCli(source?.key ?? source?.apiKey ?? body?.key ?? body?.apiKey),
|
||||
error: errorForCli(body?.error),
|
||||
valuesPrinted: false,
|
||||
fullBodyAvailable: true
|
||||
});
|
||||
}
|
||||
|
||||
function apiKeysBodyForCli(body: any, parsed: ParsedArgs) {
|
||||
if (parsed.full === true) return redactUserQueryBody(body);
|
||||
const source = body?.data && typeof body.data === "object" ? body.data : body;
|
||||
const items = firstArray(source?.items, source?.apiKeys, source?.keys, body?.items, body?.apiKeys, body?.keys);
|
||||
return pruneUndefined({
|
||||
ok: body?.ok,
|
||||
status: text(source?.status ?? body?.status) || undefined,
|
||||
actor: actorForCli(source?.actor ?? body?.actor),
|
||||
authMethod: text(source?.authMethod ?? body?.authMethod) || undefined,
|
||||
items: items.map(apiKeyMetadataForCli).filter(Boolean),
|
||||
count: source?.count ?? body?.count ?? items.length,
|
||||
valuesPrinted: false,
|
||||
fullBodyAvailable: true
|
||||
});
|
||||
}
|
||||
|
||||
function billingSummaryBodyForCli(body: any, parsed: ParsedArgs) {
|
||||
if (parsed.full === true) return redactUserQueryBody(body);
|
||||
const source = body?.data && typeof body.data === "object" ? body.data : body;
|
||||
const ledgerItems = ledgerItemsFromBody(source);
|
||||
return pruneUndefined({
|
||||
ok: body?.ok,
|
||||
status: text(source?.status ?? body?.status) || undefined,
|
||||
actor: actorForCli(source?.actor ?? body?.actor),
|
||||
authMethod: text(source?.authMethod ?? body?.authMethod) || undefined,
|
||||
credits: creditsForCli(source?.credits ?? source?.balance ?? source?.wallet),
|
||||
balance: typeof source?.balance === "number" ? source.balance : undefined,
|
||||
available: source?.available ?? source?.availableCredits,
|
||||
reserved: source?.reserved ?? source?.reservedCredits,
|
||||
plan: compactApiBody(redactUserQueryBody(source?.plan ?? source?.subscription?.plan)),
|
||||
entitlement: compactApiBody(redactUserQueryBody(source?.entitlement ?? source?.entitlements)),
|
||||
usage: usageSummaryCompact(source?.usage),
|
||||
ledger: ledgerItems.length > 0 ? { count: source?.ledger?.count ?? source?.ledgerCount ?? ledgerItems.length, items: ledgerItems.slice(0, 20).map(ledgerEntryForCli) } : undefined,
|
||||
error: errorForCli(body?.error),
|
||||
valuesPrinted: false,
|
||||
fullBodyAvailable: true
|
||||
});
|
||||
}
|
||||
|
||||
function usageSummaryBodyForCli(body: any, parsed: ParsedArgs) {
|
||||
if (parsed.full === true) return redactUserQueryBody(body);
|
||||
const source = body?.data && typeof body.data === "object" ? body.data : body;
|
||||
const usageRecords = firstArray(source?.records, source?.usageRecords, source?.usage?.records);
|
||||
const recordCount = source?.recordCount ?? source?.usage?.recordCount ?? usageRecords.length;
|
||||
return pruneUndefined({
|
||||
ok: body?.ok,
|
||||
status: text(source?.status ?? body?.status) || undefined,
|
||||
actor: actorForCli(source?.actor ?? body?.actor),
|
||||
authMethod: text(source?.authMethod ?? body?.authMethod) || undefined,
|
||||
usage: usageSummaryCompact(source?.usage ?? source),
|
||||
byService: usageByServiceForCli(source?.byService ?? source?.usage?.byService),
|
||||
records: usageRecords.slice(0, 20).map(usageRecordForCli),
|
||||
recordCount: recordCount || undefined,
|
||||
error: errorForCli(body?.error),
|
||||
valuesPrinted: false,
|
||||
fullBodyAvailable: true
|
||||
});
|
||||
}
|
||||
|
||||
function actorForCli(value: any) {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
return pruneUndefined({ id: text(value.id), username: text(value.username), displayName: text(value.displayName), role: text(value.role), status: text(value.status), email: text(value.email) || undefined });
|
||||
}
|
||||
|
||||
function apiKeyMetadataForCli(value: any) {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
return pruneUndefined({
|
||||
id: text(value.id ?? value.keyId),
|
||||
keyId: text(value.keyId ?? value.id),
|
||||
name: text(value.name ?? value.keyName),
|
||||
prefix: text(value.prefix ?? value.keyPrefix),
|
||||
source: text(value.source ?? value.sourceKind),
|
||||
status: text(value.status),
|
||||
scopes: arrayOfText(value.scopes),
|
||||
createdAt: text(value.createdAt),
|
||||
updatedAt: text(value.updatedAt),
|
||||
lastUsedAt: text(value.lastUsedAt),
|
||||
expiresAt: text(value.expiresAt),
|
||||
fingerprint: text(value.fingerprint ?? value.keyFingerprint),
|
||||
valuesRedacted: value.valuesRedacted === false ? false : true
|
||||
});
|
||||
}
|
||||
|
||||
function ledgerItemsFromBody(source: any) {
|
||||
const ledger = source?.ledger;
|
||||
return firstArray(ledger?.items, ledger?.entries, ledger?.rows, source?.ledgerItems, source?.entries, source?.rows);
|
||||
}
|
||||
|
||||
function ledgerEntryForCli(value: any) {
|
||||
return pruneUndefined({
|
||||
id: text(value?.id ?? value?.ledgerId),
|
||||
kind: text(value?.kind ?? value?.type),
|
||||
reason: text(value?.reason),
|
||||
service: text(value?.service ?? value?.serviceId),
|
||||
resourceType: text(value?.resourceType),
|
||||
deltaCredits: value?.deltaCredits ?? value?.delta,
|
||||
beforeCredits: value?.beforeCredits ?? value?.before,
|
||||
afterCredits: value?.afterCredits ?? value?.after,
|
||||
traceId: text(value?.traceId ?? value?.metadata?.traceId),
|
||||
sessionId: text(value?.sessionId ?? value?.metadata?.sessionId),
|
||||
conversationId: text(value?.conversationId ?? value?.metadata?.conversationId),
|
||||
createdAt: text(value?.createdAt),
|
||||
valuesPrinted: false
|
||||
});
|
||||
}
|
||||
|
||||
function usageSummaryCompact(value: any) {
|
||||
if (!value || typeof value !== "object") return compactApiBody(redactUserQueryBody(value));
|
||||
return pruneUndefined({
|
||||
credits: value.credits,
|
||||
totalCredits: value.totalCredits,
|
||||
recordCount: value.recordCount,
|
||||
byService: usageByServiceForCli(value.byService),
|
||||
lastUsedAt: text(value.lastUsedAt),
|
||||
fullBodyAvailable: true
|
||||
});
|
||||
}
|
||||
|
||||
function creditsForCli(value: any) {
|
||||
if (!value || typeof value !== "object") return value;
|
||||
return pruneUndefined({
|
||||
balance: value.balance ?? value.balanceCredits,
|
||||
available: value.available ?? value.availableCredits,
|
||||
reserved: value.reserved ?? value.reservedCredits,
|
||||
total: value.total ?? value.totalCredits,
|
||||
currency: text(value.currency),
|
||||
fullBodyAvailable: true
|
||||
});
|
||||
}
|
||||
|
||||
function usageByServiceForCli(value: any) {
|
||||
if (Array.isArray(value)) return value.slice(0, 20).map(usageServiceForCli);
|
||||
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, usageServiceForCli(item)]));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function usageServiceForCli(value: any) {
|
||||
if (!value || typeof value !== "object") return value;
|
||||
return pruneUndefined({
|
||||
service: text(value.service ?? value.serviceId),
|
||||
serviceId: text(value.serviceId ?? value.service),
|
||||
resourceType: text(value.resourceType),
|
||||
credits: value.credits ?? value.totalCredits,
|
||||
recordCount: value.recordCount,
|
||||
lastUsedAt: text(value.lastUsedAt),
|
||||
fullBodyAvailable: true
|
||||
});
|
||||
}
|
||||
|
||||
function errorForCli(value: any) {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
return pruneUndefined({
|
||||
code: text(value.code),
|
||||
status: text(value.status),
|
||||
message: text(value.message),
|
||||
details: value.details ? compactApiBody(redactUserQueryBody(value.details)) : undefined,
|
||||
fullBodyAvailable: true
|
||||
});
|
||||
}
|
||||
|
||||
function selfServiceAvailability(response: any, subject: string) {
|
||||
if (responseSucceeded(response)) return { status: "available", subject };
|
||||
if (response.status === 403 || response.status === 404) {
|
||||
return pruneUndefined({ status: "not_available", subject, httpStatus: response.status, reason: text(response.body?.error?.code ?? response.body?.status) || "unsupported_or_forbidden", message: text(response.body?.error?.message) || undefined });
|
||||
}
|
||||
return pruneUndefined({ status: "failed", subject, httpStatus: response.status, reason: text(response.body?.error?.code ?? response.body?.status) || undefined, message: text(response.body?.error?.message) || undefined });
|
||||
}
|
||||
|
||||
function usageRecordForCli(value: any) {
|
||||
return pruneUndefined({
|
||||
id: text(value?.id ?? value?.usageId),
|
||||
service: text(value?.service ?? value?.serviceId),
|
||||
resourceType: text(value?.resourceType),
|
||||
credits: value?.credits ?? value?.deltaCredits,
|
||||
traceId: text(value?.traceId ?? value?.metadata?.traceId),
|
||||
sessionId: text(value?.sessionId ?? value?.metadata?.sessionId),
|
||||
conversationId: text(value?.conversationId ?? value?.metadata?.conversationId),
|
||||
status: text(value?.status),
|
||||
createdAt: text(value?.createdAt),
|
||||
valuesPrinted: false
|
||||
});
|
||||
}
|
||||
|
||||
function firstArray(...values: any[]) {
|
||||
for (const value of values) {
|
||||
if (Array.isArray(value)) return value;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function arrayOfText(value: any) {
|
||||
return Array.isArray(value) ? value.map(text).filter(Boolean) : undefined;
|
||||
}
|
||||
|
||||
function adminAccessBodyForCli(body: any, parsed: ParsedArgs) {
|
||||
if (parsed.full === true) return body;
|
||||
const access = body?.access && typeof body.access === "object" ? body.access : null;
|
||||
@@ -3111,11 +3471,25 @@ function redactSecretLike(value: any): any {
|
||||
return value;
|
||||
}
|
||||
|
||||
function redactUserQueryBody(value: any): any {
|
||||
if (Array.isArray(value)) return value.map(redactUserQueryBody);
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, userQuerySecretLikeField(key) ? "[redacted]" : redactUserQueryBody(item)]));
|
||||
}
|
||||
if (typeof value === "string" && (/^sk-[A-Za-z0-9_-]{10,}/u.test(value) || /^hwl_live_[A-Za-z0-9_-]{10,}/u.test(value))) return "[redacted]";
|
||||
return value;
|
||||
}
|
||||
|
||||
function secretLikeField(key: string) {
|
||||
const lower = key.toLowerCase();
|
||||
return ["apikey", "api_key", "authorization", "auth.json", "config.toml", "authjson", "configtoml"].includes(lower) || lower.includes("password") || lower.endsWith("token");
|
||||
}
|
||||
|
||||
function userQuerySecretLikeField(key: string) {
|
||||
const lower = key.toLowerCase();
|
||||
return secretLikeField(key) || ["key", "secret", "credential", "dsn", "database_url", "databaseurl"].includes(lower) || lower.includes("secret") || lower.includes("credential");
|
||||
}
|
||||
|
||||
function traceBodyForCli(body: any, parsed: ParsedArgs) {
|
||||
if (text(parsed.render) === "web") return webTraceRenderBody(body, parsed);
|
||||
return responseBodyForCli(body, parsed);
|
||||
|
||||
Reference in New Issue
Block a user