@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
135 lines • 13.1 kB
JavaScript
/**
* Deep merge utility for objects
*
* Provides a pure, reusable deep merge function with optional security filtering.
*
* @module utils/deepMerge
*/
/**
* Check if a value is a plain object (not null, not array)
*/
function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
/**
* Validate DeepMergeOptions at runtime
*
* @param options - Options to validate
* @throws Error if options are malformed
*/
function validateOptions(options) {
if (options.skipProperties !== undefined) {
if (!Array.isArray(options.skipProperties)) {
throw new TypeError('DeepMergeOptions.skipProperties must be an array of strings');
}
for (const prop of options.skipProperties) {
if (typeof prop !== 'string') {
throw new TypeError(`DeepMergeOptions.skipProperties contains non-string value: ${typeof prop}`);
}
}
}
if (options.readOnlyFields !== undefined) {
if (!(options.readOnlyFields instanceof Set)) {
throw new TypeError('DeepMergeOptions.readOnlyFields must be a Set<string>');
}
}
}
/**
* Deep merge two objects
*
* Merge semantics:
* - Plain objects are merged recursively
* - Arrays replace entirely (not merged element-by-element)
* - Primitives replace
* - Dangerous/read-only properties are optionally skipped
*
* @param target - The target object to merge into
* @param source - The source object to merge from
* @param options - Optional merge behavior configuration
* @returns New merged object (does not mutate inputs)
*
* @example
* ```typescript
* // Basic merge
* const result = deepMerge({ a: 1 }, { b: 2 });
* // => { a: 1, b: 2 }
*
* // Nested merge
* const result = deepMerge(
* { settings: { theme: 'light', size: 10 } },
* { settings: { theme: 'dark' } }
* );
* // => { settings: { theme: 'dark', size: 10 } }
*
* // With security filtering
* const result = deepMerge(target, source, {
* skipProperties: ['__proto__', 'constructor'],
* readOnlyFields: new Set(['id', 'type'])
* });
*
* // With type preservation
* interface Config { theme: string; size: number; }
* const result = deepMerge<Config>(defaultConfig, userConfig);
* ```
*/
export function deepMerge(target, source, options) {
// Validate options at runtime if provided
if (options) {
validateOptions(options);
}
const result = { ...target };
const skipProperties = options?.skipProperties ?? [];
const readOnlyFields = options?.readOnlyFields;
for (const key of Object.keys(source)) {
// Skip dangerous properties
if (skipProperties.includes(key)) {
continue;
}
// Skip read-only fields
if (readOnlyFields?.has(key)) {
continue;
}
const sourceValue = source[key];
const targetValue = result[key];
// If both are plain objects, merge recursively
if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
result[key] = deepMerge(targetValue, sourceValue, options);
}
else {
// Arrays and primitives replace entirely
result[key] = sourceValue;
}
}
return result;
}
/**
* Default dangerous properties that should never be merged
* These can be used to prevent prototype pollution attacks
*/
export const DANGEROUS_PROPERTIES = [
'__proto__',
'constructor',
'prototype',
];
/**
* Create a deep merge function with preset options
*
* Useful for creating a configured merger that can be reused
*
* @param options - Preset options for all merges
* @returns A configured deep merge function
*
* @example
* ```typescript
* const secureMerge = createDeepMerge({
* skipProperties: DANGEROUS_PROPERTIES,
* readOnlyFields: new Set(['id', 'type'])
* });
*
* const result = secureMerge(target, source);
* ```
*/
export function createDeepMerge(options) {
return (target, source) => deepMerge(target, source, options);
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVlcE1lcmdlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL2RlZXBNZXJnZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFpQkg7O0dBRUc7QUFDSCxTQUFTLGFBQWEsQ0FBQyxLQUFjO0lBQ25DLE9BQU8sS0FBSyxLQUFLLElBQUksSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQzlFLENBQUM7QUFFRDs7Ozs7R0FLRztBQUNILFNBQVMsZUFBZSxDQUFDLE9BQXlCO0lBQ2hELElBQUksT0FBTyxDQUFDLGNBQWMsS0FBSyxTQUFTLEVBQUUsQ0FBQztRQUN6QyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsY0FBYyxDQUFDLEVBQUUsQ0FBQztZQUMzQyxNQUFNLElBQUksU0FBUyxDQUFDLDZEQUE2RCxDQUFDLENBQUM7UUFDckYsQ0FBQztRQUNELEtBQUssTUFBTSxJQUFJLElBQUksT0FBTyxDQUFDLGNBQWMsRUFBRSxDQUFDO1lBQzFDLElBQUksT0FBTyxJQUFJLEtBQUssUUFBUSxFQUFFLENBQUM7Z0JBQzdCLE1BQU0sSUFBSSxTQUFTLENBQUMsOERBQThELE9BQU8sSUFBSSxFQUFFLENBQUMsQ0FBQztZQUNuRyxDQUFDO1FBQ0gsQ0FBQztJQUNILENBQUM7SUFFRCxJQUFJLE9BQU8sQ0FBQyxjQUFjLEtBQUssU0FBUyxFQUFFLENBQUM7UUFDekMsSUFBSSxDQUFDLENBQUMsT0FBTyxDQUFDLGNBQWMsWUFBWSxHQUFHLENBQUMsRUFBRSxDQUFDO1lBQzdDLE1BQU0sSUFBSSxTQUFTLENBQUMsdURBQXVELENBQUMsQ0FBQztRQUMvRSxDQUFDO0lBQ0gsQ0FBQztBQUNILENBQUM7QUFFRDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFDRztBQUNILE1BQU0sVUFBVSxTQUFTLENBQ3ZCLE1BQVMsRUFDVCxNQUE0QyxFQUM1QyxPQUEwQjtJQUUxQiwwQ0FBMEM7SUFDMUMsSUFBSSxPQUFPLEVBQUUsQ0FBQztRQUNaLGVBQWUsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUMzQixDQUFDO0lBRUQsTUFBTSxNQUFNLEdBQUcsRUFBRSxHQUFHLE1BQU0sRUFBNkIsQ0FBQztJQUN4RCxNQUFNLGNBQWMsR0FBRyxPQUFPLEVBQUUsY0FBYyxJQUFJLEVBQUUsQ0FBQztJQUNyRCxNQUFNLGNBQWMsR0FBRyxPQUFPLEVBQUUsY0FBYyxDQUFDO0lBRS9DLEtBQUssTUFBTSxHQUFHLElBQUksTUFBTSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQ3RDLDRCQUE0QjtRQUM1QixJQUFJLGNBQWMsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztZQUNqQyxTQUFTO1FBQ1gsQ0FBQztRQUVELHdCQUF3QjtRQUN4QixJQUFJLGNBQWMsRUFBRSxHQUFHLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztZQUM3QixTQUFTO1FBQ1gsQ0FBQztRQUVELE1BQU0sV0FBVyxHQUFJLE1BQWtDLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDN0QsTUFBTSxXQUFXLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBRWhDLCtDQUErQztRQUMvQyxJQUFJLGFBQWEsQ0FBQyxXQUFXLENBQUMsSUFBSSxhQUFhLENBQUMsV0FBVyxDQUFDLEVBQUUsQ0FBQztZQUM3RCxNQUFNLENBQUMsR0FBRyxDQUFDLEdBQUcsU0FBUyxDQUFDLFdBQVcsRUFBRSxXQUFXLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFDN0QsQ0FBQzthQUFNLENBQUM7WUFDTix5Q0FBeUM7WUFDekMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxHQUFHLFdBQVcsQ0FBQztRQUM1QixDQUFDO0lBQ0gsQ0FBQztJQUVELE9BQU8sTUFBVyxDQUFDO0FBQ3JCLENBQUM7QUFFRDs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxvQkFBb0IsR0FBRztJQUNsQyxXQUFXO0lBQ1gsYUFBYTtJQUNiLFdBQVc7Q0FDWixDQUFDO0FBRUY7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBaUJHO0FBQ0gsTUFBTSxVQUFVLGVBQWUsQ0FBQyxPQUF5QjtJQUN2RCxPQUFPLENBQUMsTUFBK0IsRUFBRSxNQUErQixFQUFFLEVBQUUsQ0FDMUUsU0FBUyxDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUM7QUFDdkMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogRGVlcCBtZXJnZSB1dGlsaXR5IGZvciBvYmplY3RzXG4gKlxuICogUHJvdmlkZXMgYSBwdXJlLCByZXVzYWJsZSBkZWVwIG1lcmdlIGZ1bmN0aW9uIHdpdGggb3B0aW9uYWwgc2VjdXJpdHkgZmlsdGVyaW5nLlxuICpcbiAqIEBtb2R1bGUgdXRpbHMvZGVlcE1lcmdlXG4gKi9cblxuLyoqXG4gKiBPcHRpb25zIGZvciBkZWVwIG1lcmdlIGJlaGF2aW9yXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgRGVlcE1lcmdlT3B0aW9ucyB7XG4gIC8qKlxuICAgKiBQcm9wZXJ0aWVzIHRvIHNraXAgZHVyaW5nIG1lcmdlIChlLmcuLCBkYW5nZXJvdXMgcHJvcGVydGllcyBsaWtlIF9fcHJvdG9fXylcbiAgICovXG4gIHNraXBQcm9wZXJ0aWVzPzogc3RyaW5nW107XG5cbiAgLyoqXG4gICAqIFJlYWQtb25seSBmaWVsZHMgdGhhdCBzaG91bGQgYmUgc2tpcHBlZCBkdXJpbmcgbWVyZ2VcbiAgICovXG4gIHJlYWRPbmx5RmllbGRzPzogU2V0PHN0cmluZz47XG59XG5cbi8qKlxuICogQ2hlY2sgaWYgYSB2YWx1ZSBpcyBhIHBsYWluIG9iamVjdCAobm90IG51bGwsIG5vdCBhcnJheSlcbiAqL1xuZnVuY3Rpb24gaXNQbGFpbk9iamVjdCh2YWx1ZTogdW5rbm93bik6IHZhbHVlIGlzIFJlY29yZDxzdHJpbmcsIHVua25vd24+IHtcbiAgcmV0dXJuIHZhbHVlICE9PSBudWxsICYmIHR5cGVvZiB2YWx1ZSA9PT0gJ29iamVjdCcgJiYgIUFycmF5LmlzQXJyYXkodmFsdWUpO1xufVxuXG4vKipcbiAqIFZhbGlkYXRlIERlZXBNZXJnZU9wdGlvbnMgYXQgcnVudGltZVxuICpcbiAqIEBwYXJhbSBvcHRpb25zIC0gT3B0aW9ucyB0byB2YWxpZGF0ZVxuICogQHRocm93cyBFcnJvciBpZiBvcHRpb25zIGFyZSBtYWxmb3JtZWRcbiAqL1xuZnVuY3Rpb24gdmFsaWRhdGVPcHRpb25zKG9wdGlvbnM6IERlZXBNZXJnZU9wdGlvbnMpOiB2b2lkIHtcbiAgaWYgKG9wdGlvbnMuc2tpcFByb3BlcnRpZXMgIT09IHVuZGVmaW5lZCkge1xuICAgIGlmICghQXJyYXkuaXNBcnJheShvcHRpb25zLnNraXBQcm9wZXJ0aWVzKSkge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignRGVlcE1lcmdlT3B0aW9ucy5za2lwUHJvcGVydGllcyBtdXN0IGJlIGFuIGFycmF5IG9mIHN0cmluZ3MnKTtcbiAgICB9XG4gICAgZm9yIChjb25zdCBwcm9wIG9mIG9wdGlvbnMuc2tpcFByb3BlcnRpZXMpIHtcbiAgICAgIGlmICh0eXBlb2YgcHJvcCAhPT0gJ3N0cmluZycpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgRGVlcE1lcmdlT3B0aW9ucy5za2lwUHJvcGVydGllcyBjb250YWlucyBub24tc3RyaW5nIHZhbHVlOiAke3R5cGVvZiBwcm9wfWApO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIGlmIChvcHRpb25zLnJlYWRPbmx5RmllbGRzICE9PSB1bmRlZmluZWQpIHtcbiAgICBpZiAoIShvcHRpb25zLnJlYWRPbmx5RmllbGRzIGluc3RhbmNlb2YgU2V0KSkge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignRGVlcE1lcmdlT3B0aW9ucy5yZWFkT25seUZpZWxkcyBtdXN0IGJlIGEgU2V0PHN0cmluZz4nKTtcbiAgICB9XG4gIH1cbn1cblxuLyoqXG4gKiBEZWVwIG1lcmdlIHR3byBvYmplY3RzXG4gKlxuICogTWVyZ2Ugc2VtYW50aWNzOlxuICogLSBQbGFpbiBvYmplY3RzIGFyZSBtZXJnZWQgcmVjdXJzaXZlbHlcbiAqIC0gQXJyYXlzIHJlcGxhY2UgZW50aXJlbHkgKG5vdCBtZXJnZWQgZWxlbWVudC1ieS1lbGVtZW50KVxuICogLSBQcmltaXRpdmVzIHJlcGxhY2VcbiAqIC0gRGFuZ2Vyb3VzL3JlYWQtb25seSBwcm9wZXJ0aWVzIGFyZSBvcHRpb25hbGx5IHNraXBwZWRcbiAqXG4gKiBAcGFyYW0gdGFyZ2V0IC0gVGhlIHRhcmdldCBvYmplY3QgdG8gbWVyZ2UgaW50b1xuICogQHBhcmFtIHNvdXJjZSAtIFRoZSBzb3VyY2Ugb2JqZWN0IHRvIG1lcmdlIGZyb21cbiAqIEBwYXJhbSBvcHRpb25zIC0gT3B0aW9uYWwgbWVyZ2UgYmVoYXZpb3IgY29uZmlndXJhdGlvblxuICogQHJldHVybnMgTmV3IG1lcmdlZCBvYmplY3QgKGRvZXMgbm90IG11dGF0ZSBpbnB1dHMpXG4gKlxuICogQGV4YW1wbGVcbiAqIGBgYHR5cGVzY3JpcHRcbiAqIC8vIEJhc2ljIG1lcmdlXG4gKiBjb25zdCByZXN1bHQgPSBkZWVwTWVyZ2UoeyBhOiAxIH0sIHsgYjogMiB9KTtcbiAqIC8vID0+IHsgYTogMSwgYjogMiB9XG4gKlxuICogLy8gTmVzdGVkIG1lcmdlXG4gKiBjb25zdCByZXN1bHQgPSBkZWVwTWVyZ2UoXG4gKiAgIHsgc2V0dGluZ3M6IHsgdGhlbWU6ICdsaWdodCcsIHNpemU6IDEwIH0gfSxcbiAqICAgeyBzZXR0aW5nczogeyB0aGVtZTogJ2RhcmsnIH0gfVxuICogKTtcbiAqIC8vID0+IHsgc2V0dGluZ3M6IHsgdGhlbWU6ICdkYXJrJywgc2l6ZTogMTAgfSB9XG4gKlxuICogLy8gV2l0aCBzZWN1cml0eSBmaWx0ZXJpbmdcbiAqIGNvbnN0IHJlc3VsdCA9IGRlZXBNZXJnZSh0YXJnZXQsIHNvdXJjZSwge1xuICogICBza2lwUHJvcGVydGllczogWydfX3Byb3RvX18nLCAnY29uc3RydWN0b3InXSxcbiAqICAgcmVhZE9ubHlGaWVsZHM6IG5ldyBTZXQoWydpZCcsICd0eXBlJ10pXG4gKiB9KTtcbiAqXG4gKiAvLyBXaXRoIHR5cGUgcHJlc2VydmF0aW9uXG4gKiBpbnRlcmZhY2UgQ29uZmlnIHsgdGhlbWU6IHN0cmluZzsgc2l6ZTogbnVtYmVyOyB9XG4gKiBjb25zdCByZXN1bHQgPSBkZWVwTWVyZ2U8Q29uZmlnPihkZWZhdWx0Q29uZmlnLCB1c2VyQ29uZmlnKTtcbiAqIGBgYFxuICovXG5leHBvcnQgZnVuY3Rpb24gZGVlcE1lcmdlPFQgZXh0ZW5kcyBvYmplY3QgPSBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPj4oXG4gIHRhcmdldDogVCxcbiAgc291cmNlOiBQYXJ0aWFsPFQ+IHwgUmVjb3JkPHN0cmluZywgdW5rbm93bj4sXG4gIG9wdGlvbnM/OiBEZWVwTWVyZ2VPcHRpb25zXG4pOiBUIHtcbiAgLy8gVmFsaWRhdGUgb3B0aW9ucyBhdCBydW50aW1lIGlmIHByb3ZpZGVkXG4gIGlmIChvcHRpb25zKSB7XG4gICAgdmFsaWRhdGVPcHRpb25zKG9wdGlvbnMpO1xuICB9XG5cbiAgY29uc3QgcmVzdWx0ID0geyAuLi50YXJnZXQgfSBhcyBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPjtcbiAgY29uc3Qgc2tpcFByb3BlcnRpZXMgPSBvcHRpb25zPy5za2lwUHJvcGVydGllcyA/PyBbXTtcbiAgY29uc3QgcmVhZE9ubHlGaWVsZHMgPSBvcHRpb25zPy5yZWFkT25seUZpZWxkcztcblxuICBmb3IgKGNvbnN0IGtleSBvZiBPYmplY3Qua2V5cyhzb3VyY2UpKSB7XG4gICAgLy8gU2tpcCBkYW5nZXJvdXMgcHJvcGVydGllc1xuICAgIGlmIChza2lwUHJvcGVydGllcy5pbmNsdWRlcyhrZXkpKSB7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG5cbiAgICAvLyBTa2lwIHJlYWQtb25seSBmaWVsZHNcbiAgICBpZiAocmVhZE9ubHlGaWVsZHM/LmhhcyhrZXkpKSB7XG4gICAgICBjb250aW51ZTtcbiAgICB9XG5cbiAgICBjb25zdCBzb3VyY2VWYWx1ZSA9IChzb3VyY2UgYXMgUmVjb3JkPHN0cmluZywgdW5rbm93bj4pW2tleV07XG4gICAgY29uc3QgdGFyZ2V0VmFsdWUgPSByZXN1bHRba2V5XTtcblxuICAgIC8vIElmIGJvdGggYXJlIHBsYWluIG9iamVjdHMsIG1lcmdlIHJlY3Vyc2l2ZWx5XG4gICAgaWYgKGlzUGxhaW5PYmplY3Qoc291cmNlVmFsdWUpICYmIGlzUGxhaW5PYmplY3QodGFyZ2V0VmFsdWUpKSB7XG4gICAgICByZXN1bHRba2V5XSA9IGRlZXBNZXJnZSh0YXJnZXRWYWx1ZSwgc291cmNlVmFsdWUsIG9wdGlvbnMpO1xuICAgIH0gZWxzZSB7XG4gICAgICAvLyBBcnJheXMgYW5kIHByaW1pdGl2ZXMgcmVwbGFjZSBlbnRpcmVseVxuICAgICAgcmVzdWx0W2tleV0gPSBzb3VyY2VWYWx1ZTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4gcmVzdWx0IGFzIFQ7XG59XG5cbi8qKlxuICogRGVmYXVsdCBkYW5nZXJvdXMgcHJvcGVydGllcyB0aGF0IHNob3VsZCBuZXZlciBiZSBtZXJnZWRcbiAqIFRoZXNlIGNhbiBiZSB1c2VkIHRvIHByZXZlbnQgcHJvdG90eXBlIHBvbGx1dGlvbiBhdHRhY2tzXG4gKi9cbmV4cG9ydCBjb25zdCBEQU5HRVJPVVNfUFJPUEVSVElFUyA9IFtcbiAgJ19fcHJvdG9fXycsXG4gICdjb25zdHJ1Y3RvcicsXG4gICdwcm90b3R5cGUnLFxuXTtcblxuLyoqXG4gKiBDcmVhdGUgYSBkZWVwIG1lcmdlIGZ1bmN0aW9uIHdpdGggcHJlc2V0IG9wdGlvbnNcbiAqXG4gKiBVc2VmdWwgZm9yIGNyZWF0aW5nIGEgY29uZmlndXJlZCBtZXJnZXIgdGhhdCBjYW4gYmUgcmV1c2VkXG4gKlxuICogQHBhcmFtIG9wdGlvbnMgLSBQcmVzZXQgb3B0aW9ucyBmb3IgYWxsIG1lcmdlc1xuICogQHJldHVybnMgQSBjb25maWd1cmVkIGRlZXAgbWVyZ2UgZnVuY3Rpb25cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHlwZXNjcmlwdFxuICogY29uc3Qgc2VjdXJlTWVyZ2UgPSBjcmVhdGVEZWVwTWVyZ2Uoe1xuICogICBza2lwUHJvcGVydGllczogREFOR0VST1VTX1BST1BFUlRJRVMsXG4gKiAgIHJlYWRPbmx5RmllbGRzOiBuZXcgU2V0KFsnaWQnLCAndHlwZSddKVxuICogfSk7XG4gKlxuICogY29uc3QgcmVzdWx0ID0gc2VjdXJlTWVyZ2UodGFyZ2V0LCBzb3VyY2UpO1xuICogYGBgXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVEZWVwTWVyZ2Uob3B0aW9uczogRGVlcE1lcmdlT3B0aW9ucykge1xuICByZXR1cm4gKHRhcmdldDogUmVjb3JkPHN0cmluZywgdW5rbm93bj4sIHNvdXJjZTogUmVjb3JkPHN0cmluZywgdW5rbm93bj4pID0+XG4gICAgZGVlcE1lcmdlKHRhcmdldCwgc291cmNlLCBvcHRpb25zKTtcbn1cbiJdfQ==