UNPKG

capacitor-native-update

Version:
4 lines (3 loc) 40.2 kB
/*! Capacitor Native Update Plugin v1.4.0 | MIT License */ !function(e,t,a){var r,i,n,s,o,l,d,c,u,h,g,E;!function(e){e.APP_UPDATE="app_update",e.LIVE_UPDATE="live_update",e.BOTH="both"}(r||(r={})),function(e){e.MIN="min",e.LOW="low",e.DEFAULT="default",e.HIGH="high",e.MAX="max"}(i||(i={})),function(e){e.IMMEDIATE="immediate",e.BACKGROUND="background",e.MANUAL="manual"}(n||(n={})),function(e){e.IMMEDIATE="immediate",e.ON_NEXT_RESTART="on_next_restart",e.ON_NEXT_RESUME="on_next_resume"}(s||(s={})),function(e){e.IMMEDIATE="immediate",e.ON_NEXT_RESTART="on_next_restart",e.ON_NEXT_RESUME="on_next_resume"}(o||(o={})),function(e){e.SHA256="SHA-256",e.SHA512="SHA-512"}(l||(l={})),function(e){e.UP_TO_DATE="UP_TO_DATE",e.UPDATE_AVAILABLE="UPDATE_AVAILABLE",e.UPDATE_INSTALLED="UPDATE_INSTALLED",e.ERROR="ERROR"}(d||(d={})),function(e){e.PENDING="PENDING",e.DOWNLOADING="DOWNLOADING",e.READY="READY",e.ACTIVE="ACTIVE",e.FAILED="FAILED"}(c||(c={})),function(e){e.UNKNOWN="UNKNOWN",e.PENDING="PENDING",e.DOWNLOADING="DOWNLOADING",e.DOWNLOADED="DOWNLOADED",e.INSTALLING="INSTALLING",e.INSTALLED="INSTALLED",e.FAILED="FAILED",e.CANCELED="CANCELED"}(u||(u={})),function(e){e.NETWORK_ERROR="NETWORK_ERROR",e.SERVER_ERROR="SERVER_ERROR",e.TIMEOUT_ERROR="TIMEOUT_ERROR",e.DOWNLOAD_ERROR="DOWNLOAD_ERROR",e.STORAGE_ERROR="STORAGE_ERROR",e.SIZE_LIMIT_EXCEEDED="SIZE_LIMIT_EXCEEDED",e.VERIFICATION_ERROR="VERIFICATION_ERROR",e.CHECKSUM_ERROR="CHECKSUM_ERROR",e.SIGNATURE_ERROR="SIGNATURE_ERROR",e.INSECURE_URL="INSECURE_URL",e.INVALID_CERTIFICATE="INVALID_CERTIFICATE",e.PATH_TRAVERSAL="PATH_TRAVERSAL",e.INSTALL_ERROR="INSTALL_ERROR",e.ROLLBACK_ERROR="ROLLBACK_ERROR",e.VERSION_MISMATCH="VERSION_MISMATCH",e.PERMISSION_DENIED="PERMISSION_DENIED",e.UPDATE_NOT_AVAILABLE="UPDATE_NOT_AVAILABLE",e.UPDATE_IN_PROGRESS="UPDATE_IN_PROGRESS",e.UPDATE_CANCELLED="UPDATE_CANCELLED",e.PLATFORM_NOT_SUPPORTED="PLATFORM_NOT_SUPPORTED",e.REVIEW_NOT_SUPPORTED="REVIEW_NOT_SUPPORTED",e.QUOTA_EXCEEDED="QUOTA_EXCEEDED",e.CONDITIONS_NOT_MET="CONDITIONS_NOT_MET",e.INVALID_CONFIG="INVALID_CONFIG",e.UNKNOWN_ERROR="UNKNOWN_ERROR"}(h||(h={}));class f{constructor(){this.config=this.getDefaultConfig()}static getInstance(){return f.instance||(f.instance=new f),f.instance}getDefaultConfig(){return{filesystem:null,preferences:null,baseUrl:"",allowedHosts:[],maxBundleSize:104857600,downloadTimeout:3e4,retryAttempts:3,retryDelay:1e3,enableSignatureValidation:!0,publicKey:"",cacheExpiration:864e5,enableLogging:!1}}configure(e){this.config=Object.assign(Object.assign({},this.config),e),this.validateConfig()}validateConfig(){if(this.config.maxBundleSize<=0)throw new Error("maxBundleSize must be greater than 0");if(this.config.downloadTimeout<=0)throw new Error("downloadTimeout must be greater than 0");if(this.config.retryAttempts<0)throw new Error("retryAttempts must be non-negative");if(this.config.retryDelay<0)throw new Error("retryDelay must be non-negative")}get(e){return this.config[e]}isConfigured(){return!(!this.config.filesystem||!this.config.preferences)}}e.LogLevel=void 0,(g=e.LogLevel||(e.LogLevel={}))[g.DEBUG=0]="DEBUG",g[g.INFO=1]="INFO",g[g.WARN=2]="WARN",g[g.ERROR=3]="ERROR";class w{constructor(){this.configManager=f.getInstance()}static getInstance(){return w.instance||(w.instance=new w),w.instance}shouldLog(){return this.configManager.get("enableLogging")}sanitize(e){if("string"==typeof e){let t=e;return t=t.replace(/\/[^\s]+\/([\w.-]+)$/g,"/<path>/$1"),t=t.replace(/https?:\/\/[^:]+:[^@]+@/g,"https://***:***@"),t=t.replace(/[a-zA-Z0-9]{32,}/g,"<redacted>"),t}if("object"==typeof e&&null!==e){if(Array.isArray(e))return e.map(e=>this.sanitize(e));{const t={},a=e;for(const e in a)t[e]=e.toLowerCase().includes("key")||e.toLowerCase().includes("secret")||e.toLowerCase().includes("password")||e.toLowerCase().includes("token")?"<redacted>":this.sanitize(a[e]);return t}}return e}log(t,a,r){if(!this.shouldLog())return;const i=(new Date).toISOString(),n=r?this.sanitize(r):void 0,s={timestamp:i,level:e.LogLevel[t],message:a};switch(void 0!==n&&(s.data=n),t){case e.LogLevel.DEBUG:console.debug("[CapacitorNativeUpdate]",s);break;case e.LogLevel.INFO:console.info("[CapacitorNativeUpdate]",s);break;case e.LogLevel.WARN:console.warn("[CapacitorNativeUpdate]",s);break;case e.LogLevel.ERROR:console.error("[CapacitorNativeUpdate]",s)}}debug(t,a){this.log(e.LogLevel.DEBUG,t,a)}info(t,a){this.log(e.LogLevel.INFO,t,a)}warn(t,a){this.log(e.LogLevel.WARN,t,a)}error(t,a){const r=a instanceof Error?{name:a.name,message:a.message,stack:a.stack}:a;this.log(e.LogLevel.ERROR,t,r)}}e.ErrorCode=void 0,(E=e.ErrorCode||(e.ErrorCode={})).NOT_CONFIGURED="NOT_CONFIGURED",E.INVALID_CONFIG="INVALID_CONFIG",E.MISSING_DEPENDENCY="MISSING_DEPENDENCY",E.DOWNLOAD_FAILED="DOWNLOAD_FAILED",E.DOWNLOAD_TIMEOUT="DOWNLOAD_TIMEOUT",E.INVALID_URL="INVALID_URL",E.UNAUTHORIZED_HOST="UNAUTHORIZED_HOST",E.BUNDLE_TOO_LARGE="BUNDLE_TOO_LARGE",E.CHECKSUM_MISMATCH="CHECKSUM_MISMATCH",E.SIGNATURE_INVALID="SIGNATURE_INVALID",E.VERSION_DOWNGRADE="VERSION_DOWNGRADE",E.INVALID_BUNDLE_FORMAT="INVALID_BUNDLE_FORMAT",E.STORAGE_FULL="STORAGE_FULL",E.FILE_NOT_FOUND="FILE_NOT_FOUND",E.PERMISSION_DENIED="PERMISSION_DENIED",E.UPDATE_FAILED="UPDATE_FAILED",E.ROLLBACK_FAILED="ROLLBACK_FAILED",E.BUNDLE_NOT_READY="BUNDLE_NOT_READY",E.PLATFORM_NOT_SUPPORTED="PLATFORM_NOT_SUPPORTED",E.NATIVE_ERROR="NATIVE_ERROR";class p extends Error{constructor(e,t,a,r){super(t),this.code=e,this.message=t,this.details=a,this.originalError=r,this.name="CapacitorNativeUpdateError",Object.setPrototypeOf(this,p.prototype)}toJSON(){return{name:this.name,code:this.code,message:this.message,details:this.details,stack:this.stack}}}class I extends p{constructor(e,t,a,r){super(e,t,a,r),this.name="DownloadError"}}class y extends p{constructor(e,t,a){super(e,t,a),this.name="ValidationError"}}class D extends p{constructor(e,t,a,r){super(e,t,a,r),this.name="StorageError"}}class A extends p{constructor(e,t,a,r){super(e,t,a,r),this.name="UpdateError"}}class m{constructor(){this.configManager=f.getInstance(),this.logger=w.getInstance()}static getInstance(){return m.instance||(m.instance=new m),m.instance}async calculateChecksum(e){const t=await crypto.subtle.digest("SHA-256",e);return Array.from(new Uint8Array(t)).map(e=>e.toString(16).padStart(2,"0")).join("")}async verifyChecksum(e,t){if(!t)return this.logger.warn("No checksum provided for verification"),!0;const a=await this.calculateChecksum(e),r=a===t.toLowerCase();return r||this.logger.error("Checksum verification failed",{expected:t,actual:a}),r}async validateChecksum(e,t){return this.verifyChecksum(e,t)}async verifySignature(t,a){if(!this.configManager.get("enableSignatureValidation"))return!0;if(!this.configManager.get("publicKey"))throw new y(e.ErrorCode.SIGNATURE_INVALID,"Public key not configured for signature validation");return this.logger.debug("Signature verification not yet implemented"),!0}sanitizePath(e){return e.split("/").filter(e=>".."!==e&&"."!==e).join("/").replace(/^\/+/,"")}validateBundleId(t){if(!t||"string"!=typeof t)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID must be a non-empty string");if(!/^[a-zA-Z0-9\-_.]+$/.test(t))throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID contains invalid characters");if(t.length>100)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID is too long (max 100 characters)")}validateVersion(t){if(!t||"string"!=typeof t)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Version must be a non-empty string");if(!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.test(t))throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Version must follow semantic versioning format (e.g., 1.2.3)")}isVersionDowngrade(e,t){const a=this.parseVersion(e),r=this.parseVersion(t);return r.major<a.major||!(r.major>a.major)&&(r.minor<a.minor||!(r.minor>a.minor)&&r.patch<a.patch)}parseVersion(e){const t=e.split("-")[0].split(".");return{major:parseInt(t[0],10)||0,minor:parseInt(t[1],10)||0,patch:parseInt(t[2],10)||0}}validateUrl(t){if(!t||"string"!=typeof t)throw new y(e.ErrorCode.INVALID_URL,"URL must be a non-empty string");let a;try{a=new URL(t)}catch(t){throw new y(e.ErrorCode.INVALID_URL,"Invalid URL format")}if("https:"!==a.protocol)throw new y(e.ErrorCode.INVALID_URL,"Only HTTPS URLs are allowed");const r=this.configManager.get("allowedHosts");if(r.length>0&&!r.includes(a.hostname))throw new y(e.ErrorCode.UNAUTHORIZED_HOST,`Host ${a.hostname} is not in the allowed hosts list`);if([/^localhost$/i,/^127\./,/^10\./,/^172\.(1[6-9]|2[0-9]|3[0-1])\./,/^192\.168\./,/^::1$/,/^fc00:/i,/^fe80:/i].some(e=>e.test(a.hostname)))throw new y(e.ErrorCode.UNAUTHORIZED_HOST,"Private/local addresses are not allowed")}validateFileSize(t){if("number"!=typeof t||t<0)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"File size must be a non-negative number");const a=this.configManager.get("maxBundleSize");if(t>a)throw new y(e.ErrorCode.BUNDLE_TOO_LARGE,`File size ${t} exceeds maximum allowed size of ${a} bytes`)}generateSecureId(){const e=new Uint8Array(16);return crypto.getRandomValues(e),Array.from(e,e=>e.toString(16).padStart(2,"0")).join("")}validateMetadata(t){if(t&&"object"!=typeof t)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Metadata must be an object");if(JSON.stringify(t||{}).length>10240)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Metadata is too large (max 10KB)")}}class N{constructor(){this.STORAGE_KEY="capacitor_native_update_bundles",this.ACTIVE_BUNDLE_KEY="capacitor_native_update_active",this.preferences=null,this.cache=new Map,this.cacheExpiry=0,this.logger=w.getInstance(),this.configManager=f.getInstance()}async initialize(){if(this.preferences=this.configManager.get("preferences"),!this.preferences)throw new D(e.ErrorCode.MISSING_DEPENDENCY,"Preferences not configured. Please configure the plugin first.");await this.loadCache()}async loadCache(){if(!(Date.now()<this.cacheExpiry))try{const{value:e}=await this.preferences.get({key:this.STORAGE_KEY});if(e){const t=JSON.parse(e);this.cache.clear(),t.forEach(e=>this.cache.set(e.bundleId,e))}this.cacheExpiry=Date.now()+5e3}catch(e){this.logger.error("Failed to load bundles from storage",e),this.cache.clear()}}async saveCache(){try{const e=Array.from(this.cache.values());await this.preferences.set({key:this.STORAGE_KEY,value:JSON.stringify(e)}),this.logger.debug("Saved bundles to storage",{count:e.length})}catch(t){throw new D(e.ErrorCode.STORAGE_FULL,"Failed to save bundles to storage",void 0,t)}}async saveBundleInfo(e){this.validateBundleInfo(e),this.cache.set(e.bundleId,e),await this.saveCache(),this.logger.info("Bundle saved",{bundleId:e.bundleId,version:e.version})}validateBundleInfo(t){if(!t.bundleId||"string"!=typeof t.bundleId)throw new D(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Invalid bundle ID");if(!t.version||"string"!=typeof t.version)throw new D(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Invalid bundle version");if(!t.path||"string"!=typeof t.path)throw new D(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Invalid bundle path");if("number"!=typeof t.size||t.size<0)throw new D(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Invalid bundle size")}async getAllBundles(){return await this.loadCache(),Array.from(this.cache.values())}async getBundle(t){if(!t)throw new D(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID is required");return await this.loadCache(),this.cache.get(t)||null}async deleteBundle(t){if(!t)throw new D(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID is required");await this.loadCache(),this.cache.get(t)?(this.cache.delete(t),await this.saveCache(),await this.getActiveBundleId()===t&&await this.clearActiveBundle(),this.logger.info("Bundle deleted",{bundleId:t})):this.logger.warn("Attempted to delete non-existent bundle",{bundleId:t})}async getActiveBundle(){const e=await this.getActiveBundleId();return e?this.getBundle(e):null}async setActiveBundle(t){if(!t)throw new D(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID is required");const a=await this.getBundle(t);if(!a)throw new D(e.ErrorCode.FILE_NOT_FOUND,`Bundle ${t} not found`);const r=await this.getActiveBundle();r&&r.bundleId!==t&&(r.status="READY",await this.saveBundleInfo(r)),a.status="ACTIVE",await this.saveBundleInfo(a),await this.preferences.set({key:this.ACTIVE_BUNDLE_KEY,value:t}),this.logger.info("Active bundle set",{bundleId:t,version:a.version})}async getActiveBundleId(){try{const{value:e}=await this.preferences.get({key:this.ACTIVE_BUNDLE_KEY});return e}catch(e){return this.logger.error("Failed to get active bundle ID",e),null}}async clearActiveBundle(){await this.preferences.remove({key:this.ACTIVE_BUNDLE_KEY}),this.logger.info("Active bundle cleared")}async clearAllBundles(){await this.preferences.remove({key:this.STORAGE_KEY}),await this.preferences.remove({key:this.ACTIVE_BUNDLE_KEY}),this.cache.clear(),this.cacheExpiry=0,this.logger.info("All bundles cleared")}async cleanupOldBundles(t){if(t<1)throw new D(e.ErrorCode.INVALID_CONFIG,"Keep count must be at least 1");const a=await this.getAllBundles(),r=await this.getActiveBundleId(),i=a.sort((e,t)=>t.downloadTime-e.downloadTime),n=new Set;r&&n.add(r);let s=n.size;for(const e of i){if(s>=t)break;n.has(e.bundleId)||(n.add(e.bundleId),s++)}let o=0;for(const e of a)n.has(e.bundleId)||(await this.deleteBundle(e.bundleId),o++);o>0&&this.logger.info("Cleaned up old bundles",{deleted:o,kept:s})}async getBundlesOlderThan(t){if(t<0)throw new D(e.ErrorCode.INVALID_CONFIG,"Timestamp must be non-negative");return(await this.getAllBundles()).filter(e=>e.downloadTime<t)}async markBundleAsVerified(t){const a=await this.getBundle(t);if(!a)throw new D(e.ErrorCode.FILE_NOT_FOUND,`Bundle ${t} not found`);a.verified=!0,await this.saveBundleInfo(a),this.logger.info("Bundle marked as verified",{bundleId:t})}async getTotalStorageUsed(){return(await this.getAllBundles()).reduce((e,t)=>e+t.size,0)}async isStorageLimitExceeded(e=0){return await this.getTotalStorageUsed()+e>3*this.configManager.get("maxBundleSize")}createDefaultBundle(){return{bundleId:"default",version:"1.0.0",path:"/",downloadTime:Date.now(),size:0,status:"ACTIVE",checksum:"",verified:!0}}async cleanExpiredBundles(){const e=this.configManager.get("cacheExpiration"),t=Date.now()-e,a=await this.getBundlesOlderThan(t);for(const e of a){const t=await this.getActiveBundleId();e.bundleId!==t&&await this.deleteBundle(e.bundleId)}}}class _{constructor(){this.activeDownloads=new Map,this.filesystem=null,this.logger=w.getInstance(),this.configManager=f.getInstance()}async initialize(){if(this.filesystem=this.configManager.get("filesystem"),!this.filesystem)throw new I(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem not configured. Please configure the plugin first.")}validateUrl(t){try{const a=new URL(t);if("https:"!==a.protocol)throw new y(e.ErrorCode.INVALID_URL,"Only HTTPS URLs are allowed for security reasons");const r=this.configManager.get("allowedHosts");if(r.length>0&&!r.includes(a.hostname))throw new y(e.ErrorCode.UNAUTHORIZED_HOST,`Host ${a.hostname} is not in the allowed hosts list`)}catch(t){if(t instanceof y)throw t;throw new y(e.ErrorCode.INVALID_URL,"Invalid URL format")}}async download(t,a,r){if(this.validateUrl(t),!a)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Bundle ID is required");if(this.activeDownloads.has(a))throw new I(e.ErrorCode.DOWNLOAD_FAILED,`Download already in progress for bundle ${a}`);const i=new AbortController,n={controller:i,startTime:Date.now()};this.activeDownloads.set(a,n);try{const s=this.configManager.get("downloadTimeout"),o=setTimeout(()=>i.abort(),s),l=await fetch(t,{signal:i.signal,headers:{"Cache-Control":"no-cache",Accept:"application/octet-stream, application/zip"}});if(clearTimeout(o),!l.ok)throw new I(e.ErrorCode.DOWNLOAD_FAILED,`Download failed: ${l.status} ${l.statusText}`,{status:l.status,statusText:l.statusText});const d=l.headers.get("content-type");if(d&&!this.isValidContentType(d))throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,`Invalid content type: ${d}`);const c=l.headers.get("content-length"),u=c?parseInt(c,10):0;if(u>this.configManager.get("maxBundleSize"))throw new y(e.ErrorCode.BUNDLE_TOO_LARGE,`Bundle size ${u} exceeds maximum allowed size`);if(!u||!l.body){const e=await l.blob();return this.validateBlobSize(e),e}const h=l.body.getReader(),g=[];let E=0;for(;;){const{done:t,value:i}=await h.read();if(t)break;if(g.push(i),E+=i.length,E>this.configManager.get("maxBundleSize"))throw new y(e.ErrorCode.BUNDLE_TOO_LARGE,"Download size exceeds maximum allowed size");r&&r({percent:Math.round(E/u*100),bytesDownloaded:E,totalBytes:u,bundleId:a})}const f=new Blob(g);return this.validateBlobSize(f),this.logger.info("Download completed",{bundleId:a,size:f.size,duration:Date.now()-n.startTime}),f}catch(t){if(t instanceof Error&&"AbortError"===t.name){const a=Date.now()-n.startTime>=this.configManager.get("downloadTimeout");throw new I(a?e.ErrorCode.DOWNLOAD_TIMEOUT:e.ErrorCode.DOWNLOAD_FAILED,a?"Download timed out":"Download cancelled",void 0,t)}throw t}finally{this.activeDownloads.delete(a)}}isValidContentType(e){return["application/octet-stream","application/zip","application/x-zip-compressed","application/x-zip"].some(t=>e.includes(t))}validateBlobSize(t){if(0===t.size)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Downloaded file is empty");if(t.size>this.configManager.get("maxBundleSize"))throw new y(e.ErrorCode.BUNDLE_TOO_LARGE,`File size ${t.size} exceeds maximum allowed size`)}cancelDownload(e){const t=this.activeDownloads.get(e);t&&(t.controller.abort(),this.activeDownloads.delete(e),this.logger.info("Download cancelled",{bundleId:e}))}cancelAllDownloads(){for(const e of this.activeDownloads.values())e.controller.abort();const e=this.activeDownloads.size;this.activeDownloads.clear(),e>0&&this.logger.info("All downloads cancelled",{count:e})}isDownloading(e){return this.activeDownloads.has(e)}getActiveDownloadCount(){return this.activeDownloads.size}async downloadWithRetry(t,a,r){const i=this.configManager.get("retryAttempts"),n=this.configManager.get("retryDelay");let s=null;for(let e=0;e<i;e++)try{if(e>0){const t=Math.min(n*Math.pow(2,e-1),3e4);await new Promise(e=>setTimeout(e,t)),this.logger.debug("Retrying download",{bundleId:a,attempt:e,delay:t})}return await this.download(t,a,r)}catch(t){if(s=t,t instanceof y||t instanceof Error&&"AbortError"===t.name)throw t;this.logger.warn(`Download attempt ${e+1} failed`,{bundleId:a,error:t})}throw new I(e.ErrorCode.DOWNLOAD_FAILED,"Download failed after all retries",{attempts:i},s||void 0)}async blobToArrayBuffer(e){return e.arrayBuffer()}async saveBlob(t,r){if(!this.filesystem)throw new I(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem not initialized");const i=await this.blobToArrayBuffer(r),n=btoa(String.fromCharCode(...new Uint8Array(i))),s=`bundles/${t}/bundle.zip`;return await this.filesystem.writeFile({path:s,data:n,directory:a.Directory.Data,recursive:!0}),this.logger.debug("Bundle saved to filesystem",{bundleId:t,path:s,size:r.size}),s}async loadBlob(t){if(!this.filesystem)throw new I(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem not initialized");try{const e=`bundles/${t}/bundle.zip`,r=await this.filesystem.readFile({path:e,directory:a.Directory.Data}),i=atob(r.data),n=new Uint8Array(i.length);for(let e=0;e<i.length;e++)n[e]=i.charCodeAt(e);return new Blob([n],{type:"application/zip"})}catch(e){return this.logger.debug("Failed to load bundle from filesystem",{bundleId:t,error:e}),null}}async deleteBlob(t){if(!this.filesystem)throw new I(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem not initialized");try{const e=`bundles/${t}`;await this.filesystem.rmdir({path:e,directory:a.Directory.Data,recursive:!0}),this.logger.debug("Bundle deleted from filesystem",{bundleId:t})}catch(e){this.logger.warn("Failed to delete bundle from filesystem",{bundleId:t,error:e})}}}class C{constructor(){this.VERSION_CHECK_CACHE_KEY="capacitor_native_update_version_cache",this.CACHE_DURATION=3e5,this.preferences=null,this.memoryCache=new Map,this.logger=w.getInstance(),this.configManager=f.getInstance(),this.securityValidator=m.getInstance()}async initialize(){if(this.preferences=this.configManager.get("preferences"),!this.preferences)throw new y(e.ErrorCode.MISSING_DEPENDENCY,"Preferences not configured. Please configure the plugin first.")}async checkForUpdates(t,a,r,i){if(this.securityValidator.validateUrl(t),this.securityValidator.validateVersion(r),!a||!i)throw new y(e.ErrorCode.INVALID_CONFIG,"Channel and appId are required");const n=`${a}-${i}`,s=await this.getCachedVersionInfo(n);if(s&&s.channel===a&&Date.now()-s.timestamp<this.CACHE_DURATION)return this.logger.debug("Returning cached version info",{channel:a,version:s.data.version}),s.data;try{const s=new URL(`${t}/check`);s.searchParams.append("channel",a),s.searchParams.append("version",r),s.searchParams.append("appId",i),s.searchParams.append("platform","web");const o=await fetch(s.toString(),{method:"GET",headers:{"Content-Type":"application/json","X-App-Version":r,"X-App-Id":i},signal:AbortSignal.timeout(this.configManager.get("downloadTimeout"))});if(!o.ok)throw new Error(`Version check failed: ${o.status}`);const l=await o.json();if(!l.version)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"No version in server response");return this.securityValidator.validateVersion(l.version),l.bundleUrl&&this.securityValidator.validateUrl(l.bundleUrl),l.minAppVersion&&this.securityValidator.validateVersion(l.minAppVersion),await this.cacheVersionInfo(n,a,l),this.logger.info("Version check completed",{channel:a,currentVersion:r,latestVersion:l.version,updateAvailable:this.isNewerVersion(l.version,r)}),l}catch(e){return this.logger.error("Failed to check for updates",e),null}}isNewerVersion(e,t){try{const a=this.parseVersion(e),r=this.parseVersion(t);return a.major!==r.major?a.major>r.major:a.minor!==r.minor?a.minor>r.minor:a.patch!==r.patch?a.patch>r.patch:!(a.prerelease&&!r.prerelease||(a.prerelease||!r.prerelease)&&(!a.prerelease||!r.prerelease||!(a.prerelease>r.prerelease)))}catch(a){return this.logger.error("Failed to compare versions",{version1:e,version2:t,error:a}),!1}}isUpdateMandatory(e,t){if(!t)return!1;try{return this.securityValidator.validateVersion(e),this.securityValidator.validateVersion(t),!this.isNewerVersion(e,t)&&e!==t}catch(e){return this.logger.error("Failed to check mandatory update",e),!1}}parseVersion(t){const a=t.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/);if(!a)throw new y(e.ErrorCode.INVALID_BUNDLE_FORMAT,"Invalid version format");return{major:parseInt(a[1],10),minor:parseInt(a[2],10),patch:parseInt(a[3],10),prerelease:a[4],build:a[5]}}buildVersionString(e){let t=`${e.major}.${e.minor}.${e.patch}`;return e.prerelease&&(t+=`-${e.prerelease}`),e.build&&(t+=`+${e.build}`),t}isCompatibleWithNativeVersion(e,t,a){if(!a)return!0;try{const r=a[e];return!r||(this.securityValidator.validateVersion(t),this.securityValidator.validateVersion(r),!this.isNewerVersion(r,t))}catch(e){return this.logger.error("Failed to check compatibility",e),!1}}async getCachedVersionInfo(e){const t=this.memoryCache.get(e);if(t&&Date.now()-t.timestamp<this.CACHE_DURATION)return t;try{const{value:t}=await this.preferences.get({key:this.VERSION_CHECK_CACHE_KEY});if(!t)return null;const a=JSON.parse(t)[e];if(a&&Date.now()-a.timestamp<this.CACHE_DURATION)return this.memoryCache.set(e,a),a}catch(e){this.logger.debug("Failed to load cached version info",e)}return null}async cacheVersionInfo(e,t,a){const r={channel:t,data:a,timestamp:Date.now()};this.memoryCache.set(e,r);try{const{value:t}=await this.preferences.get({key:this.VERSION_CHECK_CACHE_KEY}),a=t?JSON.parse(t):{},i=Date.now();for(const e in a)i-a[e].timestamp>2*this.CACHE_DURATION&&delete a[e];a[e]=r,await this.preferences.set({key:this.VERSION_CHECK_CACHE_KEY,value:JSON.stringify(a)})}catch(e){this.logger.warn("Failed to cache version info",e)}}async clearVersionCache(){this.memoryCache.clear();try{await this.preferences.remove({key:this.VERSION_CHECK_CACHE_KEY})}catch(e){this.logger.warn("Failed to clear version cache",e)}}shouldBlockDowngrade(e,t){try{return this.securityValidator.isVersionDowngrade(e,t)}catch(e){return this.logger.error("Failed to check downgrade",e),!0}}}class R{constructor(){this.bundleManager=null,this.downloadManager=null,this.versionManager=null,this.initialized=!1,this.configManager=f.getInstance(),this.logger=w.getInstance(),this.securityValidator=m.getInstance()}static getInstance(){return R.instance||(R.instance=new R),R.instance}async initialize(t){if(this.initialized)this.logger.warn("Plugin already initialized");else try{if(this.configManager.configure(t),!t.filesystem||!t.preferences)throw new p(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem and Preferences are required for plugin initialization");this.bundleManager=new N,await this.bundleManager.initialize(),this.downloadManager=new _,await this.downloadManager.initialize(),this.versionManager=new C,await this.versionManager.initialize(),this.initialized=!0,this.logger.info("Plugin initialized successfully")}catch(e){throw this.logger.error("Failed to initialize plugin",e),e}}isInitialized(){return this.initialized&&this.configManager.isConfigured()}ensureInitialized(){if(!this.isInitialized())throw new p(e.ErrorCode.NOT_CONFIGURED,"Plugin not initialized. Please call initialize() first.")}getBundleManager(){return this.ensureInitialized(),this.bundleManager}getDownloadManager(){return this.ensureInitialized(),this.downloadManager}getVersionManager(){return this.ensureInitialized(),this.versionManager}getConfigManager(){return this.configManager}getLogger(){return this.logger}getSecurityValidator(){return this.securityValidator}async reset(){this.logger.info("Resetting plugin state"),this.bundleManager&&await this.bundleManager.clearAllBundles(),this.versionManager&&await this.versionManager.clearVersionCache(),this.downloadManager&&this.downloadManager.cancelAllDownloads(),this.bundleManager=null,this.downloadManager=null,this.versionManager=null,this.initialized=!1,this.logger.info("Plugin reset complete")}async cleanup(){this.logger.info("Cleaning up plugin resources"),this.downloadManager&&this.downloadManager.cancelAllDownloads(),this.bundleManager&&await this.bundleManager.cleanExpiredBundles(),this.logger.info("Cleanup complete")}}class O{constructor(){this.initialized=!1,this.pluginManager=R.getInstance()}async initialize(e){await this.pluginManager.initialize(e),this.initialized=!0}isInitialized(){return this.initialized&&this.pluginManager.isInitialized()}async reset(){await this.pluginManager.reset()}async cleanup(){await this.pluginManager.cleanup()}async configure(t){var a,r;if(!this.initialized)throw new p(e.ErrorCode.NOT_CONFIGURED,"Plugin not initialized. Call initialize() first.");const i=this.pluginManager.getConfigManager();(null===(a=t.liveUpdate)||void 0===a?void 0:a.allowedHosts)&&i.configure({allowedHosts:t.liveUpdate.allowedHosts}),(null===(r=t.liveUpdate)||void 0===r?void 0:r.maxBundleSize)&&i.configure({maxBundleSize:t.liveUpdate.maxBundleSize})}async getSecurityInfo(){return{enforceHttps:!0,certificatePinning:{enabled:!1,certificates:[]},validateInputs:!0,secureStorage:!0}}async sync(e){const t=this.pluginManager.getBundleManager();try{const e=await t.getActiveBundle();return{status:d.UP_TO_DATE,version:(null==e?void 0:e.version)||"1.0.0"}}catch(e){return{status:d.ERROR,error:{code:h.UNKNOWN_ERROR,message:e instanceof Error?e.message:"Sync failed"}}}}async download(e){const t=this.pluginManager.getDownloadManager(),a=this.pluginManager.getBundleManager(),r=await t.downloadWithRetry(e.url,e.version),i=await t.saveBlob(e.version,r),n={bundleId:e.version,version:e.version,path:i,downloadTime:Date.now(),size:r.size,status:c.READY,checksum:e.checksum,signature:e.signature,verified:!1};return await a.saveBundleInfo(n),n}async set(e){const t=this.pluginManager.getBundleManager();await t.setActiveBundle(e.bundleId)}async reload(){"undefined"!=typeof window&&window.location.reload()}async current(){const t=this.pluginManager.getBundleManager(),a=await t.getActiveBundle();if(!a)throw new p(e.ErrorCode.FILE_NOT_FOUND,"No active bundle found");return a}async list(){return this.pluginManager.getBundleManager().getAllBundles()}async delete(e){const t=this.pluginManager.getBundleManager();if(e.bundleId)await t.deleteBundle(e.bundleId);else if(void 0!==e.keepVersions){const a=(await t.getAllBundles()).sort((e,t)=>t.downloadTime-e.downloadTime);for(let r=e.keepVersions;r<a.length;r++)await t.deleteBundle(a[r].bundleId)}}async notifyAppReady(){const e=this.pluginManager.getBundleManager(),t=await e.getActiveBundle();t&&(t.status=c.ACTIVE,await e.saveBundleInfo(t))}async getLatest(){return{available:!1}}async setChannel(e){const t=this.pluginManager.getConfigManager().get("preferences");t&&await t.set({key:"update_channel",value:e})}async setUpdateUrl(e){this.pluginManager.getConfigManager().configure({baseUrl:e})}async validateUpdate(e){const t=this.pluginManager.getSecurityValidator();try{const a=await t.validateChecksum(new ArrayBuffer(0),e.checksum);return{isValid:a,details:{checksumValid:a,signatureValid:!0,sizeValid:!0,versionValid:!0}}}catch(e){return{isValid:!1,error:e instanceof Error?e.message:"Validation failed"}}}async getAppUpdateInfo(){return{updateAvailable:!1,currentVersion:"1.0.0"}}async performImmediateUpdate(){throw new p(e.ErrorCode.PLATFORM_NOT_SUPPORTED,"Native app updates are not supported on web")}async startFlexibleUpdate(){throw new p(e.ErrorCode.PLATFORM_NOT_SUPPORTED,"Native app updates are not supported on web")}async completeFlexibleUpdate(){throw new p(e.ErrorCode.PLATFORM_NOT_SUPPORTED,"Native app updates are not supported on web")}async openAppStore(t){throw new p(e.ErrorCode.PLATFORM_NOT_SUPPORTED,"App store is not available on web")}async requestReview(){return{shown:!1,error:"Reviews are not supported on web"}}async canRequestReview(){return{allowed:!1,reason:"Reviews are not supported on web"}}async enableBackgroundUpdates(e){const t=this.pluginManager.getConfigManager().get("preferences");t&&await t.set({key:"background_update_config",value:JSON.stringify(e)})}async disableBackgroundUpdates(){const e=this.pluginManager.getConfigManager().get("preferences");e&&await e.remove({key:"background_update_config"})}async getBackgroundUpdateStatus(){return{enabled:!1,isRunning:!1,checkCount:0,failureCount:0}}async scheduleBackgroundCheck(t){throw new p(e.ErrorCode.PLATFORM_NOT_SUPPORTED,"Background updates are not supported on web")}async triggerBackgroundCheck(){return{success:!1,updatesFound:!1,notificationSent:!1,error:{code:h.PLATFORM_NOT_SUPPORTED,message:"Background updates are not supported on web"}}}async setNotificationPreferences(e){const t=this.pluginManager.getConfigManager().get("preferences");t&&await t.set({key:"notification_preferences",value:JSON.stringify(e)})}async getNotificationPermissions(){return{granted:!1,canRequest:!1}}async requestNotificationPermissions(){return!1}}const v=t.registerPlugin("CapacitorNativeUpdate",{web:()=>new O});e.BundleManager=N,e.CacheManager=class{constructor(){this.filesystem=null,this.memoryCache=new Map,this.CACHE_DIR="cache",this.logger=w.getInstance(),this.configManager=f.getInstance()}async initialize(){if(this.filesystem=this.configManager.get("filesystem"),!this.filesystem)throw new Error("Filesystem not configured");try{await this.filesystem.mkdir({path:this.CACHE_DIR,directory:a.Directory.Data,recursive:!0})}catch(e){this.logger.debug("Cache directory may already exist",e)}await this.cleanExpiredCache()}async set(e,t,a){const r=Date.now()+(a||this.configManager.get("cacheExpiration")),i={data:t,timestamp:Date.now(),expiry:r};this.memoryCache.set(e,i),this.shouldPersist(t)&&await this.persistToFile(e,i),this.logger.debug("Cache entry set",{key:e,expiry:new Date(r)})}async get(e){const t=this.memoryCache.get(e);if(t){if(Date.now()<t.expiry)return t.data;this.memoryCache.delete(e)}const a=await this.loadFromFile(e);if(a){if(Date.now()<a.expiry)return this.memoryCache.set(e,a),a.data;await this.removeFile(e)}return null}async has(e){return null!==await this.get(e)}async remove(e){this.memoryCache.delete(e),await this.removeFile(e),this.logger.debug("Cache entry removed",{key:e})}async clear(){this.memoryCache.clear();try{await this.filesystem.rmdir({path:this.CACHE_DIR,directory:a.Directory.Data,recursive:!0}),await this.filesystem.mkdir({path:this.CACHE_DIR,directory:a.Directory.Data,recursive:!0})}catch(e){this.logger.warn("Failed to clear cache directory",e)}this.logger.info("Cache cleared")}async cleanExpiredCache(){const e=Date.now();let t=0;for(const[a,r]of this.memoryCache)e>=r.expiry&&(this.memoryCache.delete(a),t++);try{const r=await this.filesystem.readdir({path:this.CACHE_DIR,directory:a.Directory.Data});for(const a of r.files){const r=a.name.replace(".json",""),i=await this.loadFromFile(r);(!i||e>=i.expiry)&&(await this.removeFile(r),t++)}}catch(e){this.logger.debug("Failed to clean filesystem cache",e)}t>0&&this.logger.info("Cleaned expired cache entries",{count:t})}async getStats(){let e=0,t=0;try{const r=await this.filesystem.readdir({path:this.CACHE_DIR,directory:a.Directory.Data});e=r.files.length;for(const e of r.files)t+=(await this.filesystem.stat({path:`${this.CACHE_DIR}/${e.name}`,directory:a.Directory.Data})).size||0}catch(e){this.logger.debug("Failed to get cache stats",e)}return{memoryEntries:this.memoryCache.size,fileEntries:e,totalSize:t}}async cacheBundleMetadata(e){const t=`bundle_meta_${e.bundleId}`;await this.set(t,e,864e5)}async getCachedBundleMetadata(e){return this.get(`bundle_meta_${e}`)}shouldPersist(e){return"object"==typeof e||"string"==typeof e&&e.length>1024}async persistToFile(e,t){if(this.filesystem)try{const r=`${this.CACHE_DIR}/${e}.json`,i=JSON.stringify(t);await this.filesystem.writeFile({path:r,data:i,directory:a.Directory.Data,encoding:a.Encoding.UTF8})}catch(t){this.logger.warn("Failed to persist cache to file",{key:e,error:t})}}async loadFromFile(e){if(!this.filesystem)return null;try{const t=`${this.CACHE_DIR}/${e}.json`,r=await this.filesystem.readFile({path:t,directory:a.Directory.Data,encoding:a.Encoding.UTF8});return JSON.parse(r.data)}catch(e){return null}}async removeFile(e){if(this.filesystem)try{const t=`${this.CACHE_DIR}/${e}.json`;await this.filesystem.deleteFile({path:t,directory:a.Directory.Data})}catch(t){this.logger.debug("Failed to remove cache file",{key:e,error:t})}}},e.CapacitorNativeUpdate=v,e.CapacitorNativeUpdateError=p,e.ConfigManager=f,e.ConfigurationError=class extends p{constructor(t,a){super(e.ErrorCode.INVALID_CONFIG,t,a),this.name="ConfigurationError"}},e.DownloadError=I,e.DownloadManager=_,e.Logger=w,e.PluginManager=R,e.SecurityValidator=m,e.StorageError=D,e.UpdateErrorClass=A,e.UpdateManager=class{constructor(){this.filesystem=null,this.updateInProgress=!1,this.currentState=null,this.pluginManager=R.getInstance(),this.securityValidator=m.getInstance()}async initialize(){if(this.filesystem=this.pluginManager.getConfigManager().get("filesystem"),!this.filesystem)throw new A(e.ErrorCode.MISSING_DEPENDENCY,"Filesystem not configured")}async applyUpdate(t,a){if(this.updateInProgress)throw new A(e.ErrorCode.UPDATE_FAILED,"Another update is already in progress");const r=this.pluginManager.getLogger(),i=this.pluginManager.getBundleManager();try{this.updateInProgress=!0,r.info("Starting bundle update",{bundleId:t});const n=await i.getBundle(t);if(!n)throw new A(e.ErrorCode.FILE_NOT_FOUND,`Bundle ${t} not found`);if("READY"!==n.status&&"ACTIVE"!==n.status)throw new A(e.ErrorCode.BUNDLE_NOT_READY,`Bundle ${t} is not ready for installation`);const s=await i.getActiveBundle();this.currentState={currentBundle:s,newBundle:n,backupPath:null,startTime:Date.now()},await this.validateUpdate(s,n,a),s&&"default"!==s.bundleId&&(this.currentState.backupPath=await this.createBackup(s)),await this.performUpdate(n),await this.verifyUpdate(n),await i.setActiveBundle(t),(null==a?void 0:a.cleanupOldBundles)&&await i.cleanupOldBundles(a.keepBundleCount||3),r.info("Bundle update completed successfully",{bundleId:t,version:n.version,duration:Date.now()-this.currentState.startTime}),this.currentState=null}catch(e){throw r.error("Bundle update failed",e),this.currentState&&await this.rollback(),e}finally{this.updateInProgress=!1}}async validateUpdate(t,a,r){const i=this.pluginManager.getLogger(),n=this.pluginManager.getVersionManager();if(t&&!(null==r?void 0:r.allowDowngrade)&&n.shouldBlockDowngrade(t.version,a.version))throw new y(e.ErrorCode.VERSION_DOWNGRADE,`Cannot downgrade from ${t.version} to ${a.version}`);if(!a.verified){i.warn("Bundle not verified, verifying now",{bundleId:a.bundleId});const t=this.pluginManager.getDownloadManager(),r=await t.loadBlob(a.bundleId);if(!r)throw new A(e.ErrorCode.FILE_NOT_FOUND,"Bundle data not found");const n=await r.arrayBuffer();if(!await this.securityValidator.verifyChecksum(n,a.checksum))throw new y(e.ErrorCode.CHECKSUM_MISMATCH,"Bundle checksum verification failed");if(a.signature&&!await this.securityValidator.verifySignature(n,a.signature))throw new y(e.ErrorCode.SIGNATURE_INVALID,"Bundle signature verification failed");await this.pluginManager.getBundleManager().markBundleAsVerified(a.bundleId)}i.debug("Bundle validation passed",{bundleId:a.bundleId})}async createBackup(t){const r=`backups/${t.bundleId}_${Date.now()}`,i=this.pluginManager.getLogger();try{return await this.filesystem.mkdir({path:r,directory:a.Directory.Data,recursive:!0}),await this.filesystem.copy({from:t.path,to:r,directory:a.Directory.Data}),i.info("Backup created",{bundleId:t.bundleId,backupPath:r}),r}catch(t){throw i.error("Failed to create backup",t),new A(e.ErrorCode.UPDATE_FAILED,"Failed to create backup",void 0,t)}}async performUpdate(t){const r=this.pluginManager.getLogger();try{const e=`active/${t.bundleId}`;await this.filesystem.mkdir({path:e,directory:a.Directory.Data,recursive:!0}),await this.filesystem.copy({from:t.path,to:e,directory:a.Directory.Data}),t.path=e,r.debug("Bundle files installed",{bundleId:t.bundleId,targetPath:e})}catch(t){throw new A(e.ErrorCode.UPDATE_FAILED,"Failed to install bundle files",void 0,t)}}async verifyUpdate(t){try{const e=`${t.path}/index.html`;await this.filesystem.stat({path:e,directory:a.Directory.Data})}catch(t){throw new A(e.ErrorCode.UPDATE_FAILED,"Bundle verification failed after installation",void 0,t)}}async rollback(){var t;if(!this.currentState)throw new A(e.ErrorCode.ROLLBACK_FAILED,"No update state to rollback");const r=this.pluginManager.getLogger();r.warn("Starting rollback",{from:this.currentState.newBundle.bundleId,to:(null===(t=this.currentState.currentBundle)||void 0===t?void 0:t.bundleId)||"default"});try{const e=this.pluginManager.getBundleManager();if(this.currentState.backupPath&&this.currentState.currentBundle){const t=`active/${this.currentState.currentBundle.bundleId}`;await this.filesystem.copy({from:this.currentState.backupPath,to:t,directory:a.Directory.Data}),this.currentState.currentBundle.path=t,await e.saveBundleInfo(this.currentState.currentBundle)}this.currentState.currentBundle?await e.setActiveBundle(this.currentState.currentBundle.bundleId):await e.clearActiveBundle(),r.info("Rollback completed successfully")}catch(t){throw r.error("Rollback failed",t),new A(e.ErrorCode.ROLLBACK_FAILED,"Failed to rollback update",void 0,t)}finally{if(this.currentState.backupPath)try{await this.filesystem.rmdir({path:this.currentState.backupPath,directory:a.Directory.Data,recursive:!0})}catch(e){r.warn("Failed to clean up backup",e)}}}getUpdateProgress(){var e,t;return{inProgress:this.updateInProgress,bundleId:null===(e=this.currentState)||void 0===e?void 0:e.newBundle.bundleId,startTime:null===(t=this.currentState)||void 0===t?void 0:t.startTime}}async cancelUpdate(){this.updateInProgress&&this.currentState&&(this.pluginManager.getLogger().warn("Cancelling update",{bundleId:this.currentState.newBundle.bundleId}),await this.rollback(),this.updateInProgress=!1,this.currentState=null)}},e.ValidationError=y,e.VersionManager=C}({},capacitorExports,capacitorFilesystem); //# sourceMappingURL=plugin.js.map