native-update 1.0.9 → 1.1.1
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.
- package/Readme.md +35 -17
- package/android/build.gradle +5 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/AppUpdatePlugin.kt +1 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundNotificationManager.kt +12 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/NotificationActionReceiver.kt +1 -2
- package/cli/cap-update.js +45 -0
- package/cli/commands/backend-create.js +582 -0
- package/cli/commands/bundle-create.js +113 -0
- package/cli/commands/bundle-sign.js +58 -0
- package/cli/commands/bundle-verify.js +55 -0
- package/cli/commands/init.js +146 -0
- package/cli/commands/keys-generate.js +92 -0
- package/cli/commands/monitor.js +68 -0
- package/cli/commands/server-start.js +96 -0
- package/cli/index.js +269 -0
- package/cli/package.json +12 -0
- package/docs/BUNDLE_SIGNING.md +16 -9
- package/docs/LIVE_UPDATES_GUIDE.md +1 -1
- package/docs/README.md +1 -0
- package/docs/cli-reference.md +321 -0
- package/docs/getting-started/configuration.md +3 -2
- package/docs/getting-started/quick-start.md +53 -1
- package/docs/guides/deployment-guide.md +9 -7
- package/docs/guides/key-management.md +284 -0
- package/docs/guides/migration-from-codepush.md +9 -5
- package/docs/guides/testing-guide.md +4 -4
- package/package.json +15 -2
|
@@ -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
|
+
}
|