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.
Files changed (258) hide show
  1. package/Readme.md +28 -14
  2. package/android/manifest-additions.xml +28 -0
  3. package/android/src/main/AndroidManifest.xml +3 -7
  4. package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundNotificationManager.kt +21 -13
  5. package/android/src/main/java/com/aoneahsan/nativeupdate/LiveUpdatePlugin.kt +196 -29
  6. package/android/src/main/java/com/aoneahsan/nativeupdate/NativeUpdatePlugin.kt +21 -36
  7. package/cli/AGENTS.md +15 -1
  8. package/cli/CLAUDE.md +15 -1
  9. package/cli/commands/backend-create.js +56 -15
  10. package/cli/commands/bundle-create.js +39 -7
  11. package/cli/commands/monitor.js +72 -51
  12. package/cli/index.js +10 -24
  13. package/cli/package.json +11 -3
  14. package/dist/esm/app-review/app-review-manager.js +0 -1
  15. package/dist/esm/app-review/index.js +0 -1
  16. package/dist/esm/app-review/platform-review-handler.js +0 -1
  17. package/dist/esm/app-review/review-conditions-checker.js +0 -1
  18. package/dist/esm/app-review/review-rate-limiter.js +0 -1
  19. package/dist/esm/app-review/types.js +0 -1
  20. package/dist/esm/app-update/app-update-checker.js +0 -1
  21. package/dist/esm/app-update/app-update-installer.d.ts +1 -0
  22. package/dist/esm/app-update/app-update-installer.js +19 -8
  23. package/dist/esm/app-update/app-update-manager.js +0 -1
  24. package/dist/esm/app-update/app-update-notifier.js +0 -1
  25. package/dist/esm/app-update/index.js +0 -1
  26. package/dist/esm/app-update/platform-app-update.js +12 -3
  27. package/dist/esm/app-update/types.js +0 -1
  28. package/dist/esm/background-update/background-scheduler.js +0 -1
  29. package/dist/esm/background-update/index.js +0 -1
  30. package/dist/esm/background-update/notification-manager.js +0 -1
  31. package/dist/esm/config/support.js +0 -1
  32. package/dist/esm/core/analytics.d.ts +13 -0
  33. package/dist/esm/core/analytics.js +15 -1
  34. package/dist/esm/core/cache-manager.js +0 -1
  35. package/dist/esm/core/config.d.ts +0 -15
  36. package/dist/esm/core/config.js +0 -4
  37. package/dist/esm/core/errors.d.ts +1 -0
  38. package/dist/esm/core/errors.js +1 -1
  39. package/dist/esm/core/event-emitter.d.ts +0 -3
  40. package/dist/esm/core/event-emitter.js +8 -3
  41. package/dist/esm/core/logger.js +0 -1
  42. package/dist/esm/core/performance.js +0 -1
  43. package/dist/esm/core/plugin-manager.js +3 -1
  44. package/dist/esm/core/security.d.ts +11 -0
  45. package/dist/esm/core/security.js +27 -1
  46. package/dist/esm/definitions.d.ts +0 -11
  47. package/dist/esm/definitions.js +0 -1
  48. package/dist/esm/index.d.ts +3 -2
  49. package/dist/esm/index.js +3 -1
  50. package/dist/esm/live-update/bundle-manager.d.ts +8 -0
  51. package/dist/esm/live-update/bundle-manager.js +20 -1
  52. package/dist/esm/live-update/certificate-pinning.js +0 -1
  53. package/dist/esm/live-update/delta-processor.d.ts +1 -1
  54. package/dist/esm/live-update/delta-processor.js +3 -1
  55. package/dist/esm/live-update/download-manager.js +9 -2
  56. package/dist/esm/live-update/rollout-checker.d.ts +1 -1
  57. package/dist/esm/live-update/rollout-checker.js +5 -6
  58. package/dist/esm/live-update/update-manager.js +0 -1
  59. package/dist/esm/live-update/version-manager.d.ts +0 -12
  60. package/dist/esm/live-update/version-manager.js +0 -68
  61. package/dist/esm/plugin.js +69 -43
  62. package/dist/esm/security/crypto.js +11 -7
  63. package/dist/esm/security/validator.js +0 -1
  64. package/dist/esm/types/rollout.d.ts +85 -0
  65. package/dist/esm/types/rollout.js +25 -0
  66. package/dist/esm/web.d.ts +8 -0
  67. package/dist/esm/web.js +148 -54
  68. package/dist/plugin.cjs.js +1 -2
  69. package/dist/plugin.esm.js +1 -2
  70. package/dist/plugin.js +2 -3
  71. package/docs/CHANGELOG.md +124 -0
  72. package/docs/CLAUDE.md +1 -1
  73. package/docs/MANUAL-TASKS.md +17 -0
  74. package/docs/MARKETING_WEBSITE_TRACKER.md +18 -2
  75. package/docs/MIGRATION.md +83 -0
  76. package/docs/PACKAGES.md +167 -0
  77. package/docs/PERFORMANCE.md +110 -0
  78. package/docs/PROJECT_COMPLETION_TRACKER.md +4 -35
  79. package/docs/README.md +0 -1
  80. package/docs/REMAINING_FEATURES.md +123 -76
  81. package/docs/ROADMAP.md +200 -120
  82. package/docs/SECURITY-IF-REPO-PUBLIC.md +232 -0
  83. package/docs/TESTING_REQUIREMENTS.md +1 -112
  84. package/docs/ai-knowledge-base/00-project-profile.md +38 -0
  85. package/docs/ai-knowledge-base/01-features.md +46 -0
  86. package/docs/ai-knowledge-base/02-routes.md +72 -0
  87. package/docs/ai-knowledge-base/03-tech-stack.md +57 -0
  88. package/docs/ai-knowledge-base/README.md +23 -0
  89. package/docs/analytics-tracking.md +37 -0
  90. package/docs/capacitor-official-plugins.md +46 -0
  91. package/docs/capawesome-plugins.md +29 -0
  92. package/docs/deployment/HOSTINGER_DEPLOY.md +75 -34
  93. package/docs/deployment/PRODUCTION-DEPLOY-CHECKLIST.md +184 -0
  94. package/docs/docs-site/plan.md +201 -0
  95. package/docs/docs-site/scope.md +127 -0
  96. package/docs/docs-site/tracker.json +235 -0
  97. package/docs/error-handling-tracking.md +37 -0
  98. package/docs/guides/testing-guide.md +15 -41
  99. package/docs/package-audit.md +41 -0
  100. package/docs/play-store/ASO-METADATA.md +5 -0
  101. package/docs/play-store/DATA-SAFETY.md +5 -0
  102. package/docs/play-store/DECLARATIONS.md +5 -0
  103. package/docs/play-store/README.md +27 -0
  104. package/docs/play-store/RELEASE-NOTES.md +99 -0
  105. package/docs/play-store/RELEASE-STEPS.md +6 -0
  106. package/docs/play-store/STORE-LISTING.md +7 -0
  107. package/docs/play-store/SUBMISSION-CHECKLIST.md +5 -0
  108. package/docs/production-readiness.md +1 -1
  109. package/docs/project-audit/2026-05-16/00-FINALIZATION-PLAN.md +123 -0
  110. package/docs/project-audit/2026-05-16/01-BATCH-PLAN.md +227 -0
  111. package/docs/project-audit/2026-05-16/02-AUDIT-FINDINGS.md +144 -0
  112. package/docs/project-audit/2026-05-16/sessions/batch-01.md +42 -0
  113. package/docs/project-audit/2026-05-16/sessions/batch-02.md +74 -0
  114. package/docs/project-audit/2026-05-16/sessions/batch-03.md +71 -0
  115. package/docs/project-audit/2026-05-16/sessions/batch-04.md +88 -0
  116. package/docs/project-audit/2026-05-16/sessions/batch-05.md +63 -0
  117. package/docs/project-audit/2026-05-16/sessions/batch-06.md +103 -0
  118. package/docs/project-audit/2026-05-16/sessions/batch-07.md +61 -0
  119. package/docs/project-audit/2026-05-16/sessions/batch-08.md +101 -0
  120. package/docs/project-audit/2026-05-16/sessions/batch-09.md +105 -0
  121. package/docs/project-audit/2026-05-16/sessions/batch-10.md +102 -0
  122. package/docs/project-audit/AUDIT-HISTORY.md +74 -0
  123. package/docs/project-audit/BATCH-PLAN.md +168 -0
  124. package/docs/project-audit/MARKETING-WEBSITE-FINDINGS-2026-05-11.md +73 -0
  125. package/docs/project-audit/PRODUCTION-READINESS-AUDIT-2026-05-08.md +275 -0
  126. package/docs/project-finalization/00-tracker.json +60 -0
  127. package/docs/project-knowledge-base/01-system-overview.md +39 -30
  128. package/docs/project-knowledge-base/02-routes-pages-forms-users.md +87 -81
  129. package/docs/project-knowledge-base/03-tech-stack-modules-services.md +140 -108
  130. package/docs/project-knowledge-base/04-data-models-integrations.md +191 -254
  131. package/docs/project-knowledge-base/05-docs-corpus-inventory.md +83 -20
  132. package/docs/project-knowledge-base/06-operations-testing-legal-content.md +67 -29
  133. package/docs/project-knowledge-base/README.md +27 -14
  134. package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-05-19.md +81 -0
  135. package/docs/rules-tracker.md +102 -0
  136. package/docs/security/PRE-LAUNCH-PENTEST-CHECKLIST.md +116 -0
  137. package/docs/social-media-marketing/README.md +81 -0
  138. package/docs/social-media-marketing/posts-batch-01.md +613 -0
  139. package/docs/social-media-marketing/posts-batch-02.md +528 -0
  140. package/docs/social-media-marketing/whatsapp-messages-batch-01.md +458 -0
  141. package/docs/social-media-marketing/whatsapp-status-batch-01.md +355 -0
  142. package/docs/tracking/finalization-2026-05-16-tracker.json +346 -0
  143. package/docs/tracking/full-audit-tracker.json +121 -0
  144. package/docs/tracking/production-readiness-audit-tracker.json +429 -0
  145. package/ios/Plugin/BackgroundUpdate/BackgroundUpdatePlugin.swift +16 -3
  146. package/ios/Plugin/LiveUpdate/LiveUpdatePlugin.swift +71 -47
  147. package/ios/Plugin/NativeUpdatePlugin.swift +14 -9
  148. package/package.json +29 -29
  149. package/cli/node_modules/.yarn-integrity +0 -16
  150. package/cli/node_modules/commander/LICENSE +0 -22
  151. package/cli/node_modules/commander/Readme.md +0 -1148
  152. package/cli/node_modules/commander/esm.mjs +0 -16
  153. package/cli/node_modules/commander/index.js +0 -26
  154. package/cli/node_modules/commander/lib/argument.js +0 -145
  155. package/cli/node_modules/commander/lib/command.js +0 -2179
  156. package/cli/node_modules/commander/lib/error.js +0 -43
  157. package/cli/node_modules/commander/lib/help.js +0 -462
  158. package/cli/node_modules/commander/lib/option.js +0 -329
  159. package/cli/node_modules/commander/lib/suggestSimilar.js +0 -100
  160. package/cli/node_modules/commander/package-support.json +0 -16
  161. package/cli/node_modules/commander/package.json +0 -80
  162. package/cli/node_modules/commander/typings/esm.d.mts +0 -3
  163. package/cli/node_modules/commander/typings/index.d.ts +0 -884
  164. package/cli/yarn.lock +0 -8
  165. package/dist/esm/__tests__/bundle-manager.test.d.ts +0 -1
  166. package/dist/esm/__tests__/bundle-manager.test.js +0 -151
  167. package/dist/esm/__tests__/bundle-manager.test.js.map +0 -1
  168. package/dist/esm/__tests__/config.test.d.ts +0 -1
  169. package/dist/esm/__tests__/config.test.js +0 -70
  170. package/dist/esm/__tests__/config.test.js.map +0 -1
  171. package/dist/esm/__tests__/delta-processor.test.d.ts +0 -1
  172. package/dist/esm/__tests__/delta-processor.test.js +0 -77
  173. package/dist/esm/__tests__/delta-processor.test.js.map +0 -1
  174. package/dist/esm/__tests__/firestore-schema.test.d.ts +0 -1
  175. package/dist/esm/__tests__/firestore-schema.test.js +0 -74
  176. package/dist/esm/__tests__/firestore-schema.test.js.map +0 -1
  177. package/dist/esm/__tests__/integration.test.d.ts +0 -1
  178. package/dist/esm/__tests__/integration.test.js +0 -78
  179. package/dist/esm/__tests__/integration.test.js.map +0 -1
  180. package/dist/esm/__tests__/manifest-reader.test.d.ts +0 -1
  181. package/dist/esm/__tests__/manifest-reader.test.js +0 -271
  182. package/dist/esm/__tests__/manifest-reader.test.js.map +0 -1
  183. package/dist/esm/__tests__/rollout-checker.test.d.ts +0 -1
  184. package/dist/esm/__tests__/rollout-checker.test.js +0 -210
  185. package/dist/esm/__tests__/rollout-checker.test.js.map +0 -1
  186. package/dist/esm/__tests__/security-enforcement.test.d.ts +0 -1
  187. package/dist/esm/__tests__/security-enforcement.test.js +0 -95
  188. package/dist/esm/__tests__/security-enforcement.test.js.map +0 -1
  189. package/dist/esm/__tests__/security.test.d.ts +0 -1
  190. package/dist/esm/__tests__/security.test.js +0 -54
  191. package/dist/esm/__tests__/security.test.js.map +0 -1
  192. package/dist/esm/__tests__/version-manager.test.d.ts +0 -1
  193. package/dist/esm/__tests__/version-manager.test.js +0 -45
  194. package/dist/esm/__tests__/version-manager.test.js.map +0 -1
  195. package/dist/esm/app-review/app-review-manager.js.map +0 -1
  196. package/dist/esm/app-review/index.js.map +0 -1
  197. package/dist/esm/app-review/platform-review-handler.js.map +0 -1
  198. package/dist/esm/app-review/review-conditions-checker.js.map +0 -1
  199. package/dist/esm/app-review/review-rate-limiter.js.map +0 -1
  200. package/dist/esm/app-review/types.js.map +0 -1
  201. package/dist/esm/app-update/app-update-checker.js.map +0 -1
  202. package/dist/esm/app-update/app-update-installer.js.map +0 -1
  203. package/dist/esm/app-update/app-update-manager.js.map +0 -1
  204. package/dist/esm/app-update/app-update-notifier.js.map +0 -1
  205. package/dist/esm/app-update/index.js.map +0 -1
  206. package/dist/esm/app-update/platform-app-update.js.map +0 -1
  207. package/dist/esm/app-update/types.js.map +0 -1
  208. package/dist/esm/background-update/background-scheduler.js.map +0 -1
  209. package/dist/esm/background-update/index.js.map +0 -1
  210. package/dist/esm/background-update/notification-manager.js.map +0 -1
  211. package/dist/esm/config/support.js.map +0 -1
  212. package/dist/esm/core/analytics.js.map +0 -1
  213. package/dist/esm/core/cache-manager.js.map +0 -1
  214. package/dist/esm/core/config.js.map +0 -1
  215. package/dist/esm/core/errors.js.map +0 -1
  216. package/dist/esm/core/event-emitter.js.map +0 -1
  217. package/dist/esm/core/logger.js.map +0 -1
  218. package/dist/esm/core/performance.js.map +0 -1
  219. package/dist/esm/core/plugin-manager.js.map +0 -1
  220. package/dist/esm/core/security.js.map +0 -1
  221. package/dist/esm/definitions.js.map +0 -1
  222. package/dist/esm/firestore/firestore-client.d.ts +0 -109
  223. package/dist/esm/firestore/firestore-client.js +0 -264
  224. package/dist/esm/firestore/firestore-client.js.map +0 -1
  225. package/dist/esm/firestore/index.d.ts +0 -11
  226. package/dist/esm/firestore/index.js +0 -11
  227. package/dist/esm/firestore/index.js.map +0 -1
  228. package/dist/esm/firestore/manifest-reader.d.ts +0 -87
  229. package/dist/esm/firestore/manifest-reader.js +0 -294
  230. package/dist/esm/firestore/manifest-reader.js.map +0 -1
  231. package/dist/esm/firestore/schema.d.ts +0 -507
  232. package/dist/esm/firestore/schema.js +0 -73
  233. package/dist/esm/firestore/schema.js.map +0 -1
  234. package/dist/esm/index.js.map +0 -1
  235. package/dist/esm/live-update/bundle-manager.js.map +0 -1
  236. package/dist/esm/live-update/certificate-pinning.js.map +0 -1
  237. package/dist/esm/live-update/delta-processor.js.map +0 -1
  238. package/dist/esm/live-update/download-manager.js.map +0 -1
  239. package/dist/esm/live-update/rollout-checker.js.map +0 -1
  240. package/dist/esm/live-update/update-manager.js.map +0 -1
  241. package/dist/esm/live-update/version-manager.js.map +0 -1
  242. package/dist/esm/plugin.js.map +0 -1
  243. package/dist/esm/security/crypto.js.map +0 -1
  244. package/dist/esm/security/validator.js.map +0 -1
  245. package/dist/esm/web.js.map +0 -1
  246. package/dist/plugin.cjs.js.map +0 -1
  247. package/dist/plugin.esm.js.map +0 -1
  248. package/dist/plugin.js.map +0 -1
  249. package/docs/FIREBASE_INTEGRATION_TRACKER.md +0 -321
  250. package/docs/FIREBASE_QUERIES_AND_INDEXES_AUDIT.md +0 -221
  251. package/docs/examples/firebase-backend-example.md +0 -27
  252. package/docs/features/laravel-nova-backend/credentials.ignore.md +0 -34
  253. package/docs/guides/no-cost-backend-implementation-plan.md +0 -77
  254. package/docs/guides/no-cost-firestore-google-drive-backend.md +0 -60
  255. package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-03-16.md +0 -454
  256. package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-03-24.md +0 -66
  257. package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-03-25.md +0 -67
  258. /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.4.9`
19
- - Verified on: `2026-03-25`
20
- - Tests: `81/81` passing via `yarn test:run`
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-03-25.md`
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-03-25.md`
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. Firebase Functions Backend
371
+ #### 2. Laravel + Nova SaaS Backend
372
372
 
373
- **[Firebase Backend Example](https://nativeupdate.aoneahsan.com/docs/examples/firebase-backend-example)**
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
- - ✅ Serverless architecture with Firebase
376
- - ✅ Cloud Functions for bundle management
377
- - ✅ Firestore for bundle metadata
378
- - ✅ Firebase Storage for bundle hosting
379
- - ✅ Auto-scaling infrastructure
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
- private const val ACTION_UPDATE_NOW = "com.aoneahsan.nativeupdate.UPDATE_NOW"
26
- private const val ACTION_UPDATE_LATER = "com.aoneahsan.nativeupdate.UPDATE_LATER"
27
- private const val ACTION_DISMISS = "com.aoneahsan.nativeupdate.DISMISS"
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
- // Update Now action
263
- val updateNowIntent = Intent(ACTION_UPDATE_NOW).apply {
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
- // Update Later action
280
- val updateLaterIntent = Intent(ACTION_UPDATE_LATER)
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
- // Dismiss action
295
- val dismissIntent = Intent(ACTION_DISMISS)
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
- // Create bundle info
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
- // Set active bundle
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
- // In Android, we need to restart the activity or reload the WebView
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
- @CapacitorPlugin(
12
- name = "NativeUpdate",
13
- permissions = [
14
- Permission(
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
- if (!hasRequiredPermissions()) {
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
- **Last Updated**: 2026-04-02
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