capacitor-native-update
Version:
Native Update Plugin for Capacitor
270 lines • 10.2 kB
JavaScript
import { ConfigManager } from '../core/config';
import { Logger } from '../core/logger';
import { SecurityValidator } from '../core/security';
import { ValidationError, ErrorCode } from '../core/errors';
/**
* Manages version checking and comparison
*/
export class VersionManager {
constructor() {
this.VERSION_CHECK_CACHE_KEY = 'capacitor_native_update_version_cache';
this.CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
this.preferences = null;
this.memoryCache = new Map();
this.logger = Logger.getInstance();
this.configManager = ConfigManager.getInstance();
this.securityValidator = SecurityValidator.getInstance();
}
/**
* Initialize the version manager
*/
async initialize() {
this.preferences = this.configManager.get('preferences');
if (!this.preferences) {
throw new ValidationError(ErrorCode.MISSING_DEPENDENCY, 'Preferences not configured. Please configure the plugin first.');
}
}
/**
* Check for latest version from server
*/
async checkForUpdates(serverUrl, channel, currentVersion, appId) {
// Validate inputs
this.securityValidator.validateUrl(serverUrl);
this.securityValidator.validateVersion(currentVersion);
if (!channel || !appId) {
throw new ValidationError(ErrorCode.INVALID_CONFIG, 'Channel and appId are required');
}
// Check cache first
const cacheKey = `${channel}-${appId}`;
const cached = await this.getCachedVersionInfo(cacheKey);
if (cached &&
cached.channel === channel &&
Date.now() - cached.timestamp < this.CACHE_DURATION) {
this.logger.debug('Returning cached version info', { channel, version: cached.data.version });
return cached.data;
}
try {
const url = new URL(`${serverUrl}/check`);
url.searchParams.append('channel', channel);
url.searchParams.append('version', currentVersion);
url.searchParams.append('appId', appId);
url.searchParams.append('platform', 'web'); // Will be overridden by native platforms
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-App-Version': currentVersion,
'X-App-Id': appId,
},
signal: AbortSignal.timeout(this.configManager.get('downloadTimeout')),
});
if (!response.ok) {
throw new Error(`Version check failed: ${response.status}`);
}
const data = await response.json();
// Validate response
if (!data.version) {
throw new ValidationError(ErrorCode.INVALID_BUNDLE_FORMAT, 'No version in server response');
}
this.securityValidator.validateVersion(data.version);
// Additional validation
if (data.bundleUrl) {
this.securityValidator.validateUrl(data.bundleUrl);
}
if (data.minAppVersion) {
this.securityValidator.validateVersion(data.minAppVersion);
}
// Cache the result
await this.cacheVersionInfo(cacheKey, channel, data);
this.logger.info('Version check completed', {
channel,
currentVersion,
latestVersion: data.version,
updateAvailable: this.isNewerVersion(data.version, currentVersion),
});
return data;
}
catch (error) {
this.logger.error('Failed to check for updates', error);
return null;
}
}
/**
* Compare two versions
*/
isNewerVersion(version1, version2) {
try {
const v1 = this.parseVersion(version1);
const v2 = this.parseVersion(version2);
if (v1.major !== v2.major)
return v1.major > v2.major;
if (v1.minor !== v2.minor)
return v1.minor > v2.minor;
if (v1.patch !== v2.patch)
return v1.patch > v2.patch;
// If main versions are equal, check pre-release
if (v1.prerelease && !v2.prerelease)
return false; // v1 is pre-release, v2 is not
if (!v1.prerelease && v2.prerelease)
return true; // v1 is not pre-release, v2 is
if (v1.prerelease && v2.prerelease) {
return v1.prerelease > v2.prerelease;
}
return false; // Versions are equal
}
catch (error) {
this.logger.error('Failed to compare versions', { version1, version2, error });
return false;
}
}
/**
* Check if update is mandatory based on minimum version
*/
isUpdateMandatory(currentVersion, minimumVersion) {
if (!minimumVersion)
return false;
try {
this.securityValidator.validateVersion(currentVersion);
this.securityValidator.validateVersion(minimumVersion);
return !this.isNewerVersion(currentVersion, minimumVersion) && currentVersion !== minimumVersion;
}
catch (error) {
this.logger.error('Failed to check mandatory update', error);
return false;
}
}
/**
* Parse version metadata
*/
parseVersion(version) {
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/);
if (!match) {
throw new ValidationError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Invalid version format');
}
return {
major: parseInt(match[1], 10),
minor: parseInt(match[2], 10),
patch: parseInt(match[3], 10),
prerelease: match[4],
build: match[5],
};
}
/**
* Generate version string from components
*/
buildVersionString(components) {
let version = `${components.major}.${components.minor}.${components.patch}`;
if (components.prerelease) {
version += `-${components.prerelease}`;
}
if (components.build) {
version += `+${components.build}`;
}
return version;
}
/**
* Check if version is compatible with native version requirements
*/
isCompatibleWithNativeVersion(bundleVersion, nativeVersion, compatibility) {
if (!compatibility)
return true;
try {
// Check if there's a specific native version requirement for this bundle version
const requiredNativeVersion = compatibility[bundleVersion];
if (!requiredNativeVersion)
return true;
this.securityValidator.validateVersion(nativeVersion);
this.securityValidator.validateVersion(requiredNativeVersion);
return !this.isNewerVersion(requiredNativeVersion, nativeVersion);
}
catch (error) {
this.logger.error('Failed to check compatibility', error);
return false;
}
}
/**
* Get version from cache
*/
async getCachedVersionInfo(cacheKey) {
// Check memory cache first
const memCached = this.memoryCache.get(cacheKey);
if (memCached && Date.now() - memCached.timestamp < this.CACHE_DURATION) {
return memCached;
}
// Check persistent cache
try {
const { value } = await this.preferences.get({ key: this.VERSION_CHECK_CACHE_KEY });
if (!value)
return null;
const allCached = JSON.parse(value);
const cached = allCached[cacheKey];
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
// Update memory cache
this.memoryCache.set(cacheKey, cached);
return cached;
}
}
catch (error) {
this.logger.debug('Failed to load cached version info', error);
}
return null;
}
/**
* Cache version info
*/
async cacheVersionInfo(cacheKey, channel, data) {
const cacheEntry = {
channel,
data,
timestamp: Date.now(),
};
// Update memory cache
this.memoryCache.set(cacheKey, cacheEntry);
// Update persistent cache
try {
const { value } = await this.preferences.get({ key: this.VERSION_CHECK_CACHE_KEY });
const allCached = value ? JSON.parse(value) : {};
// Clean old entries
const now = Date.now();
for (const key in allCached) {
if (now - allCached[key].timestamp > this.CACHE_DURATION * 2) {
delete allCached[key];
}
}
allCached[cacheKey] = cacheEntry;
await this.preferences.set({
key: this.VERSION_CHECK_CACHE_KEY,
value: JSON.stringify(allCached)
});
}
catch (error) {
this.logger.warn('Failed to cache version info', error);
}
}
/**
* Clear version cache
*/
async clearVersionCache() {
this.memoryCache.clear();
try {
await this.preferences.remove({ key: this.VERSION_CHECK_CACHE_KEY });
}
catch (error) {
this.logger.warn('Failed to clear version cache', error);
}
}
/**
* Check if downgrade protection should block update
*/
shouldBlockDowngrade(currentVersion, newVersion) {
try {
return this.securityValidator.isVersionDowngrade(currentVersion, newVersion);
}
catch (error) {
this.logger.error('Failed to check downgrade', error);
// Default to safe behavior - block if we can't determine
return true;
}
}
}
//# sourceMappingURL=version-manager.js.map