@cldmv/slothlet 3.9.2 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +16 -16
  2. package/REFERENCE.md +19 -19
  3. package/dist/lib/builders/api_builder.mjs +1 -1
  4. package/dist/lib/builders/builder.mjs +1 -1
  5. package/dist/lib/builders/modes-processor.mjs +1 -1
  6. package/dist/lib/handlers/api-cache-manager.mjs +1 -1
  7. package/dist/lib/handlers/api-manager.mjs +1 -1
  8. package/dist/lib/handlers/hook-manager.mjs +1 -1
  9. package/dist/lib/handlers/permission-manager.mjs +1 -1
  10. package/dist/lib/helpers/config.mjs +1 -1
  11. package/dist/lib/helpers/generate-manifest.mjs +1 -1
  12. package/dist/lib/helpers/module-discovery.mjs +1 -1
  13. package/dist/lib/helpers/module-manifest-validator.mjs +1 -1
  14. package/dist/lib/i18n/languages/de-de.json +90 -64
  15. package/dist/lib/i18n/languages/en-gb.json +90 -64
  16. package/dist/lib/i18n/languages/en-us.json +90 -64
  17. package/dist/lib/i18n/languages/es-es.json +90 -64
  18. package/dist/lib/i18n/languages/es-mx.json +90 -64
  19. package/dist/lib/i18n/languages/fr-fr.json +90 -64
  20. package/dist/lib/i18n/languages/hi-in.json +90 -64
  21. package/dist/lib/i18n/languages/ja-jp.json +90 -64
  22. package/dist/lib/i18n/languages/ko-kr.json +90 -64
  23. package/dist/lib/i18n/languages/pt-br.json +90 -64
  24. package/dist/lib/i18n/languages/ru-ru.json +92 -66
  25. package/dist/lib/i18n/languages/zh-cn.json +90 -64
  26. package/dist/lib/modes/eager.mjs +1 -1
  27. package/dist/lib/modes/lazy.mjs +1 -1
  28. package/package.json +7 -4
  29. package/types/dist/lib/builders/api_builder.d.mts.map +1 -1
  30. package/types/dist/lib/builders/builder.d.mts.map +1 -1
  31. package/types/dist/lib/builders/modes-processor.d.mts.map +1 -1
  32. package/types/dist/lib/handlers/api-cache-manager.d.mts.map +1 -1
  33. package/types/dist/lib/handlers/api-manager.d.mts.map +1 -1
  34. package/types/dist/lib/handlers/hook-manager.d.mts +4 -0
  35. package/types/dist/lib/handlers/hook-manager.d.mts.map +1 -1
  36. package/types/dist/lib/handlers/permission-manager.d.mts +2 -0
  37. package/types/dist/lib/handlers/permission-manager.d.mts.map +1 -1
  38. package/types/dist/lib/helpers/config.d.mts +3 -0
  39. package/types/dist/lib/helpers/config.d.mts.map +1 -1
  40. package/types/dist/lib/helpers/generate-manifest.d.mts +1 -0
  41. package/types/dist/lib/helpers/generate-manifest.d.mts.map +1 -1
  42. package/types/dist/lib/helpers/module-discovery.d.mts.map +1 -1
  43. package/types/dist/lib/helpers/module-manifest-validator.d.mts.map +1 -1
  44. package/types/dist/lib/modes/eager.d.mts +2 -1
  45. package/types/dist/lib/modes/eager.d.mts.map +1 -1
  46. package/types/dist/lib/modes/lazy.d.mts +2 -1
  47. package/types/dist/lib/modes/lazy.d.mts.map +1 -1
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{translate}from"@cldmv/slothlet/i18n";import{ComponentBase}from"@cldmv/slothlet/factories/component-base";import{UnifiedWrapper,resolveWrapper}from"@cldmv/slothlet/handlers/unified-wrapper";import{fsp,path}from"@cldmv/slothlet/helpers/platform";class ApiManager extends ComponentBase{static slothletProperty="apiManager";constructor(slothlet){super(slothlet);this.state={addHistory:[],initialConfig:slothlet?.config||null,operationHistory:[]}}normalizeApiPath(apiPath){if(apiPath===""||apiPath===null||apiPath===void 0){return{apiPath:"",parts:[]}}if(Array.isArray(apiPath)){if(apiPath.length===0){return{apiPath:"",parts:[]}}for(let i=0;i<apiPath.length;i++){if(typeof apiPath[i]!=="string"){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,segment:apiPath[i],index:i,reason:translate("API_PATH_REASON_ARRAY_ELEMENTS"),validationError:true})}if(apiPath[i].trim()===""){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,segment:apiPath[i],index:i,reason:translate("API_PATH_REASON_ARRAY_EMPTY_SEGMENTS"),validationError:true})}}if(apiPath[0]==="slothlet"||apiPath.length===1&&(apiPath[0]==="shutdown"||apiPath[0]==="destroy")){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,reason:translate("API_PATH_REASON_RESERVED_NAME"),index:void 0,segment:void 0,validationError:true})}return{apiPath:apiPath.join("."),parts:apiPath}}if(typeof apiPath!=="string"){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,reason:translate("API_PATH_REASON_INVALID_TYPE"),index:void 0,segment:void 0,validationError:true})}const normalized=apiPath.trim();if(normalized===""){return{apiPath:"",parts:[]}}const parts=normalized.split(".");if(parts.length===0||parts.some(part=>part.trim()==="")){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath:normalized,reason:translate("API_PATH_REASON_EMPTY_SEGMENTS"),index:void 0,segment:void 0,validationError:true})}if(parts[0]==="slothlet"||normalized==="shutdown"||normalized==="destroy"){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath:normalized,reason:translate("API_PATH_REASON_RESERVED_NAME"),index:void 0,segment:void 0,validationError:true})}return{apiPath:normalized,parts}}async resolvePath(inputPath){if(!inputPath||typeof inputPath!=="string"){throw new this.SlothletError("INVALID_CONFIG_DIR_INVALID",{dir:inputPath,validationError:true})}const resolvedPath=this.slothlet.helpers.resolver.resolvePathFromCaller(inputPath);if(this.slothlet.envTarget==="browser"){const lastSegment=resolvedPath.split("/").pop();const isFile=lastSegment.includes(".");return{resolvedPath,isDirectory:!isFile,isFile}}try{const stats=await fsp.stat(resolvedPath);return{resolvedPath,isDirectory:stats.isDirectory(),isFile:stats.isFile()}}catch(error){if(error instanceof this.SlothletError){throw error}throw new this.SlothletError("INVALID_CONFIG_DIR_INVALID",{dir:resolvedPath,validationError:true})}}async resolveFolderPath(folderPath){if(!folderPath||typeof folderPath!=="string"){throw new this.SlothletError("INVALID_CONFIG_DIR_INVALID",{dir:folderPath,validationError:true})}const resolvedPath=this.slothlet.helpers.resolver.resolvePathFromCaller(folderPath);try{const stats=await fsp.stat(resolvedPath);if(!stats.isDirectory()){throw new this.SlothletError("INVALID_CONFIG_DIR_INVALID",{dir:resolvedPath,validationError:true})}}catch(error){if(error instanceof this.SlothletError){throw error}throw new this.SlothletError("INVALID_CONFIG_DIR_INVALID",{dir:resolvedPath,validationError:true})}return resolvedPath}buildDefaultModuleId(apiPath,____resolvedFolderPath){const randomSuffix=Math.random().toString(36).substring(2,8);const prefix=apiPath||"auto";return`${prefix}_${randomSuffix}`}getValueAtPath(root,parts){let current=root;for(const part of parts){if(!current||typeof current!=="object"&&typeof current!=="function"){return void 0}current=current[part]}return current}ensureParentPath(root,parts,options={}){const{moduleID,sourceFolder}=options;let current=root;for(let i=0;i<parts.length-1;i+=1){const part=parts[i];const next=current[part];if(next===void 0){const containerPath=parts.slice(0,i+1).join(".");const containerWrapper=new UnifiedWrapper(this.slothlet,{mode:this.____config.mode,apiPath:containerPath,moduleID,sourceFolder});containerWrapper.___setImpl({},moduleID);current[part]=containerWrapper.createProxy();current=current[part];continue}if(next&&(typeof next==="object"||typeof next==="function")){current=next;continue}throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath:parts.slice(0,i+1).join("."),reason:translate("API_PATH_REASON_NOT_TRAVERSABLE"),index:void 0,segment:void 0,validationError:true})}return current}setOwnedProperty(apiPath,value,callerWrapper){const{parts}=this.normalizeApiPath(apiPath);if(parts.length===0){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath:String(apiPath??""),reason:translate("API_PATH_REASON_EMPTY_SEGMENTS"),index:void 0,segment:void 0,validationError:true})}const RESERVED=new Set(["__proto__","prototype","constructor"]);for(const segment of parts){if(RESERVED.has(segment)){throw new this.SlothletError("LOOSE_SET_RESERVED_KEY",{apiPath:parts.join("."),segment,validationError:true})}}const callerModuleID=callerWrapper?.____slothletInternal?.moduleID??null;const callerWrapperPath=callerWrapper?.____slothletInternal?.apiPath??null;let ownedRoot=null;if(callerModuleID&&this.slothlet.handlers?.ownership?.getModuleEndpoint){const endpoint=this.slothlet.handlers.ownership.getModuleEndpoint(callerModuleID);if(endpoint!=null){ownedRoot=endpoint}}if(ownedRoot===null)ownedRoot=".";if(ownedRoot&&ownedRoot!=="."&&ownedRoot!==""){const ownedParts=String(ownedRoot).split(".").filter(Boolean);const isUnderOwn=ownedParts.length<=parts.length&&ownedParts.every((seg,i)=>parts[i]===seg);if(!isUnderOwn){const callerApiPath=callerWrapperPath??ownedRoot;throw new this.SlothletError("LOOSE_SET_NOT_OWNED",{apiPath:parts.join("."),callerApiPath,validationError:true})}}const callerSourceFolder=callerWrapper?.____slothletInternal?.sourceFolder??null;const root=this.slothlet.boundApi;const parent=this.ensureParentPath(root,parts,{moduleID:callerModuleID,sourceFolder:callerSourceFolder});const finalKey=parts[parts.length-1];const isWrappable=typeof value==="function"||value!==null&&typeof value==="object";if(isWrappable){const wrapper=new UnifiedWrapper(this.slothlet,{mode:this.____config.mode,apiPath:parts.join("."),moduleID:callerModuleID,isCallable:typeof value==="function",sourceFolder:callerSourceFolder});wrapper.___setImpl(value,callerModuleID);parent[finalKey]=wrapper.createProxy()}else{parent[finalKey]=value}}isWrapperProxy(value){return!!(value&&(typeof value==="object"||typeof value==="function")&&resolveWrapper(value)!==null)}async syncWrapper(existingProxy,nextProxy,config,collisionMode="replace",moduleID=null){if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_ENTRY_EXISTING",apiPath:resolveWrapper(existingProxy)?.apiPath});this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_ENTRY_NEXT",apiPath:resolveWrapper(nextProxy)?.apiPath})}if(!this.isWrapperProxy(existingProxy)||!this.isWrapperProxy(nextProxy)){return false}const existingWrapper=resolveWrapper(existingProxy)??existingProxy;const nextWrapper=resolveWrapper(nextProxy)??nextProxy;if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_EXISTING",apiPath:existingWrapper.apiPath});this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_NEXT",apiPath:nextWrapper.apiPath})}if(nextWrapper.____slothletInternal.materializeFunc&&collisionMode!=="merge"){existingWrapper.____slothletInternal.materializeFunc=nextWrapper.____slothletInternal.materializeFunc}const existingChildKeys=Object.keys(existingWrapper).filter(k=>!k.startsWith("_")&&!k.startsWith("__"));const nextChildKeys=Object.keys(nextWrapper).filter(k=>!k.startsWith("_")&&!k.startsWith("__"));if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_BEFORE_MERGE",existingCacheSize:existingChildKeys.length,nextCacheSize:nextChildKeys.length});this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_NEXT_IMPL_KEYS",implKeys:Object.keys(nextWrapper.____slothletInternal.impl||{})});this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_NEXT_CHILDCACHE_KEYS",childCacheKeys:nextChildKeys})}if(collisionMode==="replace"){if(existingWrapper.___setImpl&&nextWrapper.____slothletInternal.impl!==void 0){existingWrapper.___setImpl(nextWrapper.____slothletInternal.impl,moduleID)}else if(nextWrapper.____slothletInternal.impl===void 0){existingWrapper.____slothletInternal.impl=null}else{if(nextWrapper.____slothletInternal.impl!==void 0){existingWrapper.____slothletInternal.impl=nextWrapper.____slothletInternal.impl;if(typeof nextWrapper.____slothletInternal.impl==="function"||nextWrapper.____slothletInternal.impl&&typeof nextWrapper.____slothletInternal.impl.default==="function"){existingWrapper.isCallable=true}}}for(const key of existingChildKeys){delete existingWrapper[key]}existingWrapper.___adoptImplChildren();for(const key of nextChildKeys){const childValue=nextWrapper[key];Object.defineProperty(existingWrapper,key,{value:childValue,writable:false,enumerable:true,configurable:true})}}else if(collisionMode==="merge"){for(const key of nextChildKeys){const isInternal=typeof key==="string"&&(key.startsWith("_")||key.startsWith("__"));if(!isInternal&&!Object.prototype.hasOwnProperty.call(existingWrapper,key)){const childValue=nextWrapper[key];Object.defineProperty(existingWrapper,key,{value:childValue,writable:false,enumerable:true,configurable:true})}else if(!isInternal){const existingChild=existingWrapper[key];const nextChild=nextWrapper[key];if(this.isWrapperProxy(existingChild)&&this.isWrapperProxy(nextChild)){const syncWrapper_nextChildWrapper=resolveWrapper(nextChild)??nextChild;const syncWrapper_hasGrandChildren=Object.keys(syncWrapper_nextChildWrapper).some(k=>!k.startsWith("_")&&!k.startsWith("__"));if(syncWrapper_hasGrandChildren){await this.syncWrapper(existingChild,nextChild,config,collisionMode,moduleID)}}}}}else if(collisionMode==="merge-replace"){for(const key of nextChildKeys){const childValue=nextWrapper[key];const isInternal=typeof key==="string"&&(key.startsWith("_")||key.startsWith("__"));if(!isInternal&&Object.prototype.hasOwnProperty.call(existingWrapper,key)){const existingChild=existingWrapper[key];if(this.isWrapperProxy(existingChild)&&this.isWrapperProxy(childValue)){await this.syncWrapper(existingChild,childValue,config,collisionMode,moduleID)}else{delete existingWrapper[key];Object.defineProperty(existingWrapper,key,{value:childValue,writable:false,enumerable:true,configurable:true})}}else if(!isInternal){Object.defineProperty(existingWrapper,key,{value:childValue,writable:false,enumerable:true,configurable:true})}}}if(existingWrapper.____slothletInternal.state){const isActuallyMaterialized=existingWrapper.____slothletInternal.impl&&typeof existingWrapper.____slothletInternal.impl!=="function";existingWrapper.____slothletInternal.state.materialized=isActuallyMaterialized;existingWrapper.____slothletInternal.state.inFlight=false}return true}async mutateApiValue(existingValue,nextValue,options,config){if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_CALLED",existingType:typeof existingValue,nextType:typeof nextValue});this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_WRAPPER_STATUS",existingIsWrapper:this.isWrapperProxy(existingValue),nextIsWrapper:this.isWrapperProxy(nextValue)});this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_NEXT_VALUE",nextValue});this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_NEXT_VALUE_KEYS",nextValueKeys:nextValue?Object.keys(nextValue):[]})}if(existingValue===nextValue){return}if(this.isWrapperProxy(existingValue)&&this.isWrapperProxy(nextValue)){if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_SYNC_WRAPPERS"})}await this.syncWrapper(existingValue,nextValue,config,options.collisionMode,options.moduleID);return}if(this.isWrapperProxy(existingValue)&&!this.isWrapperProxy(nextValue)){const nextIsObjectLike=nextValue&&(typeof nextValue==="object"||typeof nextValue==="function");const nextHasKeys=nextIsObjectLike&&Object.keys(nextValue).length>0;if(nextHasKeys){if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_MERGE_INTO_WRAPPER"});this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_MERGE_KEYS",keys:Object.keys(nextValue)})}await this.slothlet.builders.apiAssignment.mergeApiObjects(existingValue,nextValue,{removeMissing:options.removeMissing,mutateExisting:true,allowOverwrite:true,syncWrapper:this.syncWrapper.bind(this),collisionMode:options.collisionMode,moduleID:options.moduleID});return}const existingValueRaw=resolveWrapper(existingValue);if(existingValueRaw!==null){if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_SETIMPL_FALLBACK"})}existingValueRaw.___setImpl(resolveWrapper(nextValue)?.__impl??nextValue);return}}if(existingValue&&typeof existingValue==="object"&&nextValue&&typeof nextValue==="object"){await this.slothlet.builders.apiAssignment.mergeApiObjects(existingValue,nextValue,{removeMissing:options.removeMissing,mutateExisting:true,allowOverwrite:true,syncWrapper:this.syncWrapper.bind(this),collisionMode:options.collisionMode,moduleID:options.moduleID});return existingValue}return nextValue}async setValueAtPath(root,parts,value,options){const parent=this.ensureParentPath(root,parts,{moduleID:options.moduleID,sourceFolder:options.sourceFolder});const finalKey=parts[parts.length-1];const existing=parent?parent[finalKey]:void 0;const collisionMode=options.collisionMode||"merge";const moduleID=options.moduleID;this.slothlet.debug("api",{key:"DEBUG_MODE_SET_VALUE_AT_PATH",finalKey,existingType:typeof existing,valueType:typeof value,collisionMode,options});if(existing!==void 0){if(collisionMode==="error"){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath:parts.join("."),reason:translate("API_PATH_REASON_COLLISION_ERROR"),index:void 0,segment:void 0,validationError:true})}if(collisionMode==="skip"){this.slothlet.debug("api",{key:"DEBUG_MODE_SET_VALUE_AT_PATH_SKIP_COLLISION",path:parts.join("."),mode:"skip"});return false}if(collisionMode==="warn"){if(this.slothlet&&!this.____config?.silent){new this.SlothletWarning("WARNING_HOT_RELOAD_PATH_COLLISION",{apiPath:parts.join(".")})}return false}if(collisionMode==="replace"){const existingIsObject=typeof existing==="object"||typeof existing==="function";const valueIsObject=typeof value==="object"||typeof value==="function";if(existingIsObject&&valueIsObject){this.slothlet.debug("api",{key:"DEBUG_MODE_SET_VALUE_AT_PATH_REPLACE_MERGE",path:parts.join("."),mode:"replace"});await this.mutateApiValue(existing,value,{removeMissing:false,allowOverwrite:true,collisionMode:"replace",moduleID},this.____config);return true}else{parent[finalKey]=value;return true}}if(collisionMode==="merge"||collisionMode==="merge-replace"){const existingIsObject=typeof existing==="object"||typeof existing==="function";const valueIsObject=typeof value==="object"||typeof value==="function";if(existingIsObject&&valueIsObject){this.slothlet.debug("api",{key:"DEBUG_MODE_SET_VALUE_AT_PATH_MERGE_PROPS",mode:collisionMode});await this.mutateApiValue(existing,value,{removeMissing:false,allowOverwrite:true,collisionMode},this.____config);return true}else{if(this.slothlet&&!this.____config?.silent){new this.SlothletWarning("WARNING_HOT_RELOAD_MERGE_PRIMITIVES",{apiPath:parts.join(".")})}return false}}}this.slothlet.debug("api",{key:"DEBUG_MODE_SET_VALUE_AT_PATH_ASSIGN",finalKey});parent[finalKey]=value;return true}async deletePath(root,parts){let current=root;const stack=[];for(const part of parts.slice(0,-1)){if(!current||typeof current!=="object"&&typeof current!=="function"){return false}stack.push({parent:current,key:part});current=current[part]}const finalKey=parts[parts.length-1];if(!current||typeof current!=="object"&&typeof current!=="function"){return false}if(!Object.prototype.hasOwnProperty.call(current,finalKey)){return false}const removedImpl=current[finalKey];const apiPath=parts.join(".");if(removedImpl&&this.slothlet.handlers?.lifecycle){const metadata=this.slothlet.handlers.metadata?.getMetadata?.(removedImpl);await this.slothlet.handlers.lifecycle.emit("impl:removed",{apiPath,impl:removedImpl,source:"removal",moduleID:metadata?.moduleID,filePath:metadata?.filePath,sourceFolder:metadata?.sourceFolder})}if(resolveWrapper(current)){const wrapper=resolveWrapper(current);const isInternal=typeof finalKey==="string"&&(finalKey.startsWith("_")||finalKey.startsWith("__"));if(!isInternal&&finalKey in wrapper){delete wrapper[finalKey]}if(wrapper.____slothletInternal.impl&&typeof wrapper.____slothletInternal.impl==="object"){delete wrapper.____slothletInternal.impl[finalKey]}}delete current[finalKey];if(removedImpl&&(typeof removedImpl==="object"||typeof removedImpl==="function")){if(resolveWrapper(removedImpl)){const wrapper=resolveWrapper(removedImpl);if(wrapper.____slothletInternal.impl!==void 0){wrapper.____slothletInternal.impl=null}const childKeys=Object.keys(wrapper).filter(k=>!k.startsWith("_")&&!k.startsWith("__"));for(const key of childKeys){delete wrapper[key]}if(wrapper.____slothletInternal.state){wrapper.____slothletInternal.state.materialized=false;wrapper.____slothletInternal.state.inFlight=false}}}if(this.slothlet.handlers?.metadata){const rootSegment=apiPath.split(".")[0];this.slothlet.handlers.metadata.removeUserMetadataByApiPath(rootSegment)}for(let i=stack.length-1;i>=0;i-=1){const{parent,key}=stack[i];const value=parent[key];if(value&&(typeof value==="object"||typeof value==="function")&&Object.keys(value).length===0){delete parent[key]}}return true}async restoreApiPath(apiPath,moduleID){const normalizedModuleId=moduleID||null;const historyEntry=this.state.addHistory.slice().reverse().find(entry=>entry.apiPath===apiPath&&(normalizedModuleId?entry.moduleID===normalizedModuleId:true));if(historyEntry){await this.addApiComponent({apiPath:historyEntry.apiPath,folderPath:historyEntry.folderPath,options:{...historyEntry.options,metadata:historyEntry.metadata,mutateExisting:true,forceOverwrite:true,collisionMode:"replace",recordHistory:false}});return}if(normalizedModuleId==="base"||normalizedModuleId==="core"){const baseApi=await this.slothlet.builders.builder.buildAPI({dir:this.____config.dir,mode:this.____config.mode,moduleID:"base"});const{parts}=this.normalizeApiPath(apiPath);let baseValue=this.getValueAtPath(baseApi,parts);if(baseValue===void 0){await this.deletePath(this.slothlet.api,parts);await this.deletePath(this.slothlet.boundApi,parts);return}const baseValueRaw=resolveWrapper(baseValue);if(baseValue&&baseValueRaw!==null){baseValue=baseValueRaw.__impl}await this.setValueAtPath(this.slothlet.api,parts,baseValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:normalizedModuleId});await this.setValueAtPath(this.slothlet.boundApi,parts,baseValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:normalizedModuleId})}}#normalizePermissionShorthandEntry(entry){const getValueType=value=>{if(value===null)return"null";if(Array.isArray(value))return"array";return typeof value};if(typeof entry==="string"){return{target:entry,condition:void 0}}if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_NOT_OBJECT"),received:getValueType(entry),validationError:true})}if(typeof entry.target!=="string"||!entry.target){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_TARGET_REQUIRED"),received:typeof entry.target,validationError:true})}return{target:entry.target,condition:entry.condition}}async addApiComponent(params){const{apiPath,folderPath,options={},versionConfig=null}=params||{};if(Array.isArray(folderPath)){const moduleIDs=[];for(const singlePath of folderPath){const moduleID2=await this.addApiComponent({apiPath,folderPath:singlePath,options,versionConfig});moduleIDs.push(moduleID2)}return moduleIDs}const{metadata={},...restOptions}=options;if(!this.slothlet||!this.slothlet.isLoaded){throw new this.SlothletError("INVALID_CONFIG_NOT_LOADED",{operation:"addApi",validationError:true})}const{apiPath:normalizedPath,parts}=this.normalizeApiPath(apiPath);let effectivePath=normalizedPath;let effectiveParts=parts;if(versionConfig?.version!==void 0&&versionConfig?.version!==null){if(normalizedPath===""){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,reason:translate("API_PATH_REASON_VERSIONED_ROOT"),index:void 0,segment:void 0,validationError:true})}if(typeof versionConfig.version!=="string"||!String(versionConfig.version).trim()){throw new this.SlothletError("INVALID_CONFIG_VERSION_TAG",{received:versionConfig.version,validationError:true})}const versionTag=String(versionConfig.version).trim();effectiveParts=[versionTag,...parts];effectivePath=effectiveParts.join(".")}const{resolvedPath,isDirectory,isFile}=await this.resolvePath(folderPath);if(!isDirectory&&!isFile){throw new this.SlothletError("INVALID_CONFIG_PATH_TYPE",{path:resolvedPath,validationError:true})}if(isFile){const ext=path.extname(resolvedPath);if(![".mjs",".cjs",".js"].includes(ext)){throw new this.SlothletError("INVALID_CONFIG_FILE_TYPE",{path:resolvedPath,extension:ext,validationError:true})}}const resolvedFolderPath=resolvedPath;let collisionMode;if(restOptions.forceOverwrite){collisionMode="replace"}else{collisionMode=restOptions.collisionMode||this.____config.api?.collision?.api||"error"}const mutateExisting=!!(restOptions.mutateExisting||collisionMode==="merge");const moduleID=restOptions.moduleID?String(restOptions.moduleID):this.buildDefaultModuleId(normalizedPath,resolvedFolderPath);if(restOptions.forceOverwrite&&!moduleID){throw new this.SlothletError("INVALID_CONFIG_FORCE_OVERWRITE_REQUIRES_MODULE_ID",{apiPath:normalizedPath,validationError:true})}let dirForBuild=resolvedFolderPath;let fileFilter=null;if(isFile){dirForBuild=path.dirname(resolvedFolderPath);const fileName=path.basename(resolvedFolderPath);fileFilter=file=>file===fileName}const newApi=await this.slothlet.builders.builder.buildAPI({dir:dirForBuild,mode:this.____config.mode,apiPathPrefix:effectivePath,collisionContext:"addApi",moduleID,collisionMode,fileFilter});if(this.slothlet.handlers.apiCacheManager){this.slothlet.handlers.apiCacheManager.set(moduleID,{endpoint:effectivePath,moduleID,api:newApi,folderPath:resolvedFolderPath,mode:this.____config.mode,sanitizeOptions:this.____config.sanitize||{},collisionMode,config:{...this.____config},timestamp:Date.now()})}this.slothlet.debug("api",{key:"DEBUG_MODE_ADD_API_COMPONENT_BUILD_RETURN",topLevelKeys:Object.keys(newApi),dottedKeys:Object.keys(newApi).filter(k=>k.includes(".")),wrappers:Object.keys(newApi).filter(k=>resolveWrapper(newApi[k])!==null).map(k=>{const _w=resolveWrapper(newApi[k]);return{key:k,apiPath:_w.apiPath,implKeys:Object.keys(_w.____slothletInternal.impl||{}),childCacheSize:Object.keys(_w).filter(k2=>!k2.startsWith("_")&&!k2.startsWith("__")).length,childCacheKeys:Object.keys(_w).filter(k2=>!k2.startsWith("_")&&!k2.startsWith("__"))}}),nonWrappers:Object.keys(newApi).filter(k=>resolveWrapper(newApi[k])===null).map(k=>({key:k,type:typeof newApi[k]}))});let apiToMerge=newApi;if(isFile&&Object.keys(newApi).length===1){const fileName=Object.keys(newApi)[0];apiToMerge=newApi[fileName]}if(!isFile&&normalizedPath){const lastPart=normalizedPath.includes(".")?normalizedPath.split(".").pop():normalizedPath;if(lastPart&&Object.prototype.hasOwnProperty.call(apiToMerge,lastPart)){const dupValue=apiToMerge[lastPart];const dupType=typeof dupValue;if(dupValue!==null&&(dupType==="object"||dupType==="function")){const dupWrapper=resolveWrapper(dupValue);const dupFilePath=dupWrapper?.____slothletInternal?.filePath;const dupFileDir=dupFilePath?dupFilePath.replace(/\\/g,"/").split("/").slice(0,-1).join("/"):null;const normalizedFolderPath=resolvedFolderPath.replace(/\\/g,"/").replace(/\/$/,"");const expectedDir=normalizedFolderPath+"/"+lastPart;const isDirectChild=dupFileDir===expectedDir||dupFileDir===normalizedFolderPath;if(isDirectChild){const hoisted={};for(const k of Object.keys(apiToMerge)){if(k!==lastPart)hoisted[k]=apiToMerge[k]}if(dupWrapper){for(const k of Object.keys(dupWrapper).filter(k2=>!k2.startsWith("_")&&!k2.startsWith("__"))){hoisted[k]=dupWrapper[k]}}else{for(const k of Object.keys(dupValue)){hoisted[k]=dupValue[k]}}apiToMerge=hoisted;this.slothlet.debug("api",{key:"DEBUG_MODE_RULE_13_DEDUP_HOISTED_KEY",lastPart,newKeys:Object.keys(apiToMerge)})}}}}if(this.____config.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_ADD_API_COMPONENT_MERGE_KEYS",keys:Object.keys(apiToMerge),isRootLevel:parts.length===0})}let anyAssignmentSucceeded=false;if(parts.length===0){for(const key of Object.keys(newApi)){const result1=await this.setValueAtPath(this.slothlet.api,[key],newApi[key],{mutateExisting,collisionMode,moduleID,sourceFolder:resolvedFolderPath});const result2=await this.setValueAtPath(this.slothlet.boundApi,[key],newApi[key],{mutateExisting,collisionMode,moduleID,sourceFolder:resolvedFolderPath});if(result1||result2){anyAssignmentSucceeded=true}}}else{if(resolveWrapper(apiToMerge)===null){const isCallableNamespace=typeof apiToMerge==="function";const containerWrapper=new UnifiedWrapper(this.slothlet,{apiPath:effectivePath,mode:this.____config.mode,isCallable:isCallableNamespace,moduleID,filePath:resolvedFolderPath,sourceFolder:resolvedFolderPath});containerWrapper.___setImpl(apiToMerge,moduleID);apiToMerge=containerWrapper.createProxy()}const result1=await this.setValueAtPath(this.slothlet.api,effectiveParts,apiToMerge,{mutateExisting,collisionMode,moduleID,sourceFolder:resolvedFolderPath});const result2=await this.setValueAtPath(this.slothlet.boundApi,effectiveParts,apiToMerge,{mutateExisting,collisionMode,moduleID,sourceFolder:resolvedFolderPath});if(result1||result2){anyAssignmentSucceeded=true}}if(anyAssignmentSucceeded){const pendingMaterializations=[];const seenWrappers=new Set;const collectPendingMaterializations=(obj,depth=0)=>{if(!obj||typeof obj!=="object"||depth>10)return;if(obj.__isVersionDispatcher===true)return;const wrapper=resolveWrapper(obj);if(wrapper){if(seenWrappers.has(wrapper))return;seenWrappers.add(wrapper);if(wrapper.____slothletInternal.materializationPromise){pendingMaterializations.push(wrapper.____slothletInternal.materializationPromise)}const childKeys=Object.keys(wrapper).filter(k=>!k.startsWith("_")&&!k.startsWith("__"));for(const key of childKeys){collectPendingMaterializations(wrapper[key],depth+1)}}for(const key of Object.keys(obj)){if(key!=="____slothletInternal"){collectPendingMaterializations(obj[key],depth+1)}}};if(effectiveParts.length===0){for(const key of Object.keys(newApi)){if(this.slothlet.api[key]){collectPendingMaterializations(this.slothlet.api[key])}}}else{let current=this.slothlet.api;for(const part of effectiveParts){if(current&&current[part]){current=current[part]}else{break}}if(current){collectPendingMaterializations(current)}}if(pendingMaterializations.length>0){if(this.____config.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_AWAITING_PENDING_MATERIALIZATIONS",count:pendingMaterializations.length,apiPath:normalizedPath})}await Promise.all(pendingMaterializations)}}if(anyAssignmentSucceeded&&metadata&&Object.keys(metadata).length>0&&this.slothlet.handlers.metadata){if(parts.length===0){for(const key of Object.keys(newApi)){this.slothlet.handlers.metadata.registerUserMetadata(key,metadata)}}else{const rootSegment=effectiveParts[0];this.slothlet.handlers.metadata.registerUserMetadata(rootSegment,metadata)}}if(this.slothlet.handlers.ownership&&moduleID){this.slothlet.handlers.ownership.registerSubtree(apiToMerge,moduleID,effectivePath);this.slothlet.handlers.ownership.setModuleEndpoint(moduleID,effectivePath)}if(this.slothlet.handlers.ownership){if(restOptions.recordHistory!==false){this.state.addHistory.push({apiPath:normalizedPath,folderPath:resolvedFolderPath,options:{...restOptions,metadata,moduleID},moduleID,versionConfig:versionConfig||null});this.state.operationHistory.push({type:"add",apiPath:normalizedPath,folderPath:resolvedFolderPath,options:{...restOptions,metadata,moduleID},moduleID,versionConfig:versionConfig||null})}}if(versionConfig?.version&&this.slothlet.handlers.versionManager){const versionTag=String(versionConfig.version).trim();try{this.slothlet.handlers.versionManager.registerVersion(normalizedPath,versionTag,moduleID,versionConfig.metadata??{},versionConfig.default??false)}catch(error){await this._rollbackFailedVersionedAdd({moduleID,effectivePath,normalizedPath});throw error}}if(restOptions.permissions&&this.slothlet.handlers?.permissionManager){const perms=restOptions.permissions;const callerPattern=`${normalizedPath}.**`;if(Array.isArray(perms.deny)){for(const entry of perms.deny){const{target,condition}=this.#normalizePermissionShorthandEntry(entry);this.slothlet.handlers.permissionManager.addRule({caller:callerPattern,target,effect:"deny",condition},moduleID)}}if(Array.isArray(perms.allow)){for(const entry of perms.allow){const{target,condition}=this.#normalizePermissionShorthandEntry(entry);this.slothlet.handlers.permissionManager.addRule({caller:callerPattern,target,effect:"allow",condition},moduleID)}}}return moduleID}async _rollbackFailedVersionedAdd({moduleID,effectivePath,normalizedPath}){let addIndex=-1;for(let i=this.state.operationHistory.length-1;i>=0;i--){const entry=this.state.operationHistory[i];if(entry?.type==="add"&&entry?.apiPath===normalizedPath&&entry?.moduleID===moduleID){addIndex=i;break}}if(addIndex!==-1){this.state.operationHistory.splice(addIndex,1)}this.state.addHistory=this.state.addHistory.filter(entry=>entry?.moduleID!==moduleID);try{await this.removeApiComponent(moduleID||effectivePath,{recordHistory:false})}catch{}}#sweepOrphanedCaches(){const cacheManager=this.slothlet.handlers?.apiCacheManager;if(!cacheManager)return;const root=this.slothlet.boundApi;for(const moduleID of cacheManager.getAllModuleIDs()){const entry=cacheManager.get(moduleID);if(!entry||entry.endpoint==="."||entry.endpoint==="")continue;const parts=String(entry.endpoint).split(".").filter(Boolean);if(this.getValueAtPath(root,parts)===void 0){cacheManager.delete(moduleID)}}}async removeApiComponent(pathOrModuleId,options={}){const recordHistory=options.recordHistory!==false;if(typeof pathOrModuleId!=="string"||!pathOrModuleId){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"pathOrModuleId",expected:"non-empty string",received:typeof pathOrModuleId,validationError:true})}let apiPath=null;let moduleID=null;if(this.slothlet.handlers.ownership){const candidateModuleID=pathOrModuleId.split(":")[0];const registeredModules=Array.from(this.slothlet.handlers.ownership.moduleToPath.keys());let matchingModule=null;for(let i=registeredModules.length-1;i>=0;i--){const candidate=registeredModules[i];if(candidate===candidateModuleID||candidate.startsWith(`${candidateModuleID}_`)){matchingModule=candidate;break}}if(matchingModule){moduleID=matchingModule}else{const owner=this.slothlet.handlers.ownership.getCurrentOwner(pathOrModuleId);if(owner){apiPath=pathOrModuleId;moduleID=owner.moduleID}else{return false}}}else{const isModuleId=!pathOrModuleId.includes(".");apiPath=isModuleId?null:pathOrModuleId;moduleID=isModuleId?pathOrModuleId.split(":")[0]:null}if(!this.slothlet||!this.slothlet.isLoaded){throw new this.SlothletError("INVALID_CONFIG_NOT_LOADED",{operation:"removeApi",validationError:true})}if(apiPath&&moduleID){const normalizedPath2=this.normalizeApiPath(apiPath).apiPath;const moduleIDKey=String(moduleID);const history=this.slothlet.handlers.ownership?.getPathHistory?.(normalizedPath2)||[];const ownershipResult2=this.slothlet.handlers.ownership?.removePath?.(normalizedPath2,moduleIDKey)||{action:"none",removedModuleId:null,restoreModuleId:null};const pathParts=this.normalizeApiPath(apiPath).parts;if(ownershipResult2.action==="delete"){await this.deletePath(this.slothlet.api,pathParts);await this.deletePath(this.slothlet.boundApi,pathParts);if(this.slothlet.handlers.metadata){const rootSegment=normalizedPath2.split(".")[0];this.slothlet.handlers.metadata.removeUserMetadataByApiPath(rootSegment)}if(this.slothlet.handlers.versionManager){const versionKey=this.slothlet.handlers.versionManager.getVersionKeyForModule(moduleIDKey);if(versionKey){this.slothlet.handlers.versionManager.unregisterVersion(versionKey.logicalPath,versionKey.versionTag)}if(this.slothlet.handlers.versionManager.hasDispatcher(normalizedPath2)){this.slothlet.handlers.versionManager.teardownDispatcher(normalizedPath2)}}this.#sweepOrphanedCaches();this.state.operationHistory.push({type:"remove",apiPath:normalizedPath2});return true}if(ownershipResult2.action==="restore"){const restoredValue=this.slothlet.handlers.ownership?.getCurrentValue?.(normalizedPath2);const restoredModuleId=this.slothlet.handlers.ownership?.getCurrentOwner?.(normalizedPath2)?.moduleID;if(restoredValue!==void 0&&restoredModuleId){await this.setValueAtPath(this.slothlet.api,pathParts,restoredValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:restoredModuleId});await this.setValueAtPath(this.slothlet.boundApi,pathParts,restoredValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:restoredModuleId});this.state.operationHistory.push({type:"remove",apiPath:normalizedPath2});return true}await this.restoreApiPath(normalizedPath2,ownershipResult2.restoreModuleId);this.state.operationHistory.push({type:"remove",apiPath:normalizedPath2});return true}if(ownershipResult2.action==="none"&&history.length===0){await this.deletePath(this.slothlet.api,pathParts);await this.deletePath(this.slothlet.boundApi,pathParts);return true}return false}if(moduleID){const moduleIDKey=String(moduleID);const result=this.slothlet.handlers.ownership?.unregister?.(moduleIDKey)||{removed:[],rolledBack:[]};if(this.slothlet.handlers.versionManager){const versionKey=this.slothlet.handlers.versionManager.getVersionKeyForModule(moduleIDKey);if(versionKey){this.slothlet.handlers.versionManager.unregisterVersion(versionKey.logicalPath,versionKey.versionTag)}}const allPaths=[...result.removed,...result.rolledBack.map(r=>r.apiPath)];const uniquePaths=[...new Set(allPaths)];const pathsToDelete=[];const pathsToRollback=[];for(const path2 of uniquePaths){const currentOwner=this.slothlet.handlers.ownership?.getCurrentOwner?.(path2);const hasChildrenWithOtherOwners=uniquePaths.some(p=>{if(p===path2||!p.startsWith(path2+"."))return false;const childOwner=this.slothlet.handlers.ownership?.getCurrentOwner?.(p);return childOwner&&childOwner.moduleID!==moduleIDKey});if(currentOwner&&currentOwner.moduleID!==moduleIDKey){pathsToRollback.push({apiPath:path2,restoredTo:currentOwner.moduleID})}else if(!hasChildrenWithOtherOwners){pathsToDelete.push(path2)}}pathsToDelete.sort((a,b)=>{const depthA=(a.match(/\./g)||[]).length;const depthB=(b.match(/\./g)||[]).length;return depthB-depthA});for(const removedPath of pathsToDelete){const{parts:parts2}=this.normalizeApiPath(removedPath);await this.deletePath(this.slothlet.api,parts2);await this.deletePath(this.slothlet.boundApi,parts2);if(this.slothlet.handlers.metadata){const rootSegment=removedPath.split(".")[0];this.slothlet.handlers.metadata.removeUserMetadataByApiPath(rootSegment)}}if(pathsToDelete.length>0){const rootSegment=pathsToDelete[0].split(".")[0];if(rootSegment in this.slothlet.api){delete this.slothlet.api[rootSegment]}if(rootSegment in this.slothlet.boundApi){delete this.slothlet.boundApi[rootSegment]}}for(const rollback of pathsToRollback){const{parts:parts2}=this.normalizeApiPath(rollback.apiPath);const previousImpl=this.slothlet.handlers.ownership?.getCurrentValue?.(rollback.apiPath);if(previousImpl!==void 0){const existingWrapper=this.getValueAtPath(this.slothlet.api,parts2);const existingWrapperRaw=resolveWrapper(existingWrapper);if(existingWrapperRaw){existingWrapperRaw.___setImpl(previousImpl,rollback.restoredTo)}const existingBoundWrapper=this.getValueAtPath(this.slothlet.boundApi,parts2);const existingBoundWrapperRaw=resolveWrapper(existingBoundWrapper);if(existingBoundWrapperRaw){existingBoundWrapperRaw.___setImpl(previousImpl,rollback.restoredTo)}}}this.state.addHistory=this.state.addHistory.filter(entry=>String(entry.moduleID)!==moduleIDKey);if(this.slothlet.handlers.apiCacheManager){const deleted=this.slothlet.handlers.apiCacheManager.delete(moduleIDKey);if(deleted){this.slothlet.debug("cache",{key:"DEBUG_MODE_CACHE_DELETED_MODULE_REMOVED",moduleID:moduleIDKey})}}this.#sweepOrphanedCaches();if(recordHistory&&pathsToDelete.length>0){const rootSegment=pathsToDelete[0].split(".")[0];this.state.operationHistory.push({type:"remove",apiPath:rootSegment})}return pathsToDelete.length>0||pathsToRollback.length>0}if(!apiPath){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,reason:translate("API_PATH_REASON_REQUIRED"),index:void 0,segment:void 0,validationError:true})}const{apiPath:normalizedPath,parts}=this.normalizeApiPath(apiPath);const ownershipResult=this.slothlet.handlers.ownership?.removePath?.(normalizedPath,null)||{action:"none",removedModuleId:null,restoreModuleId:null};const pathExists=this.getValueAtPath(this.slothlet.api,parts)!==void 0;if(ownershipResult.action==="none"){if(pathExists){await this.deletePath(this.slothlet.api,parts);await this.deletePath(this.slothlet.boundApi,parts);if(this.slothlet.handlers.metadata){this.slothlet.handlers.metadata.removeUserMetadataByApiPath(normalizedPath)}if(recordHistory){this.state.operationHistory.push({type:"remove",apiPath:normalizedPath})}return true}return false}if(ownershipResult.action==="delete"){await this.deletePath(this.slothlet.api,parts);await this.deletePath(this.slothlet.boundApi,parts);if(this.slothlet.handlers.metadata){this.slothlet.handlers.metadata.removeUserMetadataByApiPath(normalizedPath)}if(recordHistory){this.state.operationHistory.push({type:"remove",apiPath:normalizedPath})}return true}if(ownershipResult.action==="restore"){const restoredValue=this.slothlet.handlers.ownership?.getCurrentValue?.(normalizedPath);const restoredModuleId=this.slothlet.handlers.ownership?.getCurrentOwner?.(normalizedPath)?.moduleID;if(restoredValue!==void 0&&restoredModuleId){await this.setValueAtPath(this.slothlet.api,parts,restoredValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:restoredModuleId});await this.setValueAtPath(this.slothlet.boundApi,parts,restoredValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:restoredModuleId});if(recordHistory){this.state.operationHistory.push({type:"remove",apiPath:normalizedPath})}return true}await this.restoreApiPath(normalizedPath,ownershipResult.restoreModuleId);if(recordHistory){this.state.operationHistory.push({type:"remove",apiPath:normalizedPath})}return true}return false}async reloadApiComponent(params){const{apiPath,moduleID,options}=params||{};if(!this.slothlet||!this.slothlet.isLoaded){throw new this.SlothletError("INVALID_CONFIG_NOT_LOADED",{operation:"reloadApi",validationError:true})}if(moduleID){await this._reloadByModuleID(moduleID);return}if(apiPath){await this._reloadByApiPath(apiPath,options);return}throw new this.SlothletError("INVALID_ARGUMENT",{argument:"params",expected:"{ moduleID } or { apiPath }",received:params,validationError:true})}async _reloadByModuleID(moduleID,{forceReplace=true}={}){const cacheManager=this.slothlet.handlers.apiCacheManager;if(!cacheManager){throw new this.SlothletError("CACHE_MANAGER_NOT_AVAILABLE",{operation:"reload",validationError:true})}if(!cacheManager.has(moduleID)){throw new this.SlothletError("CACHE_NOT_FOUND",{moduleID,operation:"reload",validationError:true})}const oldEntry=cacheManager.get(moduleID);this.slothlet.debug("reload",{key:"DEBUG_MODE_RELOADING_MODULE_BY_ID",moduleID,endpoint:oldEntry.endpoint,folderPath:oldEntry.folderPath});const freshApi=await cacheManager.rebuildCache(moduleID);cacheManager.set(moduleID,{...oldEntry,api:freshApi,timestamp:Date.now()});this.slothlet.debug("reload",{key:"DEBUG_MODE_FRESH_API_KEYS_BEFORE_RESTORE",moduleID,endpoint:oldEntry.endpoint,freshApiKeys:Object.keys(freshApi||{})});await this._restoreApiTree(freshApi,oldEntry.endpoint,moduleID,oldEntry.collisionMode,forceReplace);this.slothlet.debug("reload",{key:"DEBUG_MODE_FRESH_API_KEYS_AFTER_RESTORE",moduleID,endpoint:oldEntry.endpoint,freshApiKeys:Object.keys(freshApi||{})});this.slothlet.debug("reload",{key:"DEBUG_MODE_MODULE_RELOAD_COMPLETE",moduleID});if(this.slothlet.handlers.versionManager){this.slothlet.handlers.versionManager.onVersionedModuleReload(moduleID)}}async _reloadByApiPath(apiPath,options={}){this.slothlet.debug("reload",{key:"DEBUG_MODE_RELOADING_BY_API_PATH",apiPath});const moduleIDsToReload=this._findAffectedCaches(apiPath);if(moduleIDsToReload.length===0){this.slothlet.debug("reload",{key:"DEBUG_MODE_NO_CACHES_ATTEMPTING_RESTORE",apiPath});if(apiPath!=="."&&apiPath!==""){await this.restoreApiPath(apiPath,"base")}return}const cacheManager=this.slothlet.handlers.apiCacheManager;moduleIDsToReload.sort((a,b)=>{const entryA=cacheManager.get(a);const entryB=cacheManager.get(b);if(entryA?.endpoint==="."&&entryB?.endpoint!==".")return-1;if(entryB?.endpoint==="."&&entryA?.endpoint!==".")return 1;const indexA=this.state.addHistory.findIndex(h=>h.moduleID===a);const indexB=this.state.addHistory.findIndex(h=>h.moduleID===b);return indexA-indexB});const endpointOrder=new Map;for(const moduleID of moduleIDsToReload){const entry=cacheManager.get(moduleID);const ep=entry?.endpoint??".";if(!endpointOrder.has(ep))endpointOrder.set(ep,[]);endpointOrder.get(ep).push(moduleID)}for(const[,moduleIDs]of endpointOrder){for(let i=0;i<moduleIDs.length;i++){await this._reloadByModuleID(moduleIDs[i],{forceReplace:i===0})}}const reloadMetadata=options?.metadata;if(reloadMetadata&&typeof reloadMetadata==="object"&&Object.keys(reloadMetadata).length>0){if(this.slothlet.handlers.metadata){const targetPath=apiPath==="."?null:apiPath.split(".")[0];if(targetPath){this.slothlet.handlers.metadata.registerUserMetadata(targetPath,reloadMetadata)}}}this.slothlet.debug("reload",{key:"DEBUG_MODE_API_PATH_RELOAD_COMPLETE",apiPath,reloadedModules:moduleIDsToReload.length,loadOrder:moduleIDsToReload})}_findAffectedCaches(apiPath){const cacheManager=this.slothlet.handlers.apiCacheManager;if(!cacheManager)return[];const allModuleIDs=cacheManager.getAllModuleIDs();if(apiPath==="."||apiPath===""||apiPath==null){const baseModules=[];for(const moduleID of allModuleIDs){const entry=cacheManager.get(moduleID);if(entry&&entry.endpoint==="."){baseModules.push(moduleID)}}return baseModules}const exactMatches=[];for(const moduleID of allModuleIDs){const entry=cacheManager.get(moduleID);if(entry&&entry.endpoint===apiPath){exactMatches.push(moduleID)}}if(exactMatches.length>0)return exactMatches;const children=[];const pathPrefix=apiPath+".";for(const moduleID of allModuleIDs){const entry=cacheManager.get(moduleID);if(entry?.endpoint?.startsWith(pathPrefix)){children.push(moduleID)}}if(children.length>0)return children;const ownership=this.slothlet.handlers.ownership;const history=ownership?.getPathHistory?.(apiPath);if(history&&history.length>0){const owned=[];for(const{moduleID}of history){if(cacheManager.has(moduleID)){owned.push(moduleID)}}if(owned.length>0)return owned}let bestMatch=null;let bestLength=-1;for(const moduleID of allModuleIDs){const entry=cacheManager.get(moduleID);if(!entry?.endpoint)continue;const ep=entry.endpoint;if(ep==="."||apiPath.startsWith(ep+".")){if(ep.length>bestLength){bestLength=ep.length;bestMatch=moduleID}}}if(bestMatch)return[bestMatch];return[]}_collectCustomProperties(existingProxy,freshApi){const customProps={};if(!existingProxy||typeof existingProxy!=="object"&&typeof existingProxy!=="function"){return customProps}const wrapper=resolveWrapper(existingProxy);if(!wrapper){return customProps}const freshKeys=new Set(freshApi?Object.keys(freshApi):[]);const ownKeys=Object.keys(wrapper).filter(k=>!ComponentBase.INTERNAL_KEYS.has(k));for(const key of ownKeys){try{const val=wrapper[key];if(val&&(typeof val==="object"||typeof val==="function")&&resolveWrapper(val)){continue}if(!freshKeys.has(key)){customProps[key]=val}else{customProps[key]=val}}catch{}}return customProps}_restoreCustomProperties(proxy,customProps){if(!proxy||!customProps||typeof customProps!=="object"){return}for(const[key,value]of Object.entries(customProps)){try{proxy[key]=value}catch{}}}async _restoreApiTree(freshApi,endpoint,moduleID,collisionMode,forceReplace=true){if(!freshApi||typeof freshApi!=="object"&&typeof freshApi!=="function"){return}const parts=endpoint==="."?[]:endpoint.split(".");if(parts.length===0){for(const key of Object.keys(freshApi)){if(typeof key==="string"&&(key.startsWith("_")||key.startsWith("__")))continue;if(key==="slothlet"||key==="shutdown"||key==="destroy")continue;const existingAtKey=this.slothlet.api[key];const freshValue=freshApi[key];if(existingAtKey&&resolveWrapper(existingAtKey)!==null){const customProps=this._collectCustomProperties(existingAtKey,freshValue);const freshWrapper=resolveWrapper(freshValue);const isLazyFresh=freshWrapper&&freshWrapper.____slothletInternal.mode==="lazy"&&!freshWrapper.____slothletInternal.state.materialized&&typeof freshWrapper.____slothletInternal.materializeFunc==="function";this.slothlet.debug("reload",{key:"DEBUG_MODE_RESTORE_ROOT_KEY_INSPECT",rootKey:key,hasFreshWrapper:!!freshWrapper,freshMode:freshWrapper?.____slothletInternal.mode,freshMaterialized:freshWrapper?.____slothletInternal.state?.materialized,hasMaterializeFunc:typeof freshWrapper?.____slothletInternal.materializeFunc==="function",isLazyFresh,existingMaterialized:resolveWrapper(existingAtKey)?.____slothletInternal?.state?.materialized});if(isLazyFresh){resolveWrapper(existingAtKey).___resetLazy(freshWrapper.____slothletInternal.materializeFunc);this._restoreCustomProperties(existingAtKey,customProps);this.slothlet.debug("reload",{key:"DEBUG_MODE_ROOT_KEY_RESET_LAZY",rootKey:key,restoredCustomProps:Object.keys(customProps)})}else{let implForReload;if(freshValue&&resolveWrapper(freshValue)!==null){implForReload=freshWrapper?UnifiedWrapper._extractFullImpl(freshWrapper):freshValue}else{implForReload=freshValue}if(typeof implForReload==="function"){const extracted={};for(const k of Object.keys(implForReload)){extracted[k]=implForReload[k]}implForReload=extracted}const wrapper=resolveWrapper(existingAtKey);const originalCollisionMode=wrapper?wrapper.____slothletInternal.state.collisionMode:null;if(forceReplace&&wrapper){wrapper.____slothletInternal.state.collisionMode="replace"}if(wrapper&&originalCollisionMode!==null){wrapper.____slothletInternal.state.collisionMode=originalCollisionMode}this._restoreCustomProperties(existingAtKey,customProps);this.slothlet.debug("reload",{key:"DEBUG_MODE_ROOT_KEY_UPDATED_SETIMPL",rootKey:key,restoredCustomProps:Object.keys(customProps)})}}else if(existingAtKey===void 0){const cacheManager=this.slothlet.handlers.apiCacheManager;const cacheEntry=cacheManager.get(moduleID);const resolvedFolderPath=cacheEntry?.folderPath||"";await this.setValueAtPath(this.slothlet.api,[key],freshValue,{mutateExisting:true,collisionMode,moduleID,sourceFolder:resolvedFolderPath});if(this.slothlet.boundApi){await this.setValueAtPath(this.slothlet.boundApi,[key],freshValue,{mutateExisting:true,collisionMode,moduleID,sourceFolder:resolvedFolderPath})}}}}else{const existing=this.getValueAtPath(this.slothlet.api,parts);this.slothlet.debug("reload",{key:"DEBUG_MODE_RESTORE_NESTED_PATH",endpoint,moduleID,partsPath:parts.join("."),existingFound:!!existing,hasSetImpl:existing?resolveWrapper(existing)!==null:false,freshApiKeys:Object.keys(freshApi)});if(existing&&resolveWrapper(existing)!==null){const customProps=this._collectCustomProperties(existing,freshApi);const wrapper=resolveWrapper(existing);const originalCollisionMode=wrapper?wrapper.____slothletInternal.state.collisionMode:null;if(forceReplace&&wrapper){wrapper.____slothletInternal.state.collisionMode="replace";this.slothlet.debug("reload",{key:"DEBUG_MODE_RESTORE_FORCING_REPLACE",endpoint,originalCollisionMode,wrapperApiPath:wrapper.____slothletInternal.apiPath})}let implForReload;if(resolveWrapper(freshApi)!==null){const freshWrapper=resolveWrapper(freshApi);implForReload=freshWrapper?UnifiedWrapper._extractFullImpl(freshWrapper):freshApi}else if(typeof freshApi==="function"){implForReload={};for(const key of Object.keys(freshApi)){implForReload[key]=freshApi[key]}}else{implForReload=freshApi}if(parts.length>0&&implForReload&&typeof implForReload==="object"){const lastEndpointPart=parts[parts.length-1];if(lastEndpointPart&&Object.prototype.hasOwnProperty.call(implForReload,lastEndpointPart)){const dupValue=implForReload[lastEndpointPart];const dupWrapperForDedup=resolveWrapper(dupValue);if(dupWrapperForDedup){const hoisted={};for(const k of Object.keys(implForReload)){if(k!==lastEndpointPart)hoisted[k]=implForReload[k]}for(const k of Object.keys(dupWrapperForDedup).filter(k2=>!k2.startsWith("_")&&!k2.startsWith("__"))){hoisted[k]=dupWrapperForDedup[k]}implForReload=hoisted}}}if(implForReload&&typeof implForReload==="object"){for(const key of Object.keys(implForReload)){const val=implForReload[key];if(resolveWrapper(val)!==null){const childWrapper=resolveWrapper(val);if(childWrapper.____slothletInternal.state.materialized){implForReload[key]=UnifiedWrapper._extractFullImpl(childWrapper)}}}}resolveWrapper(existing).___setImpl(implForReload,moduleID);if(wrapper&&originalCollisionMode!==null){wrapper.____slothletInternal.state.collisionMode=originalCollisionMode}this._restoreCustomProperties(existing,customProps);this.slothlet.debug("reload",{key:"DEBUG_MODE_UPDATED_WRAPPER_IMPL",endpoint,moduleID,forcedReplaceMode:true,restoredCustomProps:Object.keys(customProps)})}else{const cacheManager=this.slothlet.handlers.apiCacheManager;const cacheEntry=cacheManager.get(moduleID);const resolvedFolderPath=cacheEntry?.folderPath||"";let implForContainer=freshApi;if(typeof freshApi==="function"){implForContainer={};for(const key of Object.keys(freshApi)){implForContainer[key]=freshApi[key]}}const containerWrapper=new UnifiedWrapper(this.slothlet,{apiPath:endpoint,mode:this.____config.mode,moduleID,filePath:resolvedFolderPath,sourceFolder:resolvedFolderPath});containerWrapper.___setImpl(implForContainer,moduleID);const apiToSet=containerWrapper.createProxy();await this.setValueAtPath(this.slothlet.api,parts,apiToSet,{mutateExisting:true,collisionMode,moduleID,sourceFolder:resolvedFolderPath});if(this.slothlet.boundApi){await this.setValueAtPath(this.slothlet.boundApi,parts,apiToSet,{mutateExisting:true,collisionMode,moduleID,sourceFolder:resolvedFolderPath})}this.slothlet.debug("reload",{key:"DEBUG_MODE_CREATED_NEW_WRAPPER_UNEXPECTED",endpoint,moduleID})}}}}export{ApiManager};
17
+ import{translate}from"@cldmv/slothlet/i18n";import{ComponentBase}from"@cldmv/slothlet/factories/component-base";import{UnifiedWrapper,resolveWrapper}from"@cldmv/slothlet/handlers/unified-wrapper";import{fsp,path}from"@cldmv/slothlet/helpers/platform";class ApiManager extends ComponentBase{static slothletProperty="apiManager";constructor(slothlet){super(slothlet);this.state={addHistory:[],initialConfig:slothlet?.config||null,operationHistory:[]}}normalizeApiPath(apiPath){if(apiPath===""||apiPath===null||apiPath===void 0){return{apiPath:"",parts:[]}}if(Array.isArray(apiPath)){if(apiPath.length===0){return{apiPath:"",parts:[]}}for(let i=0;i<apiPath.length;i++){if(typeof apiPath[i]!=="string"){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,segment:apiPath[i],index:i,reason:translate("API_PATH_REASON_ARRAY_ELEMENTS"),validationError:true})}if(apiPath[i].trim()===""){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,segment:apiPath[i],index:i,reason:translate("API_PATH_REASON_ARRAY_EMPTY_SEGMENTS"),validationError:true})}}if(apiPath[0]==="slothlet"||apiPath.length===1&&(apiPath[0]==="shutdown"||apiPath[0]==="destroy")){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,reason:translate("API_PATH_REASON_RESERVED_NAME"),index:void 0,segment:void 0,validationError:true})}return{apiPath:apiPath.join("."),parts:apiPath}}if(typeof apiPath!=="string"){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,reason:translate("API_PATH_REASON_INVALID_TYPE"),index:void 0,segment:void 0,validationError:true})}const normalized=apiPath.trim();if(normalized===""){return{apiPath:"",parts:[]}}const parts=normalized.split(".");if(parts.length===0||parts.some(part=>part.trim()==="")){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath:normalized,reason:translate("API_PATH_REASON_EMPTY_SEGMENTS"),index:void 0,segment:void 0,validationError:true})}if(parts[0]==="slothlet"||normalized==="shutdown"||normalized==="destroy"){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath:normalized,reason:translate("API_PATH_REASON_RESERVED_NAME"),index:void 0,segment:void 0,validationError:true})}return{apiPath:normalized,parts}}async resolvePath(inputPath){if(!inputPath||typeof inputPath!=="string"){throw new this.SlothletError("INVALID_CONFIG_DIR_INVALID",{dir:inputPath,validationError:true})}const resolvedPath=this.slothlet.helpers.resolver.resolvePathFromCaller(inputPath);if(this.slothlet.envTarget==="browser"){const lastSegment=resolvedPath.split("/").pop();const isFile=lastSegment.includes(".");return{resolvedPath,isDirectory:!isFile,isFile}}try{const stats=await fsp.stat(resolvedPath);return{resolvedPath,isDirectory:stats.isDirectory(),isFile:stats.isFile()}}catch(error){if(error instanceof this.SlothletError){throw error}throw new this.SlothletError("INVALID_CONFIG_DIR_INVALID",{dir:resolvedPath,validationError:true})}}async resolveFolderPath(folderPath){if(!folderPath||typeof folderPath!=="string"){throw new this.SlothletError("INVALID_CONFIG_DIR_INVALID",{dir:folderPath,validationError:true})}const resolvedPath=this.slothlet.helpers.resolver.resolvePathFromCaller(folderPath);try{const stats=await fsp.stat(resolvedPath);if(!stats.isDirectory()){throw new this.SlothletError("INVALID_CONFIG_DIR_INVALID",{dir:resolvedPath,validationError:true})}}catch(error){if(error instanceof this.SlothletError){throw error}throw new this.SlothletError("INVALID_CONFIG_DIR_INVALID",{dir:resolvedPath,validationError:true})}return resolvedPath}buildDefaultModuleId(apiPath,____resolvedFolderPath){const randomSuffix=Math.random().toString(36).substring(2,8);const prefix=apiPath||"auto";return`${prefix}_${randomSuffix}`}getValueAtPath(root,parts){let current=root;for(const part of parts){if(!current||typeof current!=="object"&&typeof current!=="function"){return void 0}current=current[part]}return current}ensureParentPath(root,parts,options={}){const{moduleID,sourceFolder}=options;let current=root;for(let i=0;i<parts.length-1;i+=1){const part=parts[i];const next=current[part];if(next===void 0){const containerPath=parts.slice(0,i+1).join(".");const containerWrapper=new UnifiedWrapper(this.slothlet,{mode:this.____config.mode,apiPath:containerPath,moduleID,sourceFolder});containerWrapper.___setImpl({},moduleID);current[part]=containerWrapper.createProxy();current=current[part];continue}if(next&&(typeof next==="object"||typeof next==="function")){current=next;continue}throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath:parts.slice(0,i+1).join("."),reason:translate("API_PATH_REASON_NOT_TRAVERSABLE"),index:void 0,segment:void 0,validationError:true})}return current}setOwnedProperty(apiPath,value,callerWrapper){const{parts}=this.normalizeApiPath(apiPath);if(parts.length===0){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath:String(apiPath??""),reason:translate("API_PATH_REASON_EMPTY_SEGMENTS"),index:void 0,segment:void 0,validationError:true})}const RESERVED=new Set(["__proto__","prototype","constructor"]);for(const segment of parts){if(RESERVED.has(segment)){throw new this.SlothletError("LOOSE_SET_RESERVED_KEY",{apiPath:parts.join("."),segment,validationError:true})}}const callerModuleID=callerWrapper?.____slothletInternal?.moduleID??null;const callerWrapperPath=callerWrapper?.____slothletInternal?.apiPath??null;let ownedRoot=null;if(callerModuleID&&this.slothlet.handlers?.ownership?.getModuleEndpoint){const endpoint=this.slothlet.handlers.ownership.getModuleEndpoint(callerModuleID);if(endpoint!=null){ownedRoot=endpoint}}if(ownedRoot===null)ownedRoot=".";if(ownedRoot&&ownedRoot!=="."&&ownedRoot!==""){const ownedParts=String(ownedRoot).split(".").filter(Boolean);const isUnderOwn=ownedParts.length<=parts.length&&ownedParts.every((seg,i)=>parts[i]===seg);if(!isUnderOwn){const callerApiPath=callerWrapperPath??ownedRoot;throw new this.SlothletError("LOOSE_SET_NOT_OWNED",{apiPath:parts.join("."),callerApiPath,validationError:true})}}const callerSourceFolder=callerWrapper?.____slothletInternal?.sourceFolder??null;const root=this.slothlet.boundApi;const parent=this.ensureParentPath(root,parts,{moduleID:callerModuleID,sourceFolder:callerSourceFolder});const finalKey=parts[parts.length-1];const isWrappable=typeof value==="function"||value!==null&&typeof value==="object";if(isWrappable){const wrapper=new UnifiedWrapper(this.slothlet,{mode:this.____config.mode,apiPath:parts.join("."),moduleID:callerModuleID,isCallable:typeof value==="function",sourceFolder:callerSourceFolder});wrapper.___setImpl(value,callerModuleID);parent[finalKey]=wrapper.createProxy()}else{parent[finalKey]=value}}isWrapperProxy(value){return!!(value&&(typeof value==="object"||typeof value==="function")&&resolveWrapper(value)!==null)}async syncWrapper(existingProxy,nextProxy,config,collisionMode="replace",moduleID=null){if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_ENTRY_EXISTING",apiPath:resolveWrapper(existingProxy)?.apiPath});this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_ENTRY_NEXT",apiPath:resolveWrapper(nextProxy)?.apiPath})}if(!this.isWrapperProxy(existingProxy)||!this.isWrapperProxy(nextProxy)){return false}const existingWrapper=resolveWrapper(existingProxy)??existingProxy;const nextWrapper=resolveWrapper(nextProxy)??nextProxy;if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_EXISTING",apiPath:existingWrapper.apiPath});this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_NEXT",apiPath:nextWrapper.apiPath})}if(nextWrapper.____slothletInternal.materializeFunc&&collisionMode!=="merge"){existingWrapper.____slothletInternal.materializeFunc=nextWrapper.____slothletInternal.materializeFunc}const existingChildKeys=Object.keys(existingWrapper).filter(k=>!k.startsWith("_")&&!k.startsWith("__"));const nextChildKeys=Object.keys(nextWrapper).filter(k=>!k.startsWith("_")&&!k.startsWith("__"));if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_BEFORE_MERGE",existingCacheSize:existingChildKeys.length,nextCacheSize:nextChildKeys.length});this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_NEXT_IMPL_KEYS",implKeys:Object.keys(nextWrapper.____slothletInternal.impl||{})});this.slothlet.debug("api",{key:"DEBUG_MODE_SYNC_WRAPPER_NEXT_CHILDCACHE_KEYS",childCacheKeys:nextChildKeys})}if(collisionMode==="replace"){if(existingWrapper.___setImpl&&nextWrapper.____slothletInternal.impl!==void 0){existingWrapper.___setImpl(nextWrapper.____slothletInternal.impl,moduleID)}else if(nextWrapper.____slothletInternal.impl===void 0){existingWrapper.____slothletInternal.impl=null}else{if(nextWrapper.____slothletInternal.impl!==void 0){existingWrapper.____slothletInternal.impl=nextWrapper.____slothletInternal.impl;if(typeof nextWrapper.____slothletInternal.impl==="function"||nextWrapper.____slothletInternal.impl&&typeof nextWrapper.____slothletInternal.impl.default==="function"){existingWrapper.isCallable=true}}}for(const key of existingChildKeys){delete existingWrapper[key]}existingWrapper.___adoptImplChildren();for(const key of nextChildKeys){const childValue=nextWrapper[key];Object.defineProperty(existingWrapper,key,{value:childValue,writable:false,enumerable:true,configurable:true})}}else if(collisionMode==="merge"){for(const key of nextChildKeys){const isInternal=typeof key==="string"&&(key.startsWith("_")||key.startsWith("__"));if(!isInternal&&!Object.prototype.hasOwnProperty.call(existingWrapper,key)){const childValue=nextWrapper[key];Object.defineProperty(existingWrapper,key,{value:childValue,writable:false,enumerable:true,configurable:true})}else if(!isInternal){const existingChild=existingWrapper[key];const nextChild=nextWrapper[key];if(this.isWrapperProxy(existingChild)&&this.isWrapperProxy(nextChild)){const syncWrapper_nextChildWrapper=resolveWrapper(nextChild)??nextChild;const syncWrapper_hasGrandChildren=Object.keys(syncWrapper_nextChildWrapper).some(k=>!k.startsWith("_")&&!k.startsWith("__"));if(syncWrapper_hasGrandChildren){await this.syncWrapper(existingChild,nextChild,config,collisionMode,moduleID)}}}}}else if(collisionMode==="merge-replace"){for(const key of nextChildKeys){const childValue=nextWrapper[key];const isInternal=typeof key==="string"&&(key.startsWith("_")||key.startsWith("__"));if(!isInternal&&Object.prototype.hasOwnProperty.call(existingWrapper,key)){const existingChild=existingWrapper[key];if(this.isWrapperProxy(existingChild)&&this.isWrapperProxy(childValue)){await this.syncWrapper(existingChild,childValue,config,collisionMode,moduleID)}else{delete existingWrapper[key];Object.defineProperty(existingWrapper,key,{value:childValue,writable:false,enumerable:true,configurable:true})}}else if(!isInternal){Object.defineProperty(existingWrapper,key,{value:childValue,writable:false,enumerable:true,configurable:true})}}}if(existingWrapper.____slothletInternal.state){const isActuallyMaterialized=existingWrapper.____slothletInternal.impl&&typeof existingWrapper.____slothletInternal.impl!=="function";existingWrapper.____slothletInternal.state.materialized=isActuallyMaterialized;existingWrapper.____slothletInternal.state.inFlight=false}return true}async mutateApiValue(existingValue,nextValue,options,config){if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_CALLED",existingType:typeof existingValue,nextType:typeof nextValue});this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_WRAPPER_STATUS",existingIsWrapper:this.isWrapperProxy(existingValue),nextIsWrapper:this.isWrapperProxy(nextValue)});this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_NEXT_VALUE",nextValue});this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_NEXT_VALUE_KEYS",nextValueKeys:nextValue?Object.keys(nextValue):[]})}if(existingValue===nextValue){return}if(this.isWrapperProxy(existingValue)&&this.isWrapperProxy(nextValue)){if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_SYNC_WRAPPERS"})}await this.syncWrapper(existingValue,nextValue,config,options.collisionMode,options.moduleID);return}if(this.isWrapperProxy(existingValue)&&!this.isWrapperProxy(nextValue)){const nextIsObjectLike=nextValue&&(typeof nextValue==="object"||typeof nextValue==="function");const nextHasKeys=nextIsObjectLike&&Object.keys(nextValue).length>0;if(nextHasKeys){if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_MERGE_INTO_WRAPPER"});this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_MERGE_KEYS",keys:Object.keys(nextValue)})}await this.slothlet.builders.apiAssignment.mergeApiObjects(existingValue,nextValue,{removeMissing:options.removeMissing,mutateExisting:true,allowOverwrite:true,syncWrapper:this.syncWrapper.bind(this),collisionMode:options.collisionMode,moduleID:options.moduleID});return}const existingValueRaw=resolveWrapper(existingValue);if(existingValueRaw!==null){if(config?.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_MUTATE_API_VALUE_SETIMPL_FALLBACK"})}existingValueRaw.___setImpl(resolveWrapper(nextValue)?.__impl??nextValue);return}}if(existingValue&&typeof existingValue==="object"&&nextValue&&typeof nextValue==="object"){await this.slothlet.builders.apiAssignment.mergeApiObjects(existingValue,nextValue,{removeMissing:options.removeMissing,mutateExisting:true,allowOverwrite:true,syncWrapper:this.syncWrapper.bind(this),collisionMode:options.collisionMode,moduleID:options.moduleID});return existingValue}return nextValue}async setValueAtPath(root,parts,value,options){const parent=this.ensureParentPath(root,parts,{moduleID:options.moduleID,sourceFolder:options.sourceFolder});const finalKey=parts[parts.length-1];const existing=parent?parent[finalKey]:void 0;const collisionMode=options.collisionMode||"merge";const moduleID=options.moduleID;this.slothlet.debug("api",{key:"DEBUG_MODE_SET_VALUE_AT_PATH",finalKey,existingType:typeof existing,valueType:typeof value,collisionMode,options});if(existing!==void 0){if(collisionMode==="error"){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath:parts.join("."),reason:translate("API_PATH_REASON_COLLISION_ERROR"),index:void 0,segment:void 0,validationError:true})}if(collisionMode==="skip"){this.slothlet.debug("api",{key:"DEBUG_MODE_SET_VALUE_AT_PATH_SKIP_COLLISION",path:parts.join("."),mode:"skip"});return false}if(collisionMode==="warn"){if(this.slothlet&&!this.____config?.silent){new this.SlothletWarning("WARNING_HOT_RELOAD_PATH_COLLISION",{apiPath:parts.join(".")})}return false}if(collisionMode==="replace"){const existingIsObject=typeof existing==="object"||typeof existing==="function";const valueIsObject=typeof value==="object"||typeof value==="function";if(existingIsObject&&valueIsObject){this.slothlet.debug("api",{key:"DEBUG_MODE_SET_VALUE_AT_PATH_REPLACE_MERGE",path:parts.join("."),mode:"replace"});await this.mutateApiValue(existing,value,{removeMissing:false,allowOverwrite:true,collisionMode:"replace",moduleID},this.____config);return true}else{parent[finalKey]=value;return true}}if(collisionMode==="merge"||collisionMode==="merge-replace"){const existingIsObject=typeof existing==="object"||typeof existing==="function";const valueIsObject=typeof value==="object"||typeof value==="function";if(existingIsObject&&valueIsObject){this.slothlet.debug("api",{key:"DEBUG_MODE_SET_VALUE_AT_PATH_MERGE_PROPS",mode:collisionMode});await this.mutateApiValue(existing,value,{removeMissing:false,allowOverwrite:true,collisionMode},this.____config);return true}else{if(this.slothlet&&!this.____config?.silent){new this.SlothletWarning("WARNING_HOT_RELOAD_MERGE_PRIMITIVES",{apiPath:parts.join(".")})}return false}}}this.slothlet.debug("api",{key:"DEBUG_MODE_SET_VALUE_AT_PATH_ASSIGN",finalKey});parent[finalKey]=value;return true}async deletePath(root,parts){let current=root;const stack=[];for(const part of parts.slice(0,-1)){if(!current||typeof current!=="object"&&typeof current!=="function"){return false}stack.push({parent:current,key:part});current=current[part]}const finalKey=parts[parts.length-1];if(!current||typeof current!=="object"&&typeof current!=="function"){return false}if(!Object.prototype.hasOwnProperty.call(current,finalKey)){return false}const removedImpl=current[finalKey];const apiPath=parts.join(".");if(removedImpl&&this.slothlet.handlers?.lifecycle){const metadata=this.slothlet.handlers.metadata?.getMetadata?.(removedImpl);await this.slothlet.handlers.lifecycle.emit("impl:removed",{apiPath,impl:removedImpl,source:"removal",moduleID:metadata?.moduleID,filePath:metadata?.filePath,sourceFolder:metadata?.sourceFolder})}if(resolveWrapper(current)){const wrapper=resolveWrapper(current);const isInternal=typeof finalKey==="string"&&(finalKey.startsWith("_")||finalKey.startsWith("__"));if(!isInternal&&finalKey in wrapper){delete wrapper[finalKey]}if(wrapper.____slothletInternal.impl&&typeof wrapper.____slothletInternal.impl==="object"){delete wrapper.____slothletInternal.impl[finalKey]}}delete current[finalKey];if(removedImpl&&(typeof removedImpl==="object"||typeof removedImpl==="function")){if(resolveWrapper(removedImpl)){const wrapper=resolveWrapper(removedImpl);if(wrapper.____slothletInternal.impl!==void 0){wrapper.____slothletInternal.impl=null}const childKeys=Object.keys(wrapper).filter(k=>!k.startsWith("_")&&!k.startsWith("__"));for(const key of childKeys){delete wrapper[key]}if(wrapper.____slothletInternal.state){wrapper.____slothletInternal.state.materialized=false;wrapper.____slothletInternal.state.inFlight=false}}}if(this.slothlet.handlers?.metadata){const rootSegment=apiPath.split(".")[0];this.slothlet.handlers.metadata.removeUserMetadataByApiPath(rootSegment)}for(let i=stack.length-1;i>=0;i-=1){const{parent,key}=stack[i];const value=parent[key];if(value&&(typeof value==="object"||typeof value==="function")&&Object.keys(value).length===0){delete parent[key]}}return true}async restoreApiPath(apiPath,moduleID){const normalizedModuleId=moduleID||null;const historyEntry=this.state.addHistory.slice().reverse().find(entry=>entry.apiPath===apiPath&&(normalizedModuleId?entry.moduleID===normalizedModuleId:true));if(historyEntry){await this.addApiComponent({apiPath:historyEntry.apiPath,folderPath:historyEntry.folderPath,options:{...historyEntry.options,metadata:historyEntry.metadata,mutateExisting:true,forceOverwrite:true,collisionMode:"replace",recordHistory:false}});return}if(normalizedModuleId==="base"||normalizedModuleId==="core"){const baseApi=await this.slothlet.builders.builder.buildAPI({dir:this.____config.dir,mode:this.____config.mode,moduleID:"base"});const{parts}=this.normalizeApiPath(apiPath);let baseValue=this.getValueAtPath(baseApi,parts);if(baseValue===void 0){await this.deletePath(this.slothlet.api,parts);await this.deletePath(this.slothlet.boundApi,parts);return}const baseValueRaw=resolveWrapper(baseValue);if(baseValue&&baseValueRaw!==null){baseValue=baseValueRaw.__impl}await this.setValueAtPath(this.slothlet.api,parts,baseValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:normalizedModuleId});await this.setValueAtPath(this.slothlet.boundApi,parts,baseValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:normalizedModuleId})}}#normalizePermissionShorthandEntry(entry){const getValueType=value=>{if(value===null)return"null";if(Array.isArray(value))return"array";return typeof value};if(typeof entry==="string"){return{target:entry,condition:void 0}}if(!entry||typeof entry!=="object"||Array.isArray(entry)){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_NOT_OBJECT"),received:getValueType(entry),validationError:true})}if(typeof entry.target!=="string"||!entry.target){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_TARGET_REQUIRED"),received:typeof entry.target,validationError:true})}return{target:entry.target,condition:entry.condition}}async addApiComponent(params){const{apiPath,folderPath,options={},versionConfig=null}=params||{};if(Array.isArray(folderPath)){const moduleIDs=[];for(const singlePath of folderPath){const moduleID2=await this.addApiComponent({apiPath,folderPath:singlePath,options,versionConfig});moduleIDs.push(moduleID2)}return moduleIDs}let effectiveOptions=options;if(folderPath&&typeof folderPath==="object"&&!Array.isArray(folderPath)&&Object.prototype.hasOwnProperty.call(folderPath,"exports")){const siblingOptions={...folderPath};delete siblingOptions.exports;if(Object.keys(siblingOptions).length>0)effectiveOptions={...siblingOptions,...options}}const{metadata={},...restOptions}=effectiveOptions;if(!this.slothlet||!this.slothlet.isLoaded){throw new this.SlothletError("INVALID_CONFIG_NOT_LOADED",{operation:"addApi",validationError:true})}const{apiPath:normalizedPath,parts}=this.normalizeApiPath(apiPath);let effectivePath=normalizedPath;let effectiveParts=parts;if(versionConfig?.version!==void 0&&versionConfig?.version!==null){if(normalizedPath===""){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,reason:translate("API_PATH_REASON_VERSIONED_ROOT"),index:void 0,segment:void 0,validationError:true})}if(typeof versionConfig.version!=="string"||!String(versionConfig.version).trim()){throw new this.SlothletError("INVALID_CONFIG_VERSION_TAG",{received:versionConfig.version,validationError:true})}const versionTag=String(versionConfig.version).trim();effectiveParts=[versionTag,...parts];effectivePath=effectiveParts.join(".")}const isSynthetic=typeof folderPath==="function"||folderPath!==null&&typeof folderPath==="object"&&!Array.isArray(folderPath);let syntheticExports=null;let resolvedPath,isDirectory,isFile;if(isSynthetic){const isPlainObject=value=>{if(value===null||typeof value!=="object")return false;const proto=Object.getPrototypeOf(value);return proto===Object.prototype||proto===null};const describeNonPlain=value=>value===null?"null":Array.isArray(value)?"array":typeof value==="object"?`${value.constructor?.name??"non-plain object"} instance`:typeof value;if(typeof folderPath==="function"){syntheticExports={default:folderPath}}else if(Object.prototype.hasOwnProperty.call(folderPath,"exports")){const inner=folderPath.exports;if(!isPlainObject(inner)){throw new this.SlothletError("INVALID_CONFIG_SYNTHETIC_EXPORTS_SHAPE",{received:describeNonPlain(inner),validationError:true})}syntheticExports=inner}else if(isPlainObject(folderPath)){syntheticExports=folderPath}else{throw new this.SlothletError("INVALID_CONFIG_SYNTHETIC_INPUT",{received:describeNonPlain(folderPath),validationError:true})}if(parts.length===0&&typeof syntheticExports.default==="function"){const def=syntheticExports.default;const{default:___default,...named}=syntheticExports;if(!def.name||def.name==="default"){if(this.slothlet&&!this.____config?.silent){new this.SlothletWarning("WARN_SYNTHETIC_ROOT_UNNAMED",{})}syntheticExports=named}else{if(Object.prototype.hasOwnProperty.call(named,def.name)&&this.slothlet&&!this.____config?.silent){new this.SlothletWarning("WARN_SYNTHETIC_ROOT_COLLISION",{name:def.name})}syntheticExports={[def.name]:def,...named}}}isFile=false;resolvedPath=`synthetic:${normalizedPath||"root"}`}else{({resolvedPath,isDirectory,isFile}=await this.resolvePath(folderPath));if(!isDirectory&&!isFile){throw new this.SlothletError("INVALID_CONFIG_PATH_TYPE",{path:resolvedPath,validationError:true})}if(isFile){const ext=path.extname(resolvedPath);if(![".mjs",".cjs",".js"].includes(ext)){throw new this.SlothletError("INVALID_CONFIG_FILE_TYPE",{path:resolvedPath,extension:ext,validationError:true})}}}const resolvedFolderPath=resolvedPath;let collisionMode;if(restOptions.forceOverwrite){collisionMode="replace"}else{collisionMode=restOptions.collisionMode||this.____config.api?.collision?.api||"error"}const mutateExisting=!!(restOptions.mutateExisting||collisionMode==="merge");const moduleID=restOptions.moduleID?String(restOptions.moduleID):this.buildDefaultModuleId(normalizedPath,resolvedFolderPath);if(restOptions.forceOverwrite&&!moduleID){throw new this.SlothletError("INVALID_CONFIG_FORCE_OVERWRITE_REQUIRES_MODULE_ID",{apiPath:normalizedPath,validationError:true})}let dirForBuild=resolvedFolderPath;let fileFilter=null;if(isFile){dirForBuild=path.dirname(resolvedFolderPath);const fileName=path.basename(resolvedFolderPath);fileFilter=file=>file===fileName}const newApi=await this.slothlet.builders.builder.buildAPI({dir:dirForBuild,mode:this.____config.mode,apiPathPrefix:effectivePath,collisionContext:"addApi",moduleID,collisionMode,fileFilter,...isSynthetic?{syntheticExports,syntheticName:parts.length?parts[parts.length-1]:"synthetic"}:{}});if(this.slothlet.handlers.apiCacheManager){this.slothlet.handlers.apiCacheManager.set(moduleID,{endpoint:effectivePath,moduleID,api:newApi,folderPath:resolvedFolderPath,syntheticExports:isSynthetic?syntheticExports:null,mode:this.____config.mode,sanitizeOptions:this.____config.sanitize||{},collisionMode,config:{...this.____config},timestamp:Date.now()})}this.slothlet.debug("api",{key:"DEBUG_MODE_ADD_API_COMPONENT_BUILD_RETURN",topLevelKeys:Object.keys(newApi),dottedKeys:Object.keys(newApi).filter(k=>k.includes(".")),wrappers:Object.keys(newApi).filter(k=>resolveWrapper(newApi[k])!==null).map(k=>{const _w=resolveWrapper(newApi[k]);return{key:k,apiPath:_w.apiPath,implKeys:Object.keys(_w.____slothletInternal.impl||{}),childCacheSize:Object.keys(_w).filter(k2=>!k2.startsWith("_")&&!k2.startsWith("__")).length,childCacheKeys:Object.keys(_w).filter(k2=>!k2.startsWith("_")&&!k2.startsWith("__"))}}),nonWrappers:Object.keys(newApi).filter(k=>resolveWrapper(newApi[k])===null).map(k=>({key:k,type:typeof newApi[k]}))});let apiToMerge=newApi;if((isFile||isSynthetic)&&Object.keys(newApi).length===1){const fileName=Object.keys(newApi)[0];apiToMerge=newApi[fileName]}if(!isFile&&normalizedPath){const lastPart=normalizedPath.includes(".")?normalizedPath.split(".").pop():normalizedPath;if(lastPart&&Object.prototype.hasOwnProperty.call(apiToMerge,lastPart)){const dupValue=apiToMerge[lastPart];const dupType=typeof dupValue;if(dupValue!==null&&(dupType==="object"||dupType==="function")){const dupWrapper=resolveWrapper(dupValue);const dupFilePath=dupWrapper?.____slothletInternal?.filePath;const dupFileDir=dupFilePath?dupFilePath.replace(/\\/g,"/").split("/").slice(0,-1).join("/"):null;const normalizedFolderPath=resolvedFolderPath.replace(/\\/g,"/").replace(/\/$/,"");const expectedDir=normalizedFolderPath+"/"+lastPart;const isDirectChild=dupFileDir===expectedDir||dupFileDir===normalizedFolderPath;if(isDirectChild){const hoisted={};for(const k of Object.keys(apiToMerge)){if(k!==lastPart)hoisted[k]=apiToMerge[k]}if(dupWrapper){for(const k of Object.keys(dupWrapper).filter(k2=>!k2.startsWith("_")&&!k2.startsWith("__"))){hoisted[k]=dupWrapper[k]}}else{for(const k of Object.keys(dupValue)){hoisted[k]=dupValue[k]}}apiToMerge=hoisted;this.slothlet.debug("api",{key:"DEBUG_MODE_RULE_13_DEDUP_HOISTED_KEY",lastPart,newKeys:Object.keys(apiToMerge)})}}}}if(this.____config.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_ADD_API_COMPONENT_MERGE_KEYS",keys:Object.keys(apiToMerge),isRootLevel:parts.length===0})}let anyAssignmentSucceeded=false;let rootKeys=[];if(parts.length===0){const rootSource=isSynthetic?apiToMerge:newApi;rootKeys=Object.keys(rootSource);if(rootKeys.length===0&&isSynthetic&&this.slothlet&&!this.____config?.silent){new this.SlothletWarning("WARN_SYNTHETIC_ROOT_EMPTY",{apiPath:normalizedPath||"(root)"})}for(const key of rootKeys){const result1=await this.setValueAtPath(this.slothlet.api,[key],rootSource[key],{mutateExisting,collisionMode,moduleID,sourceFolder:resolvedFolderPath});const result2=await this.setValueAtPath(this.slothlet.boundApi,[key],rootSource[key],{mutateExisting,collisionMode,moduleID,sourceFolder:resolvedFolderPath});if(result1||result2){anyAssignmentSucceeded=true}}}else{if(resolveWrapper(apiToMerge)===null){const isCallableNamespace=typeof apiToMerge==="function";const containerWrapper=new UnifiedWrapper(this.slothlet,{apiPath:effectivePath,mode:this.____config.mode,isCallable:isCallableNamespace,moduleID,filePath:resolvedFolderPath,sourceFolder:resolvedFolderPath});containerWrapper.___setImpl(apiToMerge,moduleID);apiToMerge=containerWrapper.createProxy()}const result1=await this.setValueAtPath(this.slothlet.api,effectiveParts,apiToMerge,{mutateExisting,collisionMode,moduleID,sourceFolder:resolvedFolderPath});const result2=await this.setValueAtPath(this.slothlet.boundApi,effectiveParts,apiToMerge,{mutateExisting,collisionMode,moduleID,sourceFolder:resolvedFolderPath});if(result1||result2){anyAssignmentSucceeded=true}}if(anyAssignmentSucceeded){const pendingMaterializations=[];const seenWrappers=new Set;const collectPendingMaterializations=(obj,depth=0)=>{if(!obj||typeof obj!=="object"||depth>10)return;if(obj.__isVersionDispatcher===true)return;const wrapper=resolveWrapper(obj);if(wrapper){if(seenWrappers.has(wrapper))return;seenWrappers.add(wrapper);if(wrapper.____slothletInternal.materializationPromise){pendingMaterializations.push(wrapper.____slothletInternal.materializationPromise)}const childKeys=Object.keys(wrapper).filter(k=>!k.startsWith("_")&&!k.startsWith("__"));for(const key of childKeys){collectPendingMaterializations(wrapper[key],depth+1)}}for(const key of Object.keys(obj)){if(key!=="____slothletInternal"){collectPendingMaterializations(obj[key],depth+1)}}};if(effectiveParts.length===0){for(const key of rootKeys){if(this.slothlet.api[key]){collectPendingMaterializations(this.slothlet.api[key])}}}else{let current=this.slothlet.api;for(const part of effectiveParts){if(current&&current[part]){current=current[part]}else{break}}if(current){collectPendingMaterializations(current)}}if(pendingMaterializations.length>0){if(this.____config.debug?.api){this.slothlet.debug("api",{key:"DEBUG_MODE_AWAITING_PENDING_MATERIALIZATIONS",count:pendingMaterializations.length,apiPath:normalizedPath})}await Promise.all(pendingMaterializations)}}if(anyAssignmentSucceeded&&metadata&&Object.keys(metadata).length>0&&this.slothlet.handlers.metadata){if(parts.length===0){for(const key of Object.keys(newApi)){this.slothlet.handlers.metadata.registerUserMetadata(key,metadata)}}else{const rootSegment=effectiveParts[0];this.slothlet.handlers.metadata.registerUserMetadata(rootSegment,metadata)}}if(this.slothlet.handlers.ownership&&moduleID){this.slothlet.handlers.ownership.registerSubtree(apiToMerge,moduleID,effectivePath);this.slothlet.handlers.ownership.setModuleEndpoint(moduleID,effectivePath)}if(this.slothlet.handlers.ownership){if(restOptions.recordHistory!==false){this.state.addHistory.push({apiPath:normalizedPath,folderPath:resolvedFolderPath,options:{...restOptions,metadata,moduleID},moduleID,versionConfig:versionConfig||null});this.state.operationHistory.push({type:"add",apiPath:normalizedPath,folderPath:resolvedFolderPath,options:{...restOptions,metadata,moduleID},moduleID,versionConfig:versionConfig||null})}}if(versionConfig?.version&&this.slothlet.handlers.versionManager){const versionTag=String(versionConfig.version).trim();try{this.slothlet.handlers.versionManager.registerVersion(normalizedPath,versionTag,moduleID,versionConfig.metadata??{},versionConfig.default??false)}catch(error){await this._rollbackFailedVersionedAdd({moduleID,effectivePath,normalizedPath});throw error}}if(restOptions.permissions&&this.slothlet.handlers?.permissionManager){const perms=restOptions.permissions;const callerPattern=`${normalizedPath}.**`;if(Array.isArray(perms.deny)){for(const entry of perms.deny){const{target,condition}=this.#normalizePermissionShorthandEntry(entry);this.slothlet.handlers.permissionManager.addRule({caller:callerPattern,target,effect:"deny",condition},moduleID)}}if(Array.isArray(perms.allow)){for(const entry of perms.allow){const{target,condition}=this.#normalizePermissionShorthandEntry(entry);this.slothlet.handlers.permissionManager.addRule({caller:callerPattern,target,effect:"allow",condition},moduleID)}}}return moduleID}async _rollbackFailedVersionedAdd({moduleID,effectivePath,normalizedPath}){let addIndex=-1;for(let i=this.state.operationHistory.length-1;i>=0;i--){const entry=this.state.operationHistory[i];if(entry?.type==="add"&&entry?.apiPath===normalizedPath&&entry?.moduleID===moduleID){addIndex=i;break}}if(addIndex!==-1){this.state.operationHistory.splice(addIndex,1)}this.state.addHistory=this.state.addHistory.filter(entry=>entry?.moduleID!==moduleID);try{await this.removeApiComponent(moduleID||effectivePath,{recordHistory:false})}catch{}}#sweepOrphanedCaches(){const cacheManager=this.slothlet.handlers?.apiCacheManager;if(!cacheManager)return;const root=this.slothlet.boundApi;for(const moduleID of cacheManager.getAllModuleIDs()){const entry=cacheManager.get(moduleID);if(!entry||entry.endpoint==="."||entry.endpoint==="")continue;const parts=String(entry.endpoint).split(".").filter(Boolean);if(this.getValueAtPath(root,parts)===void 0){cacheManager.delete(moduleID)}}}async removeApiComponent(pathOrModuleId,options={}){const recordHistory=options.recordHistory!==false;if(typeof pathOrModuleId!=="string"||!pathOrModuleId){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"pathOrModuleId",expected:"non-empty string",received:typeof pathOrModuleId,validationError:true})}let apiPath=null;let moduleID;if(this.slothlet.handlers.ownership){const candidateModuleID=pathOrModuleId.split(":")[0];const registeredModules=Array.from(this.slothlet.handlers.ownership.moduleToPath.keys());let matchingModule=null;for(let i=registeredModules.length-1;i>=0;i--){const candidate=registeredModules[i];if(candidate===candidateModuleID||candidate.startsWith(`${candidateModuleID}_`)){matchingModule=candidate;break}}if(matchingModule){moduleID=matchingModule}else{const owner=this.slothlet.handlers.ownership.getCurrentOwner(pathOrModuleId);if(owner){apiPath=pathOrModuleId;moduleID=owner.moduleID}else{return false}}}else{const isModuleId=!pathOrModuleId.includes(".");apiPath=isModuleId?null:pathOrModuleId;moduleID=isModuleId?pathOrModuleId.split(":")[0]:null}if(!this.slothlet||!this.slothlet.isLoaded){throw new this.SlothletError("INVALID_CONFIG_NOT_LOADED",{operation:"removeApi",validationError:true})}if(apiPath&&moduleID){const normalizedPath2=this.normalizeApiPath(apiPath).apiPath;const moduleIDKey=String(moduleID);const history=this.slothlet.handlers.ownership?.getPathHistory?.(normalizedPath2)||[];const ownershipResult2=this.slothlet.handlers.ownership?.removePath?.(normalizedPath2,moduleIDKey)||{action:"none",removedModuleId:null,restoreModuleId:null};const pathParts=this.normalizeApiPath(apiPath).parts;if(ownershipResult2.action==="delete"){await this.deletePath(this.slothlet.api,pathParts);await this.deletePath(this.slothlet.boundApi,pathParts);if(this.slothlet.handlers.metadata){const rootSegment=normalizedPath2.split(".")[0];this.slothlet.handlers.metadata.removeUserMetadataByApiPath(rootSegment)}if(this.slothlet.handlers.versionManager){const versionKey=this.slothlet.handlers.versionManager.getVersionKeyForModule(moduleIDKey);if(versionKey){this.slothlet.handlers.versionManager.unregisterVersion(versionKey.logicalPath,versionKey.versionTag)}if(this.slothlet.handlers.versionManager.hasDispatcher(normalizedPath2)){this.slothlet.handlers.versionManager.teardownDispatcher(normalizedPath2)}}this.#sweepOrphanedCaches();this.state.operationHistory.push({type:"remove",apiPath:normalizedPath2});return true}if(ownershipResult2.action==="restore"){const restoredValue=this.slothlet.handlers.ownership?.getCurrentValue?.(normalizedPath2);const restoredModuleId=this.slothlet.handlers.ownership?.getCurrentOwner?.(normalizedPath2)?.moduleID;if(restoredValue!==void 0&&restoredModuleId){await this.setValueAtPath(this.slothlet.api,pathParts,restoredValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:restoredModuleId});await this.setValueAtPath(this.slothlet.boundApi,pathParts,restoredValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:restoredModuleId});this.state.operationHistory.push({type:"remove",apiPath:normalizedPath2});return true}await this.restoreApiPath(normalizedPath2,ownershipResult2.restoreModuleId);this.state.operationHistory.push({type:"remove",apiPath:normalizedPath2});return true}if(ownershipResult2.action==="none"&&history.length===0){await this.deletePath(this.slothlet.api,pathParts);await this.deletePath(this.slothlet.boundApi,pathParts);return true}return false}if(moduleID){const moduleIDKey=String(moduleID);const result=this.slothlet.handlers.ownership?.unregister?.(moduleIDKey)||{removed:[],rolledBack:[]};if(this.slothlet.handlers.versionManager){const versionKey=this.slothlet.handlers.versionManager.getVersionKeyForModule(moduleIDKey);if(versionKey){this.slothlet.handlers.versionManager.unregisterVersion(versionKey.logicalPath,versionKey.versionTag)}}const allPaths=[...result.removed,...result.rolledBack.map(r=>r.apiPath)];const uniquePaths=[...new Set(allPaths)];const pathsToDelete=[];const pathsToRollback=[];for(const path2 of uniquePaths){const currentOwner=this.slothlet.handlers.ownership?.getCurrentOwner?.(path2);const hasChildrenWithOtherOwners=uniquePaths.some(p=>{if(p===path2||!p.startsWith(path2+"."))return false;const childOwner=this.slothlet.handlers.ownership?.getCurrentOwner?.(p);return childOwner&&childOwner.moduleID!==moduleIDKey});if(currentOwner&&currentOwner.moduleID!==moduleIDKey){pathsToRollback.push({apiPath:path2,restoredTo:currentOwner.moduleID})}else if(!hasChildrenWithOtherOwners){pathsToDelete.push(path2)}}pathsToDelete.sort((a,b)=>{const depthA=(a.match(/\./g)||[]).length;const depthB=(b.match(/\./g)||[]).length;return depthB-depthA});for(const removedPath of pathsToDelete){const{parts:parts2}=this.normalizeApiPath(removedPath);await this.deletePath(this.slothlet.api,parts2);await this.deletePath(this.slothlet.boundApi,parts2);if(this.slothlet.handlers.metadata){const rootSegment=removedPath.split(".")[0];this.slothlet.handlers.metadata.removeUserMetadataByApiPath(rootSegment)}}if(pathsToDelete.length>0){const rootSegment=pathsToDelete[0].split(".")[0];if(rootSegment in this.slothlet.api){delete this.slothlet.api[rootSegment]}if(rootSegment in this.slothlet.boundApi){delete this.slothlet.boundApi[rootSegment]}}for(const rollback of pathsToRollback){const{parts:parts2}=this.normalizeApiPath(rollback.apiPath);const previousImpl=this.slothlet.handlers.ownership?.getCurrentValue?.(rollback.apiPath);if(previousImpl!==void 0){const existingWrapper=this.getValueAtPath(this.slothlet.api,parts2);const existingWrapperRaw=resolveWrapper(existingWrapper);if(existingWrapperRaw){existingWrapperRaw.___setImpl(previousImpl,rollback.restoredTo)}const existingBoundWrapper=this.getValueAtPath(this.slothlet.boundApi,parts2);const existingBoundWrapperRaw=resolveWrapper(existingBoundWrapper);if(existingBoundWrapperRaw){existingBoundWrapperRaw.___setImpl(previousImpl,rollback.restoredTo)}}}this.state.addHistory=this.state.addHistory.filter(entry=>String(entry.moduleID)!==moduleIDKey);if(this.slothlet.handlers.apiCacheManager){const deleted=this.slothlet.handlers.apiCacheManager.delete(moduleIDKey);if(deleted){this.slothlet.debug("cache",{key:"DEBUG_MODE_CACHE_DELETED_MODULE_REMOVED",moduleID:moduleIDKey})}}this.#sweepOrphanedCaches();if(recordHistory&&pathsToDelete.length>0){const rootSegment=pathsToDelete[0].split(".")[0];this.state.operationHistory.push({type:"remove",apiPath:rootSegment})}return pathsToDelete.length>0||pathsToRollback.length>0}if(!apiPath){throw new this.SlothletError("INVALID_CONFIG_API_PATH_INVALID",{apiPath,reason:translate("API_PATH_REASON_REQUIRED"),index:void 0,segment:void 0,validationError:true})}const{apiPath:normalizedPath,parts}=this.normalizeApiPath(apiPath);const ownershipResult=this.slothlet.handlers.ownership?.removePath?.(normalizedPath,null)||{action:"none",removedModuleId:null,restoreModuleId:null};const pathExists=this.getValueAtPath(this.slothlet.api,parts)!==void 0;if(ownershipResult.action==="none"){if(pathExists){await this.deletePath(this.slothlet.api,parts);await this.deletePath(this.slothlet.boundApi,parts);if(this.slothlet.handlers.metadata){this.slothlet.handlers.metadata.removeUserMetadataByApiPath(normalizedPath)}if(recordHistory){this.state.operationHistory.push({type:"remove",apiPath:normalizedPath})}return true}return false}if(ownershipResult.action==="delete"){await this.deletePath(this.slothlet.api,parts);await this.deletePath(this.slothlet.boundApi,parts);if(this.slothlet.handlers.metadata){this.slothlet.handlers.metadata.removeUserMetadataByApiPath(normalizedPath)}if(recordHistory){this.state.operationHistory.push({type:"remove",apiPath:normalizedPath})}return true}if(ownershipResult.action==="restore"){const restoredValue=this.slothlet.handlers.ownership?.getCurrentValue?.(normalizedPath);const restoredModuleId=this.slothlet.handlers.ownership?.getCurrentOwner?.(normalizedPath)?.moduleID;if(restoredValue!==void 0&&restoredModuleId){await this.setValueAtPath(this.slothlet.api,parts,restoredValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:restoredModuleId});await this.setValueAtPath(this.slothlet.boundApi,parts,restoredValue,{mutateExisting:true,allowOverwrite:true,collisionMode:"replace",moduleID:restoredModuleId});if(recordHistory){this.state.operationHistory.push({type:"remove",apiPath:normalizedPath})}return true}await this.restoreApiPath(normalizedPath,ownershipResult.restoreModuleId);if(recordHistory){this.state.operationHistory.push({type:"remove",apiPath:normalizedPath})}return true}return false}async reloadApiComponent(params){const{apiPath,moduleID,options}=params||{};if(!this.slothlet||!this.slothlet.isLoaded){throw new this.SlothletError("INVALID_CONFIG_NOT_LOADED",{operation:"reloadApi",validationError:true})}if(moduleID){await this._reloadByModuleID(moduleID);return}if(apiPath){await this._reloadByApiPath(apiPath,options);return}throw new this.SlothletError("INVALID_ARGUMENT",{argument:"params",expected:"{ moduleID } or { apiPath }",received:params,validationError:true})}async _reloadByModuleID(moduleID,{forceReplace=true}={}){const cacheManager=this.slothlet.handlers.apiCacheManager;if(!cacheManager){throw new this.SlothletError("CACHE_MANAGER_NOT_AVAILABLE",{operation:"reload",validationError:true})}if(!cacheManager.has(moduleID)){throw new this.SlothletError("CACHE_NOT_FOUND",{moduleID,operation:"reload",validationError:true})}const oldEntry=cacheManager.get(moduleID);this.slothlet.debug("reload",{key:"DEBUG_MODE_RELOADING_MODULE_BY_ID",moduleID,endpoint:oldEntry.endpoint,folderPath:oldEntry.folderPath});const freshApi=await cacheManager.rebuildCache(moduleID);cacheManager.set(moduleID,{...oldEntry,api:freshApi,timestamp:Date.now()});this.slothlet.debug("reload",{key:"DEBUG_MODE_FRESH_API_KEYS_BEFORE_RESTORE",moduleID,endpoint:oldEntry.endpoint,freshApiKeys:Object.keys(freshApi||{})});await this._restoreApiTree(freshApi,oldEntry.endpoint,moduleID,oldEntry.collisionMode,forceReplace);this.slothlet.debug("reload",{key:"DEBUG_MODE_FRESH_API_KEYS_AFTER_RESTORE",moduleID,endpoint:oldEntry.endpoint,freshApiKeys:Object.keys(freshApi||{})});this.slothlet.debug("reload",{key:"DEBUG_MODE_MODULE_RELOAD_COMPLETE",moduleID});if(this.slothlet.handlers.versionManager){this.slothlet.handlers.versionManager.onVersionedModuleReload(moduleID)}}async _reloadByApiPath(apiPath,options={}){this.slothlet.debug("reload",{key:"DEBUG_MODE_RELOADING_BY_API_PATH",apiPath});const moduleIDsToReload=this._findAffectedCaches(apiPath);if(moduleIDsToReload.length===0){this.slothlet.debug("reload",{key:"DEBUG_MODE_NO_CACHES_ATTEMPTING_RESTORE",apiPath});if(apiPath!=="."&&apiPath!==""){await this.restoreApiPath(apiPath,"base")}return}const cacheManager=this.slothlet.handlers.apiCacheManager;moduleIDsToReload.sort((a,b)=>{const entryA=cacheManager.get(a);const entryB=cacheManager.get(b);if(entryA?.endpoint==="."&&entryB?.endpoint!==".")return-1;if(entryB?.endpoint==="."&&entryA?.endpoint!==".")return 1;const indexA=this.state.addHistory.findIndex(h=>h.moduleID===a);const indexB=this.state.addHistory.findIndex(h=>h.moduleID===b);return indexA-indexB});const endpointOrder=new Map;for(const moduleID of moduleIDsToReload){const entry=cacheManager.get(moduleID);const ep=entry?.endpoint??".";if(!endpointOrder.has(ep))endpointOrder.set(ep,[]);endpointOrder.get(ep).push(moduleID)}for(const[,moduleIDs]of endpointOrder){for(let i=0;i<moduleIDs.length;i++){await this._reloadByModuleID(moduleIDs[i],{forceReplace:i===0})}}const reloadMetadata=options?.metadata;if(reloadMetadata&&typeof reloadMetadata==="object"&&Object.keys(reloadMetadata).length>0){if(this.slothlet.handlers.metadata){const targetPath=apiPath==="."?null:apiPath.split(".")[0];if(targetPath){this.slothlet.handlers.metadata.registerUserMetadata(targetPath,reloadMetadata)}}}this.slothlet.debug("reload",{key:"DEBUG_MODE_API_PATH_RELOAD_COMPLETE",apiPath,reloadedModules:moduleIDsToReload.length,loadOrder:moduleIDsToReload})}_findAffectedCaches(apiPath){const cacheManager=this.slothlet.handlers.apiCacheManager;if(!cacheManager)return[];const allModuleIDs=cacheManager.getAllModuleIDs();if(apiPath==="."||apiPath===""||apiPath==null){const baseModules=[];for(const moduleID of allModuleIDs){const entry=cacheManager.get(moduleID);if(entry&&entry.endpoint==="."){baseModules.push(moduleID)}}return baseModules}const exactMatches=[];for(const moduleID of allModuleIDs){const entry=cacheManager.get(moduleID);if(entry&&entry.endpoint===apiPath){exactMatches.push(moduleID)}}if(exactMatches.length>0)return exactMatches;const children=[];const pathPrefix=apiPath+".";for(const moduleID of allModuleIDs){const entry=cacheManager.get(moduleID);if(entry?.endpoint?.startsWith(pathPrefix)){children.push(moduleID)}}if(children.length>0)return children;const ownership=this.slothlet.handlers.ownership;const history=ownership?.getPathHistory?.(apiPath);if(history&&history.length>0){const owned=[];for(const{moduleID}of history){if(cacheManager.has(moduleID)){owned.push(moduleID)}}if(owned.length>0)return owned}let bestMatch=null;let bestLength=-1;for(const moduleID of allModuleIDs){const entry=cacheManager.get(moduleID);if(!entry?.endpoint)continue;const ep=entry.endpoint;if(ep==="."||apiPath.startsWith(ep+".")){if(ep.length>bestLength){bestLength=ep.length;bestMatch=moduleID}}}if(bestMatch)return[bestMatch];return[]}_collectCustomProperties(existingProxy,freshApi){const customProps={};if(!existingProxy||typeof existingProxy!=="object"&&typeof existingProxy!=="function"){return customProps}const wrapper=resolveWrapper(existingProxy);if(!wrapper){return customProps}const freshKeys=new Set(freshApi?Object.keys(freshApi):[]);const ownKeys=Object.keys(wrapper).filter(k=>!ComponentBase.INTERNAL_KEYS.has(k));for(const key of ownKeys){try{const val=wrapper[key];if(val&&(typeof val==="object"||typeof val==="function")&&resolveWrapper(val)){continue}if(!freshKeys.has(key)){customProps[key]=val}else{customProps[key]=val}}catch{}}return customProps}_restoreCustomProperties(proxy,customProps){if(!proxy||!customProps||typeof customProps!=="object"){return}for(const[key,value]of Object.entries(customProps)){try{proxy[key]=value}catch{}}}async _restoreApiTree(freshApi,endpoint,moduleID,collisionMode,forceReplace=true){if(!freshApi||typeof freshApi!=="object"&&typeof freshApi!=="function"){return}const parts=endpoint==="."?[]:endpoint.split(".");if(parts.length===0){for(const key of Object.keys(freshApi)){if(typeof key==="string"&&(key.startsWith("_")||key.startsWith("__")))continue;if(key==="slothlet"||key==="shutdown"||key==="destroy")continue;const existingAtKey=this.slothlet.api[key];const freshValue=freshApi[key];if(existingAtKey&&resolveWrapper(existingAtKey)!==null){const customProps=this._collectCustomProperties(existingAtKey,freshValue);const freshWrapper=resolveWrapper(freshValue);const isLazyFresh=freshWrapper&&freshWrapper.____slothletInternal.mode==="lazy"&&!freshWrapper.____slothletInternal.state.materialized&&typeof freshWrapper.____slothletInternal.materializeFunc==="function";this.slothlet.debug("reload",{key:"DEBUG_MODE_RESTORE_ROOT_KEY_INSPECT",rootKey:key,hasFreshWrapper:!!freshWrapper,freshMode:freshWrapper?.____slothletInternal.mode,freshMaterialized:freshWrapper?.____slothletInternal.state?.materialized,hasMaterializeFunc:typeof freshWrapper?.____slothletInternal.materializeFunc==="function",isLazyFresh,existingMaterialized:resolveWrapper(existingAtKey)?.____slothletInternal?.state?.materialized});if(isLazyFresh){resolveWrapper(existingAtKey).___resetLazy(freshWrapper.____slothletInternal.materializeFunc);this._restoreCustomProperties(existingAtKey,customProps);this.slothlet.debug("reload",{key:"DEBUG_MODE_ROOT_KEY_RESET_LAZY",rootKey:key,restoredCustomProps:Object.keys(customProps)})}else{let implForReload;if(freshValue&&resolveWrapper(freshValue)!==null){implForReload=freshWrapper?UnifiedWrapper._extractFullImpl(freshWrapper):freshValue}else{implForReload=freshValue}resolveWrapper(existingAtKey).___setImpl(implForReload,moduleID);this._restoreCustomProperties(existingAtKey,customProps);this.slothlet.debug("reload",{key:"DEBUG_MODE_ROOT_KEY_UPDATED_SETIMPL",rootKey:key,restoredCustomProps:Object.keys(customProps)})}}else if(existingAtKey===void 0){const cacheManager=this.slothlet.handlers.apiCacheManager;const cacheEntry=cacheManager.get(moduleID);const resolvedFolderPath=cacheEntry?.folderPath||"";await this.setValueAtPath(this.slothlet.api,[key],freshValue,{mutateExisting:true,collisionMode,moduleID,sourceFolder:resolvedFolderPath});if(this.slothlet.boundApi){await this.setValueAtPath(this.slothlet.boundApi,[key],freshValue,{mutateExisting:true,collisionMode,moduleID,sourceFolder:resolvedFolderPath})}}}}else{const existing=this.getValueAtPath(this.slothlet.api,parts);this.slothlet.debug("reload",{key:"DEBUG_MODE_RESTORE_NESTED_PATH",endpoint,moduleID,partsPath:parts.join("."),existingFound:!!existing,hasSetImpl:existing?resolveWrapper(existing)!==null:false,freshApiKeys:Object.keys(freshApi)});if(existing&&resolveWrapper(existing)!==null){const customProps=this._collectCustomProperties(existing,freshApi);const wrapper=resolveWrapper(existing);const originalCollisionMode=wrapper?wrapper.____slothletInternal.state.collisionMode:null;if(forceReplace&&wrapper){wrapper.____slothletInternal.state.collisionMode="replace";this.slothlet.debug("reload",{key:"DEBUG_MODE_RESTORE_FORCING_REPLACE",endpoint,originalCollisionMode,wrapperApiPath:wrapper.____slothletInternal.apiPath})}let implForReload;if(resolveWrapper(freshApi)!==null){const freshWrapper=resolveWrapper(freshApi);implForReload=freshWrapper?UnifiedWrapper._extractFullImpl(freshWrapper):freshApi}else{implForReload=freshApi}if(parts.length>0&&implForReload&&typeof implForReload==="object"){const lastEndpointPart=parts[parts.length-1];if(lastEndpointPart&&Object.prototype.hasOwnProperty.call(implForReload,lastEndpointPart)){const dupValue=implForReload[lastEndpointPart];const dupWrapperForDedup=resolveWrapper(dupValue);if(dupWrapperForDedup){const hoisted={};for(const k of Object.keys(implForReload)){if(k!==lastEndpointPart)hoisted[k]=implForReload[k]}for(const k of Object.keys(dupWrapperForDedup).filter(k2=>!k2.startsWith("_")&&!k2.startsWith("__"))){hoisted[k]=dupWrapperForDedup[k]}implForReload=hoisted}}}if(implForReload&&typeof implForReload==="object"){for(const key of Object.keys(implForReload)){const val=implForReload[key];if(resolveWrapper(val)!==null){const childWrapper=resolveWrapper(val);if(childWrapper.____slothletInternal.state.materialized){implForReload[key]=UnifiedWrapper._extractFullImpl(childWrapper)}}}}resolveWrapper(existing).___setImpl(implForReload,moduleID);if(wrapper&&originalCollisionMode!==null){wrapper.____slothletInternal.state.collisionMode=originalCollisionMode}this._restoreCustomProperties(existing,customProps);this.slothlet.debug("reload",{key:"DEBUG_MODE_UPDATED_WRAPPER_IMPL",endpoint,moduleID,forcedReplaceMode:true,restoredCustomProps:Object.keys(customProps)})}else{const cacheManager=this.slothlet.handlers.apiCacheManager;const cacheEntry=cacheManager.get(moduleID);const resolvedFolderPath=cacheEntry?.folderPath||"";let implForContainer=freshApi;if(typeof freshApi==="function"){implForContainer={};for(const key of Object.keys(freshApi)){implForContainer[key]=freshApi[key]}}const containerWrapper=new UnifiedWrapper(this.slothlet,{apiPath:endpoint,mode:this.____config.mode,moduleID,filePath:resolvedFolderPath,sourceFolder:resolvedFolderPath});containerWrapper.___setImpl(implForContainer,moduleID);const apiToSet=containerWrapper.createProxy();await this.setValueAtPath(this.slothlet.api,parts,apiToSet,{mutateExisting:true,collisionMode,moduleID,sourceFolder:resolvedFolderPath});if(this.slothlet.boundApi){await this.setValueAtPath(this.slothlet.boundApi,parts,apiToSet,{mutateExisting:true,collisionMode,moduleID,sourceFolder:resolvedFolderPath})}this.slothlet.debug("reload",{key:"DEBUG_MODE_CREATED_NEW_WRAPPER_UNEXPECTED",endpoint,moduleID})}}}}export{ApiManager};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"@cldmv/slothlet/factories/component-base";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";import{normalizeHookConfig}from"@cldmv/slothlet/helpers/config";const ERROR_HOOK_PROCESSED=Symbol.for("@cldmv/slothlet/hook-error-processed");class HookManager extends ComponentBase{static slothletProperty="hookManager";#hooks={before:{before:{},primary:{},after:{}},after:{before:{},primary:{},after:{}},always:{before:{},primary:{},after:{}},error:{before:{},primary:{},after:{}}};#byId=new Map;#idCounter=0;#validTypes=new Set(["before","after","always","error"]);#validSubsets=new Set(["before","primary","after"]);constructor(slothlet){super(slothlet);const hookConfig=normalizeHookConfig(slothlet.config?.hook);this.enabled=hookConfig.enabled;this.defaultPattern=hookConfig.pattern||"**";this.suppressErrors=hookConfig.suppressErrors||false;this.enabledPatterns=new Set;this.patternFilterActive=false;if(this.defaultPattern!=="**"){this.enabledPatterns.add(this.defaultPattern);this.patternFilterActive=true}this.hooks=new Map;this.registrationOrder=0;this.reportedErrors=new WeakSet}#globalFilterCache=new Map;#pathMatchesGlobalFilter(apiPath){if(!this.patternFilterActive){return true}for(const pattern of this.enabledPatterns){let matcher=this.#globalFilterCache.get(pattern);if(!matcher){matcher=this.#compilePattern(pattern);this.#globalFilterCache.set(pattern,matcher)}if(matcher(apiPath)){return true}}return false}on(typePattern,handler,options={}){let{type,pattern}=this.#parseTypePattern(typePattern);if(options.pattern!==void 0){pattern=options.pattern}if(!this.#validTypes.has(type)){throw new this.slothlet.SlothletError("INVALID_HOOK_TYPE",{type,validTypes:Array.from(this.#validTypes)})}if(typeof handler!=="function"){throw new this.slothlet.SlothletError("INVALID_HOOK_HANDLER",{receivedType:typeof handler,validationError:true})}const id=options.id||this.#generateId();if(this.#byId.has(id)){throw new this.slothlet.SlothletError("DUPLICATE_HOOK_ID",{id,validationError:true})}const subset=options.subset||"primary";if(!this.#validSubsets.has(subset)){throw new this.slothlet.SlothletError("INVALID_HOOK_SUBSET",{subset,validSubsets:Array.from(this.#validSubsets)})}this.#compilePattern(pattern);const lockCaller=options.lockCaller!==false;const effectiveHandler=lockCaller?this.#pinHandler(handler):handler;const hook={id,type,pattern,handler:effectiveHandler,lockCaller,priority:options.priority||0,subset,enabled:true,_compiled:null};const typeIndex=this.#hooks[type];const subsetIndex=typeIndex[subset];if(!subsetIndex[pattern]){subsetIndex[pattern]=[]}subsetIndex[pattern].push(hook);this.#byId.set(id,hook);return id}#pinHandler(handler){if(typeof handler._slothletOriginal==="function")return handler;const slothlet=this.slothlet;const capturedWrapper=slothlet.contextManager?.tryGetContext?.()?.currentWrapper??null;if(!capturedWrapper)return handler;const pinned=function slothlet_pinnedHook(...args){return slothlet.contextManager.runInContext(slothlet.instanceID,handler,this,args,capturedWrapper,true)};pinned._slothletOriginal=handler;return pinned}remove(filter={}){let removed=0;if(filter.id){const hook=this.#byId.get(filter.id);if(hook){this.#removeHook(hook);removed=1}return removed}const types=filter.type?[filter.type]:Array.from(this.#validTypes);for(const type of types){const typeIndex=this.#hooks[type];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];if(filter.pattern){const patternHooks=subsetIndex[filter.pattern];if(patternHooks){removed+=patternHooks.length;patternHooks.forEach(hook=>this.#byId.delete(hook.id));delete subsetIndex[filter.pattern]}}else{for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];removed+=patternHooks.length;patternHooks.forEach(hook=>this.#byId.delete(hook.id));delete subsetIndex[pattern]}}}}return removed}enable(filter={}){if(typeof filter==="string"){filter={pattern:filter}}this.enabled=true;return this.#setEnabledState(filter,true)}disable(filter={}){if(typeof filter==="string"){filter={pattern:filter}}if(Object.keys(filter).length===0){this.enabled=false}return this.#setEnabledState(filter,false)}enablePattern(pattern){this.#compilePattern(pattern);this.enabledPatterns.add(pattern);this.patternFilterActive=true;return this.enabledPatterns.size}disablePattern(pattern){this.enabledPatterns.delete(pattern);this.#globalFilterCache.delete(pattern);if(this.enabledPatterns.size===0){this.patternFilterActive=false}return this.enabledPatterns.size}resetPatternFilter(){this.enabledPatterns.clear();this.#globalFilterCache.clear();this.patternFilterActive=false;if(this.defaultPattern!=="**"){this.enabledPatterns.add(this.defaultPattern);this.patternFilterActive=true}}list(filter={}){if(typeof filter==="string"){if(this.#validTypes.has(filter)){filter={type:filter}}else{filter={pattern:filter}}}const hooks=[];if(filter.id){const hook=this.#byId.get(filter.id);if(hook&&(filter.enabled===void 0||hook.enabled===filter.enabled)){hooks.push(this.#serializeHook(hook))}return{registeredHooks:hooks}}const types=filter.type?[filter.type]:Array.from(this.#validTypes);let patternMatcher=null;if(filter.pattern){patternMatcher=this.#compilePattern(filter.pattern)}for(const type of types){const typeIndex=this.#hooks[type];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];for(const hook of patternHooks){if(filter.enabled!==void 0&&hook.enabled!==filter.enabled){continue}if(patternMatcher&&!patternMatcher(hook.pattern)){continue}hooks.push(this.#serializeHook(hook))}}}}return{registeredHooks:hooks}}getHooksForPath(type,apiPath){if(this.enabled===false){return[]}if(!this.#pathMatchesGlobalFilter(apiPath)){return[]}const typeIndex=this.#hooks[type];if(!typeIndex){return[]}const hooks=[];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];const subsetHooks=[];for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];if(pattern===apiPath){subsetHooks.push(...patternHooks.filter(h=>h.enabled));continue}for(const hook of patternHooks){if(!hook.enabled)continue;if(!hook._compiled){hook._compiled=this.#compilePattern(hook.pattern)}if(hook._compiled(apiPath)){subsetHooks.push(hook)}}}subsetHooks.sort((a,b)=>b.priority-a.priority);hooks.push(...subsetHooks)}return hooks}executeBeforeHooks(path,args,api,ctx){const hooks=this.getHooksForPath("before",path);for(const hook of hooks){try{const result=hook.handler({path,args,api,ctx});if(result&&typeof result==="object"&&typeof result.then==="function"){throw new this.SlothletError("HOOK_BEFORE_RETURNED_PROMISE",{id:hook.id,path},null,{validationError:true})}if(result!==void 0&&!Array.isArray(result)){return{args,shortCircuit:true,value:result}}if(Array.isArray(result)){args=result}}catch(error){const sourceInfo={type:"before",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}return{args,shortCircuit:true,value:void 0}}}return{args,shortCircuit:false}}executeAfterHooks(path,result,args,api,ctx){const hooks=this.getHooksForPath("after",path);const originalResult=result;let currentResult=result;for(const hook of hooks){try{const hookContext={path,args,result:currentResult,api,ctx};const transformed=hook.handler(hookContext);if(transformed!==void 0){currentResult=transformed}}catch(error){const sourceInfo={type:"after",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}}}if(currentResult===originalResult){return{modified:false}}else{return{modified:true,result:currentResult}}}executeAlwaysHooks(path,args,resultOrError,hasError=false,errors=[],api,ctx){const hooks=this.getHooksForPath("always",path);for(const hook of hooks){try{hook.handler({path,args,result:hasError?void 0:resultOrError,hasError,errors,api,ctx})}catch(error){const sourceInfo={type:"always",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx)}}}executeErrorHooks(path,error,source,args,api,ctx){if(error&&typeof error==="object"){error[ERROR_HOOK_PROCESSED]=true}const hooks=this.getHooksForPath("error",path);for(const hook of hooks){try{hook.handler({path,args,error,errorType:error?.constructor?.name||"Error",source,timestamp:new Date,api,ctx})}catch(hookError){this.slothlet.debug("hooks",`Error hook failed for ${path}:`,hookError)}}}#parseTypePattern(typePattern){if(typeof typePattern!=="string"){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"string in format 'type:pattern'"})}const colonIndex=typePattern.indexOf(":");if(colonIndex===-1){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"string in format 'type:pattern' with at least one colon"})}const type=typePattern.substring(0,colonIndex);const pattern=typePattern.substring(colonIndex+1);if(!type||!pattern){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"non-empty type and pattern"})}return{type,pattern}}#compilePattern(pattern){return compilePattern(pattern,{onMaxDepth:maxDepth=>{throw new this.SlothletError("HOOK_BRACE_EXPANSION_MAX_DEPTH",{maxDepth},null,{validationError:true})}})}getCompilePatternForDiagnostics(){return this.#compilePattern.bind(this)}#generateId(){return`hook-${++this.#idCounter}`}#removeHook(hook){const typeIndex=this.#hooks[hook.type];const subsetIndex=typeIndex[hook.subset];const patternHooks=subsetIndex[hook.pattern];if(!patternHooks){throw new this.slothlet.SlothletError("INTERNAL_HOOK_STATE_CORRUPT",{hookId:hook.id,type:hook.type,subset:hook.subset,pattern:hook.pattern,detail:"patternHooks array missing from subsetIndex \u2014 #byId and #hooks are desynced"})}const index=patternHooks.indexOf(hook);if(index===-1){throw new this.slothlet.SlothletError("INTERNAL_HOOK_STATE_CORRUPT",{hookId:hook.id,type:hook.type,subset:hook.subset,pattern:hook.pattern,detail:"hook object not found in patternHooks array \u2014 #byId and #hooks are desynced"})}patternHooks.splice(index,1);if(patternHooks.length===0){delete subsetIndex[hook.pattern]}this.#byId.delete(hook.id)}#setEnabledState(filter,enabled){let affected=0;if(filter.id){const hook=this.#byId.get(filter.id);if(hook){hook.enabled=enabled;affected=1}return affected}const types=filter.type?[filter.type]:Array.from(this.#validTypes);for(const type of types){const typeIndex=this.#hooks[type];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];if(filter.pattern){const patternHooks=subsetIndex[filter.pattern]||[];for(const hook of patternHooks){hook.enabled=enabled;affected++}}else{for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];for(const hook of patternHooks){hook.enabled=enabled;affected++}}}}}return affected}#serializeHook(hook){return{id:hook.id,type:hook.type,pattern:hook.pattern,priority:hook.priority,subset:hook.subset,enabled:hook.enabled,lockCaller:hook.lockCaller}}exportHooks(){const registrations=[];for(const hook of this.#byId.values()){registrations.push({typePattern:`${hook.type}:${hook.pattern}`,handler:hook.handler,options:{id:hook.id,priority:hook.priority,subset:hook.subset,lockCaller:hook.lockCaller},enabled:hook.enabled})}return registrations}importHooks(registrations){if(!Array.isArray(registrations))return;for(const reg of registrations){this.on(reg.typePattern,reg.handler,reg.options);if(!reg.enabled){this.disable({id:reg.options.id})}}}async shutdown(){this.#hooks={before:{before:{},primary:{},after:{}},after:{before:{},primary:{},after:{}},always:{before:{},primary:{},after:{}},error:{before:{},primary:{},after:{}}};this.#byId.clear();this.#idCounter=0}}export{HookManager};
17
+ import{ComponentBase}from"@cldmv/slothlet/factories/component-base";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";import{normalizeHookConfig}from"@cldmv/slothlet/helpers/config";const ERROR_HOOK_PROCESSED=Symbol.for("@cldmv/slothlet/hook-error-processed");const REPLAY_IDENTITY=Symbol("@cldmv/slothlet/hook-replay-identity");class HookManager extends ComponentBase{static slothletProperty="hookManager";#hooks={before:{before:{},primary:{},after:{}},after:{before:{},primary:{},after:{}},always:{before:{},primary:{},after:{}},error:{before:{},primary:{},after:{}}};#byId=new Map;#idCounter=0;#validTypes=new Set(["before","after","always","error"]);#validSubsets=new Set(["before","primary","after"]);constructor(slothlet){super(slothlet);const hookConfig=normalizeHookConfig(slothlet.config?.hook);this.enabled=hookConfig.enabled;this.defaultPattern=hookConfig.pattern||"**";this.suppressErrors=hookConfig.suppressErrors||false;this.pinEnforced=hookConfig.pin;this.enabledPatterns=new Set;this.patternFilterActive=false;if(this.defaultPattern!=="**"){this.enabledPatterns.add(this.defaultPattern);this.patternFilterActive=true}this.hooks=new Map;this.registrationOrder=0;this.reportedErrors=new WeakSet}#globalFilterCache=new Map;#pathMatchesGlobalFilter(apiPath){if(!this.patternFilterActive){return true}for(const pattern of this.enabledPatterns){let matcher=this.#globalFilterCache.get(pattern);if(!matcher){matcher=this.#compilePattern(pattern);this.#globalFilterCache.set(pattern,matcher)}if(matcher(apiPath)){return true}}return false}on(typePattern,handler,options={}){let{type,pattern}=this.#parseTypePattern(typePattern);if(options.pattern!==void 0){pattern=options.pattern}if(typeof handler!=="function"){throw new this.slothlet.SlothletError("INVALID_HOOK_HANDLER",{receivedType:typeof handler,validationError:true})}const id=options.id||this.#generateId();if(this.#byId.has(id)){throw new this.slothlet.SlothletError("DUPLICATE_HOOK_ID",{id,validationError:true})}const subset=options.subset||"primary";if(!this.#validSubsets.has(subset)){throw new this.slothlet.SlothletError("INVALID_HOOK_SUBSET",{subset,validSubsets:Array.from(this.#validSubsets)})}this.#compilePattern(pattern);const ownerWrapper=this.slothlet.contextManager?.tryGetContext?.()?.currentWrapper??null;const replayIdentity=options[REPLAY_IDENTITY]??null;const ownerPath=replayIdentity?replayIdentity.ownerPath:ownerWrapper?.____slothletInternal?.apiPath??null;const ownerFilePath=replayIdentity?replayIdentity.ownerFilePath:ownerWrapper?.____slothletInternal?.filePath??null;const permissionManager=this.slothlet.handlers?.permissionManager;if(permissionManager?.isEnabled?.()&&ownerPath&&!this.#isGlobPattern(pattern)){const runtimeContext=this.slothlet.contextManager?.tryGetContext?.()?.context??null;if(!permissionManager.enforceHookAccess(ownerPath,pattern,type,ownerFilePath,null,runtimeContext)){throw new this.slothlet.SlothletError("PERMISSION_DENIED",{caller:ownerPath,target:`${pattern}:${type}`})}}let lockCaller=options.lockCaller!==false;if(!lockCaller&&ownerWrapper&&this.pinEnforced){lockCaller=true;if(!this.slothlet.config?.silent){new this.SlothletWarning("HOOK_UNPINNED_IGNORED",{pattern})}}const effectiveHandler=lockCaller?this.#pinHandler(handler):handler;const hook={id,type,pattern,handler:effectiveHandler,lockCaller,ownerPath,ownerFilePath,priority:options.priority||0,subset,enabled:true,_compiled:null};const typeIndex=this.#hooks[type];const subsetIndex=typeIndex[subset];if(!subsetIndex[pattern]){subsetIndex[pattern]=[]}subsetIndex[pattern].push(hook);this.#byId.set(id,hook);return id}#pinHandler(handler){if(typeof handler._slothletOriginal==="function")return handler;const slothlet=this.slothlet;const capturedWrapper=slothlet.contextManager?.tryGetContext?.()?.currentWrapper??null;if(!capturedWrapper)return handler;const pinned=function slothlet_pinnedHook(...args){return slothlet.contextManager.runInContext(slothlet.instanceID,handler,this,args,capturedWrapper,true)};pinned._slothletOriginal=handler;return pinned}remove(filter={}){let removed=0;if(filter.id){const hook=this.#byId.get(filter.id);if(hook){this.#removeHook(hook);removed=1}return removed}const types=filter.type?[filter.type]:Array.from(this.#validTypes);for(const type of types){const typeIndex=this.#hooks[type];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];if(filter.pattern){const patternHooks=subsetIndex[filter.pattern];if(patternHooks){removed+=patternHooks.length;patternHooks.forEach(hook=>this.#byId.delete(hook.id));delete subsetIndex[filter.pattern]}}else{for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];removed+=patternHooks.length;patternHooks.forEach(hook=>this.#byId.delete(hook.id));delete subsetIndex[pattern]}}}}return removed}enable(filter={}){if(typeof filter==="string"){filter={pattern:filter}}this.enabled=true;return this.#setEnabledState(filter,true)}disable(filter={}){if(typeof filter==="string"){filter={pattern:filter}}if(Object.keys(filter).length===0){this.enabled=false}return this.#setEnabledState(filter,false)}enablePattern(pattern){this.#compilePattern(pattern);this.enabledPatterns.add(pattern);this.patternFilterActive=true;return this.enabledPatterns.size}disablePattern(pattern){this.enabledPatterns.delete(pattern);this.#globalFilterCache.delete(pattern);if(this.enabledPatterns.size===0){this.patternFilterActive=false}return this.enabledPatterns.size}resetPatternFilter(){this.enabledPatterns.clear();this.#globalFilterCache.clear();this.patternFilterActive=false;if(this.defaultPattern!=="**"){this.enabledPatterns.add(this.defaultPattern);this.patternFilterActive=true}}setPinEnforced(value){this.pinEnforced=value===true;return this.pinEnforced}list(filter={}){if(typeof filter==="string"){if(this.#validTypes.has(filter)){filter={type:filter}}else{filter={pattern:filter}}}const hooks=[];if(filter.id){const hook=this.#byId.get(filter.id);if(hook&&(filter.enabled===void 0||hook.enabled===filter.enabled)){hooks.push(this.#serializeHook(hook))}return{registeredHooks:hooks}}const types=filter.type?[filter.type]:Array.from(this.#validTypes);let patternMatcher=null;if(filter.pattern){patternMatcher=this.#compilePattern(filter.pattern)}for(const type of types){const typeIndex=this.#hooks[type];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];for(const hook of patternHooks){if(filter.enabled!==void 0&&hook.enabled!==filter.enabled){continue}if(patternMatcher&&!patternMatcher(hook.pattern)){continue}hooks.push(this.#serializeHook(hook))}}}}return{registeredHooks:hooks}}getHooksForPath(type,apiPath){if(this.enabled===false){return[]}if(!this.#pathMatchesGlobalFilter(apiPath)){return[]}const typeIndex=this.#hooks[type];if(!typeIndex){return[]}const hooks=[];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];const subsetHooks=[];for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];if(pattern===apiPath){subsetHooks.push(...patternHooks.filter(h=>h.enabled));continue}for(const hook of patternHooks){if(!hook.enabled)continue;if(!hook._compiled){hook._compiled=this.#compilePattern(hook.pattern)}if(hook._compiled(apiPath)){subsetHooks.push(hook)}}}subsetHooks.sort((a,b)=>b.priority-a.priority);hooks.push(...subsetHooks)}const permissionManager=this.slothlet.handlers?.permissionManager;if(permissionManager?.isEnabled?.()&&hooks.length>0){const runtimeContext=this.slothlet.contextManager?.tryGetContext?.()?.context??null;return hooks.filter(hook=>permissionManager.checkHookAccess(hook.ownerPath,apiPath,type,hook.ownerFilePath,null,runtimeContext))}return hooks}executeBeforeHooks(path,args,api,ctx){const hooks=this.getHooksForPath("before",path);for(const hook of hooks){try{const result=hook.handler({path,args,api,ctx});if(result&&typeof result==="object"&&typeof result.then==="function"){throw new this.SlothletError("HOOK_BEFORE_RETURNED_PROMISE",{id:hook.id,path},null,{validationError:true})}if(result!==void 0&&!Array.isArray(result)){return{args,shortCircuit:true,value:result}}if(Array.isArray(result)){args=result}}catch(error){const sourceInfo={type:"before",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}return{args,shortCircuit:true,value:void 0}}}return{args,shortCircuit:false}}executeAfterHooks(path,result,args,api,ctx){const hooks=this.getHooksForPath("after",path);const originalResult=result;let currentResult=result;for(const hook of hooks){try{const hookContext={path,args,result:currentResult,api,ctx};const transformed=hook.handler(hookContext);if(transformed!==void 0){currentResult=transformed}}catch(error){const sourceInfo={type:"after",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx);if(!this.suppressErrors){throw error}}}if(currentResult===originalResult){return{modified:false}}else{return{modified:true,result:currentResult}}}executeAlwaysHooks(path,args,resultOrError,hasError=false,errors=[],api,ctx){const hooks=this.getHooksForPath("always",path);for(const hook of hooks){try{hook.handler({path,args,result:hasError?void 0:resultOrError,hasError,errors,api,ctx})}catch(error){const sourceInfo={type:"always",subset:hook.subset,hookTag:hook.id,hookId:hook.id,timestamp:Date.now(),stack:error.stack};this.executeErrorHooks(path,error,sourceInfo,args,api,ctx)}}}executeErrorHooks(path,error,source,args,api,ctx){if(error&&typeof error==="object"){error[ERROR_HOOK_PROCESSED]=true}const hooks=this.getHooksForPath("error",path);for(const hook of hooks){try{hook.handler({path,args,error,errorType:error?.constructor?.name||"Error",source,timestamp:new Date,api,ctx})}catch(hookError){this.slothlet.debug("hooks",`Error hook failed for ${path}:`,hookError)}}}#parseTypePattern(typePattern){if(typeof typePattern!=="string"){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"expected a string in the form 'pattern:type' (e.g. 'math.*:before')"})}const firstColon=typePattern.indexOf(":");if(firstColon===-1){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"missing ':' \u2014 use 'pattern:type' (e.g. 'math.*:before')"})}const lastColon=typePattern.lastIndexOf(":");const trailing=typePattern.substring(lastColon+1);if(this.#validTypes.has(trailing)){const pattern=typePattern.substring(0,lastColon);if(!pattern){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"empty path pattern \u2014 use 'pattern:type' (e.g. 'math.*:before')"})}return{type:trailing,pattern}}const leading=typePattern.substring(0,firstColon);if(this.#validTypes.has(leading)){const pattern=typePattern.substring(firstColon+1);if(!pattern){throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:"empty path pattern \u2014 use 'pattern:type' (e.g. 'math.*:before')"})}if(!this.slothlet.config?.silent){new this.SlothletWarning("HOOK_TYPEPATTERN_PREFIX_DEPRECATED",{given:typePattern,suggested:`${pattern}:${leading}`})}return{type:leading,pattern}}throw new this.slothlet.SlothletError("INVALID_TYPE_PATTERN",{typePattern,expected:`no hook type found \u2014 end with one of: ${Array.from(this.#validTypes).join(", ")} (e.g. 'math.*:before')`})}#compilePattern(pattern){return compilePattern(pattern,{onMaxDepth:maxDepth=>{throw new this.SlothletError("HOOK_BRACE_EXPANSION_MAX_DEPTH",{maxDepth},null,{validationError:true})}})}#isGlobPattern(pattern){return/[*?{!]/.test(pattern)}getCompilePatternForDiagnostics(){return this.#compilePattern.bind(this)}#generateId(){return`hook-${++this.#idCounter}`}#removeHook(hook){const typeIndex=this.#hooks[hook.type];const subsetIndex=typeIndex[hook.subset];const patternHooks=subsetIndex[hook.pattern];if(!patternHooks){throw new this.slothlet.SlothletError("INTERNAL_HOOK_STATE_CORRUPT",{hookId:hook.id,type:hook.type,subset:hook.subset,pattern:hook.pattern,detail:"patternHooks array missing from subsetIndex \u2014 #byId and #hooks are desynced"})}const index=patternHooks.indexOf(hook);if(index===-1){throw new this.slothlet.SlothletError("INTERNAL_HOOK_STATE_CORRUPT",{hookId:hook.id,type:hook.type,subset:hook.subset,pattern:hook.pattern,detail:"hook object not found in patternHooks array \u2014 #byId and #hooks are desynced"})}patternHooks.splice(index,1);if(patternHooks.length===0){delete subsetIndex[hook.pattern]}this.#byId.delete(hook.id)}#setEnabledState(filter,enabled){let affected=0;if(filter.id){const hook=this.#byId.get(filter.id);if(hook){hook.enabled=enabled;affected=1}return affected}const types=filter.type?[filter.type]:Array.from(this.#validTypes);for(const type of types){const typeIndex=this.#hooks[type];for(const subset of["before","primary","after"]){const subsetIndex=typeIndex[subset];if(filter.pattern){const patternHooks=subsetIndex[filter.pattern]||[];for(const hook of patternHooks){hook.enabled=enabled;affected++}}else{for(const pattern in subsetIndex){const patternHooks=subsetIndex[pattern];for(const hook of patternHooks){hook.enabled=enabled;affected++}}}}}return affected}#serializeHook(hook){return{id:hook.id,type:hook.type,pattern:hook.pattern,priority:hook.priority,subset:hook.subset,enabled:hook.enabled,lockCaller:hook.lockCaller}}exportHooks(){const registrations=[];for(const hook of this.#byId.values()){registrations.push({typePattern:`${hook.pattern}:${hook.type}`,handler:hook.handler,options:{id:hook.id,priority:hook.priority,subset:hook.subset,lockCaller:hook.lockCaller},ownerPath:hook.ownerPath,ownerFilePath:hook.ownerFilePath,enabled:hook.enabled})}return registrations}importHooks(registrations){if(!Array.isArray(registrations))return;for(const reg of registrations){this.on(reg.typePattern,reg.handler,{...reg.options,[REPLAY_IDENTITY]:{ownerPath:reg.ownerPath??null,ownerFilePath:reg.ownerFilePath??null}});if(!reg.enabled){this.disable({id:reg.options.id})}}}async shutdown(){this.#hooks={before:{before:{},primary:{},after:{}},after:{before:{},primary:{},after:{}},always:{before:{},primary:{},after:{}},error:{before:{},primary:{},after:{}}};this.#byId.clear();this.#idCounter=0}}export{HookManager};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"@cldmv/slothlet/factories/component-base";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";import{translate}from"@cldmv/slothlet/i18n";let ruleIdCounter=0;class PermissionManager extends ComponentBase{static slothletProperty="permissionManager";#rules=new Map;#defaultPolicy="allow";#enabled=false;#audit="default";#readGating=false;#resolvedCache=new Map;#compiledCache=new Map;constructor(slothlet){super(slothlet);const permConfig=slothlet.config?.permissions;if(permConfig){this.#defaultPolicy=permConfig.defaultPolicy||"allow";this.#enabled=permConfig.enabled!==false;this.#audit=permConfig.audit||"default";this.#readGating=permConfig.readGating!==false;if(Array.isArray(permConfig.rules)){for(const rule of permConfig.rules){this.addRule(rule,null)}}}this.addRule({caller:"**",target:"slothlet.permissions.control.**",effect:"deny"},"__builtin__");this.addRule({caller:"**",target:"slothlet.lockCaller",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.bind",effect:"allow"},"__builtin__")}addRule(rule,ownerModuleID=null,ruleId=null){this.#validateRule(rule);const id=ruleId||`perm-${++ruleIdCounter}`;const entry={id,caller:rule.caller,target:rule.target,effect:rule.effect,condition:rule.condition??null,ownerModuleID,registeredAt:Date.now()};this.#rules.set(id,entry);this.#clearCache();this.debug("permissions",{key:"DEBUG_PERMISSION_RULE_ADDED",ruleId:id,caller:rule.caller,target:rule.target,effect:rule.effect,ownerModuleID});return id}removeRule(ruleId,callerModuleID=null){const entry=this.#rules.get(ruleId);if(!entry)return false;if(callerModuleID&&entry.ownerModuleID&&callerModuleID===entry.ownerModuleID){throw new this.SlothletError("PERMISSION_SELF_MODIFY",{ruleId,moduleID:callerModuleID})}this.#rules.delete(ruleId);this.#clearCache();this.debug("permissions",{key:"DEBUG_PERMISSION_RULE_REMOVED",ruleId,caller:entry.caller,target:entry.target,effect:entry.effect});return true}checkAccess(callerPath,targetPath,callerFilePath=null,targetFilePath=null,runtimeContext=null,options=null){const normalizedOptions=options==null?{}:options;if(typeof normalizedOptions!=="object"||Array.isArray(normalizedOptions)){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"options",expected:"object with optional boolean useCache",received:Array.isArray(normalizedOptions)?"array":typeof normalizedOptions,validationError:true})}const{useCache=true}=normalizedOptions;if(typeof useCache!=="boolean"){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"options.useCache",expected:"boolean",received:typeof useCache,validationError:true})}return this.#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,useCache).allowed}matchesCondition(condition,runtimeContext=null){if(condition==null)return true;this.#assertValidConditionPayload(condition,"INVALID_ARGUMENT");return this.#matchesConditionUnchecked(condition,runtimeContext)}#matchesConditionUnchecked(condition,runtimeContext=null){if(condition==null)return true;const ctx=runtimeContext??{};if(Array.isArray(condition)){return condition.some(entry=>this.#singleConditionMatches(entry,ctx))}return this.#singleConditionMatches(condition,ctx)}enforceAccess(callerPath,targetPath,callerFilePath=null,targetFilePath=null,runtimeContext=null){const result=this.#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,true);if(result.event){this.#emitAuditEvent(result.event,result.payload)}return result.allowed}getRulesForPath(targetPath){const matching=[];for(const entry of this.#rules.values()){const targetMatcher=this.#getCompiledPattern(entry.target);if(targetMatcher(targetPath)){matching.push(this.#serializeRule(entry))}}return matching}getRulesByModule(moduleID){const matching=[];for(const entry of this.#rules.values()){if(entry.ownerModuleID===moduleID){matching.push(this.#serializeRule(entry))}}return matching}getRulesForCaller(callerPath){const matching=[];for(const entry of this.#rules.values()){const callerMatcher=this.#getCompiledPattern(entry.caller);if(callerMatcher(callerPath)){matching.push(this.#serializeRule(entry))}}return matching}enable(){this.#enabled=true;this.#clearCache()}disable(){this.#enabled=false;this.#clearCache()}isEnabled(){return this.#enabled}isReadGatingEnabled(){return this.#readGating}setReadGating(value){if(typeof value!=="boolean"){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"value",expected:"boolean",received:typeof value,validationError:true})}this.#readGating=value}exportRules(){const rules=[];for(const entry of this.#rules.values()){rules.push({rule:{caller:entry.caller,target:entry.target,effect:entry.effect},ownerModuleID:entry.ownerModuleID})}return rules}importRules(registrations){if(!Array.isArray(registrations))return;for(const reg of registrations){this.addRule(reg.rule,reg.ownerModuleID)}}async shutdown(){this.#rules.clear();this.#resolvedCache.clear();this.#compiledCache.clear();this.#enabled=false;this.#defaultPolicy="allow";this.#audit="default";this.#readGating=false}#validateRule(rule){if(!rule||typeof rule!=="object"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_NOT_OBJECT"),received:typeof rule})}if(typeof rule.caller!=="string"||!rule.caller){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_CALLER_REQUIRED"),received:typeof rule.caller})}if(typeof rule.target!=="string"||!rule.target){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_TARGET_REQUIRED"),received:typeof rule.target})}if(rule.effect!=="allow"&&rule.effect!=="deny"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_EFFECT_INVALID"),received:rule.effect})}if(rule.condition!==void 0&&rule.condition!==null){this.#assertValidConditionPayload(rule.condition,"INVALID_PERMISSION_RULE")}}#assertValidConditionPayload(condition,errorCode){const getValueType=value=>{if(value===null)return"null";if(Array.isArray(value))return"array";return typeof value};const isPlainObject=value=>{if(value===null||typeof value!=="object")return false;const proto=Object.getPrototypeOf(value);return proto===Object.prototype||proto===null};const isValidConditionEntry=value=>typeof value==="function"||isPlainObject(value);const entries=Array.isArray(condition)?condition:[condition];const invalidEntry=entries.length===0?condition:entries.find(entry=>!isValidConditionEntry(entry));if(entries.length>0&&invalidEntry===void 0)return;const received=getValueType(invalidEntry);if(errorCode==="INVALID_PERMISSION_RULE"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_CONDITION_INVALID"),received})}throw new this.SlothletError("INVALID_ARGUMENT",{argument:"condition",expected:translate("PERM_RULE_CONDITION_INVALID"),received,validationError:true})}#deepObjectMatches(pattern,ctx){if(ctx==null||typeof ctx!=="object")return false;for(const[key,val]of Object.entries(pattern)){const proto=val!==null&&typeof val==="object"?Object.getPrototypeOf(val):null;const isPlainNested=proto===Object.prototype||proto===null;if(val!==null&&typeof val==="object"&&isPlainNested){if(!this.#deepObjectMatches(val,ctx[key]))return false}else{if(ctx[key]!==val)return false}}return true}#singleConditionMatches(conditionEntry,ctx){if(typeof conditionEntry==="function"){try{return!!conditionEntry(ctx)}catch{return false}}return this.#deepObjectMatches(conditionEntry,ctx)}#conditionMatches(entry,runtimeContext){return this.#matchesConditionUnchecked(entry.condition,runtimeContext)}#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,useCache){const isControlTarget=targetPath?.startsWith("slothlet.permissions.control.");if(!this.#enabled&&!isControlTarget)return{allowed:true,event:null,payload:null};if(callerFilePath&&targetFilePath&&callerFilePath===targetFilePath){return{allowed:true,event:"permission:self-bypass",payload:{caller:callerPath,target:targetPath,filePath:callerFilePath}}}const cacheKey=`${callerPath}::${targetPath}`;if(useCache&&this.#resolvedCache.has(cacheKey)){return this.#resolvedCache.get(cacheKey)}const entry=this.#evaluate(callerPath,targetPath,runtimeContext);if(useCache&&!entry.hasConditionalRules){this.#resolvedCache.set(cacheKey,entry)}return entry}#evaluate(callerPath,targetPath,runtimeContext=null){const matches=[];for(const entry of this.#rules.values()){const callerMatcher=this.#getCompiledPattern(entry.caller);const targetMatcher=this.#getCompiledPattern(entry.target);if(callerMatcher(callerPath)&&targetMatcher(targetPath)){matches.push(entry)}}const hasConditionalRules=matches.some(m=>m.condition!=null);const conditioned=matches.filter(entry=>this.#conditionMatches(entry,runtimeContext));if(conditioned.length===0){const allowed2=this.#defaultPolicy==="allow";return{allowed:allowed2,event:"permission:default",payload:{caller:callerPath,target:targetPath,policy:this.#defaultPolicy},hasConditionalRules}}conditioned.sort((a,b)=>{const specA=this.#computeSpecificity(a,callerPath,targetPath);const specB=this.#computeSpecificity(b,callerPath,targetPath);if(specA!==specB)return specB-specA;return a.registeredAt-b.registeredAt});const highestSpec=this.#computeSpecificity(conditioned[0],callerPath,targetPath);const topTier=conditioned.filter(m=>this.#computeSpecificity(m,callerPath,targetPath)===highestSpec);const winner=topTier[topTier.length-1];const allowed=winner.effect==="allow";return{allowed,event:allowed?"permission:allowed":"permission:denied",payload:{caller:callerPath,target:targetPath,rule:this.#serializeRule(winner),conditionMatched:winner.condition!=null},hasConditionalRules}}#computeSpecificity(entry,callerPath,targetPath){return this.#patternSpecificity(entry.caller,callerPath)+this.#patternSpecificity(entry.target,targetPath)}#patternSpecificity(pattern,_path){if(!pattern.includes("*")&&!pattern.includes("?")&&!pattern.includes("{")){return 3}if(pattern.includes("**")){return 1}return 2}#getCompiledPattern(pattern){let matcher=this.#compiledCache.get(pattern);if(!matcher){matcher=compilePattern(pattern);this.#compiledCache.set(pattern,matcher)}return matcher}#clearCache(){this.#resolvedCache.clear()}#emitAuditEvent(event,payload){this.debug("permissions",{key:event==="permission:denied"?"DEBUG_PERMISSION_DENIED":event==="permission:allowed"?"DEBUG_PERMISSION_ALLOWED":event==="permission:self-bypass"?"DEBUG_PERMISSION_SELF_BYPASS":"DEBUG_PERMISSION_DEFAULT",...payload});const alwaysEmit=event==="permission:denied"||event==="permission:self-bypass";if(!alwaysEmit&&this.#audit!=="verbose")return;const lifecycle=this.slothlet.handlers?.lifecycle;if(lifecycle){lifecycle.emit(event,{...payload,timestamp:Date.now()})}}#serializeRule(entry){return{id:entry.id,caller:entry.caller,target:entry.target,effect:entry.effect,condition:entry.condition??null,ownerModuleID:entry.ownerModuleID,registeredAt:entry.registeredAt}}debug(category,data){this.slothlet.debug(category,data)}}export{PermissionManager};
17
+ import{ComponentBase}from"@cldmv/slothlet/factories/component-base";import{compilePattern}from"@cldmv/slothlet/helpers/pattern-matcher";import{translate}from"@cldmv/slothlet/i18n";let ruleIdCounter=0;const HOOK_TARGET_TYPES=new Set(["before","after","always","error","hook"]);class PermissionManager extends ComponentBase{static slothletProperty="permissionManager";#rules=new Map;#defaultPolicy="allow";#enabled=false;#audit="default";#readGating=false;#resolvedCache=new Map;#compiledCache=new Map;constructor(slothlet){super(slothlet);const permConfig=slothlet.config?.permissions;if(permConfig){this.#defaultPolicy=permConfig.defaultPolicy||"allow";this.#enabled=permConfig.enabled!==false;this.#audit=permConfig.audit||"default";this.#readGating=permConfig.readGating!==false;if(Array.isArray(permConfig.rules)){for(const rule of permConfig.rules){this.addRule(rule,null)}}}this.addRule({caller:"**",target:"slothlet.permissions.control.**",effect:"deny"},"__builtin__");this.addRule({caller:"**",target:"slothlet.lockCaller",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.bind",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.hook.**",effect:"deny"},"__builtin__");this.addRule({caller:"**",target:"slothlet.hook.list",effect:"allow"},"__builtin__");this.addRule({caller:"**",target:"slothlet.hook.on",effect:"allow"},"__builtin__")}addRule(rule,ownerModuleID=null,ruleId=null){this.#validateRule(rule);const id=ruleId||`perm-${++ruleIdCounter}`;const hookTarget=this.#parseHookTarget(rule.target);const entry={id,caller:rule.caller,target:rule.target,effect:rule.effect,condition:rule.condition??null,hookType:hookTarget?hookTarget.hookType:null,hookPathPattern:hookTarget?hookTarget.pathPattern:null,ownerModuleID,registeredAt:Date.now()};this.#rules.set(id,entry);this.#clearCache();this.debug("permissions",{key:"DEBUG_PERMISSION_RULE_ADDED",ruleId:id,caller:rule.caller,target:rule.target,effect:rule.effect,ownerModuleID});return id}removeRule(ruleId,callerModuleID=null){const entry=this.#rules.get(ruleId);if(!entry)return false;if(callerModuleID&&entry.ownerModuleID&&callerModuleID===entry.ownerModuleID){throw new this.SlothletError("PERMISSION_SELF_MODIFY",{ruleId,moduleID:callerModuleID})}this.#rules.delete(ruleId);this.#clearCache();this.debug("permissions",{key:"DEBUG_PERMISSION_RULE_REMOVED",ruleId,caller:entry.caller,target:entry.target,effect:entry.effect});return true}checkAccess(callerPath,targetPath,callerFilePath=null,targetFilePath=null,runtimeContext=null,options=null){const normalizedOptions=options==null?{}:options;if(typeof normalizedOptions!=="object"||Array.isArray(normalizedOptions)){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"options",expected:"object with optional boolean useCache",received:Array.isArray(normalizedOptions)?"array":typeof normalizedOptions,validationError:true})}const{useCache=true}=normalizedOptions;if(typeof useCache!=="boolean"){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"options.useCache",expected:"boolean",received:typeof useCache,validationError:true})}return this.#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,useCache).allowed}matchesCondition(condition,runtimeContext=null){if(condition==null)return true;this.#assertValidConditionPayload(condition,"INVALID_ARGUMENT");return this.#matchesConditionUnchecked(condition,runtimeContext)}#matchesConditionUnchecked(condition,runtimeContext=null){if(condition==null)return true;const ctx=runtimeContext??{};if(Array.isArray(condition)){return condition.some(entry=>this.#singleConditionMatches(entry,ctx))}return this.#singleConditionMatches(condition,ctx)}enforceAccess(callerPath,targetPath,callerFilePath=null,targetFilePath=null,runtimeContext=null){const result=this.#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,true);if(result.event){this.#emitAuditEvent(result.event,result.payload)}return result.allowed}enforceHookAccess(callerPath,hookPath,hookType,callerFilePath=null,targetFilePath=null,runtimeContext=null){const result=this.#resolveHookAccess(callerPath,hookPath,hookType,callerFilePath,targetFilePath,runtimeContext);if(result.event){this.#emitAuditEvent(result.event,result.payload)}return result.allowed}checkHookAccess(callerPath,hookPath,hookType,callerFilePath=null,targetFilePath=null,runtimeContext=null){return this.#resolveHookAccess(callerPath,hookPath,hookType,callerFilePath,targetFilePath,runtimeContext).allowed}getRulesForPath(targetPath){const matching=[];for(const entry of this.#rules.values()){const targetMatcher=this.#getCompiledPattern(entry.target);if(targetMatcher(targetPath)){matching.push(this.#serializeRule(entry))}}return matching}getRulesByModule(moduleID){const matching=[];for(const entry of this.#rules.values()){if(entry.ownerModuleID===moduleID){matching.push(this.#serializeRule(entry))}}return matching}getRulesForCaller(callerPath){const matching=[];for(const entry of this.#rules.values()){const callerMatcher=this.#getCompiledPattern(entry.caller);if(callerMatcher(callerPath)){matching.push(this.#serializeRule(entry))}}return matching}enable(){this.#enabled=true;this.#clearCache()}disable(){this.#enabled=false;this.#clearCache()}isEnabled(){return this.#enabled}isReadGatingEnabled(){return this.#readGating}setReadGating(value){if(typeof value!=="boolean"){throw new this.SlothletError("INVALID_ARGUMENT",{argument:"value",expected:"boolean",received:typeof value,validationError:true})}this.#readGating=value}exportRules(){const rules=[];for(const entry of this.#rules.values()){rules.push({rule:{caller:entry.caller,target:entry.target,effect:entry.effect},ownerModuleID:entry.ownerModuleID})}return rules}importRules(registrations){if(!Array.isArray(registrations))return;for(const reg of registrations){this.addRule(reg.rule,reg.ownerModuleID)}}async shutdown(){this.#rules.clear();this.#resolvedCache.clear();this.#compiledCache.clear();this.#enabled=false;this.#defaultPolicy="allow";this.#audit="default";this.#readGating=false}#validateRule(rule){if(!rule||typeof rule!=="object"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_NOT_OBJECT"),received:typeof rule})}if(typeof rule.caller!=="string"||!rule.caller){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_CALLER_REQUIRED"),received:typeof rule.caller})}if(typeof rule.target!=="string"||!rule.target){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_TARGET_REQUIRED"),received:typeof rule.target})}if(rule.effect!=="allow"&&rule.effect!=="deny"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_EFFECT_INVALID"),received:rule.effect})}if(rule.condition!==void 0&&rule.condition!==null){this.#assertValidConditionPayload(rule.condition,"INVALID_PERMISSION_RULE")}}#assertValidConditionPayload(condition,errorCode){const getValueType=value=>{if(value===null)return"null";if(Array.isArray(value))return"array";return typeof value};const isPlainObject=value=>{if(value===null||typeof value!=="object")return false;const proto=Object.getPrototypeOf(value);return proto===Object.prototype||proto===null};const isValidConditionEntry=value=>typeof value==="function"||isPlainObject(value);const entries=Array.isArray(condition)?condition:[condition];const invalidEntry=entries.length===0?condition:entries.find(entry=>!isValidConditionEntry(entry));if(entries.length>0&&invalidEntry===void 0)return;const received=getValueType(invalidEntry);if(errorCode==="INVALID_PERMISSION_RULE"){throw new this.SlothletError("INVALID_PERMISSION_RULE",{reason:translate("PERM_RULE_CONDITION_INVALID"),received})}throw new this.SlothletError("INVALID_ARGUMENT",{argument:"condition",expected:translate("PERM_RULE_CONDITION_INVALID"),received,validationError:true})}#deepObjectMatches(pattern,ctx){if(ctx==null||typeof ctx!=="object")return false;for(const[key,val]of Object.entries(pattern)){const proto=val!==null&&typeof val==="object"?Object.getPrototypeOf(val):null;const isPlainNested=proto===Object.prototype||proto===null;if(val!==null&&typeof val==="object"&&isPlainNested){if(!this.#deepObjectMatches(val,ctx[key]))return false}else{if(ctx[key]!==val)return false}}return true}#singleConditionMatches(conditionEntry,ctx){if(typeof conditionEntry==="function"){try{return!!conditionEntry(ctx)}catch{return false}}return this.#deepObjectMatches(conditionEntry,ctx)}#conditionMatches(entry,runtimeContext){return this.#matchesConditionUnchecked(entry.condition,runtimeContext)}#resolveAccess(callerPath,targetPath,callerFilePath,targetFilePath,runtimeContext,useCache){const isControlTarget=targetPath?.startsWith("slothlet.permissions.control.");if(!this.#enabled&&!isControlTarget)return{allowed:true,event:null,payload:null};if(callerFilePath&&targetFilePath&&callerFilePath===targetFilePath){return{allowed:true,event:"permission:self-bypass",payload:{caller:callerPath,target:targetPath,filePath:callerFilePath}}}const cacheKey=`${callerPath}::${targetPath}`;if(useCache&&this.#resolvedCache.has(cacheKey)){return this.#resolvedCache.get(cacheKey)}const entry=this.#evaluate(callerPath,targetPath,runtimeContext);if(useCache&&!entry.hasConditionalRules){this.#resolvedCache.set(cacheKey,entry)}return entry}#evaluate(callerPath,targetPath,runtimeContext=null){const matches=[];for(const entry of this.#rules.values()){if(entry.hookType!=null)continue;const callerMatcher=this.#getCompiledPattern(entry.caller);const targetMatcher=this.#getCompiledPattern(entry.target);if(callerMatcher(callerPath)&&targetMatcher(targetPath)){matches.push(entry)}}const hasConditionalRules=matches.some(m=>m.condition!=null);const conditioned=matches.filter(entry=>this.#conditionMatches(entry,runtimeContext));if(conditioned.length===0){const allowed2=this.#defaultPolicy==="allow";return{allowed:allowed2,event:"permission:default",payload:{caller:callerPath,target:targetPath,policy:this.#defaultPolicy},hasConditionalRules}}conditioned.sort((a,b)=>{const specA=this.#computeSpecificity(a,callerPath,targetPath);const specB=this.#computeSpecificity(b,callerPath,targetPath);if(specA!==specB)return specB-specA;return a.registeredAt-b.registeredAt});const highestSpec=this.#computeSpecificity(conditioned[0],callerPath,targetPath);const topTier=conditioned.filter(m=>this.#computeSpecificity(m,callerPath,targetPath)===highestSpec);const winner=topTier[topTier.length-1];const allowed=winner.effect==="allow";return{allowed,event:allowed?"permission:allowed":"permission:denied",payload:{caller:callerPath,target:targetPath,rule:this.#serializeRule(winner),conditionMatched:winner.condition!=null},hasConditionalRules}}#resolveHookAccess(callerPath,hookPath,hookType,callerFilePath,targetFilePath,runtimeContext){if(callerPath==null)return{allowed:true,event:null,payload:null};const hookDecision=this.#evaluateHook(callerPath,hookPath,hookType,runtimeContext);if(hookDecision.matched){return{allowed:hookDecision.allowed,event:hookDecision.event,payload:hookDecision.payload}}const callDecision=this.#resolveAccess(callerPath,hookPath,callerFilePath,targetFilePath,runtimeContext,true);return{allowed:callDecision.allowed,event:callDecision.event,payload:callDecision.payload}}#evaluateHook(callerPath,hookPath,hookType,runtimeContext){const matches=[];for(const entry of this.#rules.values()){if(entry.hookType==null)continue;if(entry.hookType!=="hook"&&entry.hookType!==hookType)continue;const callerMatcher=this.#getCompiledPattern(entry.caller);const pathMatcher=this.#getCompiledPattern(entry.hookPathPattern);if(callerMatcher(callerPath)&&pathMatcher(hookPath)){matches.push(entry)}}const conditioned=matches.filter(entry=>this.#conditionMatches(entry,runtimeContext));if(conditioned.length===0){return{matched:false,allowed:false,event:null,payload:null}}const spec=e=>this.#patternSpecificity(e.caller,callerPath)+this.#patternSpecificity(e.hookPathPattern,hookPath)+(e.hookType==="hook"?0:1);conditioned.sort((a,b)=>{const specA=spec(a);const specB=spec(b);if(specA!==specB)return specB-specA;return a.registeredAt-b.registeredAt});const highest=spec(conditioned[0]);const topTier=conditioned.filter(m=>spec(m)===highest);const winner=topTier[topTier.length-1];const allowed=winner.effect==="allow";return{matched:true,allowed,event:allowed?"permission:allowed":"permission:denied",payload:{caller:callerPath,target:`${hookPath}:${hookType}`,rule:this.#serializeRule(winner),conditionMatched:winner.condition!=null}}}#parseHookTarget(target){const lastColon=target.lastIndexOf(":");if(lastColon===-1)return null;const type=target.substring(lastColon+1);if(!HOOK_TARGET_TYPES.has(type))return null;const pathPattern=target.substring(0,lastColon);if(!pathPattern)return null;return{pathPattern,hookType:type}}#computeSpecificity(entry,callerPath,targetPath){return this.#patternSpecificity(entry.caller,callerPath)+this.#patternSpecificity(entry.target,targetPath)}#patternSpecificity(pattern,___path){if(!pattern.includes("*")&&!pattern.includes("?")&&!pattern.includes("{")){return 3}if(pattern.includes("**")){return 1}return 2}#getCompiledPattern(pattern){let matcher=this.#compiledCache.get(pattern);if(!matcher){matcher=compilePattern(pattern);this.#compiledCache.set(pattern,matcher)}return matcher}#clearCache(){this.#resolvedCache.clear()}#emitAuditEvent(event,payload){this.debug("permissions",{key:event==="permission:denied"?"DEBUG_PERMISSION_DENIED":event==="permission:allowed"?"DEBUG_PERMISSION_ALLOWED":event==="permission:self-bypass"?"DEBUG_PERMISSION_SELF_BYPASS":"DEBUG_PERMISSION_DEFAULT",...payload});const alwaysEmit=event==="permission:denied"||event==="permission:self-bypass";if(!alwaysEmit&&this.#audit!=="verbose")return;const lifecycle=this.slothlet.handlers?.lifecycle;if(lifecycle){lifecycle.emit(event,{...payload,timestamp:Date.now()})}}#serializeRule(entry){return{id:entry.id,caller:entry.caller,target:entry.target,effect:entry.effect,condition:entry.condition??null,ownerModuleID:entry.ownerModuleID,registeredAt:entry.registeredAt}}debug(category,data){this.slothlet.debug(category,data)}}export{PermissionManager};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{ComponentBase}from"@cldmv/slothlet/factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{isNode as IS_NODE}from"@cldmv/slothlet/helpers/platform";function normalizeHookConfig(hook){const hookConfig={enabled:false,pattern:"**",suppressErrors:false};if(hook===true||hook===false){hookConfig.enabled=hook;hookConfig.pattern=hook?"**":null}else if(typeof hook==="string"){hookConfig.enabled=true;hookConfig.pattern=hook}else if(hook&&typeof hook==="object"){hookConfig.enabled=hook.enabled!==false;hookConfig.pattern=hook.pattern||"**";hookConfig.suppressErrors=hook.suppressErrors||false}return hookConfig}class Config extends ComponentBase{static slothletProperty="config";normalizeCollision(collision){const validModes=["skip","warn","replace","merge","merge-replace","error"];const defaultMode="merge";if(typeof collision==="string"){const normalized=collision.toLowerCase();const mode=validModes.includes(normalized)?normalized:defaultMode;return{initial:mode,api:mode}}if(collision&&typeof collision==="object"){const validateMode=m=>{if(!m)return defaultMode;const normalized=String(m).toLowerCase();return validModes.includes(normalized)?normalized:defaultMode};return{initial:validateMode(collision.initial),api:validateMode(collision.api)}}return{initial:defaultMode,api:defaultMode}}normalizeRuntime(runtime){if(!runtime||typeof runtime!=="string"){return"async"}const normalized=runtime.toLowerCase().trim();if(normalized==="async"||normalized==="asynclocal"||normalized==="asynclocalstorage"){return"async"}if(normalized==="live"||normalized==="livebindings"||normalized==="experimental"){return"live"}return"async"}normalizeMode(mode){if(!mode||typeof mode!=="string"){return"eager"}const normalized=mode.toLowerCase().trim();if(normalized==="lazy"||normalized==="deferred"||normalized==="proxy"){return"lazy"}if(normalized==="eager"||normalized==="immediate"||normalized==="preload"){return"eager"}return"eager"}normalizeMutations(mutations){const defaults={add:true,remove:true,reload:true,permissions:true};if(!mutations||typeof mutations!=="object"){return defaults}return{add:mutations.add===false?false:true,remove:mutations.remove===false?false:true,reload:mutations.reload===false?false:true,permissions:mutations.permissions===false?false:true}}normalizeDebug(debug){if(!debug){return{builder:false,api:false,index:false,modes:false,wrapper:false,ownership:false,context:false,initialization:false,materialize:false,versioning:false,permissions:false}}if(debug===true){return{builder:true,api:true,index:true,modes:true,wrapper:true,ownership:true,context:true,initialization:true,materialize:true,versioning:true,permissions:true}}if(typeof debug==="object"){return{builder:debug.builder||false,api:debug.api||false,index:debug.index||false,modes:debug.modes||false,wrapper:debug.wrapper||false,ownership:debug.ownership||false,context:debug.context||false,initialization:debug.initialization||false,materialize:debug.materialize||false,versioning:debug.versioning||false,permissions:debug.permissions||false}}return{builder:false,api:false,index:false,modes:false,wrapper:false,ownership:false,context:false,initialization:false,materialize:false,versioning:false,permissions:false}}normalizeEnvTarget(platform,hasManifest=false){if(platform==="browser")return"browser";if(platform==="node")return"node";if(hasManifest)return"browser";return IS_NODE?"node":"browser"}normalizeHook(hook){return normalizeHookConfig(hook)}transformConfig(config={}){const hasManifest=config.manifest!=null;const envTarget=this.normalizeEnvTarget(config.platform,hasManifest);const rawBase=config.base??config.dir;if(config.dir!==void 0&&config.base===void 0&&!config.silent){new this.SlothletWarning("V3_CONFIG_DEPRECATED",{option:"dir",replacement:"base"})}if(!rawBase){throw new this.SlothletError("INVALID_CONFIG_DIR_MISSING",{},null,{validationError:true})}if(envTarget==="browser"){if(!config.manifest||typeof config.manifest!=="object"||Array.isArray(config.manifest)){throw new this.SlothletError("INVALID_CONFIG_BROWSER_REQUIRES_MANIFEST",{},null,{validationError:true})}if(!Array.isArray(config.manifest.files)||!Array.isArray(config.manifest.directories)){throw new this.SlothletError("INVALID_CONFIG_BROWSER_MANIFEST_INVALID",{received:typeof config.manifest},null,{validationError:true})}if(config.resolveModuleSpecifier!==void 0&&config.resolveModuleSpecifier!==null&&typeof config.resolveModuleSpecifier!=="function"){throw new this.SlothletError("INVALID_CONFIG_BROWSER_RESOLVE_SPECIFIER_INVALID",{received:typeof config.resolveModuleSpecifier},null,{validationError:true})}}const resolvedDir=envTarget==="browser"?rawBase:this.slothlet.helpers.resolver.resolvePathFromCaller(rawBase);let mutations=null;if(config.allowMutation===false){mutations={add:false,remove:false,reload:false};if(!config.silent){new this.SlothletWarning("V2_CONFIG_UNSUPPORTED",{option:"allowMutation",replacement:"api.mutations: { add: false, remove: false, reload: false }",hint:"The allowMutation config option was part of v2. Use api.mutations for granular control in v3."})}}let collision=null;if(config.collision&&!config.api?.collision){collision=this.normalizeCollision(config.collision);if(!config.silent){new this.SlothletWarning("V2_CONFIG_UNSUPPORTED",{option:"collision",replacement:"api.collision",hint:"Root-level collision config was part of v2. Use api.collision in v3."})}}const apiConfig=config.api||{};const finalCollision=apiConfig.collision?this.normalizeCollision(apiConfig.collision):collision||this.normalizeCollision(null);const finalMutations=apiConfig.mutations?this.normalizeMutations(apiConfig.mutations):mutations||this.normalizeMutations(null);let scopeConfig=config.scope;if(scopeConfig&&typeof scopeConfig==="object"&&scopeConfig.merge){const validMergeStrategies=["shallow","deep"];if(!validMergeStrategies.includes(scopeConfig.merge)){throw new this.SlothletError("INVALID_CONFIG",{option:"scope.merge",value:scopeConfig.merge,expected:validMergeStrategies.join(" or "),hint:`Invalid merge strategy: "${scopeConfig.merge}". Must be "shallow" or "deep".`,validationError:true},null,{validationError:true})}}const hookConfig=this.normalizeHook(config.hook);let trackingConfig={materialization:false};if(config.tracking===true||config.tracking===false){trackingConfig.materialization=config.tracking}else if(config.tracking&&typeof config.tracking==="object"){trackingConfig.materialization=config.tracking.materialization===true}if(config.backgroundMaterialize===true){trackingConfig.materialization=true}if(config.versionDispatcher!==void 0&&config.versionDispatcher!==null){if(typeof config.versionDispatcher!=="string"&&typeof config.versionDispatcher!=="function"){throw new this.SlothletError("INVALID_CONFIG_VERSION_DISPATCHER",{received:typeof config.versionDispatcher,validationError:true})}}const permissionsConfig=this.normalizePermissions(config.permissions);const suppressFixes=this.normalizeSuppressFixes(config.suppressFixes,config.silent);let i18nConfig=null;if(config.i18n&&typeof config.i18n==="object"){i18nConfig={language:typeof config.i18n.language==="string"?config.i18n.language:void 0}}return{...config,base:resolvedDir,dir:resolvedDir,manifest:config.manifest??null,resolveModuleSpecifier:config.resolveModuleSpecifier??null,envTarget,mode:this.normalizeMode(config.mode),runtime:this.normalizeRuntime(config.runtime),apiDepth:config.apiDepth!==void 0?config.apiDepth:Infinity,reference:config.reference||null,context:config.context||null,i18n:i18nConfig,debug:this.normalizeDebug(config.debug),diagnostics:config.diagnostics===true,hook:hookConfig,collision:finalCollision,api:{collision:finalCollision,mutations:finalMutations},scope:scopeConfig,tracking:trackingConfig,backgroundMaterialize:config.backgroundMaterialize===true,silent:config.silent===true,typescript:this.normalizeTypeScript(config.typescript),env:this.normalizeEnv(config.env),versionDispatcher:config.versionDispatcher??null,permissions:permissionsConfig,suppressFixes}}normalizeSuppressFixes(suppressFixes,silent){const KNOWN_FIX_IDS=new Set(["C03_116"]);const REPO_PR_BASE="https://github.com/CLDMV/slothlet/pull/";if(!Array.isArray(suppressFixes)||suppressFixes.length===0){return new Set}const result=new Set;for(const rule of suppressFixes){if(typeof rule!=="string"||!KNOWN_FIX_IDS.has(rule)){continue}result.add(rule);if(!silent){const prNumber=rule.split("_").pop();const url=`${REPO_PR_BASE}${prNumber}`;new this.SlothletWarning("WARN_SUPPRESS_FIX_ACTIVE",{rule,url})}}return result}normalizeTypeScript(typescript){if(!typescript){return null}if(typescript===true){return{enabled:true,mode:"fast"}}if(typeof typescript==="string"){const mode=typescript.toLowerCase();if(mode==="fast"||mode==="strict"){return{enabled:true,mode}}return{enabled:true,mode:"fast"}}if(typeof typescript==="object"){const mode=typescript.mode==="strict"?"strict":"fast";return{enabled:true,mode,types:typescript.types||null,target:typescript.target||"es2020",sourcemap:typescript.sourcemap||false}}return null}normalizeEnv(env){if(!env||typeof env!=="object"){return null}const include=Array.isArray(env.include)?env.include.filter(k=>typeof k==="string"):null;if(include&&include.length>0){return{include}}return null}normalizePermissions(permissions){if(!permissions||typeof permissions!=="object"){return null}let defaultPolicy;if(permissions.defaultPolicy==="deny"){defaultPolicy="deny"}else if(permissions.defaultPolicy==="allow"||permissions.defaultPolicy===void 0){defaultPolicy="allow"}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.defaultPolicy",value:permissions.defaultPolicy,expected:'"allow" or "deny"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}const enabled=permissions.enabled!==false;let audit;if(permissions.audit==="verbose"){audit="verbose"}else if(permissions.audit==="default"||permissions.audit===void 0){audit="default"}else if(permissions.audit===true){audit="default"}else if(permissions.audit===false){audit="default"}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.audit",value:permissions.audit,expected:'"default" or "verbose"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let readGating;if(permissions.readGating===false){readGating=false}else if(permissions.readGating===true||permissions.readGating===void 0){readGating=true}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.readGating",value:permissions.readGating,expected:"boolean",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}if(permissions.rules!==void 0&&!Array.isArray(permissions.rules)){throw new SlothletError("INVALID_CONFIG",{option:"permissions.rules",value:permissions.rules,expected:"array",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}const rules=Array.isArray(permissions.rules)?permissions.rules:[];return{defaultPolicy,enabled,audit,readGating,rules}}}export{Config,normalizeHookConfig};
17
+ import{ComponentBase}from"@cldmv/slothlet/factories/component-base";import{SlothletError}from"@cldmv/slothlet/errors";import{isNode as IS_NODE}from"@cldmv/slothlet/helpers/platform";function normalizeHookConfig(hook){const hookConfig={enabled:false,pattern:"**",suppressErrors:false,pin:true};if(hook===true||hook===false){hookConfig.enabled=hook;hookConfig.pattern=hook?"**":null}else if(typeof hook==="string"){hookConfig.enabled=true;hookConfig.pattern=hook}else if(hook&&typeof hook==="object"){hookConfig.enabled=hook.enabled!==false;hookConfig.pattern=hook.pattern||"**";hookConfig.suppressErrors=hook.suppressErrors||false;hookConfig.pin=hook.pin!==false}return hookConfig}class Config extends ComponentBase{static slothletProperty="config";normalizeCollision(collision){const validModes=["skip","warn","replace","merge","merge-replace","error"];const defaultMode="merge";if(typeof collision==="string"){const normalized=collision.toLowerCase();const mode=validModes.includes(normalized)?normalized:defaultMode;return{initial:mode,api:mode}}if(collision&&typeof collision==="object"){const validateMode=m=>{if(!m)return defaultMode;const normalized=String(m).toLowerCase();return validModes.includes(normalized)?normalized:defaultMode};return{initial:validateMode(collision.initial),api:validateMode(collision.api)}}return{initial:defaultMode,api:defaultMode}}normalizeRuntime(runtime){if(!runtime||typeof runtime!=="string"){return"async"}const normalized=runtime.toLowerCase().trim();if(normalized==="async"||normalized==="asynclocal"||normalized==="asynclocalstorage"){return"async"}if(normalized==="live"||normalized==="livebindings"||normalized==="experimental"){return"live"}return"async"}normalizeMode(mode){if(!mode||typeof mode!=="string"){return"eager"}const normalized=mode.toLowerCase().trim();if(normalized==="lazy"||normalized==="deferred"||normalized==="proxy"){return"lazy"}if(normalized==="eager"||normalized==="immediate"||normalized==="preload"){return"eager"}return"eager"}normalizeMutations(mutations){const defaults={add:true,remove:true,reload:true,permissions:true};if(!mutations||typeof mutations!=="object"){return defaults}return{add:mutations.add===false?false:true,remove:mutations.remove===false?false:true,reload:mutations.reload===false?false:true,permissions:mutations.permissions===false?false:true}}normalizeDebug(debug){if(!debug){return{builder:false,api:false,index:false,modes:false,wrapper:false,ownership:false,context:false,initialization:false,materialize:false,versioning:false,permissions:false}}if(debug===true){return{builder:true,api:true,index:true,modes:true,wrapper:true,ownership:true,context:true,initialization:true,materialize:true,versioning:true,permissions:true}}if(typeof debug==="object"){return{builder:debug.builder||false,api:debug.api||false,index:debug.index||false,modes:debug.modes||false,wrapper:debug.wrapper||false,ownership:debug.ownership||false,context:debug.context||false,initialization:debug.initialization||false,materialize:debug.materialize||false,versioning:debug.versioning||false,permissions:debug.permissions||false}}return{builder:false,api:false,index:false,modes:false,wrapper:false,ownership:false,context:false,initialization:false,materialize:false,versioning:false,permissions:false}}normalizeEnvTarget(platform,hasManifest=false){if(platform==="browser")return"browser";if(platform==="node")return"node";if(hasManifest)return"browser";return IS_NODE?"node":"browser"}normalizeHook(hook){return normalizeHookConfig(hook)}transformConfig(config={}){const hasManifest=config.manifest!=null;const envTarget=this.normalizeEnvTarget(config.platform,hasManifest);const rawBase=config.base??config.dir;if(config.dir!==void 0&&config.base===void 0&&!config.silent){new this.SlothletWarning("V3_CONFIG_DEPRECATED",{option:"dir",replacement:"base"})}if(!rawBase){throw new this.SlothletError("INVALID_CONFIG_DIR_MISSING",{},null,{validationError:true})}if(envTarget==="browser"){if(!config.manifest||typeof config.manifest!=="object"||Array.isArray(config.manifest)){throw new this.SlothletError("INVALID_CONFIG_BROWSER_REQUIRES_MANIFEST",{},null,{validationError:true})}if(!Array.isArray(config.manifest.files)||!Array.isArray(config.manifest.directories)){throw new this.SlothletError("INVALID_CONFIG_BROWSER_MANIFEST_INVALID",{received:typeof config.manifest},null,{validationError:true})}if(config.resolveModuleSpecifier!==void 0&&config.resolveModuleSpecifier!==null&&typeof config.resolveModuleSpecifier!=="function"){throw new this.SlothletError("INVALID_CONFIG_BROWSER_RESOLVE_SPECIFIER_INVALID",{received:typeof config.resolveModuleSpecifier},null,{validationError:true})}}const resolvedDir=envTarget==="browser"?rawBase:this.slothlet.helpers.resolver.resolvePathFromCaller(rawBase);let mutations=null;if(config.allowMutation===false){mutations={add:false,remove:false,reload:false};if(!config.silent){new this.SlothletWarning("V2_CONFIG_UNSUPPORTED",{option:"allowMutation",replacement:"api.mutations: { add: false, remove: false, reload: false }",hint:"The allowMutation config option was part of v2. Use api.mutations for granular control in v3."})}}let collision=null;if(config.collision&&!config.api?.collision){collision=this.normalizeCollision(config.collision);if(!config.silent){new this.SlothletWarning("V2_CONFIG_UNSUPPORTED",{option:"collision",replacement:"api.collision",hint:"Root-level collision config was part of v2. Use api.collision in v3."})}}const apiConfig=config.api||{};const finalCollision=apiConfig.collision?this.normalizeCollision(apiConfig.collision):collision||this.normalizeCollision(null);const finalMutations=apiConfig.mutations?this.normalizeMutations(apiConfig.mutations):mutations||this.normalizeMutations(null);let scopeConfig=config.scope;if(scopeConfig&&typeof scopeConfig==="object"&&scopeConfig.merge){const validMergeStrategies=["shallow","deep"];if(!validMergeStrategies.includes(scopeConfig.merge)){throw new this.SlothletError("INVALID_CONFIG",{option:"scope.merge",value:scopeConfig.merge,expected:validMergeStrategies.join(" or "),hint:`Invalid merge strategy: "${scopeConfig.merge}". Must be "shallow" or "deep".`,validationError:true},null,{validationError:true})}}const hookConfig=this.normalizeHook(config.hook);let trackingConfig={materialization:false};if(config.tracking===true||config.tracking===false){trackingConfig.materialization=config.tracking}else if(config.tracking&&typeof config.tracking==="object"){trackingConfig.materialization=config.tracking.materialization===true}if(config.backgroundMaterialize===true){trackingConfig.materialization=true}if(config.versionDispatcher!==void 0&&config.versionDispatcher!==null){if(typeof config.versionDispatcher!=="string"&&typeof config.versionDispatcher!=="function"){throw new this.SlothletError("INVALID_CONFIG_VERSION_DISPATCHER",{received:typeof config.versionDispatcher,validationError:true})}}const permissionsConfig=this.normalizePermissions(config.permissions);const suppressFixes=this.normalizeSuppressFixes(config.suppressFixes,config.silent);let i18nConfig=null;if(config.i18n&&typeof config.i18n==="object"){i18nConfig={language:typeof config.i18n.language==="string"?config.i18n.language:void 0}}return{...config,base:resolvedDir,dir:resolvedDir,manifest:config.manifest??null,resolveModuleSpecifier:config.resolveModuleSpecifier??null,envTarget,mode:this.normalizeMode(config.mode),runtime:this.normalizeRuntime(config.runtime),apiDepth:config.apiDepth!==void 0?config.apiDepth:Infinity,reference:config.reference||null,context:config.context||null,i18n:i18nConfig,debug:this.normalizeDebug(config.debug),diagnostics:config.diagnostics===true,hook:hookConfig,collision:finalCollision,api:{collision:finalCollision,mutations:finalMutations},scope:scopeConfig,tracking:trackingConfig,backgroundMaterialize:config.backgroundMaterialize===true,silent:config.silent===true,typescript:this.normalizeTypeScript(config.typescript),env:this.normalizeEnv(config.env),versionDispatcher:config.versionDispatcher??null,permissions:permissionsConfig,suppressFixes}}normalizeSuppressFixes(suppressFixes,silent){const KNOWN_FIX_IDS=new Set(["C03_116"]);const REPO_PR_BASE="https://github.com/CLDMV/slothlet/pull/";if(!Array.isArray(suppressFixes)||suppressFixes.length===0){return new Set}const result=new Set;for(const rule of suppressFixes){if(typeof rule!=="string"||!KNOWN_FIX_IDS.has(rule)){continue}result.add(rule);if(!silent){const prNumber=rule.split("_").pop();const url=`${REPO_PR_BASE}${prNumber}`;new this.SlothletWarning("WARN_SUPPRESS_FIX_ACTIVE",{rule,url})}}return result}normalizeTypeScript(typescript){if(!typescript){return null}if(typescript===true){return{enabled:true,mode:"fast"}}if(typeof typescript==="string"){const mode=typescript.toLowerCase();if(mode==="fast"||mode==="strict"){return{enabled:true,mode}}return{enabled:true,mode:"fast"}}if(typeof typescript==="object"){const mode=typescript.mode==="strict"?"strict":"fast";return{enabled:true,mode,types:typescript.types||null,target:typescript.target||"es2020",sourcemap:typescript.sourcemap||false}}return null}normalizeEnv(env){if(!env||typeof env!=="object"){return null}const include=Array.isArray(env.include)?env.include.filter(k=>typeof k==="string"):null;if(include&&include.length>0){return{include}}return null}normalizePermissions(permissions){if(!permissions||typeof permissions!=="object"){return null}let defaultPolicy;if(permissions.defaultPolicy==="deny"){defaultPolicy="deny"}else if(permissions.defaultPolicy==="allow"||permissions.defaultPolicy===void 0){defaultPolicy="allow"}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.defaultPolicy",value:permissions.defaultPolicy,expected:'"allow" or "deny"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}const enabled=permissions.enabled!==false;let audit;if(permissions.audit==="verbose"){audit="verbose"}else if(permissions.audit==="default"||permissions.audit===void 0){audit="default"}else if(permissions.audit===true){audit="default"}else if(permissions.audit===false){audit="default"}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.audit",value:permissions.audit,expected:'"default" or "verbose"',hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}let readGating;if(permissions.readGating===false){readGating=false}else if(permissions.readGating===true||permissions.readGating===void 0){readGating=true}else{throw new SlothletError("INVALID_CONFIG",{option:"permissions.readGating",value:permissions.readGating,expected:"boolean",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}if(permissions.rules!==void 0&&!Array.isArray(permissions.rules)){throw new SlothletError("INVALID_CONFIG",{option:"permissions.rules",value:permissions.rules,expected:"array",hint:"HINT_INVALID_CONFIG"},null,{validationError:true})}const rules=Array.isArray(permissions.rules)?permissions.rules:[];return{defaultPolicy,enabled,audit,readGating,rules}}}export{Config,normalizeHookConfig};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import fs from"node:fs/promises";import path from"node:path";import{fileURLToPath}from"node:url";const SLOTHLET_SPEC_RE=/["'](@cldmv\/slothlet(?:\/[^"']+)?)["']/g;const DEFAULT_SLOTHLET_BASE="/node_modules/@cldmv/slothlet/";const LOADABLE_EXTENSIONS=new Set([".mjs",".cjs",".js",".ts",".mts",".cts"]);const SKIP_PREFIXES=["__","."];function isApiFile(filename){const ext=path.extname(filename);return LOADABLE_EXTENSIONS.has(ext)}function makeFileEntry(relativePath){const fullName=path.basename(relativePath);const name=path.basename(relativePath,path.extname(relativePath));return{path:relativePath,name,fullName}}async function scanDir(absDir,rootDir){let entries;try{entries=await fs.readdir(absDir,{withFileTypes:true})}catch{return{files:[],directories:[]}}const files=[];const directories=[];for(const entry of entries){if(SKIP_PREFIXES.some(p=>entry.name.startsWith(p)))continue;if(entry.isFile()){if(isApiFile(entry.name)){const absPath=path.join(absDir,entry.name);const rel=path.relative(rootDir,absPath).replace(/\\/g,"/");files.push(makeFileEntry(rel))}}else if(entry.isDirectory()){const absSubDir=path.join(absDir,entry.name);const relDir=path.relative(rootDir,absSubDir).replace(/\\/g,"/");const children=await scanDir(absSubDir,rootDir);if(children.files.length>0||children.directories.length>0){directories.push({name:entry.name,path:relDir,children})}}}return{files,directories}}async function generateManifest(dir){if(!dir||typeof dir!=="string"){throw new Error(`generateManifest: dir must be a non-empty string, received ${typeof dir}`)}const absDir=path.resolve(dir);let stat;try{stat=await fs.stat(absDir)}catch(err){throw new Error(`generateManifest: cannot read directory "${absDir}": ${err.message}`,{cause:err})}if(!stat.isDirectory()){throw new Error(`generateManifest: "${absDir}" is not a directory`)}return scanDir(absDir,absDir)}function slothletPackageRoot(){return path.resolve(path.dirname(fileURLToPath(import.meta.url)),"../../..")}async function collectSlothletSpecifiers(root){const specifiers=new Set(["@cldmv/slothlet"]);const SKIP_DIRS=new Set(["node_modules","types","coverage","tmp","tests","api_tests",".git","docs"]);async function scan(dir){const entries=await fs.readdir(dir,{withFileTypes:true});for(const e of entries){if(e.isDirectory()){if(!SKIP_DIRS.has(e.name)&&!e.name.startsWith("."))await scan(path.join(dir,e.name))}else if(/\.(mjs|cjs|js)$/.test(e.name)){const src=await fs.readFile(path.join(dir,e.name),"utf8");let m;SLOTHLET_SPEC_RE.lastIndex=0;while(m=SLOTHLET_SPEC_RE.exec(src)){if(!m[1].startsWith("@cldmv/slothlet/i18n/language/"))specifiers.add(m[1])}}}}await scan(root);return specifiers}async function generateImportMap(slothletBase=DEFAULT_SLOTHLET_BASE){const base=String(slothletBase).endsWith("/")?String(slothletBase):`${slothletBase}/`;const root=slothletPackageRoot();const imports={};for(const spec of await collectSlothletSpecifiers(root)){let resolved;try{resolved=import.meta.resolve(spec)}catch{continue}const rel=path.relative(root,fileURLToPath(resolved)).replace(/\\/g,"/");imports[spec]=base+rel}try{const sampleDir=path.dirname(fileURLToPath(import.meta.resolve("@cldmv/slothlet/i18n/language/en-us.json")));for(const f of(await fs.readdir(sampleDir)).filter(n=>n.endsWith(".json"))){const rel=path.relative(root,path.join(sampleDir,f)).replace(/\\/g,"/");imports[`@cldmv/slothlet/i18n/language/${f}`]=base+rel}}catch{}return{imports}}async function generateBrowserAssets(apiDir,options={}){const{slothletBase=DEFAULT_SLOTHLET_BASE}=options;if(typeof slothletBase!=="string"){throw new Error(`generateBrowserAssets: options.slothletBase must be a string (where @cldmv/slothlet is served in the browser), received ${typeof slothletBase}`)}const[manifest,importmap]=await Promise.all([generateManifest(apiDir),generateImportMap(slothletBase)]);return{manifest,importmap}}export{generateBrowserAssets,generateImportMap,generateManifest};
17
+ import fs from"node:fs/promises";import path from"node:path";import{fileURLToPath}from"node:url";import{SlothletError}from"@cldmv/slothlet/errors";const SLOTHLET_SPEC_RE=/["'](@cldmv\/slothlet(?:\/[^"']+)?)["']/g;const DEFAULT_SLOTHLET_BASE="/node_modules/@cldmv/slothlet/";const LOADABLE_EXTENSIONS=new Set([".mjs",".cjs",".js",".ts",".mts",".cts"]);const SKIP_PREFIXES=["__","."];function isApiFile(filename){const ext=path.extname(filename);return LOADABLE_EXTENSIONS.has(ext)}function makeFileEntry(relativePath){const fullName=path.basename(relativePath);const name=path.basename(relativePath,path.extname(relativePath));return{path:relativePath,name,fullName}}async function scanDir(absDir,rootDir){let entries;try{entries=await fs.readdir(absDir,{withFileTypes:true})}catch{return{files:[],directories:[]}}const files=[];const directories=[];for(const entry of entries){if(SKIP_PREFIXES.some(p=>entry.name.startsWith(p)))continue;if(entry.isFile()){if(isApiFile(entry.name)){const absPath=path.join(absDir,entry.name);const rel=path.relative(rootDir,absPath).replace(/\\/g,"/");files.push(makeFileEntry(rel))}}else if(entry.isDirectory()){const absSubDir=path.join(absDir,entry.name);const relDir=path.relative(rootDir,absSubDir).replace(/\\/g,"/");const children=await scanDir(absSubDir,rootDir);if(children.files.length>0||children.directories.length>0){directories.push({name:entry.name,path:relDir,children})}}}return{files,directories}}async function generateManifest(dir){if(!dir||typeof dir!=="string"){const received=typeof dir==="string"?"<empty>":dir===null?"null":Array.isArray(dir)?"array":typeof dir;throw new SlothletError("GENERATE_MANIFEST_DIR_INVALID",{received},null,{validationError:true})}const absDir=path.resolve(dir);let stat;try{stat=await fs.stat(absDir)}catch(err){throw new SlothletError("GENERATE_MANIFEST_DIR_UNREADABLE",{dir:absDir,reason:err.message},null,{validationError:true})}if(!stat.isDirectory()){throw new SlothletError("GENERATE_MANIFEST_NOT_DIRECTORY",{dir:absDir},null,{validationError:true})}return scanDir(absDir,absDir)}function slothletPackageRoot(){return path.resolve(path.dirname(fileURLToPath(import.meta.url)),"../../..")}async function collectSlothletSpecifiers(root){const specifiers=new Set(["@cldmv/slothlet"]);const pkg=JSON.parse(await fs.readFile(path.join(root,"package.json"),"utf8"));for(const key of Object.keys(pkg.exports)){if(key.includes("*")||key.endsWith(".json"))continue;specifiers.add(key==="."?"@cldmv/slothlet":`@cldmv/slothlet${key.slice(1)}`)}for(const[key,value]of Object.entries(pkg.exports)){if(!key.includes("*")||key.startsWith("./i18n/language/"))continue;const specPrefix=`@cldmv/slothlet${key.slice(1)}`.split("*")[0];const targets=[];(function collectTargets(v){if(typeof v==="string"){if(v.includes("*")&&v.endsWith(".mjs"))targets.push(v)}else if(v&&typeof v==="object"){for(const child of Object.values(v))collectTargets(child)}})(value);for(const tmpl of targets){const star=tmpl.indexOf("*");const dirRel=tmpl.slice(0,star);const suffix=tmpl.slice(star+1);let files;try{files=await fs.readdir(path.join(root,dirRel),{recursive:true})}catch{continue}for(const f of files){const norm=String(f).replace(/\\/g,"/");if(norm.endsWith(suffix))specifiers.add(specPrefix+norm.slice(0,-suffix.length))}}}const SKIP_DIRS=new Set(["node_modules","types","coverage","tmp","tests","api_tests",".git","docs"]);async function scan(dir){const entries=await fs.readdir(dir,{withFileTypes:true});for(const e of entries){if(e.isDirectory()){if(!SKIP_DIRS.has(e.name)&&!e.name.startsWith("."))await scan(path.join(dir,e.name))}else if(/\.(mjs|cjs|js)$/.test(e.name)){const src=await fs.readFile(path.join(dir,e.name),"utf8");let m;SLOTHLET_SPEC_RE.lastIndex=0;while(m=SLOTHLET_SPEC_RE.exec(src)){const spec=m[1];if(spec.startsWith("@cldmv/slothlet/i18n/language/"))continue;if(/[*<>]|\.\.\.|\/$/.test(spec)||spec.split("/").some(s=>s===""))continue;specifiers.add(spec)}}}}await scan(root);return specifiers}async function generateImportMap(slothletBase=DEFAULT_SLOTHLET_BASE){const base=String(slothletBase).endsWith("/")?String(slothletBase):`${slothletBase}/`;const root=slothletPackageRoot();const imports={};for(const spec of await collectSlothletSpecifiers(root)){let resolved;try{resolved=import.meta.resolve(spec)}catch{continue}const rel=path.relative(root,fileURLToPath(resolved)).replace(/\\/g,"/");imports[spec]=base+rel}try{const sampleDir=path.dirname(fileURLToPath(import.meta.resolve("@cldmv/slothlet/i18n/language/en-us.json")));for(const f of(await fs.readdir(sampleDir)).filter(n=>n.endsWith(".json"))){const rel=path.relative(root,path.join(sampleDir,f)).replace(/\\/g,"/");imports[`@cldmv/slothlet/i18n/language/${f}`]=base+rel}}catch{}return{imports}}async function generateBrowserAssets(apiDir,options={}){const{slothletBase=DEFAULT_SLOTHLET_BASE}=options;if(typeof slothletBase!=="string"){throw new SlothletError("GENERATE_BROWSER_ASSETS_SLOTHLET_BASE_INVALID",{received:typeof slothletBase},null,{validationError:true})}const[manifest,importmap]=await Promise.all([generateManifest(apiDir),generateImportMap(slothletBase)]);return{manifest,importmap}}export{collectSlothletSpecifiers,generateBrowserAssets,generateImportMap,generateManifest};
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import{SlothletError}from"@cldmv/slothlet/errors";import{validateModuleManifest}from"@cldmv/slothlet/helpers/module-manifest-validator";import{fsp,path}from"@cldmv/slothlet/helpers/platform";const DEFAULT_MANIFEST_FILE="slothlet.module.json";const UPWARD_WALK_CAP=20;async function discoverModules(options={}){const scanRoots=await resolveScanRoots(options.scanRoot);const manifestSource=parseManifestSource(options.manifest);const fieldSchema=options.schema??{};const filterFn=options.filter;const prefixes=normalizePrefixes(options.prefix);const candidates=[];for(const root of scanRoots){const mode=await detectScanMode(root);if(mode==="npm"){candidates.push(...await enumerateNpmPackages(root,prefixes))}else{candidates.push(...await enumerateFolderModules(root,prefixes))}}const results=[];const seenRealPaths=new Set;const realPathByNameVersion=new Map;for(const candidate of candidates){let realPath;try{realPath=await fsp.realpath(candidate.path)}catch{continue}if(seenRealPaths.has(realPath))continue;seenRealPaths.add(realPath);const pkgJsonPath=path.join(realPath,"package.json");const pkg=await readJsonOrNull(pkgJsonPath);if(!pkg||typeof pkg.name!=="string"||typeof pkg.version!=="string"){continue}const manifestPath=path.join(realPath,manifestSource.file);const manifestRaw=await loadManifestRaw(manifestPath,manifestSource,pkg.name);if(manifestRaw===void 0)continue;const remapped=applySchemaRemap(manifestRaw,fieldSchema);if(manifestSource.isOverride&&remapped.schemaVersion===void 0){remapped.schemaVersion=1}const normalized=validateModuleManifest(remapped,{packageName:pkg.name,packageVersion:pkg.version,packageDescription:pkg.description,packageRoot:realPath,manifestPath});if(typeof filterFn==="function"&&!filterFn(normalized,pkg.name)){continue}const nameVersionKey=`${pkg.name}@${pkg.version}`;if(realPathByNameVersion.has(nameVersionKey)){const otherRealPath=realPathByNameVersion.get(nameVersionKey);if(otherRealPath!==realPath){throw new SlothletError("MODULE_DUPLICATE_NAME_VERSION_MISMATCH",{packageName:pkg.name,version:pkg.version,paths:[otherRealPath,realPath].join(", ")},null,{validationError:true})}}else{realPathByNameVersion.set(nameVersionKey,realPath)}const apiDirAbs=path.resolve(realPath,normalized.apiDir);results.push(Object.freeze({packageName:pkg.name,packageRoot:realPath,mountPath:Object.freeze([...normalized.mountPath]),apiDir:apiDirAbs,manifest:deepFreeze(normalized)}))}return results}async function resolveScanRoots(opt){if(opt===void 0){return[await defaultScanRoot()]}if(typeof opt==="string"){return[path.resolve(opt)]}if(Array.isArray(opt)){return opt.map(p=>path.resolve(p))}throw new SlothletError("INVALID_ARGUMENT",{argument:"scanRoot",expected:"string or string[]",received:typeof opt},null,{validationError:true})}async function defaultScanRoot(){let current=process.cwd();for(let i=0;i<UPWARD_WALK_CAP;i++){try{const stat=await fsp.stat(path.join(current,"node_modules"));if(stat.isDirectory())return current}catch{}const parent=path.dirname(current);if(parent===current)break;current=parent}return process.cwd()}async function detectScanMode(root){try{const stat=await fsp.stat(path.join(root,"node_modules"));if(stat.isDirectory())return"npm"}catch{}return"folder"}async function enumerateNpmPackages(root,prefixes){const nodeModules=path.join(root,"node_modules");const out=[];let entries;try{entries=await fsp.readdir(nodeModules,{withFileTypes:true})}catch{return out}for(const entry of entries){if(!entry.isDirectory()&&!entry.isSymbolicLink())continue;if(entry.name.startsWith("."))continue;if(entry.name.startsWith("@")){const scopeDir=path.join(nodeModules,entry.name);let scopedEntries;try{scopedEntries=await fsp.readdir(scopeDir,{withFileTypes:true})}catch{continue}for(const scoped of scopedEntries){if(!scoped.isDirectory()&&!scoped.isSymbolicLink())continue;const fullName=`${entry.name}/${scoped.name}`;if(!matchesPrefix(fullName,prefixes))continue;out.push({path:path.join(scopeDir,scoped.name),packageName:fullName})}}else{if(!matchesPrefix(entry.name,prefixes))continue;out.push({path:path.join(nodeModules,entry.name),packageName:entry.name})}}return out}async function enumerateFolderModules(root,prefixes){const out=[];let entries;try{entries=await fsp.readdir(root,{withFileTypes:true})}catch{return out}for(const entry of entries){if(!entry.isDirectory()&&!entry.isSymbolicLink())continue;if(entry.name.startsWith("."))continue;if(!matchesPrefix(entry.name,prefixes))continue;out.push({path:path.join(root,entry.name),packageName:entry.name})}return out}function normalizePrefixes(opt){if(opt===void 0)return null;if(typeof opt==="string")return[opt];if(Array.isArray(opt)){if(opt.length===0)return null;return opt.slice()}throw new SlothletError("INVALID_ARGUMENT",{argument:"prefix",expected:"string or string[]",received:typeof opt},null,{validationError:true})}function matchesPrefix(name,prefixes){if(!prefixes)return true;for(const p of prefixes){if(name.startsWith(p))return true}return false}function parseManifestSource(opt){if(opt===void 0){return{file:DEFAULT_MANIFEST_FILE,subkey:null,isOverride:false}}if(typeof opt!=="string"||opt.length===0){throw new SlothletError("INVALID_ARGUMENT",{argument:"manifest",expected:"string",received:typeof opt},null,{validationError:true})}const hashIdx=opt.indexOf("#");if(hashIdx===-1){return{file:opt,subkey:null,isOverride:opt!==DEFAULT_MANIFEST_FILE}}const file=opt.slice(0,hashIdx);const subkey=opt.slice(hashIdx+1);if(file.length===0||subkey.length===0){throw new SlothletError("INVALID_ARGUMENT",{argument:"manifest",expected:"<file>#<dotted.key> with both parts non-empty",received:opt},null,{validationError:true})}return{file,subkey,isOverride:true}}async function readJsonOrNull(filePath){let content;try{content=await fsp.readFile(filePath,"utf8")}catch{return null}try{return JSON.parse(content)}catch{return null}}async function loadManifestRaw(manifestPath,source,packageName){let content;try{content=await fsp.readFile(manifestPath,"utf8")}catch{return void 0}let parsed;try{parsed=JSON.parse(content)}catch(err){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:`JSON parse error: ${err.message}`},err,{validationError:true})}if(source.subkey===null){return parsed}const parts=source.subkey.split(".");let cur=parsed;for(const part of parts){if(cur===null||typeof cur!=="object"||!(part in cur)){return void 0}cur=cur[part]}if(cur===null||typeof cur!=="object"||Array.isArray(cur)){return void 0}return cur}function applySchemaRemap(raw,schemaMap){if(!schemaMap||Object.keys(schemaMap).length===0){return raw}const out={...raw};for(const[canonical,legacy]of Object.entries(schemaMap)){if(legacy===canonical)continue;if(out[canonical]!==void 0)continue;if(legacy in out){out[canonical]=out[legacy];delete out[legacy]}}return out}function deepFreeze(obj){if(obj===null||typeof obj!=="object"||Object.isFrozen(obj))return obj;Object.freeze(obj);for(const key of Object.keys(obj)){const value=obj[key];if(value!==null&&typeof value==="object"){deepFreeze(value)}}return obj}export{discoverModules};
17
+ import{SlothletError}from"@cldmv/slothlet/errors";import{t}from"@cldmv/slothlet/i18n";import{validateModuleManifest}from"@cldmv/slothlet/helpers/module-manifest-validator";import{fsp,path}from"@cldmv/slothlet/helpers/platform";const DEFAULT_MANIFEST_FILE="slothlet.module.json";const UPWARD_WALK_CAP=20;async function discoverModules(options={}){const scanRoots=await resolveScanRoots(options.scanRoot);const manifestSource=parseManifestSource(options.manifest);const fieldSchema=options.schema??{};const filterFn=options.filter;const prefixes=normalizePrefixes(options.prefix);const candidates=[];for(const root of scanRoots){const mode=await detectScanMode(root);if(mode==="npm"){candidates.push(...await enumerateNpmPackages(root,prefixes))}else{candidates.push(...await enumerateFolderModules(root,prefixes))}}const results=[];const seenRealPaths=new Set;const realPathByNameVersion=new Map;for(const candidate of candidates){let realPath;try{realPath=await fsp.realpath(candidate.path)}catch{continue}if(seenRealPaths.has(realPath))continue;seenRealPaths.add(realPath);const pkgJsonPath=path.join(realPath,"package.json");const pkg=await readJsonOrNull(pkgJsonPath);if(!pkg||typeof pkg.name!=="string"||typeof pkg.version!=="string"){continue}const manifestPath=path.join(realPath,manifestSource.file);const manifestRaw=await loadManifestRaw(manifestPath,manifestSource,pkg.name);if(manifestRaw===void 0)continue;const remapped=applySchemaRemap(manifestRaw,fieldSchema);if(manifestSource.isOverride&&remapped.schemaVersion===void 0){remapped.schemaVersion=1}const normalized=validateModuleManifest(remapped,{packageName:pkg.name,packageVersion:pkg.version,packageDescription:pkg.description,packageRoot:realPath,manifestPath});if(typeof filterFn==="function"&&!filterFn(normalized,pkg.name)){continue}const nameVersionKey=`${pkg.name}@${pkg.version}`;if(realPathByNameVersion.has(nameVersionKey)){const otherRealPath=realPathByNameVersion.get(nameVersionKey);if(otherRealPath!==realPath){throw new SlothletError("MODULE_DUPLICATE_NAME_VERSION_MISMATCH",{packageName:pkg.name,version:pkg.version,paths:[otherRealPath,realPath].join(", ")},null,{validationError:true})}}else{realPathByNameVersion.set(nameVersionKey,realPath)}const apiDirAbs=path.resolve(realPath,normalized.apiDir);results.push(Object.freeze({packageName:pkg.name,packageRoot:realPath,mountPath:Object.freeze([...normalized.mountPath]),apiDir:apiDirAbs,manifest:deepFreeze(normalized)}))}return results}async function resolveScanRoots(opt){if(opt===void 0){return[await defaultScanRoot()]}if(typeof opt==="string"){return[path.resolve(opt)]}if(Array.isArray(opt)){return opt.map(p=>path.resolve(p))}throw new SlothletError("INVALID_ARGUMENT",{argument:"scanRoot",expected:"string or string[]",received:typeof opt},null,{validationError:true})}async function defaultScanRoot(){let current=process.cwd();for(let i=0;i<UPWARD_WALK_CAP;i++){try{const stat=await fsp.stat(path.join(current,"node_modules"));if(stat.isDirectory())return current}catch{}const parent=path.dirname(current);if(parent===current)break;current=parent}return process.cwd()}async function detectScanMode(root){try{const stat=await fsp.stat(path.join(root,"node_modules"));if(stat.isDirectory())return"npm"}catch{}return"folder"}async function enumerateNpmPackages(root,prefixes){const nodeModules=path.join(root,"node_modules");const out=[];let entries;try{entries=await fsp.readdir(nodeModules,{withFileTypes:true})}catch{return out}for(const entry of entries){if(!entry.isDirectory()&&!entry.isSymbolicLink())continue;if(entry.name.startsWith("."))continue;if(entry.name.startsWith("@")){const scopeDir=path.join(nodeModules,entry.name);let scopedEntries;try{scopedEntries=await fsp.readdir(scopeDir,{withFileTypes:true})}catch{continue}for(const scoped of scopedEntries){if(!scoped.isDirectory()&&!scoped.isSymbolicLink())continue;const fullName=`${entry.name}/${scoped.name}`;if(!matchesPrefix(fullName,prefixes))continue;out.push({path:path.join(scopeDir,scoped.name),packageName:fullName})}}else{if(!matchesPrefix(entry.name,prefixes))continue;out.push({path:path.join(nodeModules,entry.name),packageName:entry.name})}}return out}async function enumerateFolderModules(root,prefixes){const out=[];let entries;try{entries=await fsp.readdir(root,{withFileTypes:true})}catch{return out}for(const entry of entries){if(!entry.isDirectory()&&!entry.isSymbolicLink())continue;if(entry.name.startsWith("."))continue;if(!matchesPrefix(entry.name,prefixes))continue;out.push({path:path.join(root,entry.name),packageName:entry.name})}return out}function normalizePrefixes(opt){if(opt===void 0)return null;if(typeof opt==="string")return[opt];if(Array.isArray(opt)){if(opt.length===0)return null;return opt.slice()}throw new SlothletError("INVALID_ARGUMENT",{argument:"prefix",expected:"string or string[]",received:typeof opt},null,{validationError:true})}function matchesPrefix(name,prefixes){if(!prefixes)return true;for(const p of prefixes){if(name.startsWith(p))return true}return false}function parseManifestSource(opt){if(opt===void 0){return{file:DEFAULT_MANIFEST_FILE,subkey:null,isOverride:false}}if(typeof opt!=="string"||opt.length===0){throw new SlothletError("INVALID_ARGUMENT",{argument:"manifest",expected:"string",received:typeof opt},null,{validationError:true})}const hashIdx=opt.indexOf("#");if(hashIdx===-1){return{file:opt,subkey:null,isOverride:opt!==DEFAULT_MANIFEST_FILE}}const file=opt.slice(0,hashIdx);const subkey=opt.slice(hashIdx+1);if(file.length===0||subkey.length===0){throw new SlothletError("INVALID_ARGUMENT",{argument:"manifest",expected:"<file>#<dotted.key> with both parts non-empty",received:opt},null,{validationError:true})}return{file,subkey,isOverride:true}}async function readJsonOrNull(filePath){let content;try{content=await fsp.readFile(filePath,"utf8")}catch{return null}try{return JSON.parse(content)}catch{return null}}async function loadManifestRaw(manifestPath,source,packageName){let content;try{content=await fsp.readFile(manifestPath,"utf8")}catch{return void 0}let parsed;try{parsed=JSON.parse(content)}catch(err){throw new SlothletError("MODULE_MANIFEST_INVALID",{packageName,manifestPath,reason:t("MODULE_MANIFEST_REASON_JSON_PARSE",{error:err.message})},null,{validationError:true})}if(source.subkey===null){return parsed}const parts=source.subkey.split(".");let cur=parsed;for(const part of parts){if(cur===null||typeof cur!=="object"||!(part in cur)){return void 0}cur=cur[part]}if(cur===null||typeof cur!=="object"||Array.isArray(cur)){return void 0}return cur}function applySchemaRemap(raw,schemaMap){if(!schemaMap||Object.keys(schemaMap).length===0){return raw}const out={...raw};for(const[canonical,legacy]of Object.entries(schemaMap)){if(legacy===canonical)continue;if(out[canonical]!==void 0)continue;if(legacy in out){out[canonical]=out[legacy];delete out[legacy]}}return out}function deepFreeze(obj){if(obj===null||typeof obj!=="object"||Object.isFrozen(obj))return obj;Object.freeze(obj);for(const key of Object.keys(obj)){const value=obj[key];if(value!==null&&typeof value==="object"){deepFreeze(value)}}return obj}export{discoverModules};