import type { ConfigUiHint, ConfigUiHints } from "../api/types.ts"; // Control UI view renders config form.shared screen content. import { normalizeLowercaseStringOrEmpty } from "../lib/string-coerce.ts"; export type JsonSchema = { type?: string | string[]; title?: string; description?: string; tags?: string[]; "x-tags"?: string[]; properties?: Record; items?: JsonSchema | JsonSchema[]; additionalProperties?: JsonSchema | boolean; enum?: unknown[]; const?: unknown; default?: unknown; minLength?: number; maxLength?: number; anyOf?: JsonSchema[]; oneOf?: JsonSchema[]; allOf?: JsonSchema[]; nullable?: boolean; }; export function schemaType(schema: JsonSchema): string | undefined { if (!schema) { return undefined; } if (Array.isArray(schema.type)) { return schema.type.find((t) => t !== "null") ?? schema.type[0]; } return schema.type; } export function defaultValue(schema?: JsonSchema): unknown { if (!schema) { return ""; } if (schema.default !== undefined) { return schema.default; } const type = schemaType(schema); switch (type) { case "object": return {}; case "array": return []; case "boolean": return false; case "number": case "integer": return 0; case "string": return ""; default: return ""; } } export function pathKey(path: Array): string { return path.filter((segment) => typeof segment === "string").join("."); } export function hintForPath(path: Array, hints: ConfigUiHints) { const key = pathKey(path); const direct = hints[key]; if (direct) { return direct; } const segments = path.map(String); for (const [hintKey, hint] of Object.entries(hints)) { if (!hintKey.includes("*")) { continue; } const hintSegments = hintKey.split("."); if (hintSegments.length !== segments.length) { continue; } let match = true; for (let i = 0; i < segments.length; i += 1) { if (hintSegments[i] !== "*" && hintSegments[i] !== segments[i]) { match = false; break; } } if (match) { return hint; } } return undefined; } export function humanize(raw: string) { return raw .replace(/_/g, " ") .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/\s+/g, " ") .replace(/^./, (m) => m.toUpperCase()); } const SENSITIVE_KEY_WHITELIST_SUFFIXES = [ "maxtokens", "maxoutputtokens", "maxinputtokens", "maxcompletiontokens", "contexttokens", "totaltokens", "tokencount", "tokenlimit", "tokenbudget", "passwordfile", ] as const; const SENSITIVE_PATTERNS = [ /token$/i, /password/i, /secret/i, /api.?key/i, /serviceaccount(?:ref)?$/i, ]; const ENV_VAR_PLACEHOLDER_PATTERN = /^\$\{[^}]*\}$/; export const REDACTED_PLACEHOLDER = "[redacted - click reveal to view]"; const MAX_SENSITIVE_SCAN_DEPTH = 64; const MAX_SENSITIVE_SCAN_NODES = 20_000; type SensitiveScanState = { visited: number; }; function createSensitiveScanState(): SensitiveScanState { return { visited: 0 }; } function enterSensitiveScanNode(state: SensitiveScanState, depth: number): boolean { if (depth > MAX_SENSITIVE_SCAN_DEPTH) { return false; } state.visited += 1; if (state.visited > MAX_SENSITIVE_SCAN_NODES) { return false; } return true; } function isEnvVarPlaceholder(value: string): boolean { return ENV_VAR_PLACEHOLDER_PATTERN.test(value.trim()); } export function isSensitiveConfigPath(path: string): boolean { const lowerPath = normalizeLowercaseStringOrEmpty(path); const whitelisted = SENSITIVE_KEY_WHITELIST_SUFFIXES.some((suffix) => lowerPath.endsWith(suffix)); return !whitelisted && SENSITIVE_PATTERNS.some((pattern) => pattern.test(path)); } function isSensitiveLeafValue(value: unknown): boolean { if (typeof value === "string") { return value.trim().length > 0 && !isEnvVarPlaceholder(value); } return value !== undefined && value !== null; } function isHintSensitive(hint: ConfigUiHint | undefined): boolean { return hint?.sensitive ?? false; } export function hasSensitiveConfigData( value: unknown, path: Array, hints: ConfigUiHints, ): boolean { return hasSensitiveConfigDataInner(value, path, hints, createSensitiveScanState(), 0); } function hasSensitiveConfigDataInner( value: unknown, path: Array, hints: ConfigUiHints, scan: SensitiveScanState, depth: number, ): boolean { if (!enterSensitiveScanNode(scan, depth)) { return true; } const key = pathKey(path); const hint = hintForPath(path, hints); const pathIsSensitive = isHintSensitive(hint) || isSensitiveConfigPath(key); if (pathIsSensitive && isSensitiveLeafValue(value)) { return true; } if (Array.isArray(value)) { return value.some((item, index) => hasSensitiveConfigDataInner(item, [...path, index], hints, scan, depth + 1), ); } if (value && typeof value === "object") { return Object.entries(value as Record).some(([childKey, childValue]) => hasSensitiveConfigDataInner(childValue, [...path, childKey], hints, scan, depth + 1), ); } return false; } export function countSensitiveConfigValues( value: unknown, path: Array, hints: ConfigUiHints, ): number { return countSensitiveConfigValuesInner(value, path, hints, createSensitiveScanState(), 0); } function countSensitiveConfigValuesInner( value: unknown, path: Array, hints: ConfigUiHints, scan: SensitiveScanState, depth: number, ): number { if (!enterSensitiveScanNode(scan, depth)) { return 1; } if (value == null) { return 0; } const key = pathKey(path); const hint = hintForPath(path, hints); const pathIsSensitive = isHintSensitive(hint) || isSensitiveConfigPath(key); if (pathIsSensitive && isSensitiveLeafValue(value)) { return 1; } if (Array.isArray(value)) { return value.reduce( (count, item, index) => count + countSensitiveConfigValuesInner(item, [...path, index], hints, scan, depth + 1), 0, ); } if (value && typeof value === "object") { return Object.entries(value as Record).reduce( (count, [childKey, childValue]) => count + countSensitiveConfigValuesInner(childValue, [...path, childKey], hints, scan, depth + 1), 0, ); } return 0; }