native-update 1.4.9 → 2.0.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 +13 -1
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdatePlugin.kt +15 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdateWorker.kt +23 -7
- package/android/src/main/java/com/aoneahsan/nativeupdate/LiveUpdatePlugin.kt +152 -4
- package/android/src/main/java/com/aoneahsan/nativeupdate/NativeUpdatePlugin.kt +14 -1
- package/android/src/main/java/com/aoneahsan/nativeupdate/NotificationActionReceiver.kt +10 -1
- package/android/src/main/java/com/aoneahsan/nativeupdate/SecurityManager.kt +18 -18
- package/cli/AGENTS.md +29 -0
- package/cli/CLAUDE.md +51 -0
- package/dist/esm/__tests__/security-enforcement.test.d.ts +1 -0
- package/dist/esm/__tests__/security-enforcement.test.js +95 -0
- package/dist/esm/__tests__/security-enforcement.test.js.map +1 -0
- package/dist/esm/core/config.d.ts +6 -15
- package/dist/esm/core/config.js +1 -4
- package/dist/esm/core/config.js.map +1 -1
- package/dist/esm/core/security.d.ts +11 -3
- package/dist/esm/core/security.js +19 -6
- package/dist/esm/core/security.js.map +1 -1
- package/dist/esm/definitions.d.ts +13 -29
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +0 -2
- package/dist/esm/index.js +0 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/live-update/download-manager.d.ts +36 -5
- package/dist/esm/live-update/download-manager.js +61 -22
- package/dist/esm/live-update/download-manager.js.map +1 -1
- package/dist/esm/live-update/update-manager.d.ts +12 -1
- package/dist/esm/live-update/update-manager.js +38 -10
- package/dist/esm/live-update/update-manager.js.map +1 -1
- package/dist/esm/live-update/version-manager.d.ts +9 -0
- package/dist/esm/live-update/version-manager.js +40 -0
- package/dist/esm/live-update/version-manager.js.map +1 -1
- package/dist/esm/plugin.js +13 -46
- package/dist/esm/plugin.js.map +1 -1
- package/dist/esm/web.d.ts +18 -1
- package/dist/esm/web.js +69 -24
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +1 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.esm.js +1 -1
- package/dist/plugin.esm.js.map +1 -1
- package/dist/plugin.js +2 -2
- package/dist/plugin.js.map +1 -1
- package/docs/AGENTS.md +38 -0
- package/docs/CHANGELOG.md +151 -0
- package/docs/CLAUDE.md +101 -0
- package/docs/MIGRATION.md +87 -0
- package/docs/README.md +13 -2
- package/docs/deployment/HOSTINGER_DEPLOY.md +329 -0
- package/docs/features/laravel-nova-backend/ASSESSMENT-SUMMARY.md +96 -0
- package/docs/features/laravel-nova-backend/IMPLEMENTATION-PLAN.md +504 -0
- package/docs/features/laravel-nova-backend/credentials.ignore.md +34 -0
- package/docs/features/laravel-nova-backend/progress-tracker.json +184 -0
- package/docs/project-knowledge-base/01-system-overview.md +218 -0
- package/docs/project-knowledge-base/02-routes-pages-forms-users.md +346 -0
- package/docs/project-knowledge-base/03-tech-stack-modules-services.md +347 -0
- package/docs/project-knowledge-base/04-data-models-integrations.md +307 -0
- package/docs/project-knowledge-base/05-docs-corpus-inventory.md +193 -0
- package/docs/project-knowledge-base/06-operations-testing-legal-content.md +170 -0
- package/docs/project-knowledge-base/README.md +90 -0
- package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-03-16.md +454 -0
- package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-03-24.md +66 -0
- package/docs/project-profiles/native-update-capacitor-update-platform-project-profile-last-updated-2026-03-25.md +67 -0
- package/docs/seo-aeo-rules.json +3043 -0
- package/docs/tracking/seo-checklist-tracker.json +333 -0
- package/ios/Plugin/BackgroundUpdate/BackgroundUpdatePlugin.swift +50 -6
- package/ios/Plugin/LiveUpdate/LiveUpdatePlugin.swift +238 -8
- package/ios/Plugin/NativeUpdatePlugin.swift +8 -0
- package/ios/Plugin/Security/SecurityManager.swift +13 -14
- package/package.json +30 -31
- package/docs/play-console-rejection-rules.json +0 -428
|
@@ -11,6 +11,15 @@ class LiveUpdatePlugin {
|
|
|
11
11
|
private var session: URLSession!
|
|
12
12
|
private var downloadTask: URLSessionDownloadTask?
|
|
13
13
|
private let securityManager = SecurityManager()
|
|
14
|
+
|
|
15
|
+
// UserDefaults keys for the A2 (boot-time re-verify) and A3
|
|
16
|
+
// (crash-loop auto-rollback) defenses. Mirrors the Android constants
|
|
17
|
+
// in LiveUpdatePlugin.kt so the two platforms stay in lockstep.
|
|
18
|
+
private static let activeBundleKey = "native_update_active_bundle"
|
|
19
|
+
private static let previousBundleKey = "native_update_previous_active_bundle"
|
|
20
|
+
private static let pendingBundleKey = "native_update_pending_verify_bundle"
|
|
21
|
+
private static let pendingAttemptsKey = "native_update_pending_verify_attempts"
|
|
22
|
+
private static let maxPendingAttempts = 2
|
|
14
23
|
|
|
15
24
|
init(plugin: CAPPlugin) {
|
|
16
25
|
self.plugin = plugin
|
|
@@ -156,8 +165,10 @@ class LiveUpdatePlugin {
|
|
|
156
165
|
}
|
|
157
166
|
}
|
|
158
167
|
|
|
159
|
-
// Create bundle info
|
|
160
|
-
|
|
168
|
+
// Create bundle info. Persist the signature so the
|
|
169
|
+
// boot-time re-verify (verifyActiveBundleOnBoot) can run
|
|
170
|
+
// without needing to re-download.
|
|
171
|
+
var bundleInfo: [String: Any] = [
|
|
161
172
|
"bundleId": bundleId,
|
|
162
173
|
"version": version,
|
|
163
174
|
"path": downloadedFile.path,
|
|
@@ -167,6 +178,9 @@ class LiveUpdatePlugin {
|
|
|
167
178
|
"checksum": checksum,
|
|
168
179
|
"verified": true
|
|
169
180
|
]
|
|
181
|
+
if let signature = call.getString("signature") {
|
|
182
|
+
bundleInfo["signature"] = signature
|
|
183
|
+
}
|
|
170
184
|
|
|
171
185
|
// Save bundle info
|
|
172
186
|
saveBundleInfo(bundleInfo)
|
|
@@ -467,7 +481,22 @@ class LiveUpdatePlugin {
|
|
|
467
481
|
}
|
|
468
482
|
|
|
469
483
|
private func setActiveBundle(_ bundleId: String) {
|
|
470
|
-
UserDefaults.standard
|
|
484
|
+
let defaults = UserDefaults.standard
|
|
485
|
+
let currentActive = defaults.string(forKey: Self.activeBundleKey)
|
|
486
|
+
|
|
487
|
+
defaults.set(bundleId, forKey: Self.activeBundleKey)
|
|
488
|
+
|
|
489
|
+
// Preserve the previous active bundle as a rollback target. Skip
|
|
490
|
+
// self-activation so we don't nuke a real previous-bundle pointer.
|
|
491
|
+
if let previous = currentActive, previous != bundleId {
|
|
492
|
+
defaults.set(previous, forKey: Self.previousBundleKey)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Mark this activation as pending-verify. notifyAppReady() clears
|
|
496
|
+
// it; otherwise the next cold start counts against the bundle and
|
|
497
|
+
// eventually rolls back to the previous known-good bundle.
|
|
498
|
+
defaults.set(bundleId, forKey: Self.pendingBundleKey)
|
|
499
|
+
defaults.set(0, forKey: Self.pendingAttemptsKey)
|
|
471
500
|
}
|
|
472
501
|
|
|
473
502
|
private func clearAllBundles() {
|
|
@@ -542,7 +571,142 @@ class LiveUpdatePlugin {
|
|
|
542
571
|
}
|
|
543
572
|
|
|
544
573
|
private func markBundleAsVerified() {
|
|
545
|
-
|
|
574
|
+
// notifyAppReady() is the host app's "I booted clean on this bundle"
|
|
575
|
+
// signal. Clear the pending-verify state so cold starts after this
|
|
576
|
+
// no longer count against the bundle.
|
|
577
|
+
let defaults = UserDefaults.standard
|
|
578
|
+
defaults.set(true, forKey: "native_update_current_bundle_verified")
|
|
579
|
+
defaults.removeObject(forKey: Self.pendingBundleKey)
|
|
580
|
+
defaults.removeObject(forKey: Self.pendingAttemptsKey)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/// Called from NativeUpdatePlugin.load() on every cold start. Runs two
|
|
584
|
+
/// independent defenses, both silent when no OTA bundle is active:
|
|
585
|
+
///
|
|
586
|
+
/// A2 — re-hash the active bundle and verify it still matches the
|
|
587
|
+
/// checksum stored at install time. If signature + publicKey are
|
|
588
|
+
/// configured, also re-verify the signature. Catches on-disk
|
|
589
|
+
/// tampering on jailbroken devices.
|
|
590
|
+
/// A3 — if the active bundle was never confirmed by notifyAppReady()
|
|
591
|
+
/// on the previous launch, treat this cold start as a failed
|
|
592
|
+
/// boot. After maxPendingAttempts failures, rollback to the
|
|
593
|
+
/// previous known-good bundle to break the crash loop.
|
|
594
|
+
///
|
|
595
|
+
/// On any failure the method rolls back and fires an
|
|
596
|
+
/// updateStateChanged event with status "ROLLBACK" so the host app
|
|
597
|
+
/// and analytics layer can observe it.
|
|
598
|
+
func verifyActiveBundleOnBoot() {
|
|
599
|
+
let defaults = UserDefaults.standard
|
|
600
|
+
|
|
601
|
+
// A3: crash-loop detection.
|
|
602
|
+
if let pendingBundleId = defaults.string(forKey: Self.pendingBundleKey) {
|
|
603
|
+
let nextAttempt = defaults.integer(forKey: Self.pendingAttemptsKey) + 1
|
|
604
|
+
if nextAttempt > Self.maxPendingAttempts {
|
|
605
|
+
rollbackToPrevious(
|
|
606
|
+
reason: "auto-rollback after \(nextAttempt) failed cold starts",
|
|
607
|
+
fromBundleId: pendingBundleId
|
|
608
|
+
)
|
|
609
|
+
defaults.removeObject(forKey: Self.pendingBundleKey)
|
|
610
|
+
defaults.removeObject(forKey: Self.pendingAttemptsKey)
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
defaults.set(nextAttempt, forKey: Self.pendingAttemptsKey)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// A2: integrity re-verify. Only for non-default active bundles.
|
|
617
|
+
guard let activeBundleId = defaults.string(forKey: Self.activeBundleKey),
|
|
618
|
+
activeBundleId != "default" else {
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
guard let bundleInfo = getBundleInfoById(activeBundleId) else {
|
|
623
|
+
rollbackToPrevious(
|
|
624
|
+
reason: "active bundle info missing from UserDefaults",
|
|
625
|
+
fromBundleId: activeBundleId
|
|
626
|
+
)
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
guard let bundlePath = bundleInfo["path"] as? String, !bundlePath.isEmpty,
|
|
631
|
+
let expectedChecksum = bundleInfo["checksum"] as? String, !expectedChecksum.isEmpty else {
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
let bundleURL = URL(fileURLWithPath: bundlePath)
|
|
636
|
+
guard FileManager.default.fileExists(atPath: bundlePath) else {
|
|
637
|
+
rollbackToPrevious(
|
|
638
|
+
reason: "bundle file missing on disk",
|
|
639
|
+
fromBundleId: activeBundleId
|
|
640
|
+
)
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
do {
|
|
645
|
+
let actualChecksum = try calculateChecksum(for: bundleURL)
|
|
646
|
+
if actualChecksum != expectedChecksum {
|
|
647
|
+
rollbackToPrevious(
|
|
648
|
+
reason: "bundle checksum mismatch on boot",
|
|
649
|
+
fromBundleId: activeBundleId
|
|
650
|
+
)
|
|
651
|
+
return
|
|
652
|
+
}
|
|
653
|
+
} catch {
|
|
654
|
+
rollbackToPrevious(
|
|
655
|
+
reason: "bundle checksum computation failed: \(error.localizedDescription)",
|
|
656
|
+
fromBundleId: activeBundleId
|
|
657
|
+
)
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Signature re-verify when both a signature and a public key are
|
|
662
|
+
// available. Absence of either is treated as "not configured" and
|
|
663
|
+
// skipped — Phase A1 already enforces fail-closed at download time.
|
|
664
|
+
if let signature = bundleInfo["signature"] as? String, !signature.isEmpty,
|
|
665
|
+
let publicKey = (config?["security"] as? [String: Any])?["publicKey"] as? String,
|
|
666
|
+
!publicKey.isEmpty {
|
|
667
|
+
do {
|
|
668
|
+
let bytes = try Data(contentsOf: bundleURL)
|
|
669
|
+
if !securityManager.verifySignature(
|
|
670
|
+
data: bytes,
|
|
671
|
+
signature: signature,
|
|
672
|
+
publicKeyString: publicKey
|
|
673
|
+
) {
|
|
674
|
+
rollbackToPrevious(
|
|
675
|
+
reason: "bundle signature mismatch on boot",
|
|
676
|
+
fromBundleId: activeBundleId
|
|
677
|
+
)
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
rollbackToPrevious(
|
|
681
|
+
reason: "bundle signature check failed: \(error.localizedDescription)",
|
|
682
|
+
fromBundleId: activeBundleId
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private func rollbackToPrevious(reason: String, fromBundleId: String) {
|
|
689
|
+
let defaults = UserDefaults.standard
|
|
690
|
+
let previous = defaults.string(forKey: Self.previousBundleKey)
|
|
691
|
+
if let previous = previous {
|
|
692
|
+
defaults.set(previous, forKey: Self.activeBundleKey)
|
|
693
|
+
} else {
|
|
694
|
+
defaults.removeObject(forKey: Self.activeBundleKey)
|
|
695
|
+
}
|
|
696
|
+
defaults.removeObject(forKey: Self.previousBundleKey)
|
|
697
|
+
|
|
698
|
+
stateChangeListener?([
|
|
699
|
+
"status": "ROLLBACK",
|
|
700
|
+
"bundleId": fromBundleId,
|
|
701
|
+
"rolledBackTo": previous ?? "default",
|
|
702
|
+
"reason": reason
|
|
703
|
+
])
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private func getBundleInfoById(_ bundleId: String) -> [String: Any]? {
|
|
707
|
+
let defaults = UserDefaults.standard
|
|
708
|
+
let bundles = defaults.dictionary(forKey: "native_update_bundles") ?? [:]
|
|
709
|
+
return bundles[bundleId] as? [String: Any]
|
|
546
710
|
}
|
|
547
711
|
|
|
548
712
|
// MARK: - Bundle Extraction and WebView Configuration
|
|
@@ -574,14 +738,80 @@ class LiveUpdatePlugin {
|
|
|
574
738
|
}
|
|
575
739
|
}
|
|
576
740
|
|
|
741
|
+
/// Hard cap on total uncompressed size of an OTA bundle. Protects
|
|
742
|
+
/// against zip-bombs (a small zip that expands to gigabytes and fills
|
|
743
|
+
/// the device). Chosen as 5x the default 100MB downloaded-bundle limit
|
|
744
|
+
/// — enough headroom for typical web-app bundles after minification
|
|
745
|
+
/// reverses, below a threshold that would be hostile on mobile.
|
|
746
|
+
private static let maxUncompressedBundleBytes: UInt64 = 500 * 1024 * 1024
|
|
747
|
+
|
|
577
748
|
private func extractZipBundle(from zipUrl: URL, to destinationUrl: URL) throws {
|
|
578
|
-
// Ensure destination directory exists
|
|
749
|
+
// Ensure destination directory exists.
|
|
579
750
|
try FileManager.default.createDirectory(at: destinationUrl, withIntermediateDirectories: true, attributes: nil)
|
|
580
751
|
|
|
581
|
-
//
|
|
752
|
+
// Inspect every entry before extracting anything. Three defenses:
|
|
753
|
+
// 1. zip-slip — an entry whose destination path resolves outside
|
|
754
|
+
// the intended directory would let a malicious bundle write
|
|
755
|
+
// over any file in the app sandbox (e.g. overwriting
|
|
756
|
+
// built-in code) when unzipItem later runs.
|
|
757
|
+
// 2. symlink attack — symlinks in the archive can redirect
|
|
758
|
+
// subsequent writes outside the sandbox even if the entry
|
|
759
|
+
// itself resolves cleanly.
|
|
760
|
+
// 3. zip-bomb — sum of uncompressed sizes capped; refuses to
|
|
761
|
+
// extract an archive that would exhaust device storage.
|
|
762
|
+
guard let archive = Archive(url: zipUrl, accessMode: .read) else {
|
|
763
|
+
throw NSError(domain: "LiveUpdatePlugin", code: 7, userInfo: [
|
|
764
|
+
NSLocalizedDescriptionKey: "Could not open bundle archive for inspection"
|
|
765
|
+
])
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
let canonicalDestination = destinationUrl
|
|
769
|
+
.standardizedFileURL
|
|
770
|
+
.resolvingSymlinksInPath()
|
|
771
|
+
.path
|
|
772
|
+
let destinationPrefix = canonicalDestination.hasSuffix("/")
|
|
773
|
+
? canonicalDestination
|
|
774
|
+
: canonicalDestination + "/"
|
|
775
|
+
|
|
776
|
+
var totalUncompressed: UInt64 = 0
|
|
777
|
+
|
|
778
|
+
for entry in archive {
|
|
779
|
+
if entry.type == .symlink {
|
|
780
|
+
throw NSError(domain: "LiveUpdatePlugin", code: 8, userInfo: [
|
|
781
|
+
NSLocalizedDescriptionKey: "Bundle contains symlink entry (refused): \(entry.path)"
|
|
782
|
+
])
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if entry.path.hasPrefix("/") {
|
|
786
|
+
throw NSError(domain: "LiveUpdatePlugin", code: 9, userInfo: [
|
|
787
|
+
NSLocalizedDescriptionKey: "Bundle entry has absolute path (refused): \(entry.path)"
|
|
788
|
+
])
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
let entryURL = destinationUrl
|
|
792
|
+
.appendingPathComponent(entry.path)
|
|
793
|
+
.standardizedFileURL
|
|
794
|
+
let entryPath = entryURL.resolvingSymlinksInPath().path
|
|
795
|
+
|
|
796
|
+
if entryPath != canonicalDestination && !entryPath.hasPrefix(destinationPrefix) {
|
|
797
|
+
throw NSError(domain: "LiveUpdatePlugin", code: 10, userInfo: [
|
|
798
|
+
NSLocalizedDescriptionKey: "Bundle entry escapes destination (zip-slip refused): \(entry.path)"
|
|
799
|
+
])
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
let (newTotal, overflow) = totalUncompressed.addingReportingOverflow(entry.uncompressedSize)
|
|
803
|
+
if overflow || newTotal > Self.maxUncompressedBundleBytes {
|
|
804
|
+
throw NSError(domain: "LiveUpdatePlugin", code: 11, userInfo: [
|
|
805
|
+
NSLocalizedDescriptionKey: "Bundle exceeds maximum uncompressed size (\(Self.maxUncompressedBundleBytes) bytes) — possible zip-bomb"
|
|
806
|
+
])
|
|
807
|
+
}
|
|
808
|
+
totalUncompressed = newTotal
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// All entries passed inspection — safe to extract.
|
|
582
812
|
try FileManager.default.unzipItem(at: zipUrl, to: destinationUrl)
|
|
583
813
|
|
|
584
|
-
// Verify extraction succeeded by checking for index.html
|
|
814
|
+
// Verify extraction succeeded by checking for index.html.
|
|
585
815
|
let indexPath = destinationUrl.appendingPathComponent("index.html")
|
|
586
816
|
guard FileManager.default.fileExists(atPath: indexPath.path) else {
|
|
587
817
|
throw NSError(domain: "LiveUpdatePlugin", code: 6, userInfo: [
|
|
@@ -589,7 +819,7 @@ class LiveUpdatePlugin {
|
|
|
589
819
|
])
|
|
590
820
|
}
|
|
591
821
|
|
|
592
|
-
// Clean up the ZIP file after successful extraction
|
|
822
|
+
// Clean up the ZIP file after successful extraction.
|
|
593
823
|
try? FileManager.default.removeItem(at: zipUrl)
|
|
594
824
|
}
|
|
595
825
|
|
|
@@ -33,6 +33,14 @@ public class NativeUpdatePlugin: CAPPlugin {
|
|
|
33
33
|
appUpdatePlugin.setEventListener { [weak self] eventName, data in
|
|
34
34
|
self?.notifyListeners(eventName, data: data)
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
// Boot-time integrity re-verify + crash-loop auto-rollback. Runs
|
|
38
|
+
// before any host-app code touches the WebView so a tampered or
|
|
39
|
+
// crash-looping bundle never gets a chance to load. Signature
|
|
40
|
+
// re-verify runs again after configure() when the publicKey
|
|
41
|
+
// arrives from the host app; checksum re-verify here is
|
|
42
|
+
// sufficient to catch most tampering.
|
|
43
|
+
liveUpdatePlugin.verifyActiveBundleOnBoot()
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
@objc func initialize(_ call: CAPPluginCall) {
|
|
@@ -25,7 +25,8 @@ class SecurityManager {
|
|
|
25
25
|
"pins": getCertificatePins()
|
|
26
26
|
],
|
|
27
27
|
"validateInputs": config?["validateInputs"] as? Bool ?? true,
|
|
28
|
-
|
|
28
|
+
// Hard-coded true since v2 — secure storage is no longer opt-out.
|
|
29
|
+
"secureStorage": true
|
|
29
30
|
]
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -142,21 +143,17 @@ class SecurityManager {
|
|
|
142
143
|
return digest.map { String(format: "%02x", $0) }.joined()
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
/// Writes always land in the Keychain since v2. The old
|
|
147
|
+
/// `secureStorage: false` opt-out was removed because the default
|
|
148
|
+
/// ran in plaintext UserDefaults unless a host app explicitly opted
|
|
149
|
+
/// in — which meant most integrations stored API keys in a trivially
|
|
150
|
+
/// readable location. See matching change in SecurityManager.kt.
|
|
145
151
|
func saveSecureData(key: String, value: String) {
|
|
146
|
-
|
|
147
|
-
keychain.set(value, forKey: key)
|
|
148
|
-
} else {
|
|
149
|
-
// Fallback to UserDefaults (not recommended)
|
|
150
|
-
UserDefaults.standard.set(value, forKey: key)
|
|
151
|
-
}
|
|
152
|
+
keychain.set(value, forKey: key)
|
|
152
153
|
}
|
|
153
|
-
|
|
154
|
+
|
|
154
155
|
func getSecureData(key: String) -> String? {
|
|
155
|
-
|
|
156
|
-
return keychain.string(forKey: key)
|
|
157
|
-
} else {
|
|
158
|
-
return UserDefaults.standard.string(forKey: key)
|
|
159
|
-
}
|
|
156
|
+
return keychain.string(forKey: key)
|
|
160
157
|
}
|
|
161
158
|
|
|
162
159
|
func validatePath(_ path: String) -> Bool {
|
|
@@ -189,8 +186,10 @@ class SecurityManager {
|
|
|
189
186
|
return config?["enforceHttps"] as? Bool ?? true
|
|
190
187
|
}
|
|
191
188
|
|
|
189
|
+
/// Unconditionally true since v2. Kept so `getSecurityInfo()` still
|
|
190
|
+
/// reports the capability to callers.
|
|
192
191
|
func isSecureStorageEnabled() -> Bool {
|
|
193
|
-
return
|
|
192
|
+
return true
|
|
194
193
|
}
|
|
195
194
|
|
|
196
195
|
func isInputValidationEnabled() -> Bool {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "native-update",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"engines": {
|
|
5
|
-
"node": ">=
|
|
5
|
+
"node": ">=18.0.0"
|
|
6
6
|
},
|
|
7
7
|
"description": "Capacitor update plugin with live updates, app update checks, app reviews, and support for HTTP or Firestore-backed release delivery.",
|
|
8
8
|
"type": "module",
|
|
@@ -10,9 +10,7 @@
|
|
|
10
10
|
"module": "dist/esm/index.js",
|
|
11
11
|
"types": "dist/esm/index.d.ts",
|
|
12
12
|
"unpkg": "dist/plugin.js",
|
|
13
|
-
"bin":
|
|
14
|
-
"native-update": "./cli/index.js"
|
|
15
|
-
},
|
|
13
|
+
"bin": "./cli/index.js",
|
|
16
14
|
"files": [
|
|
17
15
|
"android/src/main/",
|
|
18
16
|
"android/build.gradle",
|
|
@@ -36,7 +34,7 @@
|
|
|
36
34
|
"license": "MIT",
|
|
37
35
|
"repository": {
|
|
38
36
|
"type": "git",
|
|
39
|
-
"url": "https://
|
|
37
|
+
"url": "git+https://github.com/aoneahsan/native-update.git"
|
|
40
38
|
},
|
|
41
39
|
"bugs": {
|
|
42
40
|
"url": "https://nativeupdate.aoneahsan.com/contact"
|
|
@@ -59,18 +57,19 @@
|
|
|
59
57
|
"android",
|
|
60
58
|
"hybrid"
|
|
61
59
|
],
|
|
62
|
-
"packageManager": "yarn@
|
|
60
|
+
"packageManager": "yarn@4.10.3",
|
|
63
61
|
"scripts": {
|
|
64
|
-
"build": "
|
|
65
|
-
"build:prod": "
|
|
62
|
+
"build": "rimraf ./dist && tsc && rollup -c rollup.config.js",
|
|
63
|
+
"build:prod": "rimraf ./dist && tsc && NODE_ENV=production rollup -c rollup.config.js",
|
|
66
64
|
"clean": "rimraf ./dist",
|
|
67
65
|
"tsc": "tsc",
|
|
68
66
|
"watch": "tsc --watch",
|
|
69
67
|
"lint": "eslint . --ext ts",
|
|
70
68
|
"prettier": "prettier --write .",
|
|
71
|
-
"prepublishOnly": "yarn
|
|
69
|
+
"prepublishOnly": "yarn build:prod",
|
|
72
70
|
"swiftlint": "cd ios && swiftlint lint --fix --format --path Plugin --verbose",
|
|
73
71
|
"test": "vitest",
|
|
72
|
+
"test:run": "vitest --run",
|
|
74
73
|
"test:ui": "vitest --ui",
|
|
75
74
|
"test:coverage": "vitest --coverage"
|
|
76
75
|
},
|
|
@@ -78,32 +77,32 @@
|
|
|
78
77
|
"archiver": "^7.0.1",
|
|
79
78
|
"chalk": "^5.6.2",
|
|
80
79
|
"commander": "^14.0.3",
|
|
81
|
-
"
|
|
82
|
-
"ora": "^9.1.0",
|
|
80
|
+
"ora": "^9.3.0",
|
|
83
81
|
"prompts": "^2.4.2"
|
|
84
82
|
},
|
|
85
83
|
"devDependencies": {
|
|
86
|
-
"@capacitor/android": "^8.0
|
|
87
|
-
"@capacitor/app": "^8.
|
|
88
|
-
"@capacitor/core": "^8.0
|
|
89
|
-
"@capacitor/device": "^8.0.
|
|
90
|
-
"@capacitor/filesystem": "^8.1.
|
|
91
|
-
"@capacitor/ios": "^8.0
|
|
92
|
-
"@capacitor/preferences": "^8.0.
|
|
84
|
+
"@capacitor/android": "^8.3.0",
|
|
85
|
+
"@capacitor/app": "^8.1.0",
|
|
86
|
+
"@capacitor/core": "^8.3.0",
|
|
87
|
+
"@capacitor/device": "^8.0.2",
|
|
88
|
+
"@capacitor/filesystem": "^8.1.2",
|
|
89
|
+
"@capacitor/ios": "^8.3.0",
|
|
90
|
+
"@capacitor/preferences": "^8.0.1",
|
|
93
91
|
"@rollup/plugin-json": "^6.1.0",
|
|
94
92
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
95
|
-
"@rollup/plugin-terser": "^0.
|
|
96
|
-
"@types/node": "^25.
|
|
97
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
98
|
-
"@typescript-eslint/parser": "^8.
|
|
99
|
-
"@vitest/ui": "^4.
|
|
100
|
-
"eslint": "^
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
93
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
94
|
+
"@types/node": "^25.6.0",
|
|
95
|
+
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
|
96
|
+
"@typescript-eslint/parser": "^8.58.2",
|
|
97
|
+
"@vitest/ui": "^4.1.4",
|
|
98
|
+
"eslint": "^10.2.0",
|
|
99
|
+
"express": "^5.2.1",
|
|
100
|
+
"happy-dom": "^20.9.0",
|
|
101
|
+
"prettier": "^3.8.3",
|
|
102
|
+
"rimraf": "^6.1.3",
|
|
103
|
+
"rollup": "^4.60.1",
|
|
104
|
+
"typescript": "^6.0.2",
|
|
105
|
+
"vitest": "^4.1.4"
|
|
107
106
|
},
|
|
108
107
|
"peerDependencies": {
|
|
109
108
|
"@capacitor/core": "^8.0.1"
|