UNPKG

capacitor-native-update

Version:
3 lines (2 loc) 39.2 kB
import{registerPlugin as e}from"@capacitor/core";import{Directory as t,Encoding as a}from"@capacitor/filesystem";var i,n,r,s,o,l,c,d,u,h,g,f;!function(e){e.APP_UPDATE="app_update",e.LIVE_UPDATE="live_update",e.BOTH="both"}(i||(i={})),function(e){e.MIN="min",e.LOW="low",e.DEFAULT="default",e.HIGH="high",e.MAX="max"}(n||(n={})),function(e){e.IMMEDIATE="immediate",e.BACKGROUND="background",e.MANUAL="manual"}(r||(r={})),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"}(c||(c={})),function(e){e.PENDING="PENDING",e.DOWNLOADING="DOWNLOADING",e.READY="READY",e.ACTIVE="ACTIVE",e.FAILED="FAILED"}(d||(d={})),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 w{constructor(){this.config=this.getDefaultConfig()}static getInstance(){return w.instance||(w.instance=new w),w.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)}}!function(e){e[e.DEBUG=0]="DEBUG",e[e.INFO=1]="INFO",e[e.WARN=2]="WARN",e[e.ERROR=3]="ERROR"}(g||(g={}));class p{constructor(){this.configManager=w.getInstance()}static getInstance(){return p.instance||(p.instance=new p),p.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(e,t,a){if(!this.shouldLog())return;const i=(new Date).toISOString(),n=a?this.sanitize(a):void 0,r={timestamp:i,level:g[e],message:t};switch(void 0!==n&&(r.data=n),e){case g.DEBUG:console.debug("[CapacitorNativeUpdate]",r);break;case g.INFO:console.info("[CapacitorNativeUpdate]",r);break;case g.WARN:console.warn("[CapacitorNativeUpdate]",r);break;case g.ERROR:console.error("[CapacitorNativeUpdate]",r)}}debug(e,t){this.log(g.DEBUG,e,t)}info(e,t){this.log(g.INFO,e,t)}warn(e,t){this.log(g.WARN,e,t)}error(e,t){const a=t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t;this.log(g.ERROR,e,a)}}!function(e){e.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"}(f||(f={}));class I extends Error{constructor(e,t,a,i){super(t),this.code=e,this.message=t,this.details=a,this.originalError=i,this.name="CapacitorNativeUpdateError",Object.setPrototypeOf(this,I.prototype)}toJSON(){return{name:this.name,code:this.code,message:this.message,details:this.details,stack:this.stack}}}class E extends I{constructor(e,t){super(f.INVALID_CONFIG,e,t),this.name="ConfigurationError"}}class y extends I{constructor(e,t,a,i){super(e,t,a,i),this.name="DownloadError"}}class D extends I{constructor(e,t,a){super(e,t,a),this.name="ValidationError"}}class A extends I{constructor(e,t,a,i){super(e,t,a,i),this.name="StorageError"}}class m extends I{constructor(e,t,a,i){super(e,t,a,i),this.name="UpdateError"}}class _{constructor(){this.configManager=w.getInstance(),this.logger=p.getInstance()}static getInstance(){return _.instance||(_.instance=new _),_.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),i=a===t.toLowerCase();return i||this.logger.error("Checksum verification failed",{expected:t,actual:a}),i}async validateChecksum(e,t){return this.verifyChecksum(e,t)}async verifySignature(e,t){if(!this.configManager.get("enableSignatureValidation"))return!0;if(!this.configManager.get("publicKey"))throw new D(f.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(e){if(!e||"string"!=typeof e)throw new D(f.INVALID_BUNDLE_FORMAT,"Bundle ID must be a non-empty string");if(!/^[a-zA-Z0-9\-_.]+$/.test(e))throw new D(f.INVALID_BUNDLE_FORMAT,"Bundle ID contains invalid characters");if(e.length>100)throw new D(f.INVALID_BUNDLE_FORMAT,"Bundle ID is too long (max 100 characters)")}validateVersion(e){if(!e||"string"!=typeof e)throw new D(f.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(e))throw new D(f.INVALID_BUNDLE_FORMAT,"Version must follow semantic versioning format (e.g., 1.2.3)")}isVersionDowngrade(e,t){const a=this.parseVersion(e),i=this.parseVersion(t);return i.major<a.major||!(i.major>a.major)&&(i.minor<a.minor||!(i.minor>a.minor)&&i.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(e){if(!e||"string"!=typeof e)throw new D(f.INVALID_URL,"URL must be a non-empty string");let t;try{t=new URL(e)}catch(e){throw new D(f.INVALID_URL,"Invalid URL format")}if("https:"!==t.protocol)throw new D(f.INVALID_URL,"Only HTTPS URLs are allowed");const a=this.configManager.get("allowedHosts");if(a.length>0&&!a.includes(t.hostname))throw new D(f.UNAUTHORIZED_HOST,`Host ${t.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(t.hostname)))throw new D(f.UNAUTHORIZED_HOST,"Private/local addresses are not allowed")}validateFileSize(e){if("number"!=typeof e||e<0)throw new D(f.INVALID_BUNDLE_FORMAT,"File size must be a non-negative number");const t=this.configManager.get("maxBundleSize");if(e>t)throw new D(f.BUNDLE_TOO_LARGE,`File size ${e} exceeds maximum allowed size of ${t} bytes`)}generateSecureId(){const e=new Uint8Array(16);return crypto.getRandomValues(e),Array.from(e,e=>e.toString(16).padStart(2,"0")).join("")}validateMetadata(e){if(e&&"object"!=typeof e)throw new D(f.INVALID_BUNDLE_FORMAT,"Metadata must be an object");if(JSON.stringify(e||{}).length>10240)throw new D(f.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=p.getInstance(),this.configManager=w.getInstance()}async initialize(){if(this.preferences=this.configManager.get("preferences"),!this.preferences)throw new A(f.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(e){throw new A(f.STORAGE_FULL,"Failed to save bundles to storage",void 0,e)}}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(e){if(!e.bundleId||"string"!=typeof e.bundleId)throw new A(f.INVALID_BUNDLE_FORMAT,"Invalid bundle ID");if(!e.version||"string"!=typeof e.version)throw new A(f.INVALID_BUNDLE_FORMAT,"Invalid bundle version");if(!e.path||"string"!=typeof e.path)throw new A(f.INVALID_BUNDLE_FORMAT,"Invalid bundle path");if("number"!=typeof e.size||e.size<0)throw new A(f.INVALID_BUNDLE_FORMAT,"Invalid bundle size")}async getAllBundles(){return await this.loadCache(),Array.from(this.cache.values())}async getBundle(e){if(!e)throw new A(f.INVALID_BUNDLE_FORMAT,"Bundle ID is required");return await this.loadCache(),this.cache.get(e)||null}async deleteBundle(e){if(!e)throw new A(f.INVALID_BUNDLE_FORMAT,"Bundle ID is required");await this.loadCache(),this.cache.get(e)?(this.cache.delete(e),await this.saveCache(),await this.getActiveBundleId()===e&&await this.clearActiveBundle(),this.logger.info("Bundle deleted",{bundleId:e})):this.logger.warn("Attempted to delete non-existent bundle",{bundleId:e})}async getActiveBundle(){const e=await this.getActiveBundleId();return e?this.getBundle(e):null}async setActiveBundle(e){if(!e)throw new A(f.INVALID_BUNDLE_FORMAT,"Bundle ID is required");const t=await this.getBundle(e);if(!t)throw new A(f.FILE_NOT_FOUND,`Bundle ${e} not found`);const a=await this.getActiveBundle();a&&a.bundleId!==e&&(a.status="READY",await this.saveBundleInfo(a)),t.status="ACTIVE",await this.saveBundleInfo(t),await this.preferences.set({key:this.ACTIVE_BUNDLE_KEY,value:e}),this.logger.info("Active bundle set",{bundleId:e,version:t.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(e){if(e<1)throw new A(f.INVALID_CONFIG,"Keep count must be at least 1");const t=await this.getAllBundles(),a=await this.getActiveBundleId(),i=t.sort((e,t)=>t.downloadTime-e.downloadTime),n=new Set;a&&n.add(a);let r=n.size;for(const t of i){if(r>=e)break;n.has(t.bundleId)||(n.add(t.bundleId),r++)}let s=0;for(const e of t)n.has(e.bundleId)||(await this.deleteBundle(e.bundleId),s++);s>0&&this.logger.info("Cleaned up old bundles",{deleted:s,kept:r})}async getBundlesOlderThan(e){if(e<0)throw new A(f.INVALID_CONFIG,"Timestamp must be non-negative");return(await this.getAllBundles()).filter(t=>t.downloadTime<e)}async markBundleAsVerified(e){const t=await this.getBundle(e);if(!t)throw new A(f.FILE_NOT_FOUND,`Bundle ${e} not found`);t.verified=!0,await this.saveBundleInfo(t),this.logger.info("Bundle marked as verified",{bundleId:e})}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 R{constructor(){this.activeDownloads=new Map,this.filesystem=null,this.logger=p.getInstance(),this.configManager=w.getInstance()}async initialize(){if(this.filesystem=this.configManager.get("filesystem"),!this.filesystem)throw new y(f.MISSING_DEPENDENCY,"Filesystem not configured. Please configure the plugin first.")}validateUrl(e){try{const t=new URL(e);if("https:"!==t.protocol)throw new D(f.INVALID_URL,"Only HTTPS URLs are allowed for security reasons");const a=this.configManager.get("allowedHosts");if(a.length>0&&!a.includes(t.hostname))throw new D(f.UNAUTHORIZED_HOST,`Host ${t.hostname} is not in the allowed hosts list`)}catch(e){if(e instanceof D)throw e;throw new D(f.INVALID_URL,"Invalid URL format")}}async download(e,t,a){if(this.validateUrl(e),!t)throw new D(f.INVALID_BUNDLE_FORMAT,"Bundle ID is required");if(this.activeDownloads.has(t))throw new y(f.DOWNLOAD_FAILED,`Download already in progress for bundle ${t}`);const i=new AbortController,n={controller:i,startTime:Date.now()};this.activeDownloads.set(t,n);try{const r=this.configManager.get("downloadTimeout"),s=setTimeout(()=>i.abort(),r),o=await fetch(e,{signal:i.signal,headers:{"Cache-Control":"no-cache",Accept:"application/octet-stream, application/zip"}});if(clearTimeout(s),!o.ok)throw new y(f.DOWNLOAD_FAILED,`Download failed: ${o.status} ${o.statusText}`,{status:o.status,statusText:o.statusText});const l=o.headers.get("content-type");if(l&&!this.isValidContentType(l))throw new D(f.INVALID_BUNDLE_FORMAT,`Invalid content type: ${l}`);const c=o.headers.get("content-length"),d=c?parseInt(c,10):0;if(d>this.configManager.get("maxBundleSize"))throw new D(f.BUNDLE_TOO_LARGE,`Bundle size ${d} exceeds maximum allowed size`);if(!d||!o.body){const e=await o.blob();return this.validateBlobSize(e),e}const u=o.body.getReader(),h=[];let g=0;for(;;){const{done:e,value:i}=await u.read();if(e)break;if(h.push(i),g+=i.length,g>this.configManager.get("maxBundleSize"))throw new D(f.BUNDLE_TOO_LARGE,"Download size exceeds maximum allowed size");a&&a({percent:Math.round(g/d*100),bytesDownloaded:g,totalBytes:d,bundleId:t})}const w=new Blob(h);return this.validateBlobSize(w),this.logger.info("Download completed",{bundleId:t,size:w.size,duration:Date.now()-n.startTime}),w}catch(e){if(e instanceof Error&&"AbortError"===e.name){const t=Date.now()-n.startTime>=this.configManager.get("downloadTimeout");throw new y(t?f.DOWNLOAD_TIMEOUT:f.DOWNLOAD_FAILED,t?"Download timed out":"Download cancelled",void 0,e)}throw e}finally{this.activeDownloads.delete(t)}}isValidContentType(e){return["application/octet-stream","application/zip","application/x-zip-compressed","application/x-zip"].some(t=>e.includes(t))}validateBlobSize(e){if(0===e.size)throw new D(f.INVALID_BUNDLE_FORMAT,"Downloaded file is empty");if(e.size>this.configManager.get("maxBundleSize"))throw new D(f.BUNDLE_TOO_LARGE,`File size ${e.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(e,t,a){const i=this.configManager.get("retryAttempts"),n=this.configManager.get("retryDelay");let r=null;for(let s=0;s<i;s++)try{if(s>0){const e=Math.min(n*Math.pow(2,s-1),3e4);await new Promise(t=>setTimeout(t,e)),this.logger.debug("Retrying download",{bundleId:t,attempt:s,delay:e})}return await this.download(e,t,a)}catch(e){if(r=e,e instanceof D||e instanceof Error&&"AbortError"===e.name)throw e;this.logger.warn(`Download attempt ${s+1} failed`,{bundleId:t,error:e})}throw new y(f.DOWNLOAD_FAILED,"Download failed after all retries",{attempts:i},r||void 0)}async blobToArrayBuffer(e){return e.arrayBuffer()}async saveBlob(e,a){if(!this.filesystem)throw new y(f.MISSING_DEPENDENCY,"Filesystem not initialized");const i=await this.blobToArrayBuffer(a),n=btoa(String.fromCharCode(...new Uint8Array(i))),r=`bundles/${e}/bundle.zip`;return await this.filesystem.writeFile({path:r,data:n,directory:t.Data,recursive:!0}),this.logger.debug("Bundle saved to filesystem",{bundleId:e,path:r,size:a.size}),r}async loadBlob(e){if(!this.filesystem)throw new y(f.MISSING_DEPENDENCY,"Filesystem not initialized");try{const a=`bundles/${e}/bundle.zip`,i=await this.filesystem.readFile({path:a,directory:t.Data}),n=atob(i.data),r=new Uint8Array(n.length);for(let e=0;e<n.length;e++)r[e]=n.charCodeAt(e);return new Blob([r],{type:"application/zip"})}catch(t){return this.logger.debug("Failed to load bundle from filesystem",{bundleId:e,error:t}),null}}async deleteBlob(e){if(!this.filesystem)throw new y(f.MISSING_DEPENDENCY,"Filesystem not initialized");try{const a=`bundles/${e}`;await this.filesystem.rmdir({path:a,directory:t.Data,recursive:!0}),this.logger.debug("Bundle deleted from filesystem",{bundleId:e})}catch(t){this.logger.warn("Failed to delete bundle from filesystem",{bundleId:e,error:t})}}}class O{constructor(){this.VERSION_CHECK_CACHE_KEY="capacitor_native_update_version_cache",this.CACHE_DURATION=3e5,this.preferences=null,this.memoryCache=new Map,this.logger=p.getInstance(),this.configManager=w.getInstance(),this.securityValidator=_.getInstance()}async initialize(){if(this.preferences=this.configManager.get("preferences"),!this.preferences)throw new D(f.MISSING_DEPENDENCY,"Preferences not configured. Please configure the plugin first.")}async checkForUpdates(e,t,a,i){if(this.securityValidator.validateUrl(e),this.securityValidator.validateVersion(a),!t||!i)throw new D(f.INVALID_CONFIG,"Channel and appId are required");const n=`${t}-${i}`,r=await this.getCachedVersionInfo(n);if(r&&r.channel===t&&Date.now()-r.timestamp<this.CACHE_DURATION)return this.logger.debug("Returning cached version info",{channel:t,version:r.data.version}),r.data;try{const r=new URL(`${e}/check`);r.searchParams.append("channel",t),r.searchParams.append("version",a),r.searchParams.append("appId",i),r.searchParams.append("platform","web");const s=await fetch(r.toString(),{method:"GET",headers:{"Content-Type":"application/json","X-App-Version":a,"X-App-Id":i},signal:AbortSignal.timeout(this.configManager.get("downloadTimeout"))});if(!s.ok)throw new Error(`Version check failed: ${s.status}`);const o=await s.json();if(!o.version)throw new D(f.INVALID_BUNDLE_FORMAT,"No version in server response");return this.securityValidator.validateVersion(o.version),o.bundleUrl&&this.securityValidator.validateUrl(o.bundleUrl),o.minAppVersion&&this.securityValidator.validateVersion(o.minAppVersion),await this.cacheVersionInfo(n,t,o),this.logger.info("Version check completed",{channel:t,currentVersion:a,latestVersion:o.version,updateAvailable:this.isNewerVersion(o.version,a)}),o}catch(e){return this.logger.error("Failed to check for updates",e),null}}isNewerVersion(e,t){try{const a=this.parseVersion(e),i=this.parseVersion(t);return a.major!==i.major?a.major>i.major:a.minor!==i.minor?a.minor>i.minor:a.patch!==i.patch?a.patch>i.patch:!(a.prerelease&&!i.prerelease||(a.prerelease||!i.prerelease)&&(!a.prerelease||!i.prerelease||!(a.prerelease>i.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(e){const t=e.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/);if(!t)throw new D(f.INVALID_BUNDLE_FORMAT,"Invalid version format");return{major:parseInt(t[1],10),minor:parseInt(t[2],10),patch:parseInt(t[3],10),prerelease:t[4],build:t[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 i=a[e];return!i||(this.securityValidator.validateVersion(t),this.securityValidator.validateVersion(i),!this.isNewerVersion(i,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 i={channel:t,data:a,timestamp:Date.now()};this.memoryCache.set(e,i);try{const{value:t}=await this.preferences.get({key:this.VERSION_CHECK_CACHE_KEY}),a=t?JSON.parse(t):{},n=Date.now();for(const e in a)n-a[e].timestamp>2*this.CACHE_DURATION&&delete a[e];a[e]=i,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 v{constructor(){this.bundleManager=null,this.downloadManager=null,this.versionManager=null,this.initialized=!1,this.configManager=w.getInstance(),this.logger=p.getInstance(),this.securityValidator=_.getInstance()}static getInstance(){return v.instance||(v.instance=new v),v.instance}async initialize(e){if(this.initialized)this.logger.warn("Plugin already initialized");else try{if(this.configManager.configure(e),!e.filesystem||!e.preferences)throw new I(f.MISSING_DEPENDENCY,"Filesystem and Preferences are required for plugin initialization");this.bundleManager=new N,await this.bundleManager.initialize(),this.downloadManager=new R,await this.downloadManager.initialize(),this.versionManager=new O,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 I(f.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 T{constructor(){this.initialized=!1,this.pluginManager=v.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(e){var t,a;if(!this.initialized)throw new I(f.NOT_CONFIGURED,"Plugin not initialized. Call initialize() first.");const i=this.pluginManager.getConfigManager();(null===(t=e.liveUpdate)||void 0===t?void 0:t.allowedHosts)&&i.configure({allowedHosts:e.liveUpdate.allowedHosts}),(null===(a=e.liveUpdate)||void 0===a?void 0:a.maxBundleSize)&&i.configure({maxBundleSize:e.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:c.UP_TO_DATE,version:(null==e?void 0:e.version)||"1.0.0"}}catch(e){return{status:c.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(),i=await t.downloadWithRetry(e.url,e.version),n=await t.saveBlob(e.version,i),r={bundleId:e.version,version:e.version,path:n,downloadTime:Date.now(),size:i.size,status:d.READY,checksum:e.checksum,signature:e.signature,verified:!1};return await a.saveBundleInfo(r),r}async set(e){const t=this.pluginManager.getBundleManager();await t.setActiveBundle(e.bundleId)}async reload(){"undefined"!=typeof window&&window.location.reload()}async current(){const e=this.pluginManager.getBundleManager(),t=await e.getActiveBundle();if(!t)throw new I(f.FILE_NOT_FOUND,"No active bundle found");return t}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 i=e.keepVersions;i<a.length;i++)await t.deleteBundle(a[i].bundleId)}}async notifyAppReady(){const e=this.pluginManager.getBundleManager(),t=await e.getActiveBundle();t&&(t.status=d.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 I(f.PLATFORM_NOT_SUPPORTED,"Native app updates are not supported on web")}async startFlexibleUpdate(){throw new I(f.PLATFORM_NOT_SUPPORTED,"Native app updates are not supported on web")}async completeFlexibleUpdate(){throw new I(f.PLATFORM_NOT_SUPPORTED,"Native app updates are not supported on web")}async openAppStore(e){throw new I(f.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(e){throw new I(f.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 L=e("CapacitorNativeUpdate",{web:()=>new T});class b{constructor(){this.filesystem=null,this.memoryCache=new Map,this.CACHE_DIR="cache",this.logger=p.getInstance(),this.configManager=w.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:t.Data,recursive:!0})}catch(e){this.logger.debug("Cache directory may already exist",e)}await this.cleanExpiredCache()}async set(e,t,a){const i=Date.now()+(a||this.configManager.get("cacheExpiration")),n={data:t,timestamp:Date.now(),expiry:i};this.memoryCache.set(e,n),this.shouldPersist(t)&&await this.persistToFile(e,n),this.logger.debug("Cache entry set",{key:e,expiry:new Date(i)})}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:t.Data,recursive:!0}),await this.filesystem.mkdir({path:this.CACHE_DIR,directory:t.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 a=0;for(const[t,i]of this.memoryCache)e>=i.expiry&&(this.memoryCache.delete(t),a++);try{const i=await this.filesystem.readdir({path:this.CACHE_DIR,directory:t.Data});for(const t of i.files){const i=t.name.replace(".json",""),n=await this.loadFromFile(i);(!n||e>=n.expiry)&&(await this.removeFile(i),a++)}}catch(e){this.logger.debug("Failed to clean filesystem cache",e)}a>0&&this.logger.info("Cleaned expired cache entries",{count:a})}async getStats(){let e=0,a=0;try{const i=await this.filesystem.readdir({path:this.CACHE_DIR,directory:t.Data});e=i.files.length;for(const e of i.files)a+=(await this.filesystem.stat({path:`${this.CACHE_DIR}/${e.name}`,directory:t.Data})).size||0}catch(e){this.logger.debug("Failed to get cache stats",e)}return{memoryEntries:this.memoryCache.size,fileEntries:e,totalSize:a}}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,i){if(this.filesystem)try{const n=`${this.CACHE_DIR}/${e}.json`,r=JSON.stringify(i);await this.filesystem.writeFile({path:n,data:r,directory:t.Data,encoding:a.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 i=`${this.CACHE_DIR}/${e}.json`,n=await this.filesystem.readFile({path:i,directory:t.Data,encoding:a.UTF8});return JSON.parse(n.data)}catch(e){return null}}async removeFile(e){if(this.filesystem)try{const a=`${this.CACHE_DIR}/${e}.json`;await this.filesystem.deleteFile({path:a,directory:t.Data})}catch(t){this.logger.debug("Failed to remove cache file",{key:e,error:t})}}}class C{constructor(){this.filesystem=null,this.updateInProgress=!1,this.currentState=null,this.pluginManager=v.getInstance(),this.securityValidator=_.getInstance()}async initialize(){if(this.filesystem=this.pluginManager.getConfigManager().get("filesystem"),!this.filesystem)throw new m(f.MISSING_DEPENDENCY,"Filesystem not configured")}async applyUpdate(e,t){if(this.updateInProgress)throw new m(f.UPDATE_FAILED,"Another update is already in progress");const a=this.pluginManager.getLogger(),i=this.pluginManager.getBundleManager();try{this.updateInProgress=!0,a.info("Starting bundle update",{bundleId:e});const n=await i.getBundle(e);if(!n)throw new m(f.FILE_NOT_FOUND,`Bundle ${e} not found`);if("READY"!==n.status&&"ACTIVE"!==n.status)throw new m(f.BUNDLE_NOT_READY,`Bundle ${e} is not ready for installation`);const r=await i.getActiveBundle();this.currentState={currentBundle:r,newBundle:n,backupPath:null,startTime:Date.now()},await this.validateUpdate(r,n,t),r&&"default"!==r.bundleId&&(this.currentState.backupPath=await this.createBackup(r)),await this.performUpdate(n),await this.verifyUpdate(n),await i.setActiveBundle(e),(null==t?void 0:t.cleanupOldBundles)&&await i.cleanupOldBundles(t.keepBundleCount||3),a.info("Bundle update completed successfully",{bundleId:e,version:n.version,duration:Date.now()-this.currentState.startTime}),this.currentState=null}catch(e){throw a.error("Bundle update failed",e),this.currentState&&await this.rollback(),e}finally{this.updateInProgress=!1}}async validateUpdate(e,t,a){const i=this.pluginManager.getLogger(),n=this.pluginManager.getVersionManager();if(e&&!(null==a?void 0:a.allowDowngrade)&&n.shouldBlockDowngrade(e.version,t.version))throw new D(f.VERSION_DOWNGRADE,`Cannot downgrade from ${e.version} to ${t.version}`);if(!t.verified){i.warn("Bundle not verified, verifying now",{bundleId:t.bundleId});const e=this.pluginManager.getDownloadManager(),a=await e.loadBlob(t.bundleId);if(!a)throw new m(f.FILE_NOT_FOUND,"Bundle data not found");const n=await a.arrayBuffer();if(!await this.securityValidator.verifyChecksum(n,t.checksum))throw new D(f.CHECKSUM_MISMATCH,"Bundle checksum verification failed");if(t.signature&&!await this.securityValidator.verifySignature(n,t.signature))throw new D(f.SIGNATURE_INVALID,"Bundle signature verification failed");await this.pluginManager.getBundleManager().markBundleAsVerified(t.bundleId)}i.debug("Bundle validation passed",{bundleId:t.bundleId})}async createBackup(e){const a=`backups/${e.bundleId}_${Date.now()}`,i=this.pluginManager.getLogger();try{return await this.filesystem.mkdir({path:a,directory:t.Data,recursive:!0}),await this.filesystem.copy({from:e.path,to:a,directory:t.Data}),i.info("Backup created",{bundleId:e.bundleId,backupPath:a}),a}catch(e){throw i.error("Failed to create backup",e),new m(f.UPDATE_FAILED,"Failed to create backup",void 0,e)}}async performUpdate(e){const a=this.pluginManager.getLogger();try{const i=`active/${e.bundleId}`;await this.filesystem.mkdir({path:i,directory:t.Data,recursive:!0}),await this.filesystem.copy({from:e.path,to:i,directory:t.Data}),e.path=i,a.debug("Bundle files installed",{bundleId:e.bundleId,targetPath:i})}catch(e){throw new m(f.UPDATE_FAILED,"Failed to install bundle files",void 0,e)}}async verifyUpdate(e){try{const a=`${e.path}/index.html`;await this.filesystem.stat({path:a,directory:t.Data})}catch(e){throw new m(f.UPDATE_FAILED,"Bundle verification failed after installation",void 0,e)}}async rollback(){var e;if(!this.currentState)throw new m(f.ROLLBACK_FAILED,"No update state to rollback");const a=this.pluginManager.getLogger();a.warn("Starting rollback",{from:this.currentState.newBundle.bundleId,to:(null===(e=this.currentState.currentBundle)||void 0===e?void 0:e.bundleId)||"default"});try{const e=this.pluginManager.getBundleManager();if(this.currentState.backupPath&&this.currentState.currentBundle){const a=`active/${this.currentState.currentBundle.bundleId}`;await this.filesystem.copy({from:this.currentState.backupPath,to:a,directory:t.Data}),this.currentState.currentBundle.path=a,await e.saveBundleInfo(this.currentState.currentBundle)}this.currentState.currentBundle?await e.setActiveBundle(this.currentState.currentBundle.bundleId):await e.clearActiveBundle(),a.info("Rollback completed successfully")}catch(e){throw a.error("Rollback failed",e),new m(f.ROLLBACK_FAILED,"Failed to rollback update",void 0,e)}finally{if(this.currentState.backupPath)try{await this.filesystem.rmdir({path:this.currentState.backupPath,directory:t.Data,recursive:!0})}catch(e){a.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)}}export{N as BundleManager,b as CacheManager,L as CapacitorNativeUpdate,I as CapacitorNativeUpdateError,w as ConfigManager,E as ConfigurationError,y as DownloadError,R as DownloadManager,f as ErrorCode,g as LogLevel,p as Logger,v as PluginManager,_ as SecurityValidator,A as StorageError,m as UpdateErrorClass,C as UpdateManager,D as ValidationError,O as VersionManager}; //# sourceMappingURL=plugin.esm.js.map