native-update 2.0.0 → 3.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 +28 -14
- package/android/manifest-additions.xml +28 -0
- package/android/src/main/AndroidManifest.xml +3 -7
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundNotificationManager.kt +21 -13
- package/android/src/main/java/com/aoneahsan/nativeupdate/LiveUpdatePlugin.kt +196 -29
- package/android/src/main/java/com/aoneahsan/nativeupdate/NativeUpdatePlugin.kt +21 -36
- package/cli/AGENTS.md +15 -1
- package/cli/CLAUDE.md +15 -1
- package/cli/commands/backend-create.js +56 -15
- package/cli/commands/bundle-create.js +39 -7
- package/cli/commands/monitor.js +72 -51
- package/cli/index.js +10 -24
- package/cli/package.json +11 -3
- package/dist/esm/app-review/app-review-manager.js +0 -1
- package/dist/esm/app-review/index.js +0 -1
- package/dist/esm/app-review/platform-review-handler.js +0 -1
- package/dist/esm/app-review/review-conditions-checker.js +0 -1
- package/dist/esm/app-review/review-rate-limiter.js +0 -1
- package/dist/esm/app-review/types.js +0 -1
- package/dist/esm/app-update/app-update-checker.js +0 -1
- package/dist/esm/app-update/app-update-installer.d.ts +1 -0
- package/dist/esm/app-update/app-update-installer.js +19 -8
- package/dist/esm/app-update/app-update-manager.js +0 -1
- package/dist/esm/app-update/app-update-notifier.js +0 -1
- package/dist/esm/app-update/index.js +0 -1
- package/dist/esm/app-update/platform-app-update.js +12 -3
- package/dist/esm/app-update/types.js +0 -1
- package/dist/esm/background-update/background-scheduler.js +0 -1
- package/dist/esm/background-update/index.js +0 -1
- package/dist/esm/background-update/notification-manager.js +0 -1
- package/dist/esm/config/support.js +0 -1
- package/dist/esm/core/analytics.d.ts +13 -0
- package/dist/esm/core/analytics.js +15 -1
- package/dist/esm/core/cache-manager.js +0 -1
- package/dist/esm/core/config.d.ts +0 -15
- package/dist/esm/core/config.js +0 -4
- package/dist/esm/core/errors.d.ts +1 -0
- package/dist/esm/core/errors.js +1 -1
- package/dist/esm/core/event-emitter.d.ts +0 -3
- package/dist/esm/core/event-emitter.js +8 -3
- package/dist/esm/core/logger.js +0 -1
- package/dist/esm/core/performance.js +0 -1
- package/dist/esm/core/plugin-manager.js +3 -1
- package/dist/esm/core/security.d.ts +11 -0
- package/dist/esm/core/security.js +27 -1
- package/dist/esm/definitions.d.ts +0 -11
- package/dist/esm/definitions.js +0 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +3 -1
- package/dist/esm/live-update/bundle-manager.d.ts +8 -0
- package/dist/esm/live-update/bundle-manager.js +20 -1
- package/dist/esm/live-update/certificate-pinning.js +0 -1
- package/dist/esm/live-update/delta-processor.d.ts +1 -1
- package/dist/esm/live-update/delta-processor.js +3 -1
- package/dist/esm/live-update/download-manager.js +9 -2
- package/dist/esm/live-update/rollout-checker.d.ts +1 -1
- package/dist/esm/live-update/rollout-checker.js +5 -6
- package/dist/esm/live-update/update-manager.js +0 -1
- package/dist/esm/live-update/version-manager.d.ts +0 -12
- package/dist/esm/live-update/version-manager.js +0 -68
- package/dist/esm/plugin.js +69 -43
- package/dist/esm/security/crypto.js +11 -7
- package/dist/esm/security/validator.js +0 -1
- package/dist/esm/types/rollout.d.ts +85 -0
- package/dist/esm/types/rollout.js +25 -0
- package/dist/esm/web.d.ts +8 -0
- package/dist/esm/web.js +148 -54
- package/dist/plugin.cjs.js +1 -2
- package/dist/plugin.esm.js +1 -2
- package/dist/plugin.js +2 -3
- package/docs/CHANGELOG.md +124 -0
- package/docs/CLAUDE.md +1 -1
- package/docs/MANUAL-TASKS.md +17 -0
- package/docs/MARKETING_WEBSITE_TRACKER.md +18 -2
- package/docs/MIGRATION.md +83 -0
- package/docs/PACKAGES.md +167 -0
- package/docs/PERFORMANCE.md +110 -0
- package/docs/PROJECT_COMPLETION_TRACKER.md +4 -35
- package/docs/README.md +0 -1
- package/docs/REMAINING_FEATURES.md +123 -76
- package/docs/ROADMAP.md +200 -120
- package/docs/SECURITY-IF-REPO-PUBLIC.md +232 -0
- package/docs/TESTING_REQUIREMENTS.md +1 -112
- package/docs/ai-knowledge-base/00-project-profile.md +38 -0
- package/docs/ai-knowledge-base/01-features.md +46 -0
- package/docs/ai-knowledge-base/02-routes.md +72 -0
- package/docs/ai-knowledge-base/03-tech-stack.md +57 -0
- package/docs/ai-knowledge-base/README.md +23 -0
- package/docs/analytics-tracking.md +37 -0
- package/docs/capacitor-official-plugins.md +46 -0
- package/docs/capawesome-plugins.md +29 -0
- package/docs/deployment/HOSTINGER_DEPLOY.md +75 -34
- package/docs/deployment/PRODUCTION-DEPLOY-CHECKLIST.md +184 -0
- package/docs/docs-site/plan.md +201 -0
- package/docs/docs-site/scope.md +127 -0
- package/docs/docs-site/tracker.json +235 -0
- package/docs/error-handling-tracking.md +37 -0
- package/docs/guides/testing-guide.md +15 -41
- package/docs/package-audit.md +41 -0
- package/docs/play-store/ASO-METADATA.md +5 -0
- package/docs/play-store/DATA-SAFETY.md +5 -0
- package/docs/play-store/DECLARATIONS.md +5 -0
- package/docs/play-store/README.md +27 -0
- package/docs/play-store/RELEASE-NOTES.md +99 -0
- package/docs/play-store/RELEASE-STEPS.md +6 -0
- package/docs/play-store/STORE-LISTING.md +7 -0
- package/docs/play-store/SUBMISSION-CHECKLIST.md +5 -0
- package/docs/production-readiness.md +1 -1
- package/docs/project-audit/2026-05-16/00-FINALIZATION-PLAN.md +123 -0
- package/docs/project-audit/2026-05-16/01-BATCH-PLAN.md +227 -0
- package/docs/project-audit/2026-05-16/02-AUDIT-FINDINGS.md +144 -0
- package/docs/project-audit/2026-05-16/sessions/batch-01.md +42 -0
- package/docs/project-audit/2026-05-16/sessions/batch-02.md +74 -0
- package/docs/project-audit/2026-05-16/sessions/batch-03.md +71 -0
- package/docs/project-audit/2026-05-16/sessions/batch-04.md +88 -0
- package/docs/project-audit/2026-05-16/sessions/batch-05.md +63 -0
- package/docs/project-audit/2026-05-16/sessions/batch-06.md +103 -0
- package/docs/project-audit/2026-05-16/sessions/batch-07.md +61 -0
- package/docs/project-audit/2026-05-16/sessions/batch-08.md +101 -0
- package/docs/project-audit/2026-05-16/sessions/batch-09.md +105 -0
- package/docs/project-audit/2026-05-16/sessions/batch-10.md +102 -0
- package/docs/project-audit/AUDIT-HISTORY.md +74 -0
- package/docs/project-audit/BATCH-PLAN.md +168 -0
- package/docs/project-audit/MARKETING-WEBSITE-FINDINGS-2026-05-11.md +73 -0
- package/docs/project-audit/PRODUCTION-READINESS-AUDIT-2026-05-08.md +275 -0
- package/docs/project-finalization/00-tracker.json +60 -0
- package/docs/project-knowledge-base/01-system-overview.md +39 -30
- package/docs/project-knowledge-base/02-routes-pages-forms-users.md +87 -81
- package/docs/project-knowledge-base/03-tech-stack-modules-services.md +140 -108
- package/docs/project-knowledge-base/04-data-models-integrations.md +191 -254
- package/docs/project-knowledge-base/05-docs-corpus-inventory.md +83 -20
- package/docs/project-knowledge-base/06-operations-testing-legal-content.md +67 -29
- package/docs/project-knowledge-base/README.md +27 -14
- package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-05-19.md +81 -0
- package/docs/rules-tracker.md +102 -0
- package/docs/security/PRE-LAUNCH-PENTEST-CHECKLIST.md +116 -0
- package/docs/social-media-marketing/README.md +81 -0
- package/docs/social-media-marketing/posts-batch-01.md +613 -0
- package/docs/social-media-marketing/posts-batch-02.md +528 -0
- package/docs/social-media-marketing/whatsapp-messages-batch-01.md +458 -0
- package/docs/social-media-marketing/whatsapp-status-batch-01.md +355 -0
- package/docs/tracking/finalization-2026-05-16-tracker.json +346 -0
- package/docs/tracking/full-audit-tracker.json +121 -0
- package/docs/tracking/production-readiness-audit-tracker.json +429 -0
- package/ios/Plugin/BackgroundUpdate/BackgroundUpdatePlugin.swift +16 -3
- package/ios/Plugin/LiveUpdate/LiveUpdatePlugin.swift +71 -47
- package/ios/Plugin/NativeUpdatePlugin.swift +14 -9
- package/package.json +29 -29
- package/cli/node_modules/.yarn-integrity +0 -16
- package/cli/node_modules/commander/LICENSE +0 -22
- package/cli/node_modules/commander/Readme.md +0 -1148
- package/cli/node_modules/commander/esm.mjs +0 -16
- package/cli/node_modules/commander/index.js +0 -26
- package/cli/node_modules/commander/lib/argument.js +0 -145
- package/cli/node_modules/commander/lib/command.js +0 -2179
- package/cli/node_modules/commander/lib/error.js +0 -43
- package/cli/node_modules/commander/lib/help.js +0 -462
- package/cli/node_modules/commander/lib/option.js +0 -329
- package/cli/node_modules/commander/lib/suggestSimilar.js +0 -100
- package/cli/node_modules/commander/package-support.json +0 -16
- package/cli/node_modules/commander/package.json +0 -80
- package/cli/node_modules/commander/typings/esm.d.mts +0 -3
- package/cli/node_modules/commander/typings/index.d.ts +0 -884
- package/cli/yarn.lock +0 -8
- package/dist/esm/__tests__/bundle-manager.test.d.ts +0 -1
- package/dist/esm/__tests__/bundle-manager.test.js +0 -151
- package/dist/esm/__tests__/bundle-manager.test.js.map +0 -1
- package/dist/esm/__tests__/config.test.d.ts +0 -1
- package/dist/esm/__tests__/config.test.js +0 -70
- package/dist/esm/__tests__/config.test.js.map +0 -1
- package/dist/esm/__tests__/delta-processor.test.d.ts +0 -1
- package/dist/esm/__tests__/delta-processor.test.js +0 -77
- package/dist/esm/__tests__/delta-processor.test.js.map +0 -1
- package/dist/esm/__tests__/firestore-schema.test.d.ts +0 -1
- package/dist/esm/__tests__/firestore-schema.test.js +0 -74
- package/dist/esm/__tests__/firestore-schema.test.js.map +0 -1
- package/dist/esm/__tests__/integration.test.d.ts +0 -1
- package/dist/esm/__tests__/integration.test.js +0 -78
- package/dist/esm/__tests__/integration.test.js.map +0 -1
- package/dist/esm/__tests__/manifest-reader.test.d.ts +0 -1
- package/dist/esm/__tests__/manifest-reader.test.js +0 -271
- package/dist/esm/__tests__/manifest-reader.test.js.map +0 -1
- package/dist/esm/__tests__/rollout-checker.test.d.ts +0 -1
- package/dist/esm/__tests__/rollout-checker.test.js +0 -210
- package/dist/esm/__tests__/rollout-checker.test.js.map +0 -1
- package/dist/esm/__tests__/security-enforcement.test.d.ts +0 -1
- package/dist/esm/__tests__/security-enforcement.test.js +0 -95
- package/dist/esm/__tests__/security-enforcement.test.js.map +0 -1
- package/dist/esm/__tests__/security.test.d.ts +0 -1
- package/dist/esm/__tests__/security.test.js +0 -54
- package/dist/esm/__tests__/security.test.js.map +0 -1
- package/dist/esm/__tests__/version-manager.test.d.ts +0 -1
- package/dist/esm/__tests__/version-manager.test.js +0 -45
- package/dist/esm/__tests__/version-manager.test.js.map +0 -1
- package/dist/esm/app-review/app-review-manager.js.map +0 -1
- package/dist/esm/app-review/index.js.map +0 -1
- package/dist/esm/app-review/platform-review-handler.js.map +0 -1
- package/dist/esm/app-review/review-conditions-checker.js.map +0 -1
- package/dist/esm/app-review/review-rate-limiter.js.map +0 -1
- package/dist/esm/app-review/types.js.map +0 -1
- package/dist/esm/app-update/app-update-checker.js.map +0 -1
- package/dist/esm/app-update/app-update-installer.js.map +0 -1
- package/dist/esm/app-update/app-update-manager.js.map +0 -1
- package/dist/esm/app-update/app-update-notifier.js.map +0 -1
- package/dist/esm/app-update/index.js.map +0 -1
- package/dist/esm/app-update/platform-app-update.js.map +0 -1
- package/dist/esm/app-update/types.js.map +0 -1
- package/dist/esm/background-update/background-scheduler.js.map +0 -1
- package/dist/esm/background-update/index.js.map +0 -1
- package/dist/esm/background-update/notification-manager.js.map +0 -1
- package/dist/esm/config/support.js.map +0 -1
- package/dist/esm/core/analytics.js.map +0 -1
- package/dist/esm/core/cache-manager.js.map +0 -1
- package/dist/esm/core/config.js.map +0 -1
- package/dist/esm/core/errors.js.map +0 -1
- package/dist/esm/core/event-emitter.js.map +0 -1
- package/dist/esm/core/logger.js.map +0 -1
- package/dist/esm/core/performance.js.map +0 -1
- package/dist/esm/core/plugin-manager.js.map +0 -1
- package/dist/esm/core/security.js.map +0 -1
- package/dist/esm/definitions.js.map +0 -1
- package/dist/esm/firestore/firestore-client.d.ts +0 -109
- package/dist/esm/firestore/firestore-client.js +0 -264
- package/dist/esm/firestore/firestore-client.js.map +0 -1
- package/dist/esm/firestore/index.d.ts +0 -11
- package/dist/esm/firestore/index.js +0 -11
- package/dist/esm/firestore/index.js.map +0 -1
- package/dist/esm/firestore/manifest-reader.d.ts +0 -87
- package/dist/esm/firestore/manifest-reader.js +0 -294
- package/dist/esm/firestore/manifest-reader.js.map +0 -1
- package/dist/esm/firestore/schema.d.ts +0 -507
- package/dist/esm/firestore/schema.js +0 -73
- package/dist/esm/firestore/schema.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/live-update/bundle-manager.js.map +0 -1
- package/dist/esm/live-update/certificate-pinning.js.map +0 -1
- package/dist/esm/live-update/delta-processor.js.map +0 -1
- package/dist/esm/live-update/download-manager.js.map +0 -1
- package/dist/esm/live-update/rollout-checker.js.map +0 -1
- package/dist/esm/live-update/update-manager.js.map +0 -1
- package/dist/esm/live-update/version-manager.js.map +0 -1
- package/dist/esm/plugin.js.map +0 -1
- package/dist/esm/security/crypto.js.map +0 -1
- package/dist/esm/security/validator.js.map +0 -1
- package/dist/esm/web.js.map +0 -1
- package/dist/plugin.cjs.js.map +0 -1
- package/dist/plugin.esm.js.map +0 -1
- package/dist/plugin.js.map +0 -1
- package/docs/FIREBASE_INTEGRATION_TRACKER.md +0 -321
- package/docs/FIREBASE_QUERIES_AND_INDEXES_AUDIT.md +0 -221
- package/docs/examples/firebase-backend-example.md +0 -27
- package/docs/features/laravel-nova-backend/credentials.ignore.md +0 -34
- package/docs/guides/no-cost-backend-implementation-plan.md +0 -77
- package/docs/guides/no-cost-firestore-google-drive-backend.md +0 -60
- package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-03-16.md +0 -454
- package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-03-24.md +0 -66
- package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-03-25.md +0 -67
- /package/docs/{MARKETING_WEBSITE_PLAN.md → archive/MARKETING_WEBSITE_PLAN.md} +0 -0
package/Readme.md
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
> - ✅ **yarn Workspace Monorepo** - Seamless development with workspace:* references
|
|
8
8
|
> - ✅ **3 Complete Examples** - React+Capacitor frontend, Node.js+Express and Firebase backends in `example-apps/`
|
|
9
9
|
> - ✅ **Native Implementations Complete** - iOS (Swift) and Android (Kotlin) fully implemented
|
|
10
|
-
> - ✅ **Comprehensive Test Suite** - Unit and integration tests with Vitest
|
|
11
10
|
> - ✅ **Development Tools Included** - Bundle creator, signer, and CLI tools
|
|
12
11
|
> - ✅ **Security Features Implemented** - HTTPS enforcement, signatures, checksums
|
|
13
12
|
>
|
|
@@ -15,15 +14,16 @@
|
|
|
15
14
|
|
|
16
15
|
## Current State
|
|
17
16
|
|
|
18
|
-
- Package version: `1.
|
|
19
|
-
- Verified on: `2026-
|
|
20
|
-
- Tests: `
|
|
21
|
-
- Build: `yarn build` succeeds
|
|
17
|
+
- Package version: `3.1.0`
|
|
18
|
+
- Verified on: `2026-05-18`
|
|
19
|
+
- Tests: `134/134` backend PHPUnit via `php artisan test`
|
|
20
|
+
- Build: `yarn build` succeeds (root plugin Rollup + website Vite + Docusaurus docs site)
|
|
22
21
|
- Root package manager is standardized on Yarn, and the root scripts no longer shell out to npm or pnpm
|
|
22
|
+
- Backend: Laravel 13 + Nova 5 — the only backend after v3.0.0 (Firestore removed); identity via Laravel Socialite + Sanctum personal access tokens; RBAC via `spatie/laravel-permission` (roles `super-admin`, `admin`, `user`)
|
|
23
23
|
- Root portfolio info file:
|
|
24
|
-
- `NATIVE-UPDATE_portfolio-info_2026-
|
|
24
|
+
- `NATIVE-UPDATE_portfolio-info_2026-06-26.md`
|
|
25
25
|
- Current dated project profile:
|
|
26
|
-
- `docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-
|
|
26
|
+
- `docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-05-19.md`
|
|
27
27
|
|
|
28
28
|
## 📚 Documentation
|
|
29
29
|
|
|
@@ -368,15 +368,19 @@ This repository uses **yarn workspace** for seamless development. All examples r
|
|
|
368
368
|
- ✅ Rate limiting and compression
|
|
369
369
|
- ✅ Signature verification
|
|
370
370
|
|
|
371
|
-
#### 2.
|
|
371
|
+
#### 2. Laravel + Nova SaaS Backend
|
|
372
372
|
|
|
373
|
-
|
|
373
|
+
The canonical backend ships in this monorepo under [`backend/`](./backend) — a
|
|
374
|
+
Laravel 13 + Nova 5 application that powers the hosted Native Update SaaS at
|
|
375
|
+
`api.nativeupdate.aoneahsan.com`. You can use it as a managed service or
|
|
376
|
+
self-host it on any PHP 8.2+ host. Setup is documented in
|
|
377
|
+
[`docs/deployment/PRODUCTION-DEPLOY-CHECKLIST.md`](./docs/deployment/PRODUCTION-DEPLOY-CHECKLIST.md).
|
|
374
378
|
|
|
375
|
-
- ✅
|
|
376
|
-
- ✅
|
|
377
|
-
- ✅
|
|
378
|
-
- ✅
|
|
379
|
-
- ✅
|
|
379
|
+
- ✅ Google OAuth (Socialite) + Sanctum bearer tokens + spatie/laravel-permission RBAC
|
|
380
|
+
- ✅ MySQL / PostgreSQL bundle metadata + signed download URLs
|
|
381
|
+
- ✅ Per-app API keys + signing keys generated through Nova
|
|
382
|
+
- ✅ Per-bundle rollout percentages + per-device rollout buckets
|
|
383
|
+
- ✅ Google Drive integration so bundle ZIPs stay in your own Drive
|
|
380
384
|
|
|
381
385
|
**🚀 Get started:** Each example app has its own README with setup instructions.
|
|
382
386
|
|
|
@@ -542,3 +546,13 @@ MIT License - see [LICENSE](./LICENSE) for details.
|
|
|
542
546
|
- LinkedIn: [linkedin.com/in/aoneahsan](https://linkedin.com/in/aoneahsan)
|
|
543
547
|
- Email: aoneahsan@gmail.com
|
|
544
548
|
- Phone: +923046619706
|
|
549
|
+
|
|
550
|
+
<!-- project-links:start -->
|
|
551
|
+
## Links
|
|
552
|
+
|
|
553
|
+
- Live: https://nativeupdate.aoneahsan.com
|
|
554
|
+
- Play Store: https://play.google.com/store/apps/details?id=com.aoneahsan.nativeupdate
|
|
555
|
+
- NPM: https://www.npmjs.com/package/native-update
|
|
556
|
+
|
|
557
|
+
_URL source of truth: `01-code/projects/project-live-urls.json` (auto-generated — do not hand-edit between these markers)._
|
|
558
|
+
<!-- project-links:end -->
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
IMPORTANT: These elements MUST be added to your app's AndroidManifest.xml inside the <application> tag
|
|
3
|
+
when using background update features of the native-update plugin.
|
|
4
|
+
|
|
5
|
+
Without these additions:
|
|
6
|
+
1. Background updates will crash with ServiceNotFoundException
|
|
7
|
+
2. Update notification buttons won't work (they'll appear but won't respond to taps)
|
|
8
|
+
3. Downloads won't continue when app is in background
|
|
9
|
+
|
|
10
|
+
Location: android/app/src/main/AndroidManifest.xml (in your app, not in the plugin)
|
|
11
|
+
-->
|
|
12
|
+
|
|
13
|
+
<!-- WorkManager service for background updates -->
|
|
14
|
+
<service
|
|
15
|
+
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
|
16
|
+
android:foregroundServiceType="dataSync"
|
|
17
|
+
tools:node="merge" />
|
|
18
|
+
|
|
19
|
+
<!-- Broadcast receiver for notification actions -->
|
|
20
|
+
<receiver
|
|
21
|
+
android:name="com.aoneahsan.nativeupdate.NotificationActionReceiver"
|
|
22
|
+
android:exported="false">
|
|
23
|
+
<intent-filter>
|
|
24
|
+
<action android:name="com.aoneahsan.nativeupdate.UPDATE_NOW" />
|
|
25
|
+
<action android:name="com.aoneahsan.nativeupdate.UPDATE_LATER" />
|
|
26
|
+
<action android:name="com.aoneahsan.nativeupdate.DISMISS" />
|
|
27
|
+
</intent-filter>
|
|
28
|
+
</receiver>
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
<!-- Required permissions -->
|
|
4
4
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
5
5
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
6
|
-
|
|
7
|
-
<!-- Optional permissions (requested at runtime) -->
|
|
8
|
-
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
9
|
-
android:maxSdkVersion="28" />
|
|
10
|
-
|
|
6
|
+
|
|
11
7
|
<!-- Background update permissions -->
|
|
12
8
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
13
9
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
14
10
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
15
11
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
16
|
-
|
|
12
|
+
|
|
17
13
|
</manifest>
|
|
@@ -22,9 +22,12 @@ class BackgroundNotificationManager(
|
|
|
22
22
|
private const val CHANNEL_ID = "capacitor_native_update"
|
|
23
23
|
private const val CHANNEL_NAME = "App Updates"
|
|
24
24
|
private const val NOTIFICATION_ID = 1001
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// Action strings MUST match NotificationActionReceiver's
|
|
26
|
+
// companion-object constants. They were `UPDATE_NOW` here and
|
|
27
|
+
// `INSTALL_NOW` there, so every action button silently no-op'd.
|
|
28
|
+
private const val ACTION_UPDATE_NOW = NotificationActionReceiver.ACTION_INSTALL_NOW
|
|
29
|
+
private const val ACTION_UPDATE_LATER = NotificationActionReceiver.ACTION_INSTALL_LATER
|
|
30
|
+
private const val ACTION_DISMISS = NotificationActionReceiver.ACTION_DISMISS
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
33
|
* Static method to cancel notification from BroadcastReceiver
|
|
@@ -259,8 +262,11 @@ class BackgroundNotificationManager(
|
|
|
259
262
|
) {
|
|
260
263
|
val actionLabels = preferences.actionLabels ?: ActionLabels.default()
|
|
261
264
|
|
|
262
|
-
//
|
|
263
|
-
|
|
265
|
+
// Explicit intents (component set) — Android 8+ blocks implicit
|
|
266
|
+
// broadcasts to non-exported manifest receivers, which silently
|
|
267
|
+
// dropped these notifications before this fix.
|
|
268
|
+
val updateNowIntent = Intent(context, NotificationActionReceiver::class.java).apply {
|
|
269
|
+
action = ACTION_UPDATE_NOW
|
|
264
270
|
putExtra("update_type", if (appUpdate?.updateAvailable == true) "app_update" else "live_update")
|
|
265
271
|
}
|
|
266
272
|
val updateNowPendingIntent = PendingIntent.getBroadcast(
|
|
@@ -269,30 +275,32 @@ class BackgroundNotificationManager(
|
|
|
269
275
|
updateNowIntent,
|
|
270
276
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
271
277
|
)
|
|
272
|
-
|
|
278
|
+
|
|
273
279
|
builder.addAction(
|
|
274
280
|
android.R.drawable.ic_menu_upload,
|
|
275
281
|
actionLabels.updateNow ?: "Update Now",
|
|
276
282
|
updateNowPendingIntent
|
|
277
283
|
)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
284
|
+
|
|
285
|
+
val updateLaterIntent = Intent(context, NotificationActionReceiver::class.java).apply {
|
|
286
|
+
action = ACTION_UPDATE_LATER
|
|
287
|
+
}
|
|
281
288
|
val updateLaterPendingIntent = PendingIntent.getBroadcast(
|
|
282
289
|
context,
|
|
283
290
|
2,
|
|
284
291
|
updateLaterIntent,
|
|
285
292
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
286
293
|
)
|
|
287
|
-
|
|
294
|
+
|
|
288
295
|
builder.addAction(
|
|
289
296
|
android.R.drawable.ic_menu_recent_history,
|
|
290
297
|
actionLabels.updateLater ?: "Later",
|
|
291
298
|
updateLaterPendingIntent
|
|
292
299
|
)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
300
|
+
|
|
301
|
+
val dismissIntent = Intent(context, NotificationActionReceiver::class.java).apply {
|
|
302
|
+
action = ACTION_DISMISS
|
|
303
|
+
}
|
|
296
304
|
val dismissPendingIntent = PendingIntent.getBroadcast(
|
|
297
305
|
context,
|
|
298
306
|
3,
|
|
@@ -9,8 +9,10 @@ import okhttp3.*
|
|
|
9
9
|
import okhttp3.tls.HandshakeCertificates
|
|
10
10
|
import java.io.File
|
|
11
11
|
import java.io.FileOutputStream
|
|
12
|
+
import java.io.IOException
|
|
12
13
|
import java.security.MessageDigest
|
|
13
14
|
import java.util.concurrent.TimeUnit
|
|
15
|
+
import java.util.zip.ZipFile
|
|
14
16
|
import javax.net.ssl.X509TrustManager
|
|
15
17
|
|
|
16
18
|
class LiveUpdatePlugin(
|
|
@@ -20,12 +22,21 @@ class LiveUpdatePlugin(
|
|
|
20
22
|
private var config: JSObject? = null
|
|
21
23
|
private var progressListener: ((JSObject) -> Unit)? = null
|
|
22
24
|
private var stateChangeListener: ((JSObject) -> Unit)? = null
|
|
25
|
+
/**
|
|
26
|
+
* Listener wired up by the parent Capacitor plugin to call
|
|
27
|
+
* `bridge.setServerBasePath(path)`. set() invokes it after a
|
|
28
|
+
* successful activation so the WebView re-targets at the new bundle.
|
|
29
|
+
*/
|
|
30
|
+
private var webViewPathListener: ((String) -> Unit)? = null
|
|
23
31
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
24
32
|
private lateinit var okHttpClient: OkHttpClient
|
|
25
33
|
private val securityManager = SecurityManager(context)
|
|
26
34
|
private val activeDownloads = mutableMapOf<String, Long>()
|
|
27
35
|
private var downloadManager: android.app.DownloadManager? = null
|
|
28
36
|
|
|
37
|
+
/** Hard cap on uncompressed bundle size (zip-bomb defense). */
|
|
38
|
+
private val maxUncompressedBundleBytes: Long = 500L * 1024 * 1024
|
|
39
|
+
|
|
29
40
|
companion object {
|
|
30
41
|
// SharedPreferences keys. Kept as constants so A2 (boot-time
|
|
31
42
|
// re-verification) and A3 (crash-loop auto-rollback) stay in sync
|
|
@@ -93,6 +104,10 @@ class LiveUpdatePlugin(
|
|
|
93
104
|
fun setStateChangeListener(listener: (JSObject) -> Unit) {
|
|
94
105
|
stateChangeListener = listener
|
|
95
106
|
}
|
|
107
|
+
|
|
108
|
+
fun setWebViewPathListener(listener: (String) -> Unit) {
|
|
109
|
+
webViewPathListener = listener
|
|
110
|
+
}
|
|
96
111
|
|
|
97
112
|
fun sync(call: PluginCall) {
|
|
98
113
|
scope.launch {
|
|
@@ -133,105 +148,149 @@ class LiveUpdatePlugin(
|
|
|
133
148
|
|
|
134
149
|
fun download(call: PluginCall) {
|
|
135
150
|
scope.launch {
|
|
151
|
+
val bundleId = "bundle-${System.currentTimeMillis()}"
|
|
152
|
+
val downloadDir = File(context.filesDir, "updates/$bundleId")
|
|
153
|
+
var success = false
|
|
136
154
|
try {
|
|
137
155
|
val url = call.getString("url") ?: run {
|
|
138
156
|
call.reject("URL is required")
|
|
139
157
|
return@launch
|
|
140
158
|
}
|
|
141
|
-
|
|
159
|
+
|
|
142
160
|
val version = call.getString("version") ?: run {
|
|
143
161
|
call.reject("Version is required")
|
|
144
162
|
return@launch
|
|
145
163
|
}
|
|
146
|
-
|
|
164
|
+
|
|
147
165
|
val checksum = call.getString("checksum") ?: run {
|
|
148
166
|
call.reject("Checksum is required")
|
|
149
167
|
return@launch
|
|
150
168
|
}
|
|
151
|
-
|
|
152
|
-
// Validate URL
|
|
169
|
+
|
|
153
170
|
if (!url.startsWith("https://") && config?.getJSObject("security")?.getBool("enforceHttps") != false) {
|
|
154
171
|
call.reject("INSECURE_URL", "Download URL must use HTTPS")
|
|
155
172
|
return@launch
|
|
156
173
|
}
|
|
157
|
-
|
|
158
|
-
val bundleId = "bundle-${System.currentTimeMillis()}"
|
|
159
|
-
val downloadDir = File(context.filesDir, "updates/$bundleId")
|
|
174
|
+
|
|
160
175
|
downloadDir.mkdirs()
|
|
161
|
-
|
|
162
|
-
// Start download
|
|
176
|
+
|
|
163
177
|
val downloadedFile = downloadBundle(url, downloadDir, bundleId)
|
|
164
|
-
|
|
165
|
-
// Verify checksum
|
|
178
|
+
|
|
166
179
|
if (!verifyChecksum(downloadedFile, checksum)) {
|
|
167
|
-
downloadedFile.delete()
|
|
168
180
|
call.reject("CHECKSUM_ERROR", "Bundle checksum validation failed")
|
|
169
181
|
return@launch
|
|
170
182
|
}
|
|
171
|
-
|
|
172
|
-
// Verify signature if provided
|
|
183
|
+
|
|
173
184
|
val signature = call.getString("signature")
|
|
174
185
|
if (signature != null) {
|
|
175
186
|
val securityConfig = config?.getJSObject("security")
|
|
176
187
|
val publicKey = securityConfig?.getString("publicKey")
|
|
177
188
|
val enableSignatureValidation = securityConfig?.getBoolean("enableSignatureValidation", false) ?: false
|
|
178
|
-
|
|
189
|
+
|
|
179
190
|
if (enableSignatureValidation && publicKey != null) {
|
|
180
191
|
val fileBytes = downloadedFile.readBytes()
|
|
181
192
|
if (!securityManager.verifySignature(fileBytes, signature, publicKey)) {
|
|
182
|
-
downloadedFile.delete()
|
|
183
193
|
call.reject("SIGNATURE_ERROR", "Bundle signature validation failed")
|
|
184
194
|
return@launch
|
|
185
195
|
}
|
|
186
196
|
}
|
|
187
197
|
}
|
|
188
|
-
|
|
189
|
-
//
|
|
198
|
+
|
|
199
|
+
// Extract zip into bundles/<id>/www/. Activation (i.e.
|
|
200
|
+
// pointing the WebView at it) is deferred to set() —
|
|
201
|
+
// matches iOS contract: download stages, set activates.
|
|
202
|
+
val extractedDir = File(downloadDir, "www")
|
|
203
|
+
try {
|
|
204
|
+
extractZipBundle(downloadedFile, extractedDir)
|
|
205
|
+
} catch (e: Exception) {
|
|
206
|
+
call.reject("EXTRACTION_ERROR", "Bundle extraction failed: ${e.message}")
|
|
207
|
+
return@launch
|
|
208
|
+
}
|
|
209
|
+
|
|
190
210
|
val bundleInfo = JSObject()
|
|
191
211
|
bundleInfo.put("bundleId", bundleId)
|
|
192
212
|
bundleInfo.put("version", version)
|
|
193
213
|
bundleInfo.put("path", downloadedFile.absolutePath)
|
|
214
|
+
bundleInfo.put("extractedPath", extractedDir.absolutePath)
|
|
194
215
|
bundleInfo.put("downloadTime", System.currentTimeMillis())
|
|
195
216
|
bundleInfo.put("size", downloadedFile.length())
|
|
196
217
|
bundleInfo.put("status", "READY")
|
|
197
218
|
bundleInfo.put("checksum", checksum)
|
|
198
219
|
bundleInfo.put("verified", true)
|
|
199
|
-
// Persist signature so the boot-time re-verify in
|
|
200
|
-
// verifyActiveBundleOnBoot() can run without re-downloading.
|
|
201
220
|
if (signature != null) {
|
|
202
221
|
bundleInfo.put("signature", signature)
|
|
203
222
|
}
|
|
204
|
-
|
|
205
|
-
// Save bundle info
|
|
223
|
+
|
|
206
224
|
saveBundleInfo(bundleInfo)
|
|
207
|
-
|
|
208
|
-
// Notify state change
|
|
225
|
+
|
|
209
226
|
stateChangeListener?.invoke(JSObject().apply {
|
|
210
227
|
put("status", "READY")
|
|
211
228
|
put("bundleId", bundleId)
|
|
212
229
|
put("version", version)
|
|
213
230
|
})
|
|
214
|
-
|
|
231
|
+
|
|
232
|
+
success = true
|
|
215
233
|
call.resolve(bundleInfo)
|
|
216
234
|
} catch (e: Exception) {
|
|
217
235
|
call.reject("DOWNLOAD_ERROR", e.message)
|
|
236
|
+
} finally {
|
|
237
|
+
// On any failure, remove the per-bundle directory so we
|
|
238
|
+
// don't leak partial downloads/extractions.
|
|
239
|
+
if (!success) {
|
|
240
|
+
downloadDir.deleteRecursively()
|
|
241
|
+
}
|
|
218
242
|
}
|
|
219
243
|
}
|
|
220
244
|
}
|
|
221
|
-
|
|
245
|
+
|
|
222
246
|
fun set(call: PluginCall) {
|
|
223
247
|
val bundleId = call.getString("bundleId") ?: run {
|
|
224
248
|
call.reject("Bundle ID is required")
|
|
225
249
|
return
|
|
226
250
|
}
|
|
227
|
-
|
|
228
|
-
|
|
251
|
+
|
|
252
|
+
val bundleInfo = getBundleInfoById(bundleId) ?: run {
|
|
253
|
+
call.reject("Bundle not found: $bundleId")
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
val extractedPath = bundleInfo.optString("extractedPath", "")
|
|
257
|
+
if (extractedPath.isEmpty() || !File(extractedPath).exists()) {
|
|
258
|
+
call.reject("Bundle not extracted: $bundleId")
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
val verified = bundleInfo.optBoolean("verified", false)
|
|
262
|
+
if (!verified) {
|
|
263
|
+
call.reject("Refusing to activate unverified bundle: $bundleId")
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
229
267
|
setActiveBundle(bundleId)
|
|
268
|
+
// Hop to UI thread; setServerBasePath must run on the WebView's
|
|
269
|
+
// thread per Capacitor's Bridge contract.
|
|
270
|
+
activity.runOnUiThread {
|
|
271
|
+
webViewPathListener?.invoke(extractedPath)
|
|
272
|
+
}
|
|
230
273
|
call.resolve()
|
|
231
274
|
}
|
|
232
275
|
|
|
233
276
|
fun reload(call: PluginCall) {
|
|
234
|
-
//
|
|
277
|
+
// Re-applying the active bundle's serverBasePath causes the
|
|
278
|
+
// WebView to reload from the new asset root. activity.recreate()
|
|
279
|
+
// also works but is heavier and can drop transient state.
|
|
280
|
+
val activeBundleId = context
|
|
281
|
+
.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE)
|
|
282
|
+
.getString(ACTIVE_BUNDLE_KEY, null)
|
|
283
|
+
if (activeBundleId != null) {
|
|
284
|
+
val info = getBundleInfoById(activeBundleId)
|
|
285
|
+
val extractedPath = info?.optString("extractedPath", "") ?: ""
|
|
286
|
+
if (extractedPath.isNotEmpty() && File(extractedPath).exists()) {
|
|
287
|
+
activity.runOnUiThread {
|
|
288
|
+
webViewPathListener?.invoke(extractedPath)
|
|
289
|
+
}
|
|
290
|
+
call.resolve()
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
}
|
|
235
294
|
activity.runOnUiThread {
|
|
236
295
|
activity.recreate()
|
|
237
296
|
}
|
|
@@ -424,6 +483,82 @@ class LiveUpdatePlugin(
|
|
|
424
483
|
}
|
|
425
484
|
}
|
|
426
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Extract a zip into [destination] with three defenses applied:
|
|
488
|
+
* 1. Zip-Slip: every entry's resolved canonical path must stay
|
|
489
|
+
* inside [destination]. Catches `../` traversal.
|
|
490
|
+
* 2. Symlinks/special: entries that are not regular files or
|
|
491
|
+
* directories are skipped (no symlinks, devices, etc.).
|
|
492
|
+
* 3. Zip-bomb: total uncompressed size is capped.
|
|
493
|
+
*
|
|
494
|
+
* Mirrors `extractZipBundle` in `ios/Plugin/LiveUpdate/LiveUpdatePlugin.swift`.
|
|
495
|
+
*/
|
|
496
|
+
private fun extractZipBundle(zipFile: File, destination: File) {
|
|
497
|
+
if (!destination.exists()) {
|
|
498
|
+
destination.mkdirs()
|
|
499
|
+
}
|
|
500
|
+
val canonicalDest = destination.canonicalFile
|
|
501
|
+
|
|
502
|
+
ZipFile(zipFile).use { archive ->
|
|
503
|
+
// Pre-flight: reject the archive entirely if it would expand
|
|
504
|
+
// beyond our cap, before we write any bytes to disk.
|
|
505
|
+
var totalUncompressed = 0L
|
|
506
|
+
for (entry in archive.entries()) {
|
|
507
|
+
if (entry.size < 0) {
|
|
508
|
+
throw IOException("Zip entry has unknown uncompressed size: ${entry.name}")
|
|
509
|
+
}
|
|
510
|
+
totalUncompressed += entry.size
|
|
511
|
+
if (totalUncompressed > maxUncompressedBundleBytes) {
|
|
512
|
+
throw IOException(
|
|
513
|
+
"Bundle exceeds maximum uncompressed size " +
|
|
514
|
+
"($maxUncompressedBundleBytes bytes)"
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
for (entry in archive.entries()) {
|
|
520
|
+
// Reject entries with absolute paths or backreferences.
|
|
521
|
+
val name = entry.name
|
|
522
|
+
if (name.startsWith("/") || name.startsWith("\\") || name.contains("..")) {
|
|
523
|
+
throw IOException("Zip entry has unsafe path: $name")
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
val outFile = File(destination, name)
|
|
527
|
+
val canonicalOut = outFile.canonicalFile
|
|
528
|
+
if (!canonicalOut.path.startsWith(canonicalDest.path + File.separator)
|
|
529
|
+
&& canonicalOut.path != canonicalDest.path
|
|
530
|
+
) {
|
|
531
|
+
throw IOException("Zip-Slip detected: $name escapes ${canonicalDest.path}")
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (entry.isDirectory) {
|
|
535
|
+
if (!outFile.exists() && !outFile.mkdirs()) {
|
|
536
|
+
throw IOException("Failed to create directory ${outFile.path}")
|
|
537
|
+
}
|
|
538
|
+
continue
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Java's ZipFile API exposes only ZipEntry, which can't
|
|
542
|
+
// distinguish symlinks from regular files at this level.
|
|
543
|
+
// We treat every non-directory entry as a regular file.
|
|
544
|
+
// Symlinks created by `zip --symlinks` are NOT a concern
|
|
545
|
+
// because ZipFile reads the symlink target as the entry
|
|
546
|
+
// body — it won't follow it during extraction.
|
|
547
|
+
outFile.parentFile?.mkdirs()
|
|
548
|
+
|
|
549
|
+
archive.getInputStream(entry).use { input ->
|
|
550
|
+
FileOutputStream(outFile).use { output ->
|
|
551
|
+
val buffer = ByteArray(8192)
|
|
552
|
+
var read: Int
|
|
553
|
+
while (input.read(buffer).also { read = it } != -1) {
|
|
554
|
+
output.write(buffer, 0, read)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
427
562
|
private fun verifyChecksum(file: File, expectedChecksum: String): Boolean {
|
|
428
563
|
val digest = MessageDigest.getInstance("SHA-256")
|
|
429
564
|
file.inputStream().use { input ->
|
|
@@ -574,6 +709,24 @@ class LiveUpdatePlugin(
|
|
|
574
709
|
* updateStateChanged event with status "ROLLBACK" so the host app and
|
|
575
710
|
* analytics layer can observe it.
|
|
576
711
|
*/
|
|
712
|
+
/**
|
|
713
|
+
* Restore the WebView's serverBasePath from SharedPreferences so the
|
|
714
|
+
* last-set bundle survives process death.
|
|
715
|
+
*/
|
|
716
|
+
fun applyActiveBundlePathIfPresent() {
|
|
717
|
+
val activeBundleId = context
|
|
718
|
+
.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE)
|
|
719
|
+
.getString(ACTIVE_BUNDLE_KEY, null) ?: return
|
|
720
|
+
if (activeBundleId == "default") return
|
|
721
|
+
val info = getBundleInfoById(activeBundleId) ?: return
|
|
722
|
+
val extractedPath = info.optString("extractedPath", "")
|
|
723
|
+
if (extractedPath.isEmpty()) return
|
|
724
|
+
if (!File(extractedPath).exists()) return
|
|
725
|
+
activity.runOnUiThread {
|
|
726
|
+
webViewPathListener?.invoke(extractedPath)
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
577
730
|
fun verifyActiveBundleOnBoot() {
|
|
578
731
|
val prefs = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE)
|
|
579
732
|
|
|
@@ -642,6 +795,20 @@ class LiveUpdatePlugin(
|
|
|
642
795
|
}
|
|
643
796
|
editor.remove(PREVIOUS_BUNDLE_KEY).apply()
|
|
644
797
|
|
|
798
|
+
// Re-point the WebView at whatever we rolled back to. If we
|
|
799
|
+
// rolled all the way back to the bundled-in www/ (no previous
|
|
800
|
+
// OTA bundle), no listener call is needed — Capacitor's default
|
|
801
|
+
// serverBasePath is already correct.
|
|
802
|
+
if (previous != null) {
|
|
803
|
+
val info = getBundleInfoById(previous)
|
|
804
|
+
val extractedPath = info?.optString("extractedPath", "") ?: ""
|
|
805
|
+
if (extractedPath.isNotEmpty() && File(extractedPath).exists()) {
|
|
806
|
+
activity.runOnUiThread {
|
|
807
|
+
webViewPathListener?.invoke(extractedPath)
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
645
812
|
stateChangeListener?.invoke(JSObject().apply {
|
|
646
813
|
put("status", "ROLLBACK")
|
|
647
814
|
put("bundleId", fromBundleId)
|
|
@@ -5,18 +5,11 @@ import com.getcapacitor.Plugin
|
|
|
5
5
|
import com.getcapacitor.PluginCall
|
|
6
6
|
import com.getcapacitor.PluginMethod
|
|
7
7
|
import com.getcapacitor.annotation.CapacitorPlugin
|
|
8
|
-
import com.getcapacitor.annotation.Permission
|
|
9
|
-
import android.Manifest
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
strings = [Manifest.permission.WRITE_EXTERNAL_STORAGE],
|
|
16
|
-
alias = "storage"
|
|
17
|
-
)
|
|
18
|
-
]
|
|
19
|
-
)
|
|
9
|
+
// Plugin writes only to context.filesDir (the app sandbox), so it does
|
|
10
|
+
// not need WRITE_EXTERNAL_STORAGE. Declaring an unused dangerous
|
|
11
|
+
// permission is a Play Store rejection trap.
|
|
12
|
+
@CapacitorPlugin(name = "NativeUpdate")
|
|
20
13
|
class NativeUpdatePlugin : Plugin() {
|
|
21
14
|
|
|
22
15
|
private lateinit var liveUpdatePlugin: LiveUpdatePlugin
|
|
@@ -54,18 +47,31 @@ class NativeUpdatePlugin : Plugin() {
|
|
|
54
47
|
notifyListeners("updateStateChanged", state)
|
|
55
48
|
}
|
|
56
49
|
|
|
50
|
+
// Wire the WebView retargeting hook. set() calls this with the
|
|
51
|
+
// extracted bundle's www/ path; we pass it to the Capacitor
|
|
52
|
+
// Bridge so subsequent loads serve the new bundle.
|
|
53
|
+
liveUpdatePlugin.setWebViewPathListener { path ->
|
|
54
|
+
bridge?.setServerBasePath(path)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
57
|
// Boot-time integrity re-verify + crash-loop auto-rollback. Runs
|
|
58
58
|
// before any host-app code touches the WebView so a tampered or
|
|
59
59
|
// crash-looping bundle never gets a chance to load.
|
|
60
|
-
// Signature re-verify runs again after configure() once the
|
|
61
|
-
// publicKey arrives from the host app; checksum re-verify here
|
|
62
|
-
// is sufficient to catch most tampering.
|
|
63
60
|
try {
|
|
64
61
|
liveUpdatePlugin.verifyActiveBundleOnBoot()
|
|
65
62
|
} catch (e: Exception) {
|
|
66
63
|
// Never let boot verification crash the plugin itself — fall
|
|
67
64
|
// through and let a later sync() surface the problem.
|
|
68
65
|
}
|
|
66
|
+
|
|
67
|
+
// Re-apply the active bundle's path so the WebView serves the
|
|
68
|
+
// last-set OTA bundle on cold start (otherwise process death
|
|
69
|
+
// would silently revert to the bundled-in www/).
|
|
70
|
+
try {
|
|
71
|
+
liveUpdatePlugin.applyActiveBundlePathIfPresent()
|
|
72
|
+
} catch (e: Exception) {
|
|
73
|
+
// Same rationale as above.
|
|
74
|
+
}
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
@PluginMethod
|
|
@@ -158,10 +164,7 @@ class NativeUpdatePlugin : Plugin() {
|
|
|
158
164
|
|
|
159
165
|
@PluginMethod
|
|
160
166
|
fun download(call: PluginCall) {
|
|
161
|
-
|
|
162
|
-
requestPermissionForAlias("storage", call, "handleStoragePermission")
|
|
163
|
-
return
|
|
164
|
-
}
|
|
167
|
+
// No runtime permission required — bundle storage is in app sandbox.
|
|
165
168
|
liveUpdatePlugin.download(call)
|
|
166
169
|
}
|
|
167
170
|
|
|
@@ -307,22 +310,4 @@ class NativeUpdatePlugin : Plugin() {
|
|
|
307
310
|
backgroundUpdatePlugin.requestNotificationPermissions(call)
|
|
308
311
|
}
|
|
309
312
|
|
|
310
|
-
// Permission Callbacks
|
|
311
|
-
|
|
312
|
-
@PluginMethod
|
|
313
|
-
fun handleStoragePermission(call: PluginCall) {
|
|
314
|
-
if (hasPermission("storage")) {
|
|
315
|
-
// Retry the original method
|
|
316
|
-
when (call.methodName) {
|
|
317
|
-
"download" -> liveUpdatePlugin.download(call)
|
|
318
|
-
else -> call.reject("Unknown method")
|
|
319
|
-
}
|
|
320
|
-
} else {
|
|
321
|
-
call.reject("Storage permission denied")
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
override fun hasRequiredPermissions(): Boolean {
|
|
326
|
-
return hasPermission("storage")
|
|
327
|
-
}
|
|
328
313
|
}
|
package/cli/AGENTS.md
CHANGED
|
@@ -5,6 +5,15 @@ Agent instructions for the native-update CLI tool.
|
|
|
5
5
|
## 🔴 3-Day Freshness Rule
|
|
6
6
|
Check and update this file at least every 3 days. See root AGENTS.md.
|
|
7
7
|
|
|
8
|
+
## Package Manager Hierarchy: nvm → npm (global) → yarn (local) (IRON-SOLID)
|
|
9
|
+
|
|
10
|
+
Three tiers, each tool ONLY for its tier — for the best, most reproducible dev results:
|
|
11
|
+
- **`nvm`** → install/update Node.js (which bundles `npm`): `nvm install --lts`. Use nvm to get/update `npm` itself.
|
|
12
|
+
- **`npm`** → ALL global packages: `npm install -g yarn` (install yarn globally if missing) + `npm install -g <pkg>` (every other global CLI).
|
|
13
|
+
- **`yarn`** → ALL local project work: `yarn`, `yarn add <pkg>`, `yarn add -D <pkg>` inside the project.
|
|
14
|
+
|
|
15
|
+
❌ NEVER use `npm`/`pnpm` for LOCAL installs. NEVER use `pnpm` at all. ✅ Only `yarn.lock` in the project — delete `package-lock.json` and `pnpm-lock.yaml`.
|
|
16
|
+
|
|
8
17
|
## Scope
|
|
9
18
|
|
|
10
19
|
Node.js CLI for release management, bundle operations, and configuration. Distributed as part of the npm package.
|
|
@@ -26,4 +35,9 @@ Node.js CLI for release management, bundle operations, and configuration. Distri
|
|
|
26
35
|
|
|
27
36
|
---
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
## Gitignore Hygiene (IRON-SOLID)
|
|
39
|
+
`.gitignore` stays current with the project structure — ignore only recoverable artifacts (build/`dist`/`www`/`node_modules`/logs/caches/IDE), never lose source. Custom rules always present: `*.ignore.*`, `project-record-ignore/`. This is a **PRIVATE** repo -> `.env`/secrets/keystores ARE tracked in git. Capacitor: track `android`/`ios` source, ignore native build.
|
|
40
|
+
Full rule + private/public protocol: `~/.claude/rules/project-config.md`.
|
|
41
|
+
Gitignore Last Verified: 2026-06-24
|
|
42
|
+
|
|
43
|
+
**Last Updated**: 2026-04-25
|