// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. actions module for scripts/src/platform-infra-sub2api-codex.ts. // Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:820-1316 for #903. import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import type { UniDeskConfig } from "../config"; import { rootPath } from "../config"; import type { RenderedCliResult } from "../output"; import { applyPk01CaddyBlock, prepareFrpcSecret, renderFrpcManifest, type PublicServiceExposure, type PublicServiceTarget } from "../platform-infra-public-service"; import { shortSha256Fingerprint } from "../platform-infra-ops-library"; import { codexPoolSentinelSummary, codexPoolSentinelRuntimeImage, readCodexPoolSentinelConfig, renderCodexPoolSentinelManifest, type CodexPoolSentinelConfig, type CodexPoolSentinelProfileSecret, } from "../platform-infra-sub2api-codex-sentinel"; import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets"; import { runSshCommandCapture, type SshCaptureResult } from "../ssh"; import type { RemoteCodexPoolMode } from "./remote"; import type { CodexPoolConfig, ConfirmOptions, DisclosureOptions, SentinelImageOptions, SentinelProbeOptions, SentinelReportOptions, SyncOptions, TraceOptions } from "./types"; import { desiredAccountNames } from "./accounts"; import { collectCodexProfiles, readCodexPoolConfig } from "./config"; import { codexPoolSentinelProbeConfigFingerprint, fingerprint } from "./config-utils"; import { apiKeyPreview, codexConsumerBaseUrl, fetchPoolApiKey, probePublicModels, validatePublicGatewayWithKey, writeLocalCodexConfig } from "./local-codex"; import { manualBindingSourcePlan, poolTarget, prepareTargetPublicExposureSecret, resolvedManualAccountProtections, secretMaterialSummary, sentinelProfileSecrets, targetPublicExposureApplyScript, targetPublicExposureSummary } from "./public-exposure"; import { codexPoolConfigSummary, compactProfile, compactSentinelProbeResult, redactProfile, renderSub2ApiTempUnschedulableCredentials } from "./redaction"; import { boolField, capture, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote"; import { cleanupProbesScript, sentinelImageBuildScript, sentinelImageStatusScript, sentinelProbeScript, sentinelReportScript, syncScript, traceScript, validateScript } from "./remote-scripts"; import { codexPoolSyncSummary, codexPoolValidationSummary, renderSentinelReport, renderTraceReport, renderedCliResult } from "./render"; import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId, targetFlag } from "./runtime-target"; import { codexPoolConfigPath, sentinelImageDockerfilePath, serviceName, sub2apiConfigPath } from "./types"; export function codexPoolPlan(options?: DisclosureOptions): Record { const resolvedOptions = options ?? { full: false, raw: false, targetId: defaultCodexPoolRuntimeTargetId() }; const pool = readCodexPoolConfig(); const runtimeTarget = codexPoolRuntimeTarget(resolvedOptions.targetId); const profiles = collectCodexProfiles(); const ok = profiles.length > 0 && profiles.every((profile) => profile.ok); const consumerBaseUrl = runtimeTarget.publicBaseUrl === null ? null : codexConsumerBaseUrl(pool, runtimeTarget); return { ok, action: "platform-infra-sub2api-codex-pool-plan", source: { directory: join(homedir(), ".codex"), configPattern: "YAML-selected config files under ~/.codex", authPattern: "YAML-selected auth files under ~/.codex", valuesPrinted: false, }, target: poolTarget(pool, runtimeTarget), config: { path: codexPoolConfigPath, pool: options.full ? pool : codexPoolConfigSummary(pool, runtimeTarget), }, profiles: options.full ? profiles.map(redactProfile) : profiles.map(compactProfile), decision: { accountType: "openai/apikey", grouping: `All discovered Codex profiles are bound to one Sub2API group named ${pool.groupName}.`, unifiedApiKey: `The client-facing API_KEY is controlled by k3s Secret ${runtimeTarget.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}.`, sentinel: pool.sentinel.monitor.enabled ? `Account sentinel is enabled as k8s CronJob ${runtimeTarget.namespace}/${pool.sentinel.cronJobName}; actions.enabled=${pool.sentinel.actions.enabled}.` : "Account sentinel monitoring is disabled by YAML.", publicExposure: runtimeTarget.publicBaseUrl === null ? `Target-level public exposure is disabled or absent in ${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure.` : `Codex consumers for target ${runtimeTarget.id} use target-level public exposure ${consumerBaseUrl}.`, idempotency: "sync reuses the group, account names, and k3s Secret when they already exist; credentials are updated from the current local Codex files for YAML-managed profiles only; managed accounts missing from YAML are preserved unless --prune-removed is explicitly provided.", configPolicy: "UniDesk-owned durable configuration remains YAML-first; local ~/.codex files and runtime Secrets are not committed.", manualAccountProtection: pool.manualAccounts.protected.length === 0 ? "No manual Sub2API accounts are protected by YAML." : `${pool.manualAccounts.protected.length} manual Sub2API account(s) are protected from UniDesk-managed credentials, prune, sentinel probe, and sentinel freeze paths; only explicitly declared proxy/group bindings are reconciled.`, }, next: ok ? { sync: `bun scripts/cli.ts platform-infra sub2api codex-pool sync${targetFlag(runtimeTarget)} --confirm` } : { fix: "Ensure every discovered config.toml profile has a base_url and either auth.json OPENAI_API_KEY or the configured env_key present in this shell." }, }; } export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promise> { const pool = readCodexPoolConfig(); const runtimeTarget = codexPoolRuntimeTarget(options.targetId); const profiles = collectCodexProfiles(); const planOk = profiles.length > 0 && profiles.every((profile) => profile.ok); if (!options.confirm || !planOk) { return { ...codexPoolPlan(options), ok: !options.confirm ? planOk : false, mode: options.confirm ? "blocked-invalid-local-profile" : "dry-run", next: options.confirm ? { fix: "Repair invalid local Codex profiles, then rerun sync --confirm." } : { confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm" }, }; } const sentinelImage = pool.sentinel.monitor.enabled && runtimeTarget.sentinelEnabled ? await runCodexPoolSentinelImage(config, pool, { action: "build", confirm: true, dryRun: false, full: options.full, raw: false, targetId: options.targetId }) : { ok: true, mode: runtimeTarget.sentinelEnabled ? "skipped-monitor-disabled" : "skipped-target-disabled" }; if (sentinelImage.ok !== true) { return { ok: false, action: "platform-infra-sub2api-codex-pool-sync", mode: "blocked-sentinel-image", sentinelImage, next: { image: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build${targetFlag(runtimeTarget)} --confirm`, }, }; } const payload = { pruneRemoved: options.pruneRemoved, sentinel: { enabledForTarget: runtimeTarget.sentinelEnabled, manifest: runtimeTarget.sentinelEnabled ? renderCodexPoolSentinelManifest(pool.sentinel, sentinelProfileSecrets(profiles), { namespace: runtimeTarget.namespace, serviceName: runtimeTarget.serviceName, serviceDns: runtimeTarget.serviceDns, appSecretName: runtimeTarget.appSecretName, adminEmailDefault: pool.adminEmailDefault, proxy: runtimeTarget.egressProxy?.applyToSentinel ? { httpProxy: runtimeTarget.egressProxy.httpProxy, noProxy: runtimeTarget.egressProxy.noProxy, } : null, }) : null, summary: codexPoolSentinelSummary(pool.sentinel), }, pool: { groupName: pool.groupName, groupDescription: pool.groupDescription, apiKeyName: pool.apiKeyName, apiKeySecretName: pool.apiKeySecretName, apiKeySecretKey: pool.apiKeySecretKey, adminEmailDefault: pool.adminEmailDefault, minOwnerBalanceUsd: pool.minOwnerBalanceUsd, minOwnerConcurrency: pool.minOwnerConcurrency, defaultAccountPriority: pool.defaultAccountPriority, defaultAccountCapacity: pool.defaultAccountCapacity, defaultAccountLoadFactor: pool.defaultAccountLoadFactor, defaultSentinelProtect: pool.defaultSentinelProtect, }, manualAccounts: { bindingSources: pool.manualAccounts.bindingSources.items.map(manualBindingSourcePlan), protected: resolvedManualAccountProtections(pool, runtimeTarget), }, profiles: profiles.map((profile) => ({ profile: profile.profile, accountName: profile.accountName, configFile: profile.configFile, authFile: profile.authFile, provider: profile.provider, baseUrl: profile.baseUrl, wireApi: profile.wireApi, model: profile.model, apiKey: profile.apiKey, apiKeySource: profile.apiKeySource, apiKeyFingerprint: fingerprint(profile.apiKey ?? ""), sentinelProbeConfigFingerprint: codexPoolSentinelProbeConfigFingerprint({ accountName: profile.accountName, profile: profile.profile, baseUrl: profile.baseUrl, apiKeyFingerprint: fingerprint(profile.apiKey ?? ""), upstreamUserAgent: profile.upstreamUserAgent, openaiResponsesWebSocketsV2Mode: profile.openaiResponsesWebSocketsV2Mode, trustUpstream: profile.trustUpstream, sentinelProtect: profile.sentinelProtect, }), openaiResponsesWebSocketsV2Mode: profile.openaiResponsesWebSocketsV2Mode, upstreamUserAgent: profile.upstreamUserAgent, trustUpstream: profile.trustUpstream, sentinelProtect: profile.sentinelProtect, priority: profile.priority, capacity: profile.capacity, loadFactor: profile.loadFactor, tempUnschedulable: profile.tempUnschedulable, tempUnschedulableCredentials: renderSub2ApiTempUnschedulableCredentials(profile.tempUnschedulable), })), }; const result = await runRemoteCodexPoolScript(config, "sync", syncScript(payload, pool, runtimeTarget), runtimeTarget); const parsed = parseJsonOutput(result.stdout); if (options.raw) { return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-codex-pool-sync", remote: compactCapture(result, { full: true }), parsed, }; } return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-codex-pool-sync", local: { profileCount: profiles.length, pruneRemoved: options.pruneRemoved, invalidProfiles: profiles.filter((profile) => !profile.ok).map(compactProfile), profiles: options.full ? profiles.map(redactProfile) : undefined, valuesPrinted: false, }, sentinelImage, remote: parsed === null ? compactCapture(result, { full: options.full || result.exitCode !== 0 }) : options.full ? parsed : codexPoolSyncSummary(parsed), next: { validate: `bun scripts/cli.ts platform-infra sub2api codex-pool validate${targetFlag(runtimeTarget)}`, }, }; } export async function codexPoolSentinelImage(config: UniDeskConfig, options: SentinelImageOptions): Promise> { const pool = readCodexPoolConfig(); return await runCodexPoolSentinelImage(config, pool, options); } export async function runCodexPoolSentinelImage(config: UniDeskConfig, pool: CodexPoolConfig, options: SentinelImageOptions): Promise> { const runtimeTarget = codexPoolRuntimeTarget(options.targetId); const target = codexPoolSentinelRuntimeImage(pool.sentinel); if (!runtimeTarget.sentinelEnabled) { return { ok: true, action: "platform-infra-sub2api-codex-pool-sentinel-image", mode: "skipped-target-disabled", target: { id: runtimeTarget.id, namespace: runtimeTarget.namespace, }, image: target, mutation: false, valuesPrinted: false, }; } if (options.action === "build" && options.dryRun) { return { ok: true, action: "platform-infra-sub2api-codex-pool-sentinel-image", mode: "dry-run", image: target, dockerfile: sentinelImageDockerfilePath, mutation: false, next: { confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build${targetFlag(runtimeTarget)} --confirm`, }, }; } const mode: RemoteCodexPoolMode = options.action === "status" ? "sentinel-image-status" : "sentinel-image-build"; const script = options.action === "status" ? sentinelImageStatusScript(pool, runtimeTarget) : sentinelImageBuildScript(pool, runtimeTarget); const result = await runRemoteCodexPoolScript(config, mode, script, runtimeTarget); const parsed = parseJsonOutput(result.stdout); if (options.raw) { return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-codex-pool-sentinel-image", mode: options.action, image: target, remote: compactCapture(result, { full: true }), parsed, }; } return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-codex-pool-sentinel-image", mode: options.action, image: target, summary: parsed, remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }), }; } export async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptions): Promise> { const pool = readCodexPoolConfig(); const runtimeTarget = codexPoolRuntimeTarget(options.targetId); const result = await runRemoteCodexPoolScript(config, "validate", validateScript(pool, runtimeTarget), runtimeTarget); const parsed = parseJsonOutput(result.stdout); if (options.raw) { return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-codex-pool-validate", remote: compactCapture(result, { full: true }), parsed, }; } return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-codex-pool-validate", summary: options.full ? parsed : codexPoolValidationSummary(parsed), remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }), }; } export async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Promise | RenderedCliResult> { const pool = readCodexPoolConfig(); const runtimeTarget = codexPoolRuntimeTarget(options.targetId); const result = await capture(config, runtimeTarget.route, ["sh"], traceScript(pool, options, runtimeTarget)); const parsed = parseJsonOutput(result.stdout); const ok = result.exitCode === 0 && boolField(parsed, "ok", false); if (options.raw) { return { ok, action: "platform-infra-sub2api-codex-pool-trace", remote: compactCapture(result, { full: true }), trace: parsed, valuesPrinted: false, }; } const text = renderTraceReport(parsed, { requestId: options.requestId ?? "", showLines: options.showLines, remote: compactCapture(result, { full: result.exitCode !== 0 || parsed === null }), }); return renderedCliResult(ok, "platform-infra sub2api codex-pool trace", text); } export async function codexPoolSentinelReport(config: UniDeskConfig, options: SentinelReportOptions): Promise | RenderedCliResult> { const pool = readCodexPoolConfig(); const runtimeTarget = codexPoolRuntimeTarget(options.targetId); const result = await capture(config, runtimeTarget.route, ["sh"], sentinelReportScript(pool, options.events, runtimeTarget)); const parsed = parseJsonOutput(result.stdout); const ok = result.exitCode === 0 && boolField(parsed, "ok", false); if (options.raw) { return { ok, action: "platform-infra-sub2api-codex-pool-sentinel-report", remote: compactCapture(result, { full: true }), report: parsed, valuesPrinted: false, }; } const text = renderSentinelReport(parsed, { events: options.events, full: options.full, remote: compactCapture(result, { full: result.exitCode !== 0 || parsed === null }), }); return renderedCliResult(ok, "platform-infra sub2api codex-pool sentinel-report", text); } export async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelProbeOptions): Promise> { const pool = readCodexPoolConfig(); const runtimeTarget = codexPoolRuntimeTarget(options.targetId); const protectedNames = new Set(pool.manualAccounts.protected.map((account) => account.accountName.toLowerCase())); const protectedRequested = options.accounts.filter((account) => protectedNames.has(account.toLowerCase())); if (protectedRequested.length > 0) { return { ok: false, action: "platform-infra-sub2api-codex-pool-sentinel-probe", error: "account-protected-manual", protected: protectedRequested, message: "Protected manual Sub2API accounts are not YAML-managed and must not be probed by the UniDesk sentinel.", valuesPrinted: false, }; } const configuredAccounts = desiredAccountNames(pool); const missing = options.accounts.filter((account) => !configuredAccounts.includes(account)); if (missing.length > 0) { return { ok: false, action: "platform-infra-sub2api-codex-pool-sentinel-probe", error: "account-not-in-yaml", missing, configuredAccounts, valuesPrinted: false, }; } if (!options.confirm) { return { ok: true, action: "platform-infra-sub2api-codex-pool-sentinel-probe", mode: "dry-run", target: poolTarget(pool, runtimeTarget), accounts: options.accounts, effect: "Would create one Kubernetes Job from the managed sentinel CronJob and force an immediate marker probe for the requested account(s).", next: { confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe${targetFlag(runtimeTarget)} --account ${options.accounts.join(",")} --confirm`, }, valuesPrinted: false, }; } const payload = { accounts: options.accounts, }; const result = await runRemoteCodexPoolScript(config, "sentinel-probe", sentinelProbeScript(payload, pool, runtimeTarget), runtimeTarget); const parsed = parseJsonOutput(result.stdout); if (options.raw) { return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-codex-pool-sentinel-probe", remote: compactCapture(result, { full: true }), parsed, }; } return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-codex-pool-sentinel-probe", summary: options.full ? parsed : compactSentinelProbeResult(parsed), remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }), }; } export async function codexPoolCleanupProbes(config: UniDeskConfig, options: ConfirmOptions): Promise> { const runtimeTarget = codexPoolRuntimeTarget(options.targetId); if (!options.confirm) { return { ok: true, action: "platform-infra-sub2api-codex-pool-cleanup-probes", mode: "dry-run", target: poolTarget(readCodexPoolConfig(), runtimeTarget), scope: "Only deletes temporary resources whose names start with unidesk-probe-.", next: { confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes${targetFlag(runtimeTarget)} --confirm` }, valuesPrinted: false, }; } const pool = readCodexPoolConfig(); const result = await capture(config, runtimeTarget.route, ["sh"], cleanupProbesScript(pool, runtimeTarget)); const parsed = parseJsonOutput(result.stdout); if (options.raw) { return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-codex-pool-cleanup-probes", remote: compactCapture(result, { full: true }), parsed, }; } return { ok: result.exitCode === 0 && boolField(parsed, "ok", false), action: "platform-infra-sub2api-codex-pool-cleanup-probes", summary: parsed, remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }), }; } export async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions): Promise> { const pool = readCodexPoolConfig(); const runtimeTarget = codexPoolRuntimeTarget(options.targetId); if (runtimeTarget.publicExposure === null || !runtimeTarget.publicExposure.enabled) { return { ok: true, action: "platform-infra-sub2api-codex-pool-expose", mode: "disabled-by-yaml", target: poolTarget(pool, runtimeTarget), source: `${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure`, }; } if (!options.confirm) { return { ok: true, action: "platform-infra-sub2api-codex-pool-expose", mode: "dry-run", target: poolTarget(pool, runtimeTarget), publicExposure: targetPublicExposureSummary(runtimeTarget), next: { confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool expose${targetFlag(runtimeTarget)} --confirm`, }, }; } const secretMaterial = prepareTargetPublicExposureSecret(runtimeTarget); const caddyResult = await applyPk01CaddyBlock(config, serviceName, runtimeTarget.publicExposure); const remoteResult = await capture(config, runtimeTarget.route, ["sh"], targetPublicExposureApplyScript(runtimeTarget, secretMaterial)); const parsed = parseJsonOutput(remoteResult.stdout); const publicProbe = await probePublicModels(pool, "without-api-key", undefined, runtimeTarget); const ok = caddyResult.ok === true && remoteResult.exitCode === 0 && boolField(parsed, "ok", false) && publicProbe.httpStatus === 401; if (options.raw) { return { ok, action: "platform-infra-sub2api-codex-pool-expose", frpcSecret: secretMaterialSummary(secretMaterial), pk01Caddy: caddyResult, remote: compactCapture(remoteResult, { full: true }), parsed, publicProbe, }; } return { ok, action: "platform-infra-sub2api-codex-pool-expose", mode: "confirmed", publicExposure: targetPublicExposureSummary(runtimeTarget), frpcSecret: secretMaterialSummary(secretMaterial), pk01Caddy: caddyResult, remote: parsed ?? compactCapture(remoteResult, { full: options.full || remoteResult.exitCode !== 0 }), publicProbe, next: { configureLocal: `bun scripts/cli.ts platform-infra sub2api codex-pool configure-local${targetFlag(runtimeTarget)} --confirm`, validate: `bun scripts/cli.ts platform-infra sub2api codex-pool validate${targetFlag(runtimeTarget)}`, }, }; } export async function codexPoolConfigureLocal(config: UniDeskConfig, options: ConfirmOptions): Promise> { const pool = readCodexPoolConfig(); const runtimeTarget = codexPoolRuntimeTarget(options.targetId); const consumerExposureAvailable = runtimeTarget.publicBaseUrl !== null; const codexDir = join(homedir(), ".codex"); const configPath = join(codexDir, "config.toml"); const authPath = join(codexDir, "auth.json"); const backupConfigPath = join(codexDir, `config.toml.${pool.localCodex.backupSuffix}`); const backupAuthPath = join(codexDir, `auth.json.${pool.localCodex.backupSuffix}`); if (!options.confirm) { return { ok: true, action: "platform-infra-sub2api-codex-pool-configure-local", mode: "dry-run", target: { codexDir, configPath, authPath, backupConfigPath, backupAuthPath, baseUrl: consumerExposureAvailable ? codexConsumerBaseUrl(pool, runtimeTarget) : null, providerName: pool.localCodex.providerName, wireApi: pool.localCodex.wireApi, modelContextWindow: pool.localCodex.modelContextWindow, modelAutoCompactTokenLimit: pool.localCodex.modelAutoCompactTokenLimit, supportsWebSockets: pool.localCodex.supportsWebSockets, responsesWebSocketsV2: pool.localCodex.responsesWebSocketsV2, responsesSmokeModel: pool.localCodex.responsesSmokeModel, valuesPrinted: false, }, next: { confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool configure-local${targetFlag(runtimeTarget)} --confirm`, }, }; } if (!consumerExposureAvailable) throw new Error(`${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure.enabled must be true; configure-local needs one consumer URL`); const keyResult = await fetchPoolApiKey(config, pool, runtimeTarget); if (keyResult.apiKey === null) { return { ok: false, action: "platform-infra-sub2api-codex-pool-configure-local", error: keyResult.error ?? "pool API key missing", valuesPrinted: false, }; } const writeResult = writeLocalCodexConfig(pool, keyResult.apiKey, runtimeTarget); const validateResult = await validatePublicGatewayWithKey(pool, keyResult.apiKey, runtimeTarget); return { ok: writeResult.ok && validateResult.ok, action: "platform-infra-sub2api-codex-pool-configure-local", mode: "confirmed", local: writeResult, validation: validateResult, apiKey: { secret: `${runtimeTarget.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`, keyPreview: apiKeyPreview(keyResult.apiKey), apiKeyFingerprint: fingerprint(keyResult.apiKey), valuesPrinted: false, }, }; }