openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
316 lines (315 loc) • 13.4 kB
JavaScript
import { i as asOptionalObjectRecord } from "./record-coerce-DItp3I4t.js";
import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { t as isBlockedObjectKey } from "./prototype-keys-CuYw53fZ.js";
import { t as parseConfigPathArrayIndex } from "./path-array-index-CvEcUJa-.js";
import { r as normalizeJsonSchemaForTypeBox } from "./json-schema-DhpWleXB.js";
import { n as sanitizeTerminalText } from "./safe-text-BGBqp1a4.js";
import { n as findJsonSchemaShapeError, t as applyJsonSchemaDefaults } from "./json-schema-defaults-CEZYvzdq.js";
import { t as PluginLruCache } from "./plugin-cache-primitives-8mVpZ8Xc.js";
import { Compile } from "typebox/schema";
import { Format } from "typebox/format";
//#region src/config/allowed-values.ts
const MAX_ALLOWED_VALUES_HINT = 12;
const MAX_ALLOWED_VALUE_CHARS = 160;
function truncateHintText(text, limit) {
if (text.length <= limit) return text;
const truncated = truncateUtf16Safe(text, limit);
return `${truncated}... (+${text.length - truncated.length} chars)`;
}
function safeStringify(value) {
if (value === void 0) return "";
try {
const serialized = JSON.stringify(value);
if (serialized !== void 0) return serialized;
} catch {}
return String(value);
}
function toAllowedValueLabel(value) {
if (typeof value === "string") return JSON.stringify(truncateHintText(value, MAX_ALLOWED_VALUE_CHARS));
return truncateHintText(safeStringify(value), MAX_ALLOWED_VALUE_CHARS);
}
function toAllowedValueValue(value) {
return typeof value === "string" ? value : safeStringify(value);
}
function toAllowedValueDedupKey(value) {
return `${value === null ? "null" : typeof value}:${toAllowedValueValue(value)}`;
}
/** Summarizes enum/allowed-value candidates for compact validation error hints. */
function summarizeAllowedValues(values) {
if (values.length === 0) return null;
const deduped = [];
const seenValues = /* @__PURE__ */ new Set();
for (const item of values) {
const dedupeKey = toAllowedValueDedupKey(item);
if (seenValues.has(dedupeKey)) continue;
seenValues.add(dedupeKey);
deduped.push({
value: toAllowedValueValue(item),
label: toAllowedValueLabel(item)
});
}
const shown = deduped.slice(0, MAX_ALLOWED_VALUES_HINT);
const hiddenCount = deduped.length - shown.length;
const formattedCore = shown.map((entry) => entry.label).join(", ");
const formatted = hiddenCount > 0 ? `${formattedCore}, ... (+${hiddenCount} more)` : formattedCore;
return {
values: shown.map((entry) => entry.value),
hiddenCount,
formatted
};
}
/** Appends an allowed-values hint unless the validation message already includes one. */
function appendAllowedValuesHint(message, summary) {
const lower = normalizeLowercaseStringOrEmpty(message);
if (lower.includes("(allowed:") || lower.includes("expected one of")) return message;
return `${message} (allowed: ${summary.formatted})`;
}
//#endregion
//#region src/plugins/schema-validator.ts
const schemaCache = new PluginLruCache(512);
const annotationOnlyFormats = [
"date-time",
"date",
"duration",
"email",
"hostname",
"idn-email",
"idn-hostname",
"ipv4",
"ipv6",
"iri-reference",
"iri",
"json-pointer-uri-fragment",
"json-pointer",
"regex",
"relative-json-pointer",
"time",
"uri-reference",
"uri-template",
"url",
"uuid"
];
function fingerprintSchema(schema) {
return JSON.stringify(schema);
}
function schemaHasDefaults(schema) {
if (!schema || typeof schema !== "object") return false;
if (Array.isArray(schema)) return schema.some((item) => schemaHasDefaults(item));
const record = schema;
if (Object.hasOwn(record, "default")) return true;
return Object.values(record).some((value) => schemaHasDefaults(value));
}
function applyValidatedSourceDefaults(value, source, defaulted) {
if (source === void 0) return value === void 0 ? defaulted : value;
const target = asOptionalObjectRecord(value);
const original = asOptionalObjectRecord(source);
const hydrated = asOptionalObjectRecord(defaulted);
if (target && original && hydrated) {
for (const [key, entry] of Object.entries(hydrated)) if (!isBlockedObjectKey(key) && (Object.hasOwn(target, key) || !Object.hasOwn(original, key))) target[key] = applyValidatedSourceDefaults(target[key], original[key], entry);
}
return value;
}
function compileSchema(schema) {
return Compile(normalizeJsonSchemaForTypeBox(schema));
}
function relaxConditionalRequiredKeywords(schema, insideConditionalBranch = false) {
if (Array.isArray(schema)) return schema.map((entry) => relaxConditionalRequiredKeywords(entry, insideConditionalBranch));
if (!schema || typeof schema !== "object") return schema;
return Object.fromEntries(Object.entries(schema).filter(([key]) => !(insideConditionalBranch && key === "required")).map(([key, value]) => [key, typeof value === "boolean" || value && typeof value === "object" ? relaxConditionalRequiredKeywords(value, insideConditionalBranch || key === "then" || key === "else") : value]));
}
function withPluginFormatSemantics(callback) {
const previousFormats = Format.Entries();
Format.Set("uri", (value) => URL.canParse(value));
for (const format of annotationOnlyFormats) Format.Set(format, () => true);
try {
return callback();
} finally {
Format.Clear();
for (const [format, check] of previousFormats) Format.Set(format, check);
}
}
function checkSchemaWithCurrentFormats(validate, value) {
if (validate.Check(value)) return null;
return validate.Errors(value)[1];
}
function isDefaultActivatedConditionalFailure(params) {
if (checkSchemaWithCurrentFormats(compileSchema(relaxConditionalRequiredKeywords(params.schema)), params.defaultedValue)) return false;
return checkSchemaWithCurrentFormats(compileSchema(params.schema), params.originalValue) === null;
}
function parseJsonSchemaIssuePath(path) {
if (!path || path === "<root>") return [];
return path.split(".").map((segment) => parseConfigPathArrayIndex(segment) ?? segment);
}
function normalizeErrorPath(instancePath) {
const path = instancePath?.replace(/^\//, "").replace(/\//g, ".");
return path && path.length > 0 ? path : "<root>";
}
function appendPathSegment(path, segment) {
const trimmed = segment.trim();
if (!trimmed) return path;
if (path === "<root>") return trimmed;
return `${path}.${trimmed}`;
}
function firstStringParam(value) {
if (typeof value === "string" && value.trim()) return value;
if (Array.isArray(value)) return value.find((entry) => typeof entry === "string" && entry.trim().length > 0) ?? null;
return null;
}
function resolveMissingProperties(error) {
const properties = error.keyword === "required" ? error.params?.requiredProperties : error.keyword === "dependentRequired" || error.keyword === "dependencies" ? error.params?.dependencies : void 0;
if (!Array.isArray(properties) || error.keyword !== "required" && properties.length !== 1) return [];
return properties.filter((property) => typeof property === "string");
}
function extractAllowedValues(error) {
if (error.keyword === "enum") {
const allowedValues = error.params?.allowedValues;
return Array.isArray(allowedValues) ? allowedValues : null;
}
if (error.keyword === "const") {
const params = error.params;
if (!params || !Object.hasOwn(params, "allowedValue")) return null;
return [params.allowedValue];
}
return null;
}
function getAllowedValuesSummary(error) {
const allowedValues = extractAllowedValues(error);
if (!allowedValues) return null;
return summarizeAllowedValues(allowedValues);
}
function resolveAdditionalProperty(error) {
if (error.keyword !== "additionalProperties") return;
return firstStringParam(error.params?.additionalProperty) ?? void 0;
}
function resolveAdditionalProperties(error) {
if (error.keyword !== "additionalProperties") return [];
const additionalProperties = error.params?.additionalProperties;
if (Array.isArray(additionalProperties)) return additionalProperties.filter((entry) => typeof entry === "string");
const additionalProperty = error.params?.additionalProperty;
return typeof additionalProperty === "string" ? [additionalProperty] : [];
}
function formatAdditionalPropertiesMessage(error) {
const additionalProperties = resolveAdditionalProperties(error);
if (additionalProperties.length === 0) return null;
return `must not have additional properties: ${additionalProperties.map((entry) => `"${entry}"`).join(", ")}`;
}
function formatValidationErrors(errors) {
if (!errors || errors.length === 0) return [{
path: "<root>",
message: "invalid config",
text: "<root>: invalid config"
}];
const seenDependencyConditions = /* @__PURE__ */ new Set();
return errors.flatMap((error) => {
if (error.keyword === "dependentRequired" || error.keyword === "dependencies") {
const condition = JSON.stringify([
error.keyword,
error.schemaPath,
error.instancePath,
error.params?.property,
error.params?.dependencies
]);
if (seenDependencyConditions.has(condition)) return [];
seenDependencyConditions.add(condition);
}
const missingProperties = resolveMissingProperties(error);
const allowedValuesSummary = getAllowedValuesSummary(error);
const additionalProperty = resolveAdditionalProperty(error);
return (missingProperties.length ? missingProperties : [void 0]).map((missingProperty) => {
const basePath = normalizeErrorPath(error.instancePath);
const path = missingProperty === void 0 ? basePath : appendPathSegment(basePath, missingProperty);
const baseMessage = missingProperty === void 0 ? formatAdditionalPropertiesMessage(error) ?? error.message ?? "invalid" : `must have required property '${missingProperty}'`;
const message = allowedValuesSummary ? appendAllowedValuesHint(baseMessage, allowedValuesSummary) : baseMessage;
const formattedError = {
path,
message,
text: `${sanitizeTerminalText(path)}: ${sanitizeTerminalText(message)}`
};
if (additionalProperty) formattedError.additionalProperty = additionalProperty;
if (allowedValuesSummary) {
formattedError.allowedValues = allowedValuesSummary.values;
formattedError.allowedValuesHiddenCount = allowedValuesSummary.hiddenCount;
}
return formattedError;
});
});
}
/**
* Validate a value against a schema supplied by a plugin manifest.
* External manifest schemas are third-party input, so every failure mode becomes
* a validation result. Bundled schemas stay on the throwing path because a malformed
* repository-owned schema is a programming error and must stay loud.
*/
function validatePluginSchemaValue(params) {
const { origin, ...validationParams } = params;
if (origin === "bundled") {
const result = validateJsonSchemaValue(validationParams);
return result.ok ? result : {
...result,
schemaError: false
};
}
try {
const result = validateJsonSchemaValue(validationParams);
return result.ok ? result : {
...result,
schemaError: false
};
} catch (error) {
const text = sanitizeTerminalText(error instanceof Error ? error.message : String(error));
return {
ok: false,
errors: [{
path: "<root>",
message: text,
text
}],
schemaError: true
};
}
}
/**
* Validate a plugin-owned value against a JSON Schema, optionally hydrating schema defaults.
* The cache key is caller-owned so repeated plugin/schema validations can reuse compiled TypeBox validators.
*/
function validateJsonSchemaValue(params) {
const schemaError = findJsonSchemaShapeError(params.schema);
if (schemaError) throw new Error(sanitizeTerminalText(`invalid schema: ${schemaError}`));
const cacheKey = params.applyDefaults ? `${params.cacheKey}::defaults` : params.cacheKey;
let cached = params.cache === false ? void 0 : schemaCache.get(cacheKey);
const schemaFingerprint = !cached || cached.schema !== params.schema ? fingerprintSchema(params.schema) : void 0;
if (!cached || cached.schema !== params.schema && cached.schemaFingerprint !== schemaFingerprint) {
const validate = compileSchema(params.schema);
cached = {
hasDefaults: params.applyDefaults ? schemaHasDefaults(params.schema) : false,
validate,
schema: params.schema,
schemaFingerprint: schemaFingerprint ?? fingerprintSchema(params.schema)
};
if (params.cache !== false) schemaCache.set(cacheKey, cached);
} else if (cached.schema !== params.schema) cached.schema = params.schema;
return withPluginFormatSemantics(() => {
const originalValue = params.sourceValue === void 0 ? params.value : params.sourceValue;
const value = params.applyDefaults && cached.hasDefaults ? applyJsonSchemaDefaults(params.schema, structuredClone(originalValue)) : originalValue;
const errors = checkSchemaWithCurrentFormats(cached.validate, value);
if (errors && !(params.applyDefaults && value !== originalValue && isDefaultActivatedConditionalFailure({
schema: params.schema,
originalValue,
defaultedValue: value
}))) return {
ok: false,
errors: formatValidationErrors(errors)
};
if (originalValue === params.value) return {
ok: true,
value
};
return {
ok: true,
value: value === originalValue ? params.value : applyValidatedSourceDefaults(structuredClone(params.value), originalValue, value)
};
});
}
//#endregion
export { summarizeAllowedValues as a, appendAllowedValuesHint as i, validateJsonSchemaValue as n, validatePluginSchemaValue as r, parseJsonSchemaIssuePath as t };