export const PLUGIN_SETTING_KEY_MAX_LENGTH = 128; export const PLUGIN_SETTING_VALUE_MAX_BYTES = 32 * 1024; export const PLUGIN_SETTING_VALUE_MAX_DEPTH = 8; export const PLUGIN_SETTING_VALUE_MAX_NODES = 2048; export const PLUGIN_SETTING_ARRAY_MAX_LENGTH = 512; export const PLUGIN_SETTING_OBJECT_MAX_KEYS = 256; const SAFE_PLUGIN_SETTING_KEY_PATTERN = /^[a-zA-Z0-9._:-]{1,128}$/u; const SECRET_LIKE_SETTING_KEY_PATTERN = /(?:secret|token|password|credential|authorization|auth[-_.:]|[-_.:]auth(?:$|[-_.:])|bearer|cookie|session|private[-_.:]?key|client[-_.:]?secret|webhook|apikey|api[-_.:]?key)/iu; const RESERVED_PLUGIN_SETTING_KEYS = new Set(["__proto__", "constructor", "prototype"]); export type PluginSettingValue = null | string | number | boolean | PluginSettingValue[] | { readonly [key: string]: PluginSettingValue }; export function isPluginSettingKeyFormatSafe(value: unknown): value is string { return typeof value === "string" && SAFE_PLUGIN_SETTING_KEY_PATTERN.test(value); } export function isPluginSettingKeyReserved(value: string): boolean { return RESERVED_PLUGIN_SETTING_KEYS.has(value); } export function isPluginSettingKeySecretLike(value: string): boolean { return SECRET_LIKE_SETTING_KEY_PATTERN.test(value); } export function isSafePluginSettingKey(value: unknown): value is string { return isPluginSettingKeyFormatSafe(value) && !isPluginSettingKeyReserved(value) && !isPluginSettingKeySecretLike(value); } export function isPluginSettingValue(value: unknown): value is PluginSettingValue { return isPluginSettingValueShapeSafe(value) && isPluginSettingValueWithinSize(value); } export function isPluginSettingValueShapeSafe( value: unknown, depth = 0, budget: { nodes: number; seen: WeakSet } = { nodes: 0, seen: new WeakSet() } ): value is PluginSettingValue { budget.nodes += 1; if (depth > PLUGIN_SETTING_VALUE_MAX_DEPTH || budget.nodes > PLUGIN_SETTING_VALUE_MAX_NODES) return false; if (value === null) return true; if (typeof value === "string" || typeof value === "boolean") return true; if (typeof value === "number") return Number.isFinite(value); if (typeof value === "object" && value !== null) { if (budget.seen.has(value)) return false; budget.seen.add(value); } if (Array.isArray(value)) { if (value.length > PLUGIN_SETTING_ARRAY_MAX_LENGTH) return false; return value.every((item) => isPluginSettingValueShapeSafe(item, depth + 1, budget)); } if (!isPlainPluginSettingsObject(value)) return false; const entries = Object.entries(value); if (entries.length > PLUGIN_SETTING_OBJECT_MAX_KEYS) return false; return entries.every( ([key, nested]) => !isPluginSettingKeyReserved(key) && !isPluginSettingKeySecretLike(key) && isPluginSettingValueShapeSafe(nested, depth + 1, budget) ); } export function isPluginSettingValueWithinSize(value: PluginSettingValue): boolean { return getUtf8ByteLength(JSON.stringify(value)) <= PLUGIN_SETTING_VALUE_MAX_BYTES; } function isPlainPluginSettingsObject(value: unknown): value is Record { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } function getUtf8ByteLength(value: string): number { return new TextEncoder().encode(value).length; }