native-update 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import archiver from 'archiver';
6
+ import { createWriteStream } from 'fs';
7
+ import { fileURLToPath } from 'url';
8
+ import { dirname, join } from 'path';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ export async function createBundle(webDir, options) {
14
+ console.log(chalk.blue('🔨 Creating update bundle...'));
15
+
16
+ try {
17
+ // Validate input directory
18
+ const stats = await fs.stat(webDir);
19
+ if (!stats.isDirectory()) {
20
+ throw new Error(`${webDir} is not a directory`);
21
+ }
22
+
23
+ // Check for index.html
24
+ const indexPath = path.join(webDir, 'index.html');
25
+ try {
26
+ await fs.access(indexPath);
27
+ } catch {
28
+ throw new Error(`No index.html found in ${webDir}. Is this a valid web build directory?`);
29
+ }
30
+
31
+ // Create output directory
32
+ const outputDir = path.resolve(options.output);
33
+ await fs.mkdir(outputDir, { recursive: true });
34
+
35
+ // Get version
36
+ let version = options.version;
37
+ if (!version) {
38
+ try {
39
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
40
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
41
+ version = packageJson.version;
42
+ } catch {
43
+ version = new Date().toISOString().split('T')[0].replace(/-/g, '.');
44
+ }
45
+ }
46
+
47
+ // Create bundle metadata
48
+ const metadata = {
49
+ version,
50
+ channel: options.channel,
51
+ created: new Date().toISOString(),
52
+ platform: 'web',
53
+ ...(options.metadata ? JSON.parse(options.metadata) : {})
54
+ };
55
+
56
+ const bundleId = `${version}-${Date.now()}`;
57
+ const bundleFileName = `bundle-${bundleId}.zip`;
58
+ const bundlePath = path.join(outputDir, bundleFileName);
59
+ const metadataPath = path.join(outputDir, `bundle-${bundleId}.json`);
60
+
61
+ console.log(chalk.gray(` Version: ${version}`));
62
+ console.log(chalk.gray(` Channel: ${options.channel}`));
63
+ console.log(chalk.gray(` Output: ${bundlePath}`));
64
+
65
+ // Create zip archive
66
+ const output = createWriteStream(bundlePath);
67
+ const archive = archiver('zip', {
68
+ zlib: { level: 9 }
69
+ });
70
+
71
+ const archivePromise = new Promise((resolve, reject) => {
72
+ output.on('close', resolve);
73
+ archive.on('error', reject);
74
+ });
75
+
76
+ archive.pipe(output);
77
+
78
+ // Add all files from web directory
79
+ archive.directory(webDir, false);
80
+
81
+ await archive.finalize();
82
+ await archivePromise;
83
+
84
+ // Calculate checksum
85
+ const fileBuffer = await fs.readFile(bundlePath);
86
+ const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
87
+
88
+ // Update metadata with file info
89
+ metadata.checksum = checksum;
90
+ metadata.size = fileBuffer.length;
91
+ metadata.filename = bundleFileName;
92
+
93
+ // Save metadata
94
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
95
+
96
+ console.log(chalk.green('✅ Bundle created successfully!'));
97
+ console.log('');
98
+ console.log(chalk.bold('Bundle Details:'));
99
+ console.log(chalk.gray(` File: ${bundlePath}`));
100
+ console.log(chalk.gray(` Size: ${(fileBuffer.length / 1024 / 1024).toFixed(2)} MB`));
101
+ console.log(chalk.gray(` Checksum: ${checksum}`));
102
+ console.log(chalk.gray(` Metadata: ${metadataPath}`));
103
+ console.log('');
104
+ console.log(chalk.yellow('Next steps:'));
105
+ console.log(chalk.gray(' 1. Sign the bundle:'));
106
+ console.log(chalk.cyan(` npx native-update bundle sign ${bundlePath} --key ./keys/private.pem`));
107
+ console.log(chalk.gray(' 2. Upload to your update server'));
108
+
109
+ } catch (error) {
110
+ console.error(chalk.red('❌ Failed to create bundle:'), error.message);
111
+ process.exit(1);
112
+ }
113
+ }
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+
6
+ export async function signBundle(bundlePath, options) {
7
+ console.log(chalk.blue('🔏 Signing bundle...'));
8
+
9
+ try {
10
+ // Validate bundle exists
11
+ await fs.access(bundlePath);
12
+
13
+ // Read private key
14
+ const privateKeyPem = await fs.readFile(options.key, 'utf-8');
15
+
16
+ // Read bundle
17
+ const bundleData = await fs.readFile(bundlePath);
18
+
19
+ // Create signature
20
+ const sign = crypto.createSign('RSA-SHA256');
21
+ sign.update(bundleData);
22
+ const signature = sign.sign(privateKeyPem, 'base64');
23
+
24
+ // Determine output path
25
+ const outputPath = options.output || bundlePath.replace('.zip', '.signed.zip');
26
+
27
+ // Create signed bundle metadata
28
+ const signedMetadata = {
29
+ originalBundle: path.basename(bundlePath),
30
+ signature,
31
+ signedAt: new Date().toISOString(),
32
+ algorithm: 'RSA-SHA256'
33
+ };
34
+
35
+ // Save signature file
36
+ const sigPath = outputPath.replace('.zip', '.sig');
37
+ await fs.writeFile(sigPath, JSON.stringify(signedMetadata, null, 2));
38
+
39
+ // Copy bundle to output path if different
40
+ if (outputPath !== bundlePath) {
41
+ await fs.copyFile(bundlePath, outputPath);
42
+ }
43
+
44
+ console.log(chalk.green('✅ Bundle signed successfully!'));
45
+ console.log('');
46
+ console.log(chalk.bold('Signed Bundle:'));
47
+ console.log(chalk.gray(` Bundle: ${outputPath}`));
48
+ console.log(chalk.gray(` Signature: ${sigPath}`));
49
+ console.log('');
50
+ console.log(chalk.yellow('Next steps:'));
51
+ console.log(chalk.gray(' 1. Upload both files to your update server'));
52
+ console.log(chalk.gray(' 2. Update your server to serve the signature'));
53
+
54
+ } catch (error) {
55
+ console.error(chalk.red('❌ Failed to sign bundle:'), error.message);
56
+ process.exit(1);
57
+ }
58
+ }
@@ -0,0 +1,55 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+
6
+ export async function verifyBundle(bundlePath, options) {
7
+ console.log(chalk.blue('🔍 Verifying bundle signature...'));
8
+
9
+ try {
10
+ // Check if bundle exists
11
+ await fs.access(bundlePath);
12
+
13
+ // Look for signature file
14
+ const sigPath = bundlePath.replace('.zip', '.sig');
15
+ let signatureData;
16
+
17
+ try {
18
+ const sigContent = await fs.readFile(sigPath, 'utf-8');
19
+ signatureData = JSON.parse(sigContent);
20
+ } catch {
21
+ throw new Error(`No signature file found at ${sigPath}`);
22
+ }
23
+
24
+ // Read public key
25
+ const publicKeyPem = await fs.readFile(options.key, 'utf-8');
26
+
27
+ // Read bundle
28
+ const bundleData = await fs.readFile(bundlePath);
29
+
30
+ // Verify signature
31
+ const verify = crypto.createVerify('RSA-SHA256');
32
+ verify.update(bundleData);
33
+ const isValid = verify.verify(publicKeyPem, signatureData.signature, 'base64');
34
+
35
+ if (isValid) {
36
+ console.log(chalk.green('✅ Bundle signature is VALID'));
37
+ console.log('');
38
+ console.log(chalk.bold('Bundle Details:'));
39
+ console.log(chalk.gray(` Signed at: ${signatureData.signedAt}`));
40
+ console.log(chalk.gray(` Algorithm: ${signatureData.algorithm}`));
41
+ console.log('');
42
+ console.log(chalk.green('This bundle can be trusted and deployed safely.'));
43
+ } else {
44
+ console.log(chalk.red('❌ Bundle signature is INVALID'));
45
+ console.log('');
46
+ console.log(chalk.red('WARNING: This bundle may have been tampered with!'));
47
+ console.log(chalk.red('Do not deploy this bundle.'));
48
+ process.exit(1);
49
+ }
50
+
51
+ } catch (error) {
52
+ console.error(chalk.red('❌ Failed to verify bundle:'), error.message);
53
+ process.exit(1);
54
+ }
55
+ }
@@ -0,0 +1,146 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import prompts from 'prompts';
5
+
6
+ export async function init(options) {
7
+ console.log(chalk.blue('🚀 Initializing Native Update in your project...'));
8
+
9
+ try {
10
+ // Check if already initialized
11
+ try {
12
+ await fs.access('./native-update.config.js');
13
+ const { overwrite } = await prompts({
14
+ type: 'confirm',
15
+ name: 'overwrite',
16
+ message: 'Config file already exists. Overwrite?',
17
+ initial: false
18
+ });
19
+
20
+ if (!overwrite) {
21
+ console.log(chalk.yellow('Initialization cancelled.'));
22
+ return;
23
+ }
24
+ } catch {
25
+ // File doesn't exist, good
26
+ }
27
+
28
+ // Prompt for configuration
29
+ const config = await prompts([
30
+ {
31
+ type: 'text',
32
+ name: 'appId',
33
+ message: 'App ID (e.g., com.example.app):',
34
+ validate: value => value.length > 0
35
+ },
36
+ {
37
+ type: 'text',
38
+ name: 'serverUrl',
39
+ message: 'Update server URL:',
40
+ initial: 'https://your-update-server.com'
41
+ },
42
+ {
43
+ type: 'select',
44
+ name: 'channel',
45
+ message: 'Default update channel:',
46
+ choices: [
47
+ { title: 'Production', value: 'production' },
48
+ { title: 'Staging', value: 'staging' },
49
+ { title: 'Development', value: 'development' }
50
+ ]
51
+ },
52
+ {
53
+ type: 'confirm',
54
+ name: 'autoUpdate',
55
+ message: 'Enable automatic updates?',
56
+ initial: true
57
+ }
58
+ ]);
59
+
60
+ // Create config file
61
+ const configContent = `export default {
62
+ appId: '${config.appId}',
63
+ serverUrl: '${config.serverUrl}',
64
+ channel: '${config.channel}',
65
+ autoUpdate: ${config.autoUpdate},
66
+
67
+ // Security
68
+ publicKey: \`-----BEGIN PUBLIC KEY-----
69
+ YOUR_PUBLIC_KEY_HERE
70
+ -----END PUBLIC KEY-----\`,
71
+
72
+ // Update behavior
73
+ updateStrategy: 'immediate', // immediate, on-app-start, on-app-resume
74
+ checkInterval: 60 * 60 * 1000, // 1 hour
75
+
76
+ // Optional callbacks
77
+ onUpdateAvailable: (update) => {
78
+ console.log('Update available:', update.version);
79
+ },
80
+
81
+ onUpdateDownloaded: (update) => {
82
+ console.log('Update downloaded:', update.version);
83
+ },
84
+
85
+ onUpdateFailed: (error) => {
86
+ console.error('Update failed:', error);
87
+ }
88
+ };
89
+ `;
90
+
91
+ await fs.writeFile('./native-update.config.js', configContent);
92
+
93
+ // Create example integration if requested
94
+ if (options.example) {
95
+ const exampleCode = `import { NativeUpdate } from 'native-update';
96
+ import config from './native-update.config.js';
97
+
98
+ // Initialize on app start
99
+ export async function initializeUpdates() {
100
+ try {
101
+ // Configure the plugin
102
+ await NativeUpdate.configure(config);
103
+
104
+ // Check for updates
105
+ const update = await NativeUpdate.checkForUpdate();
106
+
107
+ if (update.available) {
108
+ console.log(\`Update available: \${update.version}\`);
109
+
110
+ // Download in background
111
+ await NativeUpdate.downloadUpdate();
112
+
113
+ // Apply on next restart
114
+ await NativeUpdate.installOnNextRestart();
115
+ }
116
+ } catch (error) {
117
+ console.error('Update check failed:', error);
118
+ }
119
+ }
120
+
121
+ // Call this in your app initialization
122
+ initializeUpdates();
123
+ `;
124
+
125
+ await fs.writeFile('./native-update-example.js', exampleCode);
126
+ console.log(chalk.gray(' Created: native-update-example.js'));
127
+ }
128
+
129
+ console.log(chalk.green('✅ Native Update initialized successfully!'));
130
+ console.log('');
131
+ console.log(chalk.bold('Next steps:'));
132
+ console.log(chalk.gray(' 1. Generate signing keys:'));
133
+ console.log(chalk.cyan(' npx native-update keys generate'));
134
+ console.log(chalk.gray(' 2. Add public key to native-update.config.js'));
135
+ console.log(chalk.gray(' 3. Import and use the config in your app'));
136
+
137
+ if (options.backend !== 'custom') {
138
+ console.log(chalk.gray(` 4. Create ${options.backend} backend:`));
139
+ console.log(chalk.cyan(` npx native-update backend create ${options.backend}`));
140
+ }
141
+
142
+ } catch (error) {
143
+ console.error(chalk.red('❌ Initialization failed:'), error.message);
144
+ process.exit(1);
145
+ }
146
+ }
@@ -0,0 +1,92 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { generateKeyPairSync } from 'crypto';
6
+
7
+ export async function generateKeys(options) {
8
+ console.log(chalk.blue('🔑 Generating key pair...'));
9
+
10
+ try {
11
+ // Create output directory
12
+ const outputDir = path.resolve(options.output);
13
+ await fs.mkdir(outputDir, { recursive: true });
14
+
15
+ let publicKey, privateKey;
16
+
17
+ if (options.type === 'rsa') {
18
+ // Generate RSA key pair
19
+ const keySize = parseInt(options.size);
20
+ if (![2048, 4096].includes(keySize)) {
21
+ throw new Error('RSA key size must be 2048 or 4096');
22
+ }
23
+
24
+ ({ publicKey, privateKey } = generateKeyPairSync('rsa', {
25
+ modulusLength: keySize,
26
+ publicKeyEncoding: {
27
+ type: 'spki',
28
+ format: 'pem'
29
+ },
30
+ privateKeyEncoding: {
31
+ type: 'pkcs8',
32
+ format: 'pem'
33
+ }
34
+ }));
35
+ } else if (options.type === 'ec') {
36
+ // Generate EC key pair
37
+ const curveMap = {
38
+ '256': 'prime256v1',
39
+ '384': 'secp384r1'
40
+ };
41
+ const namedCurve = curveMap[options.size];
42
+ if (!namedCurve) {
43
+ throw new Error('EC key size must be 256 or 384');
44
+ }
45
+
46
+ ({ publicKey, privateKey } = generateKeyPairSync('ec', {
47
+ namedCurve,
48
+ publicKeyEncoding: {
49
+ type: 'spki',
50
+ format: 'pem'
51
+ },
52
+ privateKeyEncoding: {
53
+ type: 'pkcs8',
54
+ format: 'pem'
55
+ }
56
+ }));
57
+ } else {
58
+ throw new Error('Key type must be "rsa" or "ec"');
59
+ }
60
+
61
+ // Save keys
62
+ const timestamp = Date.now();
63
+ const privateKeyPath = path.join(outputDir, `private-${timestamp}.pem`);
64
+ const publicKeyPath = path.join(outputDir, `public-${timestamp}.pem`);
65
+
66
+ await fs.writeFile(privateKeyPath, privateKey);
67
+ await fs.writeFile(publicKeyPath, publicKey);
68
+
69
+ // Set proper permissions on private key
70
+ await fs.chmod(privateKeyPath, 0o600);
71
+
72
+ console.log(chalk.green('✅ Key pair generated successfully!'));
73
+ console.log('');
74
+ console.log(chalk.bold('Key Files:'));
75
+ console.log(chalk.gray(` Private Key: ${privateKeyPath}`));
76
+ console.log(chalk.gray(` Public Key: ${publicKeyPath}`));
77
+ console.log('');
78
+ console.log(chalk.yellow('⚠️ Security Notes:'));
79
+ console.log(chalk.red(' • Keep your private key secure and never commit it to version control'));
80
+ console.log(chalk.gray(' • Add the public key to your app configuration'));
81
+ console.log(chalk.gray(' • Use the private key for signing bundles on your CI/CD server'));
82
+ console.log('');
83
+ console.log(chalk.yellow('Next steps:'));
84
+ console.log(chalk.gray(' 1. Add to .gitignore:'));
85
+ console.log(chalk.cyan(` echo "${path.basename(privateKeyPath)}" >> .gitignore`));
86
+ console.log(chalk.gray(' 2. Configure your app with the public key'));
87
+
88
+ } catch (error) {
89
+ console.error(chalk.red('❌ Failed to generate keys:'), error.message);
90
+ process.exit(1);
91
+ }
92
+ }
@@ -0,0 +1,68 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+
4
+ export async function monitor(options) {
5
+ if (!options.server) {
6
+ console.error(chalk.red('Error: --server URL is required'));
7
+ process.exit(1);
8
+ }
9
+
10
+ console.log(chalk.blue(`📊 Monitoring updates from ${options.server}...`));
11
+ console.log(chalk.gray('Press Ctrl+C to stop'));
12
+ console.log('');
13
+
14
+ const spinner = ora('Fetching update statistics...').start();
15
+
16
+ try {
17
+ // Poll server for stats
18
+ setInterval(async () => {
19
+ try {
20
+ const headers = options.key ? { 'Authorization': `Bearer ${options.key}` } : {};
21
+
22
+ const response = await fetch(`${options.server}/api/stats`, { headers });
23
+
24
+ if (!response.ok) {
25
+ throw new Error(`Server returned ${response.status}`);
26
+ }
27
+
28
+ const stats = await response.json();
29
+
30
+ spinner.stop();
31
+ console.clear();
32
+ console.log(chalk.blue(`📊 Update Monitor - ${new Date().toLocaleTimeString()}`));
33
+ console.log(chalk.gray('─'.repeat(50)));
34
+ console.log('');
35
+
36
+ console.log(chalk.bold('Current Version:'));
37
+ console.log(chalk.gray(` Latest: ${stats.latestVersion || 'N/A'}`));
38
+ console.log(chalk.gray(` Channel: ${stats.channel || 'production'}`));
39
+ console.log('');
40
+
41
+ console.log(chalk.bold('Download Statistics:'));
42
+ console.log(chalk.gray(` Total Downloads: ${stats.totalDownloads || 0}`));
43
+ console.log(chalk.gray(` Downloads Today: ${stats.downloadsToday || 0}`));
44
+ console.log(chalk.gray(` Active Installs: ${stats.activeInstalls || 0}`));
45
+ console.log('');
46
+
47
+ if (stats.recentActivity) {
48
+ console.log(chalk.bold('Recent Activity:'));
49
+ stats.recentActivity.forEach(activity => {
50
+ console.log(chalk.gray(` ${activity.time} - ${activity.action} (${activity.version})`));
51
+ });
52
+ }
53
+
54
+ console.log('');
55
+ console.log(chalk.gray('Press Ctrl+C to stop'));
56
+
57
+ spinner.start('Updating...');
58
+ } catch (error) {
59
+ spinner.fail(`Failed to fetch stats: ${error.message}`);
60
+ spinner.start('Retrying...');
61
+ }
62
+ }, 5000); // Update every 5 seconds
63
+
64
+ } catch (error) {
65
+ spinner.fail(`Monitor failed: ${error.message}`);
66
+ process.exit(1);
67
+ }
68
+ }
@@ -0,0 +1,96 @@
1
+ import chalk from 'chalk';
2
+ import express from 'express';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname } from 'path';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ export async function startServer(options) {
12
+ console.log(chalk.blue('🚀 Starting development update server...'));
13
+
14
+ const app = express();
15
+ const port = parseInt(options.port);
16
+ const bundleDir = path.resolve(options.dir);
17
+
18
+ // Enable CORS if requested
19
+ if (options.cors) {
20
+ app.use((req, res, next) => {
21
+ res.header('Access-Control-Allow-Origin', '*');
22
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
23
+ next();
24
+ });
25
+ }
26
+
27
+ // Serve static files
28
+ app.use('/bundles', express.static(bundleDir));
29
+
30
+ // API endpoint to get latest bundle info
31
+ app.get('/api/latest', async (req, res) => {
32
+ try {
33
+ const { channel = 'production' } = req.query;
34
+
35
+ // Find all metadata files
36
+ const files = await fs.readdir(bundleDir);
37
+ const metadataFiles = files.filter(f => f.endsWith('.json') && f.startsWith('bundle-'));
38
+
39
+ // Read and parse all metadata
40
+ const bundles = await Promise.all(
41
+ metadataFiles.map(async (file) => {
42
+ const content = await fs.readFile(path.join(bundleDir, file), 'utf-8');
43
+ return JSON.parse(content);
44
+ })
45
+ );
46
+
47
+ // Filter by channel and sort by version
48
+ const channelBundles = bundles.filter(b => b.channel === channel);
49
+ const latest = channelBundles.sort((a, b) =>
50
+ b.created.localeCompare(a.created)
51
+ )[0];
52
+
53
+ if (!latest) {
54
+ return res.status(404).json({ error: 'No bundles found' });
55
+ }
56
+
57
+ // Add download URL
58
+ latest.downloadUrl = `http://localhost:${port}/bundles/${latest.filename}`;
59
+
60
+ res.json(latest);
61
+ } catch (error) {
62
+ res.status(500).json({ error: error.message });
63
+ }
64
+ });
65
+
66
+ // Health check endpoint
67
+ app.get('/health', (req, res) => {
68
+ res.json({ status: 'ok', server: 'native-update-dev' });
69
+ });
70
+
71
+ // List all bundles
72
+ app.get('/api/bundles', async (req, res) => {
73
+ try {
74
+ const files = await fs.readdir(bundleDir);
75
+ const bundles = files.filter(f => f.endsWith('.zip'));
76
+ res.json({ bundles });
77
+ } catch (error) {
78
+ res.status(500).json({ error: error.message });
79
+ }
80
+ });
81
+
82
+ app.listen(port, () => {
83
+ console.log(chalk.green(`✅ Server running at http://localhost:${port}`));
84
+ console.log('');
85
+ console.log(chalk.bold('Endpoints:'));
86
+ console.log(chalk.gray(` GET /api/latest?channel=production - Get latest bundle`));
87
+ console.log(chalk.gray(` GET /api/bundles - List all bundles`));
88
+ console.log(chalk.gray(` GET /bundles/<filename> - Download bundle`));
89
+ console.log(chalk.gray(` GET /health - Health check`));
90
+ console.log('');
91
+ console.log(chalk.yellow('Configure your app to use this server:'));
92
+ console.log(chalk.cyan(` serverUrl: 'http://localhost:${port}'`));
93
+ console.log('');
94
+ console.log(chalk.gray('Press Ctrl+C to stop'));
95
+ });
96
+ }