validation.ts
utils/settings/validation.ts
No strong subsystem tag
266
Lines
7951
Bytes
6
Exports
9
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 266 lines, 9 detected imports, and 6 detected exports.
Important relationships
Detected exports
FieldPathValidationErrorSettingsWithErrorsformatZodErrorvalidateSettingsFileContentfilterInvalidPermissionRules
Keywords
issuemessageexpectedunknownpathcodezodissuefileresultrule
Detected imports
src/services/mcp/types.jszod/v4../slowOperations.js../stringUtils.js./permissionValidation.js./schemaOutput.js./types.js./types.js./validationTips.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import type { ConfigScope } from 'src/services/mcp/types.js'
import type { ZodError, ZodIssue } from 'zod/v4'
import { jsonParse } from '../slowOperations.js'
import { plural } from '../stringUtils.js'
import { validatePermissionRule } from './permissionValidation.js'
import { generateSettingsJSONSchema } from './schemaOutput.js'
import type { SettingsJson } from './types.js'
import { SettingsSchema } from './types.js'
import { getValidationTip } from './validationTips.js'
/**
* Helper type guards for specific Zod v4 issue types
* In v4, issue types have different structures than v3
*/
function isInvalidTypeIssue(issue: ZodIssue): issue is ZodIssue & {
code: 'invalid_type'
expected: string
input: unknown
} {
return issue.code === 'invalid_type'
}
function isInvalidValueIssue(issue: ZodIssue): issue is ZodIssue & {
code: 'invalid_value'
values: unknown[]
input: unknown
} {
return issue.code === 'invalid_value'
}
function isUnrecognizedKeysIssue(
issue: ZodIssue,
): issue is ZodIssue & { code: 'unrecognized_keys'; keys: string[] } {
return issue.code === 'unrecognized_keys'
}
function isTooSmallIssue(issue: ZodIssue): issue is ZodIssue & {
code: 'too_small'
minimum: number | bigint
origin: string
} {
return issue.code === 'too_small'
}
/** Field path in dot notation (e.g., "permissions.defaultMode", "env.DEBUG") */
export type FieldPath = string
export type ValidationError = {
/** Relative file path */
file?: string
/** Field path in dot notation */
path: FieldPath
/** Human-readable error message */
message: string
/** Expected value or type */
expected?: string
/** The actual invalid value that was provided */
invalidValue?: unknown
/** Suggestion for fixing the error */
suggestion?: string
/** Link to relevant documentation */
docLink?: string
/** MCP-specific metadata - only present for MCP configuration errors */
mcpErrorMetadata?: {
/** Which configuration scope this error came from */
scope: ConfigScope
/** The server name if error is specific to a server */
serverName?: string
/** Severity of the error */
severity?: 'fatal' | 'warning'
}
}
export type SettingsWithErrors = {
settings: SettingsJson
errors: ValidationError[]
}
/**
* Format a Zod validation error into human-readable validation errors
*/
/**
* Get the type string for an unknown value (for error messages)
*/
function getReceivedType(value: unknown): string {
if (value === null) return 'null'
if (value === undefined) return 'undefined'
if (Array.isArray(value)) return 'array'
return typeof value
}
function extractReceivedFromMessage(msg: string): string | undefined {
const match = msg.match(/received (\w+)/)
return match ? match[1] : undefined
}
export function formatZodError(
error: ZodError,
filePath: string,
): ValidationError[] {
return error.issues.map((issue): ValidationError => {
const path = issue.path.map(String).join('.')
let message = issue.message
let expected: string | undefined
let enumValues: string[] | undefined
let expectedValue: string | undefined
let receivedValue: unknown
let invalidValue: unknown
if (isInvalidValueIssue(issue)) {
enumValues = issue.values.map(v => String(v))
expectedValue = enumValues.join(' | ')
receivedValue = undefined
invalidValue = undefined
} else if (isInvalidTypeIssue(issue)) {
expectedValue = issue.expected
const receivedType = extractReceivedFromMessage(issue.message)
receivedValue = receivedType ?? getReceivedType(issue.input)
invalidValue = receivedType ?? getReceivedType(issue.input)
} else if (isTooSmallIssue(issue)) {
expectedValue = String(issue.minimum)
} else if (issue.code === 'custom' && 'params' in issue) {
const params = issue.params as { received?: unknown }
receivedValue = params.received
invalidValue = receivedValue
}
const tip = getValidationTip({
path,
code: issue.code,
expected: expectedValue,
received: receivedValue,
enumValues,
message: issue.message,
value: receivedValue,
})
if (isInvalidValueIssue(issue)) {
expected = enumValues?.map(v => `"${v}"`).join(', ')
message = `Invalid value. Expected one of: ${expected}`
} else if (isInvalidTypeIssue(issue)) {
const receivedType =
extractReceivedFromMessage(issue.message) ??
getReceivedType(issue.input)
if (
issue.expected === 'object' &&
receivedType === 'null' &&
path === ''
) {
message = 'Invalid or malformed JSON'
} else {
message = `Expected ${issue.expected}, but received ${receivedType}`
}
} else if (isUnrecognizedKeysIssue(issue)) {
const keys = issue.keys.join(', ')
message = `Unrecognized ${plural(issue.keys.length, 'field')}: ${keys}`
} else if (isTooSmallIssue(issue)) {
message = `Number must be greater than or equal to ${issue.minimum}`
expected = String(issue.minimum)
}
return {
file: filePath,
path,
message,
expected,
invalidValue,
suggestion: tip?.suggestion,
docLink: tip?.docLink,
}
})
}
/**
* Validates that settings file content conforms to the SettingsSchema.
* This is used during file edits to ensure the resulting file is valid.
*/
export function validateSettingsFileContent(content: string):
| {
isValid: true
}
| {
isValid: false
error: string
fullSchema: string
} {
try {
// Parse the JSON first
const jsonData = jsonParse(content)
// Validate against SettingsSchema in strict mode
const result = SettingsSchema().strict().safeParse(jsonData)
if (result.success) {
return { isValid: true }
}
// Format the validation error in a helpful way
const errors = formatZodError(result.error, 'settings')
const errorMessage =
'Settings validation failed:\n' +
errors.map(err => `- ${err.path}: ${err.message}`).join('\n')
return {
isValid: false,
error: errorMessage,
fullSchema: generateSettingsJSONSchema(),
}
} catch (parseError) {
return {
isValid: false,
error: `Invalid JSON: ${parseError instanceof Error ? parseError.message : 'Unknown parsing error'}`,
fullSchema: generateSettingsJSONSchema(),
}
}
}
/**
* Filters invalid permission rules from raw parsed JSON data before schema validation.
* This prevents one bad rule from poisoning the entire settings file.
* Returns warnings for each filtered rule.
*/
export function filterInvalidPermissionRules(
data: unknown,
filePath: string,
): ValidationError[] {
if (!data || typeof data !== 'object') return []
const obj = data as Record<string, unknown>
if (!obj.permissions || typeof obj.permissions !== 'object') return []
const perms = obj.permissions as Record<string, unknown>
const warnings: ValidationError[] = []
for (const key of ['allow', 'deny', 'ask']) {
const rules = perms[key]
if (!Array.isArray(rules)) continue
perms[key] = rules.filter(rule => {
if (typeof rule !== 'string') {
warnings.push({
file: filePath,
path: `permissions.${key}`,
message: `Non-string value in ${key} array was removed`,
invalidValue: rule,
})
return false
}
const result = validatePermissionRule(rule)
if (!result.valid) {
let message = `Invalid permission rule "${rule}" was skipped`
if (result.error) message += `: ${result.error}`
if (result.suggestion) message += `. ${result.suggestion}`
warnings.push({
file: filePath,
path: `permissions.${key}`,
message,
invalidValue: rule,
})
return false
}
return true
})
}
return warnings
}