native-update
Version:
Foundation package for building a comprehensive update system for Capacitor apps. Provides architecture and interfaces but requires backend implementation.
335 lines • 12.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();
}
/**
* Compare two semantic versions
*/
static compareVersions(version1, version2) {
try {
// Split version and pre-release
const [v1Base, v1Pre] = version1.split('-');
const [v2Base, v2Pre] = version2.split('-');
const v1Parts = v1Base.split('.').map(Number);
const v2Parts = v2Base.split('.').map(Number);
// Compare major.minor.patch
for (let i = 0; i < 3; i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part > v2Part)
return 1;
if (v1Part < v2Part)
return -1;
}
// If base versions are equal, compare pre-release
if (v1Pre && !v2Pre)
return -1; // 1.0.0-alpha < 1.0.0
if (!v1Pre && v2Pre)
return 1; // 1.0.0 > 1.0.0-alpha
if (v1Pre && v2Pre) {
return v1Pre.localeCompare(v2Pre);
}
return 0;
}
catch (_a) {
if (version1 === version2)
return 0;
return version1 > version2 ? 1 : -1;
}
}
/**
* Validate semantic version format
*/
static isValidVersion(version) {
return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/.test(version);
}
/**
* Check if update should be performed
*/
static shouldUpdate(currentVersion, newVersion, minAppVersion) {
if (minAppVersion &&
VersionManager.compareVersions(currentVersion, minAppVersion) < 0) {
return false;
}
return VersionManager.compareVersions(currentVersion, newVersion) < 0;
}
/**
* 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