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