native-update 1.2.0 → 1.3.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 +36 -22
- package/docs/CHANGELOG.md +168 -0
- package/docs/EXAMPLE_APPS_SIMPLIFICATION_PLAN.md +384 -0
- package/docs/EXAMPLE_APPS_SIMPLIFICATION_TRACKER.md +390 -0
- package/docs/MARKETING_WEBSITE_PLAN.md +659 -0
- package/docs/MARKETING_WEBSITE_TRACKER.md +661 -0
- package/docs/ROADMAP.md +143 -0
- package/docs/SECURITY.md +356 -0
- package/docs/api/API.md +557 -0
- package/docs/api/FEATURES.md +414 -0
- package/docs/guides/key-management.md +1 -1
- package/docs/plans/PLANNING_COMPLETE_SUMMARY.md +361 -0
- package/docs/plans/TASK_1_ANDROID_EXAMPLE_APP.md +401 -0
- package/docs/plans/TASK_2_API_ENDPOINTS.md +856 -0
- package/docs/plans/TASK_2_DASHBOARD_UI_UX.md +820 -0
- package/docs/plans/TASK_2_DATABASE_SCHEMA.md +704 -0
- package/docs/plans/TASK_2_GOOGLE_DRIVE_INTEGRATION.md +646 -0
- package/docs/plans/TASK_2_SAAS_ARCHITECTURE.md +587 -0
- package/docs/plans/TASK_2_USER_AUTHENTICATION.md +600 -0
- package/docs/reports/AUDIT_SUMMARY_2025-12-26.md +203 -0
- package/docs/reports/COMPLETE_VERIFICATION.md +106 -0
- package/docs/reports/EVENT_FLOW_VERIFICATION.md +80 -0
- package/docs/reports/EXAMPLE_APPS_SIMPLIFICATION_COMPLETE.md +369 -0
- package/docs/reports/FINAL_STATUS.md +122 -0
- package/docs/reports/FINAL_VERIFICATION_CHECKLIST.md +425 -0
- package/docs/reports/MARKETING_WEBSITE_COMPLETE.md +466 -0
- package/docs/reports/PACKAGE_COMPLETENESS_REPORT.md +130 -0
- package/docs/reports/PRODUCTION_STATUS.md +115 -0
- package/docs/reports/PROJECT_RESTRUCTURE_2025-12-27.md +287 -0
- package/docs/reports/PROJECT_RESTRUCTURE_FINAL_SUMMARY.md +464 -0
- package/docs/reports/PUBLISHING_VERIFICATION.md +144 -0
- package/docs/reports/RELEASE_READY_SUMMARY.md +99 -0
- package/docs/tracking/IMPLEMENTATION_TRACKER.md +303 -0
- package/package.json +2 -3
- package/backend-template/README.md +0 -56
- package/backend-template/package.json +0 -20
- package/backend-template/server.js +0 -121
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
# Task 2: Database Schema Design
|
|
2
|
+
|
|
3
|
+
**Created:** 2025-12-27
|
|
4
|
+
**Status:** 📝 Planning
|
|
5
|
+
**Database:** Firebase Firestore
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🎯 Objectives
|
|
10
|
+
|
|
11
|
+
Design a complete, scalable Firestore database schema that supports:
|
|
12
|
+
- User management and authentication
|
|
13
|
+
- Multi-app management per user
|
|
14
|
+
- Build versioning and distribution
|
|
15
|
+
- Google Drive token storage (encrypted)
|
|
16
|
+
- Analytics tracking (future)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📊 Collections Overview
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
firestore/
|
|
24
|
+
├── users/ # User profiles
|
|
25
|
+
├── apps/ # User's applications
|
|
26
|
+
├── builds/ # Build versions
|
|
27
|
+
├── drive_tokens/ # Encrypted Drive credentials
|
|
28
|
+
└── analytics/ # Usage analytics (future)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 📁 Collection 1: `users`
|
|
34
|
+
|
|
35
|
+
### Purpose
|
|
36
|
+
Store user profile information and preferences
|
|
37
|
+
|
|
38
|
+
### Document ID
|
|
39
|
+
`{userId}` - Firebase Auth UID
|
|
40
|
+
|
|
41
|
+
### Schema
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
interface UserDocument {
|
|
45
|
+
// Identity
|
|
46
|
+
uid: string; // Firebase Auth UID (redundant but helpful)
|
|
47
|
+
email: string; // User email
|
|
48
|
+
displayName: string | null; // Full name
|
|
49
|
+
photoURL: string | null; // Profile picture URL
|
|
50
|
+
|
|
51
|
+
// Authentication
|
|
52
|
+
provider: 'email' | 'google.com'; // Auth provider
|
|
53
|
+
emailVerified: boolean; // Email verification status
|
|
54
|
+
createdAt: Timestamp; // Account creation
|
|
55
|
+
lastLogin: Timestamp; // Last login time
|
|
56
|
+
|
|
57
|
+
// Google Drive Integration
|
|
58
|
+
driveConnected: boolean; // Drive connected?
|
|
59
|
+
driveEmail: string | null; // Drive account email
|
|
60
|
+
driveConnectedAt: Timestamp | null; // When connected
|
|
61
|
+
|
|
62
|
+
// Subscription (future)
|
|
63
|
+
plan: 'free' | 'pro' | 'enterprise';
|
|
64
|
+
planStartDate: Timestamp | null;
|
|
65
|
+
planEndDate: Timestamp | null;
|
|
66
|
+
|
|
67
|
+
// Usage Stats
|
|
68
|
+
appsCount: number; // Total apps created
|
|
69
|
+
buildsCount: number; // Total builds uploaded
|
|
70
|
+
storageUsed: number; // Bytes used (in Drive)
|
|
71
|
+
|
|
72
|
+
// Preferences
|
|
73
|
+
preferences: {
|
|
74
|
+
emailNotifications: boolean; // Send email notifications
|
|
75
|
+
updateNotifications: boolean; // Notify on updates
|
|
76
|
+
theme: 'light' | 'dark' | 'auto';
|
|
77
|
+
language: 'en' | 'es' | 'fr'; // i18n support
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Metadata
|
|
81
|
+
updatedAt: Timestamp; // Last profile update
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Example Document
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"uid": "abc123xyz789",
|
|
90
|
+
"email": "user@example.com",
|
|
91
|
+
"displayName": "John Doe",
|
|
92
|
+
"photoURL": "https://lh3.googleusercontent.com/...",
|
|
93
|
+
|
|
94
|
+
"provider": "google.com",
|
|
95
|
+
"emailVerified": true,
|
|
96
|
+
"createdAt": {"seconds": 1703721600, "nanoseconds": 0},
|
|
97
|
+
"lastLogin": {"seconds": 1703808000, "nanoseconds": 0},
|
|
98
|
+
|
|
99
|
+
"driveConnected": true,
|
|
100
|
+
"driveEmail": "user@gmail.com",
|
|
101
|
+
"driveConnectedAt": {"seconds": 1703722000, "nanoseconds": 0},
|
|
102
|
+
|
|
103
|
+
"plan": "free",
|
|
104
|
+
"planStartDate": null,
|
|
105
|
+
"planEndDate": null,
|
|
106
|
+
|
|
107
|
+
"appsCount": 2,
|
|
108
|
+
"buildsCount": 5,
|
|
109
|
+
"storageUsed": 157286400,
|
|
110
|
+
|
|
111
|
+
"preferences": {
|
|
112
|
+
"emailNotifications": true,
|
|
113
|
+
"updateNotifications": true,
|
|
114
|
+
"theme": "auto",
|
|
115
|
+
"language": "en"
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
"updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Indexes Required
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
// Composite indexes
|
|
126
|
+
users: {
|
|
127
|
+
fields: ['email', 'createdAt'],
|
|
128
|
+
fields: ['plan', 'createdAt']
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Security Rules
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
match /users/{userId} {
|
|
136
|
+
allow read: if request.auth.uid == userId;
|
|
137
|
+
allow create: if request.auth.uid == userId;
|
|
138
|
+
allow update: if request.auth.uid == userId
|
|
139
|
+
&& !request.resource.data.diff(resource.data).affectedKeys()
|
|
140
|
+
.hasAny(['uid', 'email', 'createdAt', 'provider']);
|
|
141
|
+
allow delete: if request.auth.uid == userId;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 📁 Collection 2: `apps`
|
|
148
|
+
|
|
149
|
+
### Purpose
|
|
150
|
+
Store user's application configurations
|
|
151
|
+
|
|
152
|
+
### Document ID
|
|
153
|
+
Auto-generated
|
|
154
|
+
|
|
155
|
+
### Schema
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
interface AppDocument {
|
|
159
|
+
// Identity
|
|
160
|
+
id: string; // Auto-generated doc ID
|
|
161
|
+
userId: string; // Owner (Firebase Auth UID)
|
|
162
|
+
|
|
163
|
+
// App Info
|
|
164
|
+
name: string; // App display name
|
|
165
|
+
packageId: string; // com.example.app
|
|
166
|
+
icon: string | null; // App icon URL
|
|
167
|
+
description: string; // App description
|
|
168
|
+
|
|
169
|
+
// Platform Support
|
|
170
|
+
platforms: ('ios' | 'android' | 'web')[];
|
|
171
|
+
|
|
172
|
+
// Update Channels
|
|
173
|
+
channels: {
|
|
174
|
+
production: ChannelConfig;
|
|
175
|
+
staging: ChannelConfig;
|
|
176
|
+
development: ChannelConfig;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Stats
|
|
180
|
+
totalBuilds: number; // Total builds uploaded
|
|
181
|
+
activeUsers: number; // Active installs (future)
|
|
182
|
+
lastBuildDate: Timestamp | null; // Last build upload
|
|
183
|
+
|
|
184
|
+
// Metadata
|
|
185
|
+
createdAt: Timestamp;
|
|
186
|
+
updatedAt: Timestamp;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface ChannelConfig {
|
|
190
|
+
enabled: boolean; // Channel active?
|
|
191
|
+
autoUpdate: boolean; // Auto-update enabled?
|
|
192
|
+
updateStrategy: 'immediate' | 'background' | 'manual';
|
|
193
|
+
requireUserConsent: boolean; // Ask before update?
|
|
194
|
+
minVersion: string | null; // Minimum app version (semver)
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Example Document
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"id": "app_abc123",
|
|
203
|
+
"userId": "abc123xyz789",
|
|
204
|
+
|
|
205
|
+
"name": "My Awesome App",
|
|
206
|
+
"packageId": "com.example.awesome",
|
|
207
|
+
"icon": "https://storage.googleapis.com/.../icon.png",
|
|
208
|
+
"description": "A great mobile application",
|
|
209
|
+
|
|
210
|
+
"platforms": ["ios", "android"],
|
|
211
|
+
|
|
212
|
+
"channels": {
|
|
213
|
+
"production": {
|
|
214
|
+
"enabled": true,
|
|
215
|
+
"autoUpdate": true,
|
|
216
|
+
"updateStrategy": "background",
|
|
217
|
+
"requireUserConsent": false,
|
|
218
|
+
"minVersion": "1.0.0"
|
|
219
|
+
},
|
|
220
|
+
"staging": {
|
|
221
|
+
"enabled": true,
|
|
222
|
+
"autoUpdate": false,
|
|
223
|
+
"updateStrategy": "manual",
|
|
224
|
+
"requireUserConsent": true,
|
|
225
|
+
"minVersion": null
|
|
226
|
+
},
|
|
227
|
+
"development": {
|
|
228
|
+
"enabled": true,
|
|
229
|
+
"autoUpdate": true,
|
|
230
|
+
"updateStrategy": "immediate",
|
|
231
|
+
"requireUserConsent": false,
|
|
232
|
+
"minVersion": null
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
"totalBuilds": 5,
|
|
237
|
+
"activeUsers": 1234,
|
|
238
|
+
"lastBuildDate": {"seconds": 1703808000, "nanoseconds": 0},
|
|
239
|
+
|
|
240
|
+
"createdAt": {"seconds": 1703721600, "nanoseconds": 0},
|
|
241
|
+
"updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Indexes Required
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
// Composite indexes
|
|
249
|
+
apps: {
|
|
250
|
+
fields: ['userId', 'createdAt'],
|
|
251
|
+
fields: ['userId', 'platforms', 'createdAt']
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Security Rules
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
match /apps/{appId} {
|
|
259
|
+
allow read: if request.auth.uid == resource.data.userId;
|
|
260
|
+
allow create: if request.auth.uid == request.resource.data.userId
|
|
261
|
+
&& request.resource.data.keys().hasAll(['name', 'packageId', 'userId']);
|
|
262
|
+
allow update: if request.auth.uid == resource.data.userId;
|
|
263
|
+
allow delete: if request.auth.uid == resource.data.userId;
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 📁 Collection 3: `builds`
|
|
270
|
+
|
|
271
|
+
### Purpose
|
|
272
|
+
Store build metadata and distribution info
|
|
273
|
+
|
|
274
|
+
### Document ID
|
|
275
|
+
Auto-generated
|
|
276
|
+
|
|
277
|
+
### Schema
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
interface BuildDocument {
|
|
281
|
+
// Identity
|
|
282
|
+
id: string; // Auto-generated doc ID
|
|
283
|
+
userId: string; // Owner
|
|
284
|
+
appId: string; // Parent app
|
|
285
|
+
|
|
286
|
+
// Version Info
|
|
287
|
+
version: string; // Semantic version (1.0.0)
|
|
288
|
+
buildNumber: number; // Integer build number
|
|
289
|
+
versionCode: number; // Android version code
|
|
290
|
+
bundleVersion: string; // iOS bundle version
|
|
291
|
+
|
|
292
|
+
// Distribution
|
|
293
|
+
channel: 'production' | 'staging' | 'development';
|
|
294
|
+
platform: 'ios' | 'android' | 'web';
|
|
295
|
+
|
|
296
|
+
// File Info
|
|
297
|
+
fileName: string; // Original filename
|
|
298
|
+
fileSize: number; // Size in bytes
|
|
299
|
+
fileType: 'zip' | 'apk' | 'ipa';
|
|
300
|
+
mimeType: string; // MIME type
|
|
301
|
+
|
|
302
|
+
// Security
|
|
303
|
+
checksum: string; // SHA-256 hash
|
|
304
|
+
signature: string | null; // Digital signature (future)
|
|
305
|
+
|
|
306
|
+
// Google Drive
|
|
307
|
+
driveFileId: string; // Google Drive file ID
|
|
308
|
+
driveFileUrl: string; // Direct download URL (expiring)
|
|
309
|
+
driveFolderId: string; // Parent folder ID
|
|
310
|
+
|
|
311
|
+
// Release Info
|
|
312
|
+
releaseNotes: string; // What's new
|
|
313
|
+
releaseType: 'major' | 'minor' | 'patch' | 'hotfix';
|
|
314
|
+
isPreRelease: boolean; // Beta/alpha?
|
|
315
|
+
|
|
316
|
+
// Upload Info
|
|
317
|
+
uploadedAt: Timestamp; // Upload timestamp
|
|
318
|
+
uploadedBy: string; // User email
|
|
319
|
+
uploadDuration: number; // Upload time (ms)
|
|
320
|
+
|
|
321
|
+
// Status
|
|
322
|
+
status: 'uploading' | 'processing' | 'active' | 'archived' | 'failed';
|
|
323
|
+
error: string | null; // Error message if failed
|
|
324
|
+
|
|
325
|
+
// Analytics (aggregated)
|
|
326
|
+
downloads: number; // Total downloads
|
|
327
|
+
installs: number; // Successful installs
|
|
328
|
+
rollbacks: number; // Rollback count
|
|
329
|
+
errors: number; // Installation errors
|
|
330
|
+
|
|
331
|
+
// Metadata
|
|
332
|
+
updatedAt: Timestamp;
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Example Document
|
|
337
|
+
|
|
338
|
+
```json
|
|
339
|
+
{
|
|
340
|
+
"id": "build_xyz456",
|
|
341
|
+
"userId": "abc123xyz789",
|
|
342
|
+
"appId": "app_abc123",
|
|
343
|
+
|
|
344
|
+
"version": "1.0.1",
|
|
345
|
+
"buildNumber": 2,
|
|
346
|
+
"versionCode": 101,
|
|
347
|
+
"bundleVersion": "1.0.1",
|
|
348
|
+
|
|
349
|
+
"channel": "production",
|
|
350
|
+
"platform": "android",
|
|
351
|
+
|
|
352
|
+
"fileName": "app-release-1.0.1.zip",
|
|
353
|
+
"fileSize": 31457280,
|
|
354
|
+
"fileType": "zip",
|
|
355
|
+
"mimeType": "application/zip",
|
|
356
|
+
|
|
357
|
+
"checksum": "sha256:a1b2c3d4e5f6...",
|
|
358
|
+
"signature": null,
|
|
359
|
+
|
|
360
|
+
"driveFileId": "1a2b3c4d5e6f7g8h9i0j",
|
|
361
|
+
"driveFileUrl": "https://drive.google.com/uc?export=download&id=...",
|
|
362
|
+
"driveFolderId": "folder123abc",
|
|
363
|
+
|
|
364
|
+
"releaseNotes": "Bug fixes and performance improvements",
|
|
365
|
+
"releaseType": "patch",
|
|
366
|
+
"isPreRelease": false,
|
|
367
|
+
|
|
368
|
+
"uploadedAt": {"seconds": 1703808000, "nanoseconds": 0},
|
|
369
|
+
"uploadedBy": "user@example.com",
|
|
370
|
+
"uploadDuration": 12500,
|
|
371
|
+
|
|
372
|
+
"status": "active",
|
|
373
|
+
"error": null,
|
|
374
|
+
|
|
375
|
+
"downloads": 523,
|
|
376
|
+
"installs": 498,
|
|
377
|
+
"rollbacks": 2,
|
|
378
|
+
"errors": 1,
|
|
379
|
+
|
|
380
|
+
"updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Indexes Required
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
// Composite indexes
|
|
388
|
+
builds: {
|
|
389
|
+
fields: ['userId', 'uploadedAt'],
|
|
390
|
+
fields: ['appId', 'channel', 'uploadedAt'],
|
|
391
|
+
fields: ['userId', 'status', 'uploadedAt'],
|
|
392
|
+
fields: ['appId', 'platform', 'channel', 'version']
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Security Rules
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
match /builds/{buildId} {
|
|
400
|
+
allow read: if request.auth.uid == resource.data.userId;
|
|
401
|
+
allow create: if request.auth.uid == request.resource.data.userId
|
|
402
|
+
&& exists(/databases/$(database)/documents/apps/$(request.resource.data.appId))
|
|
403
|
+
&& get(/databases/$(database)/documents/apps/$(request.resource.data.appId)).data.userId == request.auth.uid;
|
|
404
|
+
allow update: if request.auth.uid == resource.data.userId;
|
|
405
|
+
allow delete: if request.auth.uid == resource.data.userId;
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## 📁 Collection 4: `drive_tokens`
|
|
412
|
+
|
|
413
|
+
### Purpose
|
|
414
|
+
Store encrypted Google Drive OAuth tokens
|
|
415
|
+
|
|
416
|
+
### Document ID
|
|
417
|
+
`{userId}` - Firebase Auth UID
|
|
418
|
+
|
|
419
|
+
### Schema
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
interface DriveTokenDocument {
|
|
423
|
+
// Identity
|
|
424
|
+
userId: string; // Firebase Auth UID
|
|
425
|
+
|
|
426
|
+
// OAuth Tokens (ENCRYPTED)
|
|
427
|
+
accessToken: string; // Encrypted access token
|
|
428
|
+
refreshToken: string; // Encrypted refresh token
|
|
429
|
+
|
|
430
|
+
// Token Metadata
|
|
431
|
+
tokenType: string; // Usually "Bearer"
|
|
432
|
+
scope: string[]; // Granted scopes
|
|
433
|
+
expiresAt: Timestamp; // Token expiration
|
|
434
|
+
|
|
435
|
+
// Encryption Info
|
|
436
|
+
encryptionMethod: 'AES-256-GCM';
|
|
437
|
+
iv: string; // Initialization vector (base64)
|
|
438
|
+
authTag: string; // Authentication tag (base64)
|
|
439
|
+
|
|
440
|
+
// Metadata
|
|
441
|
+
createdAt: Timestamp; // First connection
|
|
442
|
+
updatedAt: Timestamp; // Last token refresh
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Example Document (Encrypted)
|
|
447
|
+
|
|
448
|
+
```json
|
|
449
|
+
{
|
|
450
|
+
"userId": "abc123xyz789",
|
|
451
|
+
|
|
452
|
+
"accessToken": "encrypted:gAAAAABl...",
|
|
453
|
+
"refreshToken": "encrypted:gAAAAABl...",
|
|
454
|
+
|
|
455
|
+
"tokenType": "Bearer",
|
|
456
|
+
"scope": [
|
|
457
|
+
"https://www.googleapis.com/auth/drive.file"
|
|
458
|
+
],
|
|
459
|
+
"expiresAt": {"seconds": 1703811600, "nanoseconds": 0},
|
|
460
|
+
|
|
461
|
+
"encryptionMethod": "AES-256-GCM",
|
|
462
|
+
"iv": "base64_encoded_iv_here",
|
|
463
|
+
"authTag": "base64_encoded_tag_here",
|
|
464
|
+
|
|
465
|
+
"createdAt": {"seconds": 1703722000, "nanoseconds": 0},
|
|
466
|
+
"updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Security Rules (VERY RESTRICTIVE)
|
|
471
|
+
|
|
472
|
+
```javascript
|
|
473
|
+
match /drive_tokens/{userId} {
|
|
474
|
+
// Only backend functions can write
|
|
475
|
+
allow read, write: if false;
|
|
476
|
+
|
|
477
|
+
// Exception: Cloud Functions have admin access
|
|
478
|
+
// Client-side code should NEVER access this collection directly
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Encryption Implementation
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// Backend only - NEVER expose to client
|
|
486
|
+
import * as crypto from 'crypto';
|
|
487
|
+
|
|
488
|
+
const ENCRYPTION_KEY = process.env.DRIVE_TOKEN_ENCRYPTION_KEY; // 32 bytes
|
|
489
|
+
|
|
490
|
+
function encryptToken(token: string): {
|
|
491
|
+
encrypted: string;
|
|
492
|
+
iv: string;
|
|
493
|
+
authTag: string;
|
|
494
|
+
} {
|
|
495
|
+
const iv = crypto.randomBytes(16);
|
|
496
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
|
|
497
|
+
|
|
498
|
+
let encrypted = cipher.update(token, 'utf8', 'base64');
|
|
499
|
+
encrypted += cipher.final('base64');
|
|
500
|
+
|
|
501
|
+
const authTag = cipher.getAuthTag().toString('base64');
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
encrypted: `encrypted:${encrypted}`,
|
|
505
|
+
iv: iv.toString('base64'),
|
|
506
|
+
authTag
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function decryptToken(encrypted: string, iv: string, authTag: string): string {
|
|
511
|
+
const decipher = crypto.createDecipheriv(
|
|
512
|
+
'aes-256-gcm',
|
|
513
|
+
Buffer.from(ENCRYPTION_KEY, 'hex'),
|
|
514
|
+
Buffer.from(iv, 'base64')
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
decipher.setAuthTag(Buffer.from(authTag, 'base64'));
|
|
518
|
+
|
|
519
|
+
let decrypted = decipher.update(encrypted.replace('encrypted:', ''), 'base64', 'utf8');
|
|
520
|
+
decrypted += decipher.final('utf8');
|
|
521
|
+
|
|
522
|
+
return decrypted;
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## 📁 Collection 5: `analytics` (Future)
|
|
529
|
+
|
|
530
|
+
### Purpose
|
|
531
|
+
Track build downloads, installations, and errors
|
|
532
|
+
|
|
533
|
+
### Document ID
|
|
534
|
+
Auto-generated
|
|
535
|
+
|
|
536
|
+
### Schema
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
interface AnalyticsDocument {
|
|
540
|
+
// Identity
|
|
541
|
+
userId: string;
|
|
542
|
+
appId: string;
|
|
543
|
+
buildId: string;
|
|
544
|
+
|
|
545
|
+
// Event Info
|
|
546
|
+
eventType: 'download' | 'install' | 'rollback' | 'error' | 'check';
|
|
547
|
+
eventData: Record<string, any>; // Event-specific data
|
|
548
|
+
|
|
549
|
+
// Device Info
|
|
550
|
+
deviceId: string; // Hashed device ID
|
|
551
|
+
platform: 'ios' | 'android' | 'web';
|
|
552
|
+
osVersion: string;
|
|
553
|
+
appVersion: string;
|
|
554
|
+
|
|
555
|
+
// Location (optional)
|
|
556
|
+
country: string | null;
|
|
557
|
+
region: string | null;
|
|
558
|
+
|
|
559
|
+
// Timestamp
|
|
560
|
+
timestamp: Timestamp;
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## 🔐 Firestore Security Rules (Complete)
|
|
567
|
+
|
|
568
|
+
```javascript
|
|
569
|
+
rules_version = '2';
|
|
570
|
+
service cloud.firestore {
|
|
571
|
+
match /databases/{database}/documents {
|
|
572
|
+
|
|
573
|
+
// Helper functions
|
|
574
|
+
function isAuthenticated() {
|
|
575
|
+
return request.auth != null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function isOwner(userId) {
|
|
579
|
+
return isAuthenticated() && request.auth.uid == userId;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function emailVerified() {
|
|
583
|
+
return request.auth.token.email_verified == true;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Users collection
|
|
587
|
+
match /users/{userId} {
|
|
588
|
+
allow read: if isOwner(userId);
|
|
589
|
+
allow create: if isOwner(userId) && emailVerified();
|
|
590
|
+
allow update: if isOwner(userId)
|
|
591
|
+
&& !request.resource.data.diff(resource.data).affectedKeys()
|
|
592
|
+
.hasAny(['uid', 'email', 'createdAt', 'provider']);
|
|
593
|
+
allow delete: if isOwner(userId);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Apps collection
|
|
597
|
+
match /apps/{appId} {
|
|
598
|
+
allow read: if isOwner(resource.data.userId);
|
|
599
|
+
allow create: if isAuthenticated() && emailVerified()
|
|
600
|
+
&& request.resource.data.userId == request.auth.uid;
|
|
601
|
+
allow update, delete: if isOwner(resource.data.userId);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Builds collection
|
|
605
|
+
match /builds/{buildId} {
|
|
606
|
+
allow read: if isOwner(resource.data.userId);
|
|
607
|
+
allow create: if isAuthenticated() && emailVerified()
|
|
608
|
+
&& request.resource.data.userId == request.auth.uid
|
|
609
|
+
&& exists(/databases/$(database)/documents/apps/$(request.resource.data.appId))
|
|
610
|
+
&& get(/databases/$(database)/documents/apps/$(request.resource.data.appId)).data.userId == request.auth.uid;
|
|
611
|
+
allow update, delete: if isOwner(resource.data.userId);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Drive tokens (backend only)
|
|
615
|
+
match /drive_tokens/{userId} {
|
|
616
|
+
allow read, write: if false; // Client can NEVER access
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Analytics (write only for authenticated users)
|
|
620
|
+
match /analytics/{eventId} {
|
|
621
|
+
allow read: if false; // No client reads
|
|
622
|
+
allow create: if isAuthenticated();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## 📊 Firestore Indexes
|
|
631
|
+
|
|
632
|
+
### Required Composite Indexes
|
|
633
|
+
|
|
634
|
+
```javascript
|
|
635
|
+
// firestore.indexes.json
|
|
636
|
+
{
|
|
637
|
+
"indexes": [
|
|
638
|
+
{
|
|
639
|
+
"collectionGroup": "users",
|
|
640
|
+
"queryScope": "COLLECTION",
|
|
641
|
+
"fields": [
|
|
642
|
+
{ "fieldPath": "email", "order": "ASCENDING" },
|
|
643
|
+
{ "fieldPath": "createdAt", "order": "DESCENDING" }
|
|
644
|
+
]
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
"collectionGroup": "apps",
|
|
648
|
+
"queryScope": "COLLECTION",
|
|
649
|
+
"fields": [
|
|
650
|
+
{ "fieldPath": "userId", "order": "ASCENDING" },
|
|
651
|
+
{ "fieldPath": "createdAt", "order": "DESCENDING" }
|
|
652
|
+
]
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
"collectionGroup": "builds",
|
|
656
|
+
"queryScope": "COLLECTION",
|
|
657
|
+
"fields": [
|
|
658
|
+
{ "fieldPath": "userId", "order": "ASCENDING" },
|
|
659
|
+
{ "fieldPath": "uploadedAt", "order": "DESCENDING" }
|
|
660
|
+
]
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
"collectionGroup": "builds",
|
|
664
|
+
"queryScope": "COLLECTION",
|
|
665
|
+
"fields": [
|
|
666
|
+
{ "fieldPath": "appId", "order": "ASCENDING" },
|
|
667
|
+
{ "fieldPath": "channel", "order": "ASCENDING" },
|
|
668
|
+
{ "fieldPath": "uploadedAt", "order": "DESCENDING" }
|
|
669
|
+
]
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
"collectionGroup": "builds",
|
|
673
|
+
"queryScope": "COLLECTION",
|
|
674
|
+
"fields": [
|
|
675
|
+
{ "fieldPath": "appId", "order": "ASCENDING" },
|
|
676
|
+
{ "fieldPath": "platform", "order": "ASCENDING" },
|
|
677
|
+
{ "fieldPath": "channel", "order": "ASCENDING" },
|
|
678
|
+
{ "fieldPath": "version", "order": "DESCENDING" }
|
|
679
|
+
]
|
|
680
|
+
}
|
|
681
|
+
]
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## ✅ Implementation Checklist
|
|
688
|
+
|
|
689
|
+
- [ ] Create Firestore database
|
|
690
|
+
- [ ] Define all collections and schemas
|
|
691
|
+
- [ ] Implement security rules
|
|
692
|
+
- [ ] Deploy security rules: `firebase deploy --only firestore:rules`
|
|
693
|
+
- [ ] Create composite indexes
|
|
694
|
+
- [ ] Deploy indexes: `firebase deploy --only firestore:indexes`
|
|
695
|
+
- [ ] Test read/write permissions
|
|
696
|
+
- [ ] Setup encryption for drive_tokens
|
|
697
|
+
- [ ] Document all collections
|
|
698
|
+
- [ ] Create TypeScript interfaces
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
**Plan Status:** ✅ Complete and ready for implementation
|
|
703
|
+
**Database Type:** Firestore (NoSQL)
|
|
704
|
+
**Total Collections:** 5 (4 active + 1 future)
|