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.
- package/Readme.md +35 -17
- 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
package/Readme.md
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
- **[App Update API](./docs/api/app-update-api.md)** - Native app update methods
|
|
42
42
|
- **[App Review API](./docs/api/app-review-api.md)** - Review request methods
|
|
43
43
|
- **[Events API](./docs/api/events-api.md)** - Event listeners and handlers
|
|
44
|
+
- **[CLI Reference](./docs/cli-reference.md)** - Command-line tools documentation
|
|
44
45
|
|
|
45
46
|
### Examples
|
|
46
47
|
|
|
@@ -316,28 +317,45 @@ The **[example-app](./example-app)** directory contains a complete, production-r
|
|
|
316
317
|
|
|
317
318
|
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details.
|
|
318
319
|
|
|
319
|
-
## 🛠️
|
|
320
|
+
## 🛠️ CLI Tools & Utilities
|
|
320
321
|
|
|
321
|
-
###
|
|
322
|
+
### Zero-Install CLI Access
|
|
322
323
|
|
|
323
|
-
|
|
324
|
-
- Vitest test setup with example tests
|
|
325
|
-
- Run tests: `npm test`
|
|
326
|
-
- Coverage: `npm run test:coverage`
|
|
324
|
+
All tools are available via `npx` without cloning the repo:
|
|
327
325
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
-
|
|
326
|
+
```bash
|
|
327
|
+
# Quick start
|
|
328
|
+
npx native-update init --example
|
|
329
|
+
npx native-update backend create express --with-admin
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Available Commands
|
|
333
|
+
|
|
334
|
+
✅ **Bundle Management**
|
|
335
|
+
- Create bundles: `npx native-update bundle create ./www`
|
|
336
|
+
- Sign bundles: `npx native-update bundle sign bundle.zip --key private.pem`
|
|
337
|
+
- Verify signatures: `npx native-update bundle verify bundle.zip --key public.pem`
|
|
338
|
+
|
|
339
|
+
✅ **Key Management**
|
|
340
|
+
- Generate keys: `npx native-update keys generate --type rsa --size 4096`
|
|
341
|
+
- Supports RSA (2048/4096) and EC (256/384) keys
|
|
342
|
+
- Creates timestamped key pairs with proper permissions
|
|
343
|
+
- See [Key Management Guide](./docs/guides/key-management.md) for detailed instructions
|
|
344
|
+
|
|
345
|
+
✅ **Backend Templates**
|
|
346
|
+
- Express.js: `npx native-update backend create express --with-admin`
|
|
347
|
+
- Firebase: `npx native-update backend create firebase --with-monitoring`
|
|
348
|
+
- Vercel: `npx native-update backend create vercel`
|
|
349
|
+
|
|
350
|
+
✅ **Development Tools**
|
|
351
|
+
- Start dev server: `npx native-update server start --port 3000`
|
|
352
|
+
- Monitor updates: `npx native-update monitor --server https://your-server.com`
|
|
353
|
+
- Validate config: `npx native-update config check`
|
|
331
354
|
|
|
332
|
-
✅ **
|
|
333
|
-
-
|
|
334
|
-
- Sign bundles: `node tools/bundle-signer.js sign bundle.zip private-key.pem`
|
|
335
|
-
- Verify: `node tools/bundle-signer.js verify bundle.zip bundle.zip.sig public-key.pem`
|
|
355
|
+
✅ **Migration Tools**
|
|
356
|
+
- From CodePush: `npx native-update migrate --from codepush`
|
|
336
357
|
|
|
337
|
-
|
|
338
|
-
- Development server in `backend-template/`
|
|
339
|
-
- Start: `cd backend-template && npm install && npm start`
|
|
340
|
-
- Provides basic update API endpoints
|
|
358
|
+
See [CLI Reference](./docs/cli-reference.md) for complete documentation.
|
|
341
359
|
|
|
342
360
|
## 🏗️ Development Status
|
|
343
361
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
const packageJson = JSON.parse(
|
|
9
|
+
readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('cap-update')
|
|
14
|
+
.description('CLI for Capacitor Native Update')
|
|
15
|
+
.version('1.0.0');
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command('init')
|
|
19
|
+
.description('Initialize update configuration')
|
|
20
|
+
.option('-s, --server <url>', 'Update server URL')
|
|
21
|
+
.action((options) => {
|
|
22
|
+
console.log('Initializing Capacitor Native Update...');
|
|
23
|
+
console.log('Server:', options.server || 'Not specified');
|
|
24
|
+
// TODO: Create config file
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('bundle')
|
|
29
|
+
.description('Create update bundle')
|
|
30
|
+
.argument('<path>', 'Path to dist directory')
|
|
31
|
+
.action((path) => {
|
|
32
|
+
console.log(`Creating bundle from: ${path}`);
|
|
33
|
+
// TODO: Call bundle creator
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('sign')
|
|
38
|
+
.description('Sign update bundle')
|
|
39
|
+
.argument('<bundle>', 'Bundle file path')
|
|
40
|
+
.action((bundle) => {
|
|
41
|
+
console.log(`Signing bundle: ${bundle}`);
|
|
42
|
+
// TODO: Call bundle signer
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program.parse();
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
export async function createBackend(type, options) {
|
|
12
|
+
const spinner = ora(`Creating ${type} backend template...`).start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const outputDir = path.resolve(options.output);
|
|
16
|
+
|
|
17
|
+
// Check if directory exists
|
|
18
|
+
try {
|
|
19
|
+
await fs.access(outputDir);
|
|
20
|
+
spinner.fail(`Directory ${outputDir} already exists`);
|
|
21
|
+
return;
|
|
22
|
+
} catch {
|
|
23
|
+
// Directory doesn't exist, good
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Create output directory
|
|
27
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
28
|
+
|
|
29
|
+
switch (type) {
|
|
30
|
+
case 'express':
|
|
31
|
+
await createExpressBackend(outputDir, options);
|
|
32
|
+
break;
|
|
33
|
+
case 'firebase':
|
|
34
|
+
await createFirebaseBackend(outputDir, options);
|
|
35
|
+
break;
|
|
36
|
+
case 'vercel':
|
|
37
|
+
await createVercelBackend(outputDir, options);
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(`Unknown backend type: ${type}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
spinner.succeed(`${type} backend created successfully!`);
|
|
44
|
+
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(chalk.bold('Next steps:'));
|
|
47
|
+
console.log(chalk.gray(` 1. cd ${options.output}`));
|
|
48
|
+
console.log(chalk.gray(` 2. npm install`));
|
|
49
|
+
console.log(chalk.gray(` 3. Configure your environment variables`));
|
|
50
|
+
console.log(chalk.gray(` 4. npm run dev`));
|
|
51
|
+
|
|
52
|
+
} catch (error) {
|
|
53
|
+
spinner.fail(`Failed to create backend: ${error.message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function createExpressBackend(outputDir, options) {
|
|
59
|
+
// Create package.json
|
|
60
|
+
const packageJson = {
|
|
61
|
+
name: "native-update-backend",
|
|
62
|
+
version: "1.0.0",
|
|
63
|
+
description: "Native Update backend server",
|
|
64
|
+
type: "module",
|
|
65
|
+
scripts: {
|
|
66
|
+
dev: "node --watch server.js",
|
|
67
|
+
start: "node server.js",
|
|
68
|
+
test: "vitest"
|
|
69
|
+
},
|
|
70
|
+
dependencies: {
|
|
71
|
+
express: "^5.1.0",
|
|
72
|
+
cors: "^2.8.5",
|
|
73
|
+
"express-rate-limit": "^7.4.1",
|
|
74
|
+
multer: "^1.4.5-lts.1",
|
|
75
|
+
dotenv: "^16.4.7",
|
|
76
|
+
jsonwebtoken: "^9.0.2",
|
|
77
|
+
bcrypt: "^5.1.1"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (options.withMonitoring) {
|
|
82
|
+
packageJson.dependencies["@opentelemetry/api"] = "^1.9.0";
|
|
83
|
+
packageJson.dependencies["@opentelemetry/sdk-node"] = "^0.57.0";
|
|
84
|
+
packageJson.dependencies["winston"] = "^3.17.1";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await fs.writeFile(
|
|
88
|
+
path.join(outputDir, 'package.json'),
|
|
89
|
+
JSON.stringify(packageJson, null, 2)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Create server.js
|
|
93
|
+
const serverCode = `import express from 'express';
|
|
94
|
+
import cors from 'cors';
|
|
95
|
+
import dotenv from 'dotenv';
|
|
96
|
+
import { router as bundleRouter } from './routes/bundles.js';
|
|
97
|
+
import { router as authRouter } from './routes/auth.js';
|
|
98
|
+
${options.withMonitoring ? "import { initMonitoring } from './monitoring/index.js';" : ''}
|
|
99
|
+
|
|
100
|
+
dotenv.config();
|
|
101
|
+
|
|
102
|
+
const app = express();
|
|
103
|
+
const PORT = process.env.PORT || 3000;
|
|
104
|
+
|
|
105
|
+
${options.withMonitoring ? 'initMonitoring(app);' : ''}
|
|
106
|
+
|
|
107
|
+
app.use(cors());
|
|
108
|
+
app.use(express.json());
|
|
109
|
+
|
|
110
|
+
// Routes
|
|
111
|
+
app.use('/api/bundles', bundleRouter);
|
|
112
|
+
app.use('/api/auth', authRouter);
|
|
113
|
+
|
|
114
|
+
// Health check
|
|
115
|
+
app.get('/health', (req, res) => {
|
|
116
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
app.listen(PORT, () => {
|
|
120
|
+
console.log(\`Server running on port \${PORT}\`);
|
|
121
|
+
});
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
await fs.writeFile(path.join(outputDir, 'server.js'), serverCode);
|
|
125
|
+
|
|
126
|
+
// Create routes directory
|
|
127
|
+
await fs.mkdir(path.join(outputDir, 'routes'), { recursive: true });
|
|
128
|
+
|
|
129
|
+
// Create bundles route
|
|
130
|
+
const bundlesRoute = `import express from 'express';
|
|
131
|
+
import multer from 'multer';
|
|
132
|
+
import path from 'path';
|
|
133
|
+
import fs from 'fs/promises';
|
|
134
|
+
import crypto from 'crypto';
|
|
135
|
+
|
|
136
|
+
const router = express.Router();
|
|
137
|
+
const upload = multer({ dest: 'uploads/' });
|
|
138
|
+
|
|
139
|
+
// Get latest bundle
|
|
140
|
+
router.get('/latest', async (req, res) => {
|
|
141
|
+
try {
|
|
142
|
+
const { appId, version, channel = 'production' } = req.query;
|
|
143
|
+
|
|
144
|
+
// TODO: Implement bundle lookup logic
|
|
145
|
+
// This is a simplified example
|
|
146
|
+
const bundle = {
|
|
147
|
+
version: '1.0.1',
|
|
148
|
+
url: \`\${req.protocol}://\${req.get('host')}/bundles/latest.zip\`,
|
|
149
|
+
checksum: 'sha256:...',
|
|
150
|
+
signature: 'signature...',
|
|
151
|
+
metadata: {
|
|
152
|
+
releaseNotes: 'Bug fixes and improvements'
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
res.json(bundle);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
res.status(500).json({ error: error.message });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Upload new bundle
|
|
163
|
+
router.post('/upload', upload.single('bundle'), async (req, res) => {
|
|
164
|
+
try {
|
|
165
|
+
const { version, channel, metadata } = req.body;
|
|
166
|
+
const file = req.file;
|
|
167
|
+
|
|
168
|
+
if (!file) {
|
|
169
|
+
return res.status(400).json({ error: 'No bundle file provided' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Calculate checksum
|
|
173
|
+
const fileBuffer = await fs.readFile(file.path);
|
|
174
|
+
const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
|
175
|
+
|
|
176
|
+
// TODO: Store bundle metadata in database
|
|
177
|
+
// TODO: Move file to permanent storage
|
|
178
|
+
|
|
179
|
+
res.json({
|
|
180
|
+
success: true,
|
|
181
|
+
bundle: {
|
|
182
|
+
version,
|
|
183
|
+
channel,
|
|
184
|
+
checksum,
|
|
185
|
+
size: file.size
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
res.status(500).json({ error: error.message });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
export { router };
|
|
194
|
+
`;
|
|
195
|
+
|
|
196
|
+
await fs.writeFile(path.join(outputDir, 'routes', 'bundles.js'), bundlesRoute);
|
|
197
|
+
|
|
198
|
+
// Create .env.example
|
|
199
|
+
const envExample = `# Server Configuration
|
|
200
|
+
PORT=3000
|
|
201
|
+
|
|
202
|
+
# Security
|
|
203
|
+
JWT_SECRET=your-secret-key-here
|
|
204
|
+
API_KEY=your-api-key-here
|
|
205
|
+
|
|
206
|
+
# Storage
|
|
207
|
+
STORAGE_PATH=./storage/bundles
|
|
208
|
+
|
|
209
|
+
# Database (optional)
|
|
210
|
+
# DATABASE_URL=postgresql://user:password@localhost:5432/native_update
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
await fs.writeFile(path.join(outputDir, '.env.example'), envExample);
|
|
214
|
+
|
|
215
|
+
// Create README
|
|
216
|
+
const readme = `# Native Update Backend
|
|
217
|
+
|
|
218
|
+
Express.js backend for Native Update plugin.
|
|
219
|
+
|
|
220
|
+
## Setup
|
|
221
|
+
|
|
222
|
+
1. Copy \`.env.example\` to \`.env\` and configure
|
|
223
|
+
2. Run \`npm install\`
|
|
224
|
+
3. Run \`npm run dev\` for development
|
|
225
|
+
|
|
226
|
+
## API Endpoints
|
|
227
|
+
|
|
228
|
+
- GET /api/bundles/latest - Get latest bundle for app
|
|
229
|
+
- POST /api/bundles/upload - Upload new bundle
|
|
230
|
+
- GET /health - Health check
|
|
231
|
+
|
|
232
|
+
## Security
|
|
233
|
+
|
|
234
|
+
- Uses JWT for authentication
|
|
235
|
+
- Rate limiting enabled
|
|
236
|
+
- CORS configured
|
|
237
|
+
|
|
238
|
+
${options.withMonitoring ? '## Monitoring\n\nOpenTelemetry monitoring is configured. Set up your preferred backend (Jaeger, Zipkin, etc.)' : ''}
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
await fs.writeFile(path.join(outputDir, 'README.md'), readme);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function createVercelBackend(outputDir, options) {
|
|
245
|
+
// Create api directory
|
|
246
|
+
const apiDir = path.join(outputDir, 'api');
|
|
247
|
+
await fs.mkdir(apiDir, { recursive: true });
|
|
248
|
+
|
|
249
|
+
// Create package.json
|
|
250
|
+
const packageJson = {
|
|
251
|
+
name: "native-update-vercel",
|
|
252
|
+
version: "1.0.0",
|
|
253
|
+
description: "Native Update Vercel backend",
|
|
254
|
+
type: "module",
|
|
255
|
+
scripts: {
|
|
256
|
+
dev: "vercel dev",
|
|
257
|
+
deploy: "vercel",
|
|
258
|
+
build: "echo 'No build step required'"
|
|
259
|
+
},
|
|
260
|
+
dependencies: {
|
|
261
|
+
"@vercel/node": "^3.0.0"
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
await fs.writeFile(
|
|
266
|
+
path.join(outputDir, 'package.json'),
|
|
267
|
+
JSON.stringify(packageJson, null, 2)
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Create bundles API endpoint
|
|
271
|
+
const bundlesApi = `export default async function handler(req, res) {
|
|
272
|
+
// Enable CORS
|
|
273
|
+
res.setHeader('Access-Control-Allow-Credentials', true);
|
|
274
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
275
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT');
|
|
276
|
+
res.setHeader(
|
|
277
|
+
'Access-Control-Allow-Headers',
|
|
278
|
+
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (req.method === 'OPTIONS') {
|
|
282
|
+
res.status(200).end();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (req.method === 'GET') {
|
|
287
|
+
// Get latest bundle
|
|
288
|
+
const { appId, version, channel = 'production' } = req.query;
|
|
289
|
+
|
|
290
|
+
// TODO: Implement bundle lookup from your storage solution
|
|
291
|
+
// This is a simplified example
|
|
292
|
+
const bundle = {
|
|
293
|
+
version: '1.0.1',
|
|
294
|
+
url: \`https://\${req.headers.host}/bundles/latest.zip\`,
|
|
295
|
+
checksum: 'sha256:...',
|
|
296
|
+
signature: 'signature...',
|
|
297
|
+
metadata: {
|
|
298
|
+
releaseNotes: 'Bug fixes and improvements'
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return res.status(200).json(bundle);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (req.method === 'POST') {
|
|
306
|
+
// Handle bundle upload
|
|
307
|
+
const { version, channel, metadata } = req.body;
|
|
308
|
+
|
|
309
|
+
// TODO: Handle file upload to storage (Vercel Blob, S3, etc.)
|
|
310
|
+
// TODO: Save metadata to database (Vercel KV, external DB, etc.)
|
|
311
|
+
|
|
312
|
+
return res.status(200).json({
|
|
313
|
+
success: true,
|
|
314
|
+
bundle: {
|
|
315
|
+
version,
|
|
316
|
+
channel,
|
|
317
|
+
message: 'Bundle uploaded successfully'
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
323
|
+
}`;
|
|
324
|
+
|
|
325
|
+
await fs.writeFile(path.join(apiDir, 'bundles.js'), bundlesApi);
|
|
326
|
+
|
|
327
|
+
// Create health check endpoint
|
|
328
|
+
const healthApi = `export default function handler(req, res) {
|
|
329
|
+
res.status(200).json({
|
|
330
|
+
status: 'ok',
|
|
331
|
+
service: 'vercel-edge',
|
|
332
|
+
timestamp: new Date().toISOString()
|
|
333
|
+
});
|
|
334
|
+
}`;
|
|
335
|
+
|
|
336
|
+
await fs.writeFile(path.join(apiDir, 'health.js'), healthApi);
|
|
337
|
+
|
|
338
|
+
// Create vercel.json
|
|
339
|
+
const vercelConfig = {
|
|
340
|
+
functions: {
|
|
341
|
+
"api/*.js": {
|
|
342
|
+
maxDuration: 30
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
rewrites: [
|
|
346
|
+
{
|
|
347
|
+
source: "/api/bundles/latest",
|
|
348
|
+
destination: "/api/bundles"
|
|
349
|
+
}
|
|
350
|
+
]
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
await fs.writeFile(
|
|
354
|
+
path.join(outputDir, 'vercel.json'),
|
|
355
|
+
JSON.stringify(vercelConfig, null, 2)
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// Create .env.example
|
|
359
|
+
const envExample = `# Storage Configuration
|
|
360
|
+
STORAGE_URL=your-storage-url
|
|
361
|
+
STORAGE_KEY=your-storage-key
|
|
362
|
+
|
|
363
|
+
# Database Configuration (optional)
|
|
364
|
+
DATABASE_URL=your-database-url
|
|
365
|
+
|
|
366
|
+
# Security
|
|
367
|
+
API_KEY=your-api-key-here
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
await fs.writeFile(path.join(outputDir, '.env.example'), envExample);
|
|
371
|
+
|
|
372
|
+
// Create README
|
|
373
|
+
const readme = `# Native Update Vercel Backend
|
|
374
|
+
|
|
375
|
+
Serverless backend for Native Update plugin using Vercel Edge Functions.
|
|
376
|
+
|
|
377
|
+
## Setup
|
|
378
|
+
|
|
379
|
+
1. Install Vercel CLI: \`npm i -g vercel\`
|
|
380
|
+
2. Copy \`.env.example\` to \`.env.local\` and configure
|
|
381
|
+
3. Run \`npm install\`
|
|
382
|
+
4. Run \`vercel dev\` for local development
|
|
383
|
+
|
|
384
|
+
## Deployment
|
|
385
|
+
|
|
386
|
+
\`\`\`bash
|
|
387
|
+
vercel
|
|
388
|
+
\`\`\`
|
|
389
|
+
|
|
390
|
+
## API Endpoints
|
|
391
|
+
|
|
392
|
+
- GET /api/bundles - Get latest bundle
|
|
393
|
+
- POST /api/bundles - Upload new bundle
|
|
394
|
+
- GET /api/health - Health check
|
|
395
|
+
|
|
396
|
+
## Storage Options
|
|
397
|
+
|
|
398
|
+
- Vercel Blob Storage
|
|
399
|
+
- AWS S3
|
|
400
|
+
- Cloudflare R2
|
|
401
|
+
- Any S3-compatible storage
|
|
402
|
+
|
|
403
|
+
## Database Options
|
|
404
|
+
|
|
405
|
+
- Vercel KV
|
|
406
|
+
- Vercel Postgres
|
|
407
|
+
- PlanetScale
|
|
408
|
+
- Supabase
|
|
409
|
+
|
|
410
|
+
${options.withMonitoring ? '## Monitoring\\n\\nUse Vercel Analytics and Logs for monitoring.' : ''}
|
|
411
|
+
`;
|
|
412
|
+
|
|
413
|
+
await fs.writeFile(path.join(outputDir, 'README.md'), readme);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function createFirebaseBackend(outputDir, options) {
|
|
417
|
+
// Create functions directory
|
|
418
|
+
const functionsDir = path.join(outputDir, 'functions');
|
|
419
|
+
await fs.mkdir(functionsDir, { recursive: true });
|
|
420
|
+
|
|
421
|
+
// Create package.json for functions
|
|
422
|
+
const packageJson = {
|
|
423
|
+
name: "native-update-firebase-functions",
|
|
424
|
+
description: "Firebase Functions backend for Native Update",
|
|
425
|
+
type: "module",
|
|
426
|
+
engines: {
|
|
427
|
+
node: "22"
|
|
428
|
+
},
|
|
429
|
+
main: "index.js",
|
|
430
|
+
scripts: {
|
|
431
|
+
serve: "firebase emulators:start --only functions",
|
|
432
|
+
shell: "firebase functions:shell",
|
|
433
|
+
start: "npm run serve",
|
|
434
|
+
deploy: "firebase deploy --only functions",
|
|
435
|
+
logs: "firebase functions:log"
|
|
436
|
+
},
|
|
437
|
+
dependencies: {
|
|
438
|
+
"firebase-admin": "^12.0.0",
|
|
439
|
+
"firebase-functions": "^5.0.0",
|
|
440
|
+
cors: "^2.8.5",
|
|
441
|
+
"express": "^5.1.0"
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
await fs.writeFile(
|
|
446
|
+
path.join(functionsDir, 'package.json'),
|
|
447
|
+
JSON.stringify(packageJson, null, 2)
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// Create index.js
|
|
451
|
+
const indexJs = `import { onRequest } from 'firebase-functions/v2/https';
|
|
452
|
+
import * as admin from 'firebase-admin';
|
|
453
|
+
import express from 'express';
|
|
454
|
+
import cors from 'cors';
|
|
455
|
+
|
|
456
|
+
admin.initializeApp();
|
|
457
|
+
|
|
458
|
+
const app = express();
|
|
459
|
+
app.use(cors({ origin: true }));
|
|
460
|
+
app.use(express.json());
|
|
461
|
+
|
|
462
|
+
// Get latest bundle
|
|
463
|
+
app.get('/api/bundles/latest', async (req, res) => {
|
|
464
|
+
try {
|
|
465
|
+
const { appId, version, channel = 'production' } = req.query;
|
|
466
|
+
|
|
467
|
+
// Query Firestore for latest bundle
|
|
468
|
+
const db = admin.firestore();
|
|
469
|
+
const snapshot = await db.collection('bundles')
|
|
470
|
+
.where('appId', '==', appId)
|
|
471
|
+
.where('channel', '==', channel)
|
|
472
|
+
.where('enabled', '==', true)
|
|
473
|
+
.orderBy('createdAt', 'desc')
|
|
474
|
+
.limit(1)
|
|
475
|
+
.get();
|
|
476
|
+
|
|
477
|
+
if (snapshot.empty) {
|
|
478
|
+
return res.status(404).json({ error: 'No bundle found' });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const bundle = snapshot.docs[0].data();
|
|
482
|
+
res.json(bundle);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
res.status(500).json({ error: error.message });
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Upload bundle
|
|
489
|
+
app.post('/api/bundles/upload', async (req, res) => {
|
|
490
|
+
try {
|
|
491
|
+
const { version, channel, metadata } = req.body;
|
|
492
|
+
|
|
493
|
+
// TODO: Handle file upload to Cloud Storage
|
|
494
|
+
// TODO: Save metadata to Firestore
|
|
495
|
+
|
|
496
|
+
res.json({ success: true, version });
|
|
497
|
+
} catch (error) {
|
|
498
|
+
res.status(500).json({ error: error.message });
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Health check
|
|
503
|
+
app.get('/health', (req, res) => {
|
|
504
|
+
res.json({ status: 'ok', service: 'firebase-functions' });
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
export const api = onRequest(app);
|
|
508
|
+
`;
|
|
509
|
+
|
|
510
|
+
await fs.writeFile(path.join(functionsDir, 'index.js'), indexJs);
|
|
511
|
+
|
|
512
|
+
// Create firebase.json
|
|
513
|
+
const firebaseConfig = {
|
|
514
|
+
functions: {
|
|
515
|
+
source: "functions",
|
|
516
|
+
runtime: "nodejs22"
|
|
517
|
+
},
|
|
518
|
+
firestore: {
|
|
519
|
+
rules: "firestore.rules",
|
|
520
|
+
indexes: "firestore.indexes.json"
|
|
521
|
+
},
|
|
522
|
+
storage: {
|
|
523
|
+
rules: "storage.rules"
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
await fs.writeFile(
|
|
528
|
+
path.join(outputDir, 'firebase.json'),
|
|
529
|
+
JSON.stringify(firebaseConfig, null, 2)
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
// Create Firestore rules
|
|
533
|
+
const firestoreRules = `rules_version = '2';
|
|
534
|
+
service cloud.firestore {
|
|
535
|
+
match /databases/{database}/documents {
|
|
536
|
+
// Bundles are read-only for clients
|
|
537
|
+
match /bundles/{document=**} {
|
|
538
|
+
allow read: if true;
|
|
539
|
+
allow write: if false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Analytics can be written by clients
|
|
543
|
+
match /analytics/{document=**} {
|
|
544
|
+
allow create: if true;
|
|
545
|
+
allow read, update, delete: if false;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}`;
|
|
549
|
+
|
|
550
|
+
await fs.writeFile(path.join(outputDir, 'firestore.rules'), firestoreRules);
|
|
551
|
+
|
|
552
|
+
// Create README
|
|
553
|
+
const readme = `# Native Update Firebase Backend
|
|
554
|
+
|
|
555
|
+
Firebase Functions backend for Native Update plugin.
|
|
556
|
+
|
|
557
|
+
## Setup
|
|
558
|
+
|
|
559
|
+
1. Install Firebase CLI: \`npm install -g firebase-tools\`
|
|
560
|
+
2. Login: \`firebase login\`
|
|
561
|
+
3. Initialize: \`firebase init\`
|
|
562
|
+
4. Select your project or create new
|
|
563
|
+
5. Install dependencies: \`cd functions && npm install\`
|
|
564
|
+
6. Start emulator: \`npm run serve\`
|
|
565
|
+
|
|
566
|
+
## Deployment
|
|
567
|
+
|
|
568
|
+
\`\`\`bash
|
|
569
|
+
firebase deploy
|
|
570
|
+
\`\`\`
|
|
571
|
+
|
|
572
|
+
## Endpoints
|
|
573
|
+
|
|
574
|
+
- GET /api/bundles/latest - Get latest bundle
|
|
575
|
+
- POST /api/bundles/upload - Upload new bundle
|
|
576
|
+
- GET /health - Health check
|
|
577
|
+
|
|
578
|
+
${options.withMonitoring ? '## Monitoring\n\nUse Firebase Console for monitoring and analytics.' : ''}
|
|
579
|
+
`;
|
|
580
|
+
|
|
581
|
+
await fs.writeFile(path.join(outputDir, 'README.md'), readme);
|
|
582
|
+
}
|