@rs-native-kit/version-check 0.1.2
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/LICENSE +20 -0
- package/README.md +256 -0
- package/VersionCheck.podspec +20 -0
- package/android/build.gradle +56 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/java/com/rsnativekit/versioncheck/SecureStorageCipher.kt +57 -0
- package/android/src/main/java/com/rsnativekit/versioncheck/VersionCheckHeadlessTaskService.kt +31 -0
- package/android/src/main/java/com/rsnativekit/versioncheck/VersionCheckModule.kt +215 -0
- package/android/src/main/java/com/rsnativekit/versioncheck/VersionCheckPackage.kt +31 -0
- package/android/src/main/java/com/rsnativekit/versioncheck/VersionCheckWorker.kt +29 -0
- package/ios/PlatformBridge/Ed25519Verifier.swift +19 -0
- package/ios/VersionCheck.h +5 -0
- package/ios/VersionCheck.mm +278 -0
- package/lib/module/data/cache/MemoryCacheStore.js +30 -0
- package/lib/module/data/cache/MemoryCacheStore.js.map +1 -0
- package/lib/module/data/cache/PersistentCacheStore.js +56 -0
- package/lib/module/data/cache/PersistentCacheStore.js.map +1 -0
- package/lib/module/data/config/ConfigCache.js +29 -0
- package/lib/module/data/config/ConfigCache.js.map +1 -0
- package/lib/module/data/config/ConfigDocumentValidator.js +185 -0
- package/lib/module/data/config/ConfigDocumentValidator.js.map +1 -0
- package/lib/module/data/config/ConfigLoader.js +82 -0
- package/lib/module/data/config/ConfigLoader.js.map +1 -0
- package/lib/module/data/config/ConfigProvider.js +58 -0
- package/lib/module/data/config/ConfigProvider.js.map +1 -0
- package/lib/module/data/config/ConfigResolutionPipeline.js +268 -0
- package/lib/module/data/config/ConfigResolutionPipeline.js.map +1 -0
- package/lib/module/data/config/ConfigValidator.js +69 -0
- package/lib/module/data/config/ConfigValidator.js.map +1 -0
- package/lib/module/data/config/SignatureVerifier.js +80 -0
- package/lib/module/data/config/SignatureVerifier.js.map +1 -0
- package/lib/module/data/config/canonicalize.js +88 -0
- package/lib/module/data/config/canonicalize.js.map +1 -0
- package/lib/module/data/config/sources/EnvironmentOverrideSource.js +20 -0
- package/lib/module/data/config/sources/EnvironmentOverrideSource.js.map +1 -0
- package/lib/module/data/config/sources/LocalConfigSource.js +22 -0
- package/lib/module/data/config/sources/LocalConfigSource.js.map +1 -0
- package/lib/module/data/config/sources/RemoteConfigSource.js +38 -0
- package/lib/module/data/config/sources/RemoteConfigSource.js.map +1 -0
- package/lib/module/data/providers/amazon/AmazonAppstoreProvider.js +13 -0
- package/lib/module/data/providers/amazon/AmazonAppstoreProvider.js.map +1 -0
- package/lib/module/data/providers/apple/AppleStoreProvider.js +67 -0
- package/lib/module/data/providers/apple/AppleStoreProvider.js.map +1 -0
- package/lib/module/data/providers/custom-api/CustomApiProvider.js +51 -0
- package/lib/module/data/providers/custom-api/CustomApiProvider.js.map +1 -0
- package/lib/module/data/providers/firebase-remote-config/FirebaseRemoteConfigProvider.js +13 -0
- package/lib/module/data/providers/firebase-remote-config/FirebaseRemoteConfigProvider.js.map +1 -0
- package/lib/module/data/providers/google-play/GooglePlayProvider.js +62 -0
- package/lib/module/data/providers/google-play/GooglePlayProvider.js.map +1 -0
- package/lib/module/data/providers/huawei/HuaweiAppGalleryProvider.js +18 -0
- package/lib/module/data/providers/huawei/HuaweiAppGalleryProvider.js.map +1 -0
- package/lib/module/data/repositories/VersionRepositoryImpl.js +69 -0
- package/lib/module/data/repositories/VersionRepositoryImpl.js.map +1 -0
- package/lib/module/di/container.js +80 -0
- package/lib/module/di/container.js.map +1 -0
- package/lib/module/domain/IVersionManagerCore.js +4 -0
- package/lib/module/domain/IVersionManagerCore.js.map +1 -0
- package/lib/module/domain/VersionManagerCore.js +276 -0
- package/lib/module/domain/VersionManagerCore.js.map +1 -0
- package/lib/module/domain/engines/decision/DecisionEngine.js +40 -0
- package/lib/module/domain/engines/decision/DecisionEngine.js.map +1 -0
- package/lib/module/domain/engines/policy/IUpdatePolicyEngine.js +4 -0
- package/lib/module/domain/engines/policy/IUpdatePolicyEngine.js.map +1 -0
- package/lib/module/domain/engines/policy/PolicyEngine.js +42 -0
- package/lib/module/domain/engines/policy/PolicyEngine.js.map +1 -0
- package/lib/module/domain/engines/semver/IVersionComparator.js +4 -0
- package/lib/module/domain/engines/semver/IVersionComparator.js.map +1 -0
- package/lib/module/domain/engines/semver/SemVerEngine.js +176 -0
- package/lib/module/domain/engines/semver/SemVerEngine.js.map +1 -0
- package/lib/module/domain/errors/VersionManagerException.js +104 -0
- package/lib/module/domain/errors/VersionManagerException.js.map +1 -0
- package/lib/module/domain/models/ActionPlan.js +9 -0
- package/lib/module/domain/models/ActionPlan.js.map +1 -0
- package/lib/module/domain/models/ConfigDocument.js +4 -0
- package/lib/module/domain/models/ConfigDocument.js.map +1 -0
- package/lib/module/domain/models/Events.js +4 -0
- package/lib/module/domain/models/Events.js.map +1 -0
- package/lib/module/domain/models/LifecycleState.js +29 -0
- package/lib/module/domain/models/LifecycleState.js.map +1 -0
- package/lib/module/domain/models/ParsedVersion.js +2 -0
- package/lib/module/domain/models/ParsedVersion.js.map +1 -0
- package/lib/module/domain/models/PlatformId.js +2 -0
- package/lib/module/domain/models/PlatformId.js.map +1 -0
- package/lib/module/domain/models/PublicApiOptions.js +2 -0
- package/lib/module/domain/models/PublicApiOptions.js.map +1 -0
- package/lib/module/domain/models/RemoteVersionInfo.js +4 -0
- package/lib/module/domain/models/RemoteVersionInfo.js.map +1 -0
- package/lib/module/domain/models/StoreLookup.js +2 -0
- package/lib/module/domain/models/StoreLookup.js.map +1 -0
- package/lib/module/domain/models/Unsubscribe.js +2 -0
- package/lib/module/domain/models/Unsubscribe.js.map +1 -0
- package/lib/module/domain/models/UpdateInfo.js +4 -0
- package/lib/module/domain/models/UpdateInfo.js.map +1 -0
- package/lib/module/domain/models/VersionManagerOptions.js +4 -0
- package/lib/module/domain/models/VersionManagerOptions.js.map +1 -0
- package/lib/module/domain/ports/ICacheStore.js +2 -0
- package/lib/module/domain/ports/ICacheStore.js.map +1 -0
- package/lib/module/domain/ports/IClock.js +2 -0
- package/lib/module/domain/ports/IClock.js.map +1 -0
- package/lib/module/domain/ports/IConfigProvider.js +4 -0
- package/lib/module/domain/ports/IConfigProvider.js.map +1 -0
- package/lib/module/domain/ports/IConfigSources.js +4 -0
- package/lib/module/domain/ports/IConfigSources.js.map +1 -0
- package/lib/module/domain/ports/IPlatformBridge.js +4 -0
- package/lib/module/domain/ports/IPlatformBridge.js.map +1 -0
- package/lib/module/domain/ports/IStoreProvider.js +4 -0
- package/lib/module/domain/ports/IStoreProvider.js.map +1 -0
- package/lib/module/domain/ports/IVersionRepository.js +4 -0
- package/lib/module/domain/ports/IVersionRepository.js.map +1 -0
- package/lib/module/domain/statemachine/LifecycleStateMachine.js +55 -0
- package/lib/module/domain/statemachine/LifecycleStateMachine.js.map +1 -0
- package/lib/module/domain/usecases/CheckForUpdatesUseCase.js +31 -0
- package/lib/module/domain/usecases/CheckForUpdatesUseCase.js.map +1 -0
- package/lib/module/domain/usecases/ForceTriggerUpdateUseCase.js +20 -0
- package/lib/module/domain/usecases/ForceTriggerUpdateUseCase.js.map +1 -0
- package/lib/module/domain/usecases/ResetIgnoredVersionsUseCase.js +24 -0
- package/lib/module/domain/usecases/ResetIgnoredVersionsUseCase.js.map +1 -0
- package/lib/module/index.js +10 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/platform/contracts/IPlatformBridge.js +2 -0
- package/lib/module/platform/contracts/IPlatformBridge.js.map +1 -0
- package/lib/module/platform/createPlatformBridge.js +17 -0
- package/lib/module/platform/createPlatformBridge.js.map +1 -0
- package/lib/module/platform/createPlatformBridge.native.js +9 -0
- package/lib/module/platform/createPlatformBridge.native.js.map +1 -0
- package/lib/module/platform/native/NativePlatformBridge.js +143 -0
- package/lib/module/platform/native/NativePlatformBridge.js.map +1 -0
- package/lib/module/platform/native/NativeVersionCheck.js +33 -0
- package/lib/module/platform/native/NativeVersionCheck.js.map +1 -0
- package/lib/module/platform/web/WebPlatformBridge.js +228 -0
- package/lib/module/platform/web/WebPlatformBridge.js.map +1 -0
- package/lib/module/presentation/components/ForceUpdateScreen.js +68 -0
- package/lib/module/presentation/components/ForceUpdateScreen.js.map +1 -0
- package/lib/module/presentation/components/OptionalUpdateBanner.js +69 -0
- package/lib/module/presentation/components/OptionalUpdateBanner.js.map +1 -0
- package/lib/module/presentation/components/SoftUpdateDialog.js +101 -0
- package/lib/module/presentation/components/SoftUpdateDialog.js.map +1 -0
- package/lib/module/presentation/hooks/VersionManagerProvider.js +18 -0
- package/lib/module/presentation/hooks/VersionManagerProvider.js.map +1 -0
- package/lib/module/presentation/hooks/context.js +5 -0
- package/lib/module/presentation/hooks/context.js.map +1 -0
- package/lib/module/presentation/hooks/useUpdateState.js +26 -0
- package/lib/module/presentation/hooks/useUpdateState.js.map +1 -0
- package/lib/module/presentation/hooks/useVersionManager.js +33 -0
- package/lib/module/presentation/hooks/useVersionManager.js.map +1 -0
- package/lib/module/presentation/state/VersionManagerStore.js +47 -0
- package/lib/module/presentation/state/VersionManagerStore.js.map +1 -0
- package/lib/module/publicApi.js +114 -0
- package/lib/module/publicApi.js.map +1 -0
- package/lib/module/shared/eventbus/EventBus.js +34 -0
- package/lib/module/shared/eventbus/EventBus.js.map +1 -0
- package/lib/module/shared/logging/Logger.js +56 -0
- package/lib/module/shared/logging/Logger.js.map +1 -0
- package/lib/module/ui.js +13 -0
- package/lib/module/ui.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/data/cache/MemoryCacheStore.d.ts +12 -0
- package/lib/typescript/src/data/cache/MemoryCacheStore.d.ts.map +1 -0
- package/lib/typescript/src/data/cache/PersistentCacheStore.d.ts +14 -0
- package/lib/typescript/src/data/cache/PersistentCacheStore.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigCache.d.ts +15 -0
- package/lib/typescript/src/data/config/ConfigCache.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigDocumentValidator.d.ts +20 -0
- package/lib/typescript/src/data/config/ConfigDocumentValidator.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigLoader.d.ts +28 -0
- package/lib/typescript/src/data/config/ConfigLoader.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigProvider.d.ts +30 -0
- package/lib/typescript/src/data/config/ConfigProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigResolutionPipeline.d.ts +49 -0
- package/lib/typescript/src/data/config/ConfigResolutionPipeline.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigValidator.d.ts +10 -0
- package/lib/typescript/src/data/config/ConfigValidator.d.ts.map +1 -0
- package/lib/typescript/src/data/config/SignatureVerifier.d.ts +16 -0
- package/lib/typescript/src/data/config/SignatureVerifier.d.ts.map +1 -0
- package/lib/typescript/src/data/config/canonicalize.d.ts +13 -0
- package/lib/typescript/src/data/config/canonicalize.d.ts.map +1 -0
- package/lib/typescript/src/data/config/sources/EnvironmentOverrideSource.d.ts +17 -0
- package/lib/typescript/src/data/config/sources/EnvironmentOverrideSource.d.ts.map +1 -0
- package/lib/typescript/src/data/config/sources/LocalConfigSource.d.ts +19 -0
- package/lib/typescript/src/data/config/sources/LocalConfigSource.d.ts.map +1 -0
- package/lib/typescript/src/data/config/sources/RemoteConfigSource.d.ts +10 -0
- package/lib/typescript/src/data/config/sources/RemoteConfigSource.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/amazon/AmazonAppstoreProvider.d.ts +9 -0
- package/lib/typescript/src/data/providers/amazon/AmazonAppstoreProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/apple/AppleStoreProvider.d.ts +19 -0
- package/lib/typescript/src/data/providers/apple/AppleStoreProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/custom-api/CustomApiProvider.d.ts +14 -0
- package/lib/typescript/src/data/providers/custom-api/CustomApiProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/firebase-remote-config/FirebaseRemoteConfigProvider.d.ts +9 -0
- package/lib/typescript/src/data/providers/firebase-remote-config/FirebaseRemoteConfigProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/google-play/GooglePlayProvider.d.ts +22 -0
- package/lib/typescript/src/data/providers/google-play/GooglePlayProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/huawei/HuaweiAppGalleryProvider.d.ts +14 -0
- package/lib/typescript/src/data/providers/huawei/HuaweiAppGalleryProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/repositories/VersionRepositoryImpl.d.ts +23 -0
- package/lib/typescript/src/data/repositories/VersionRepositoryImpl.d.ts.map +1 -0
- package/lib/typescript/src/di/container.d.ts +12 -0
- package/lib/typescript/src/di/container.d.ts.map +1 -0
- package/lib/typescript/src/domain/IVersionManagerCore.d.ts +30 -0
- package/lib/typescript/src/domain/IVersionManagerCore.d.ts.map +1 -0
- package/lib/typescript/src/domain/VersionManagerCore.d.ts +94 -0
- package/lib/typescript/src/domain/VersionManagerCore.d.ts.map +1 -0
- package/lib/typescript/src/domain/engines/decision/DecisionEngine.d.ts +16 -0
- package/lib/typescript/src/domain/engines/decision/DecisionEngine.d.ts.map +1 -0
- package/lib/typescript/src/domain/engines/policy/IUpdatePolicyEngine.d.ts +17 -0
- package/lib/typescript/src/domain/engines/policy/IUpdatePolicyEngine.d.ts.map +1 -0
- package/lib/typescript/src/domain/engines/policy/PolicyEngine.d.ts +16 -0
- package/lib/typescript/src/domain/engines/policy/PolicyEngine.d.ts.map +1 -0
- package/lib/typescript/src/domain/engines/semver/IVersionComparator.d.ts +7 -0
- package/lib/typescript/src/domain/engines/semver/IVersionComparator.d.ts.map +1 -0
- package/lib/typescript/src/domain/engines/semver/SemVerEngine.d.ts +11 -0
- package/lib/typescript/src/domain/engines/semver/SemVerEngine.d.ts.map +1 -0
- package/lib/typescript/src/domain/errors/VersionManagerException.d.ts +97 -0
- package/lib/typescript/src/domain/errors/VersionManagerException.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/ActionPlan.d.ts +15 -0
- package/lib/typescript/src/domain/models/ActionPlan.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/ConfigDocument.d.ts +86 -0
- package/lib/typescript/src/domain/models/ConfigDocument.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/Events.d.ts +39 -0
- package/lib/typescript/src/domain/models/Events.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/LifecycleState.d.ts +18 -0
- package/lib/typescript/src/domain/models/LifecycleState.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/ParsedVersion.d.ts +9 -0
- package/lib/typescript/src/domain/models/ParsedVersion.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/PlatformId.d.ts +3 -0
- package/lib/typescript/src/domain/models/PlatformId.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/PublicApiOptions.d.ts +13 -0
- package/lib/typescript/src/domain/models/PublicApiOptions.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/RemoteVersionInfo.d.ts +21 -0
- package/lib/typescript/src/domain/models/RemoteVersionInfo.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/StoreLookup.d.ts +12 -0
- package/lib/typescript/src/domain/models/StoreLookup.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/Unsubscribe.d.ts +2 -0
- package/lib/typescript/src/domain/models/Unsubscribe.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/UpdateInfo.d.ts +11 -0
- package/lib/typescript/src/domain/models/UpdateInfo.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/VersionManagerOptions.d.ts +149 -0
- package/lib/typescript/src/domain/models/VersionManagerOptions.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/ICacheStore.d.ts +6 -0
- package/lib/typescript/src/domain/ports/ICacheStore.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IClock.d.ts +4 -0
- package/lib/typescript/src/domain/ports/IClock.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IConfigProvider.d.ts +11 -0
- package/lib/typescript/src/domain/ports/IConfigProvider.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IConfigSources.d.ts +25 -0
- package/lib/typescript/src/domain/ports/IConfigSources.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IPlatformBridge.d.ts +77 -0
- package/lib/typescript/src/domain/ports/IPlatformBridge.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IStoreProvider.d.ts +7 -0
- package/lib/typescript/src/domain/ports/IStoreProvider.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IVersionRepository.d.ts +8 -0
- package/lib/typescript/src/domain/ports/IVersionRepository.d.ts.map +1 -0
- package/lib/typescript/src/domain/statemachine/LifecycleStateMachine.d.ts +25 -0
- package/lib/typescript/src/domain/statemachine/LifecycleStateMachine.d.ts.map +1 -0
- package/lib/typescript/src/domain/usecases/CheckForUpdatesUseCase.d.ts +30 -0
- package/lib/typescript/src/domain/usecases/CheckForUpdatesUseCase.d.ts.map +1 -0
- package/lib/typescript/src/domain/usecases/ForceTriggerUpdateUseCase.d.ts +17 -0
- package/lib/typescript/src/domain/usecases/ForceTriggerUpdateUseCase.d.ts.map +1 -0
- package/lib/typescript/src/domain/usecases/ResetIgnoredVersionsUseCase.d.ts +7 -0
- package/lib/typescript/src/domain/usecases/ResetIgnoredVersionsUseCase.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +17 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/platform/contracts/IPlatformBridge.d.ts +2 -0
- package/lib/typescript/src/platform/contracts/IPlatformBridge.d.ts.map +1 -0
- package/lib/typescript/src/platform/createPlatformBridge.d.ts +12 -0
- package/lib/typescript/src/platform/createPlatformBridge.d.ts.map +1 -0
- package/lib/typescript/src/platform/createPlatformBridge.native.d.ts +4 -0
- package/lib/typescript/src/platform/createPlatformBridge.native.d.ts.map +1 -0
- package/lib/typescript/src/platform/native/NativePlatformBridge.d.ts +17 -0
- package/lib/typescript/src/platform/native/NativePlatformBridge.d.ts.map +1 -0
- package/lib/typescript/src/platform/native/NativeVersionCheck.d.ts +45 -0
- package/lib/typescript/src/platform/native/NativeVersionCheck.d.ts.map +1 -0
- package/lib/typescript/src/platform/web/WebPlatformBridge.d.ts +17 -0
- package/lib/typescript/src/platform/web/WebPlatformBridge.d.ts.map +1 -0
- package/lib/typescript/src/presentation/components/ForceUpdateScreen.d.ts +12 -0
- package/lib/typescript/src/presentation/components/ForceUpdateScreen.d.ts.map +1 -0
- package/lib/typescript/src/presentation/components/OptionalUpdateBanner.d.ts +12 -0
- package/lib/typescript/src/presentation/components/OptionalUpdateBanner.d.ts.map +1 -0
- package/lib/typescript/src/presentation/components/SoftUpdateDialog.d.ts +14 -0
- package/lib/typescript/src/presentation/components/SoftUpdateDialog.d.ts.map +1 -0
- package/lib/typescript/src/presentation/hooks/VersionManagerProvider.d.ts +9 -0
- package/lib/typescript/src/presentation/hooks/VersionManagerProvider.d.ts.map +1 -0
- package/lib/typescript/src/presentation/hooks/context.d.ts +3 -0
- package/lib/typescript/src/presentation/hooks/context.d.ts.map +1 -0
- package/lib/typescript/src/presentation/hooks/useUpdateState.d.ts +17 -0
- package/lib/typescript/src/presentation/hooks/useUpdateState.d.ts.map +1 -0
- package/lib/typescript/src/presentation/hooks/useVersionManager.d.ts +22 -0
- package/lib/typescript/src/presentation/hooks/useVersionManager.d.ts.map +1 -0
- package/lib/typescript/src/presentation/state/VersionManagerStore.d.ts +28 -0
- package/lib/typescript/src/presentation/state/VersionManagerStore.d.ts.map +1 -0
- package/lib/typescript/src/publicApi.d.ts +24 -0
- package/lib/typescript/src/publicApi.d.ts.map +1 -0
- package/lib/typescript/src/shared/eventbus/EventBus.d.ts +21 -0
- package/lib/typescript/src/shared/eventbus/EventBus.d.ts.map +1 -0
- package/lib/typescript/src/shared/logging/Logger.d.ts +17 -0
- package/lib/typescript/src/shared/logging/Logger.d.ts.map +1 -0
- package/lib/typescript/src/ui.d.ts +15 -0
- package/lib/typescript/src/ui.d.ts.map +1 -0
- package/package.json +192 -0
- package/src/data/cache/MemoryCacheStore.ts +36 -0
- package/src/data/cache/PersistentCacheStore.ts +63 -0
- package/src/data/config/ConfigCache.ts +30 -0
- package/src/data/config/ConfigDocumentValidator.ts +309 -0
- package/src/data/config/ConfigLoader.ts +105 -0
- package/src/data/config/ConfigProvider.ts +84 -0
- package/src/data/config/ConfigResolutionPipeline.ts +424 -0
- package/src/data/config/ConfigValidator.ts +79 -0
- package/src/data/config/SignatureVerifier.ts +90 -0
- package/src/data/config/canonicalize.ts +111 -0
- package/src/data/config/sources/EnvironmentOverrideSource.ts +22 -0
- package/src/data/config/sources/LocalConfigSource.ts +24 -0
- package/src/data/config/sources/RemoteConfigSource.ts +41 -0
- package/src/data/providers/amazon/AmazonAppstoreProvider.ts +16 -0
- package/src/data/providers/apple/AppleStoreProvider.ts +94 -0
- package/src/data/providers/custom-api/CustomApiProvider.ts +75 -0
- package/src/data/providers/firebase-remote-config/FirebaseRemoteConfigProvider.ts +16 -0
- package/src/data/providers/google-play/GooglePlayProvider.ts +75 -0
- package/src/data/providers/huawei/HuaweiAppGalleryProvider.ts +21 -0
- package/src/data/repositories/VersionRepositoryImpl.ts +90 -0
- package/src/di/container.ts +128 -0
- package/src/domain/IVersionManagerCore.ts +54 -0
- package/src/domain/VersionManagerCore.ts +425 -0
- package/src/domain/engines/decision/DecisionEngine.ts +47 -0
- package/src/domain/engines/policy/IUpdatePolicyEngine.ts +18 -0
- package/src/domain/engines/policy/PolicyEngine.ts +60 -0
- package/src/domain/engines/semver/IVersionComparator.ts +7 -0
- package/src/domain/engines/semver/SemVerEngine.ts +200 -0
- package/src/domain/errors/VersionManagerException.ts +166 -0
- package/src/domain/models/ActionPlan.ts +17 -0
- package/src/domain/models/ConfigDocument.ts +97 -0
- package/src/domain/models/Events.ts +48 -0
- package/src/domain/models/LifecycleState.ts +50 -0
- package/src/domain/models/ParsedVersion.ts +8 -0
- package/src/domain/models/PlatformId.ts +9 -0
- package/src/domain/models/PublicApiOptions.ts +14 -0
- package/src/domain/models/RemoteVersionInfo.ts +23 -0
- package/src/domain/models/StoreLookup.ts +12 -0
- package/src/domain/models/Unsubscribe.ts +1 -0
- package/src/domain/models/UpdateInfo.ts +11 -0
- package/src/domain/models/VersionManagerOptions.ts +150 -0
- package/src/domain/ports/ICacheStore.ts +5 -0
- package/src/domain/ports/IClock.ts +3 -0
- package/src/domain/ports/IConfigProvider.ts +18 -0
- package/src/domain/ports/IConfigSources.ts +44 -0
- package/src/domain/ports/IPlatformBridge.ts +97 -0
- package/src/domain/ports/IStoreProvider.ts +12 -0
- package/src/domain/ports/IVersionRepository.ts +14 -0
- package/src/domain/statemachine/LifecycleStateMachine.ts +76 -0
- package/src/domain/usecases/CheckForUpdatesUseCase.ts +63 -0
- package/src/domain/usecases/ForceTriggerUpdateUseCase.ts +35 -0
- package/src/domain/usecases/ResetIgnoredVersionsUseCase.ts +26 -0
- package/src/index.tsx +84 -0
- package/src/platform/contracts/IPlatformBridge.ts +14 -0
- package/src/platform/createPlatformBridge.native.ts +7 -0
- package/src/platform/createPlatformBridge.ts +15 -0
- package/src/platform/native/NativePlatformBridge.ts +179 -0
- package/src/platform/native/NativeVersionCheck.ts +64 -0
- package/src/platform/web/WebPlatformBridge.ts +347 -0
- package/src/presentation/components/ForceUpdateScreen.tsx +64 -0
- package/src/presentation/components/OptionalUpdateBanner.tsx +53 -0
- package/src/presentation/components/SoftUpdateDialog.tsx +84 -0
- package/src/presentation/hooks/VersionManagerProvider.tsx +23 -0
- package/src/presentation/hooks/context.ts +6 -0
- package/src/presentation/hooks/useUpdateState.ts +50 -0
- package/src/presentation/hooks/useVersionManager.ts +68 -0
- package/src/presentation/state/VersionManagerStore.ts +72 -0
- package/src/publicApi.ts +126 -0
- package/src/shared/eventbus/EventBus.ts +56 -0
- package/src/shared/logging/Logger.ts +70 -0
- package/src/ui.tsx +19 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { ConsoleLogSink } from '../../shared/logging/Logger';
|
|
2
|
+
import { resolveConfigSync } from './ConfigLoader';
|
|
3
|
+
import { CONFIG_ENVELOPE_MAX_BYTES } from './ConfigDocumentValidator';
|
|
4
|
+
import type {
|
|
5
|
+
ConfigTier,
|
|
6
|
+
RawConfigDocument,
|
|
7
|
+
SignedConfigEnvelope,
|
|
8
|
+
TrustedKeySet,
|
|
9
|
+
VersionManagerConfigDocument,
|
|
10
|
+
} from '../../domain/models/ConfigDocument';
|
|
11
|
+
import {
|
|
12
|
+
BoundaryValidationException,
|
|
13
|
+
ConfigSizeLimitExceededException,
|
|
14
|
+
SchemaValidationException,
|
|
15
|
+
SignatureVerificationException,
|
|
16
|
+
UntrustedKeyIdException,
|
|
17
|
+
toVersionManagerException,
|
|
18
|
+
} from '../../domain/errors/VersionManagerException';
|
|
19
|
+
import type { VersionManagerException } from '../../domain/errors/VersionManagerException';
|
|
20
|
+
import type {
|
|
21
|
+
IConfigCache,
|
|
22
|
+
IConfigValidator,
|
|
23
|
+
IEnvironmentOverrideSource,
|
|
24
|
+
ILocalConfigSource,
|
|
25
|
+
IRemoteConfigSource,
|
|
26
|
+
ISignatureVerifier,
|
|
27
|
+
} from '../../domain/ports/IConfigSources';
|
|
28
|
+
import type { IClock } from '../../domain/ports/IClock';
|
|
29
|
+
import type {
|
|
30
|
+
ResolvedVersionManagerConfig,
|
|
31
|
+
VersionManagerOptions,
|
|
32
|
+
} from '../../domain/models/VersionManagerOptions';
|
|
33
|
+
|
|
34
|
+
export interface ConfigResolutionPipelineDeps {
|
|
35
|
+
readonly local: ILocalConfigSource;
|
|
36
|
+
readonly remote: IRemoteConfigSource;
|
|
37
|
+
readonly envOverrides: IEnvironmentOverrideSource;
|
|
38
|
+
readonly cache: IConfigCache;
|
|
39
|
+
readonly validator: IConfigValidator;
|
|
40
|
+
readonly signatureVerifier: ISignatureVerifier;
|
|
41
|
+
readonly clock: IClock;
|
|
42
|
+
/** §7.4 — default 5, configurable. */
|
|
43
|
+
readonly circuitBreakerThreshold?: number;
|
|
44
|
+
/** §7.4 cool-down window once the breaker trips. */
|
|
45
|
+
readonly circuitBreakerCooldownMs?: number;
|
|
46
|
+
readonly onError?: (error: VersionManagerException) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ConfigResolutionResult {
|
|
50
|
+
readonly config: ResolvedVersionManagerConfig;
|
|
51
|
+
readonly tier: ConfigTier;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Doc 03's ConfigResolutionPipeline — orchestrates §3.2 (schema -> boundary -> signature
|
|
56
|
+
* validation), §4.2 (per-field precedence merge, env > remote > local > default), §4.3
|
|
57
|
+
* (access path), §7.2 (cascading fallback), §7.3 (field vs whole-document fallback), and
|
|
58
|
+
* §7.4 (quarantine + circuit breaker). Lives in src/data/config/ per doc 03 §0.
|
|
59
|
+
*/
|
|
60
|
+
export class ConfigResolutionPipeline {
|
|
61
|
+
private readonly quarantinedFingerprints = new Set<string>();
|
|
62
|
+
private consecutiveRemoteFailures = 0;
|
|
63
|
+
private circuitOpenUntil = 0;
|
|
64
|
+
|
|
65
|
+
constructor(private readonly deps: ConfigResolutionPipelineDeps) {}
|
|
66
|
+
|
|
67
|
+
async resolve(
|
|
68
|
+
defaults: VersionManagerOptions
|
|
69
|
+
): Promise<ConfigResolutionResult> {
|
|
70
|
+
// Tier 4 — always present, validated synchronously at configure()-time already by
|
|
71
|
+
// ConfigLoader.resolveConfigSync (doc 02 §1.3 fail-fast); re-run here defensively so
|
|
72
|
+
// this pipeline is safe to call standalone (e.g. from tests).
|
|
73
|
+
const tier4 = resolveConfigSync(defaults);
|
|
74
|
+
const trustedKeys: TrustedKeySet = {
|
|
75
|
+
algorithm: tier4.security.signatureAlgorithm,
|
|
76
|
+
keys: new Set(tier4.security.trustedKeyIds),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const [envelopeTier1, envelopeTier3] = await Promise.all([
|
|
80
|
+
this.deps.envOverrides.read().catch(() => ({})),
|
|
81
|
+
this.readLocal(trustedKeys),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const remoteResult = await this.readRemote(defaults, trustedKeys);
|
|
85
|
+
|
|
86
|
+
// §7.2 whole-tier cascade: if the remote tier failed outright, prefer the last known
|
|
87
|
+
// valid *fully resolved* config over recomputing fresh from local+default, so a
|
|
88
|
+
// network hiccup doesn't regress an app back to weaker settings every single check.
|
|
89
|
+
if (remoteResult.status === 'failed') {
|
|
90
|
+
const cached = await this.deps.cache.getLastValid().catch(() => null);
|
|
91
|
+
if (cached) {
|
|
92
|
+
return {
|
|
93
|
+
config: cached,
|
|
94
|
+
tier: 'remote' /* cache reflects a prior remote win */,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const tier2 = remoteResult.status === 'ok' ? remoteResult.payload : null;
|
|
100
|
+
const tier3 = envelopeTier3;
|
|
101
|
+
|
|
102
|
+
const merged = mergeConfig(tier4, tier3, tier2, envelopeTier1);
|
|
103
|
+
const tier: ConfigTier = tier2
|
|
104
|
+
? 'remote'
|
|
105
|
+
: tier3
|
|
106
|
+
? 'local'
|
|
107
|
+
: envelopeTier1 && Object.keys(envelopeTier1).length > 0
|
|
108
|
+
? 'env'
|
|
109
|
+
: 'default';
|
|
110
|
+
|
|
111
|
+
await this.deps.cache.setLastValid(merged).catch(() => {
|
|
112
|
+
// Cache-write failures never block resolution (§7.2's guarantee is about read
|
|
113
|
+
// fallback; a write failure here just means the *next* remote outage won't have a
|
|
114
|
+
// warm cache to fall back to — already surfaced via CacheWriteException elsewhere).
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return { config: merged, tier };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async readLocal(
|
|
121
|
+
trustedKeys: TrustedKeySet
|
|
122
|
+
): Promise<VersionManagerConfigDocument | null> {
|
|
123
|
+
const raw = await this.deps.local.read().catch(() => null);
|
|
124
|
+
if (raw === null || raw === undefined) return null;
|
|
125
|
+
|
|
126
|
+
const verified = await this.validateAndVerify(raw, 'local', trustedKeys);
|
|
127
|
+
return verified;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async readRemote(
|
|
131
|
+
defaults: VersionManagerOptions,
|
|
132
|
+
trustedKeys: TrustedKeySet
|
|
133
|
+
): Promise<
|
|
134
|
+
| { status: 'ok'; payload: VersionManagerConfigDocument }
|
|
135
|
+
| { status: 'absent' }
|
|
136
|
+
| { status: 'failed' }
|
|
137
|
+
> {
|
|
138
|
+
const url = defaults.remoteConfigUrl;
|
|
139
|
+
if (!url) return { status: 'absent' };
|
|
140
|
+
|
|
141
|
+
if (this.deps.clock.now() < this.circuitOpenUntil) {
|
|
142
|
+
// §7.4 circuit breaker: skip attempting remote fetch entirely during the cooldown.
|
|
143
|
+
return { status: 'failed' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let raw: RawConfigDocument;
|
|
147
|
+
try {
|
|
148
|
+
raw = await this.deps.remote.fetch(url, {
|
|
149
|
+
timeoutMs: defaults.fallback?.requestTimeoutMs ?? 8_000,
|
|
150
|
+
headers: defaults.configSources?.remoteFetchHeaders,
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.recordRemoteFailure();
|
|
154
|
+
this.report(toVersionManagerException(error));
|
|
155
|
+
return { status: 'failed' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fingerprint = fingerprintOf(raw);
|
|
159
|
+
if (this.quarantinedFingerprints.has(fingerprint)) {
|
|
160
|
+
// §7.4: same bad bytes seen again within the session — fail silently (no
|
|
161
|
+
// re-validation, no re-logging) but still counts as a circuit-breaker failure.
|
|
162
|
+
this.recordRemoteFailure();
|
|
163
|
+
return { status: 'failed' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const verified = await this.validateAndVerify(
|
|
167
|
+
raw,
|
|
168
|
+
'remote',
|
|
169
|
+
trustedKeys,
|
|
170
|
+
fingerprint
|
|
171
|
+
);
|
|
172
|
+
if (verified === null) {
|
|
173
|
+
this.recordRemoteFailure();
|
|
174
|
+
return { status: 'failed' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.consecutiveRemoteFailures = 0;
|
|
178
|
+
this.circuitOpenUntil = 0;
|
|
179
|
+
return { status: 'ok', payload: verified };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private recordRemoteFailure(): void {
|
|
183
|
+
this.consecutiveRemoteFailures++;
|
|
184
|
+
const threshold = this.deps.circuitBreakerThreshold ?? 5;
|
|
185
|
+
if (this.consecutiveRemoteFailures >= threshold) {
|
|
186
|
+
const cooldown = this.deps.circuitBreakerCooldownMs ?? 5 * 60_000;
|
|
187
|
+
this.circuitOpenUntil = this.deps.clock.now() + cooldown;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* §3.2 sequence: schema -> boundary -> canonicalize -> verify signature. Returns the
|
|
193
|
+
* validated payload (with any boundary-failed fields stripped per §7.3's field-level
|
|
194
|
+
* fallback) or null if the whole document must be rejected (schema failure or
|
|
195
|
+
* signature failure — §7.3's whole-document fallback).
|
|
196
|
+
*/
|
|
197
|
+
private async validateAndVerify(
|
|
198
|
+
raw: RawConfigDocument,
|
|
199
|
+
tier: 'local' | 'remote',
|
|
200
|
+
trustedKeys: TrustedKeySet,
|
|
201
|
+
fingerprint?: string
|
|
202
|
+
): Promise<VersionManagerConfigDocument | null> {
|
|
203
|
+
const sizeCheck =
|
|
204
|
+
typeof raw === 'string' ? raw.length : JSON.stringify(raw).length;
|
|
205
|
+
if (sizeCheck > CONFIG_ENVELOPE_MAX_BYTES) {
|
|
206
|
+
this.report(
|
|
207
|
+
new ConfigSizeLimitExceededException({
|
|
208
|
+
message: `${tier} config envelope exceeds ${CONFIG_ENVELOPE_MAX_BYTES} bytes`,
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const schemaResult = this.deps.validator.validateSchema(raw);
|
|
215
|
+
if (!schemaResult.valid) {
|
|
216
|
+
this.report(
|
|
217
|
+
new SchemaValidationException({
|
|
218
|
+
message: `${tier} config document failed schema validation: ${schemaResult.errors.join('; ')}`,
|
|
219
|
+
metadata: { tier, errors: schemaResult.errors },
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
return null; // whole-document fallback (§7.3)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const envelope = (
|
|
226
|
+
typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
227
|
+
) as SignedConfigEnvelope;
|
|
228
|
+
|
|
229
|
+
// Signature verification — required for remote always, required for local unless
|
|
230
|
+
// algorithm is "none" in a dev build (enforced inside SignatureVerifier, §3.1).
|
|
231
|
+
if (
|
|
232
|
+
envelope.signature.algorithm !== 'none' &&
|
|
233
|
+
!trustedKeys.keys.has(envelope.signature.keyId)
|
|
234
|
+
) {
|
|
235
|
+
this.report(
|
|
236
|
+
new UntrustedKeyIdException({
|
|
237
|
+
message: `${tier} config signature.keyId "${envelope.signature.keyId}" is not in security.trustedKeyIds`,
|
|
238
|
+
metadata: { tier },
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
if (fingerprint) this.quarantinedFingerprints.add(fingerprint);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const sigResult = await this.deps.signatureVerifier.verify(
|
|
246
|
+
envelope,
|
|
247
|
+
trustedKeys
|
|
248
|
+
);
|
|
249
|
+
if (!sigResult.valid) {
|
|
250
|
+
this.report(
|
|
251
|
+
new SignatureVerificationException({
|
|
252
|
+
message: `${tier} config signature verification failed: ${sigResult.reason ?? 'unknown reason'}`,
|
|
253
|
+
metadata: { tier },
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
if (fingerprint) this.quarantinedFingerprints.add(fingerprint);
|
|
257
|
+
return null; // whole-document fallback (§7.3) — an untrusted signature taints every field
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// §7.3 field-level fallback: only the offending fields are dropped, not the whole doc.
|
|
261
|
+
const boundaryResult = this.deps.validator.validateBoundaries(raw);
|
|
262
|
+
if (!boundaryResult.valid) {
|
|
263
|
+
this.report(
|
|
264
|
+
new BoundaryValidationException({
|
|
265
|
+
message: `${tier} config document failed boundary validation on: ${Array.from(boundaryResult.fieldErrors.keys()).join(', ')}`,
|
|
266
|
+
metadata: {
|
|
267
|
+
tier,
|
|
268
|
+
fields: Array.from(boundaryResult.fieldErrors.entries()),
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
return stripFields(envelope.payload, boundaryResult.fieldErrors);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return envelope.payload;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private report(error: VersionManagerException): void {
|
|
279
|
+
if (this.deps.onError) {
|
|
280
|
+
this.deps.onError(error);
|
|
281
|
+
} else {
|
|
282
|
+
new ConsoleLogSink().write({
|
|
283
|
+
level: 'error',
|
|
284
|
+
message: error.message,
|
|
285
|
+
timestamp: this.deps.clock.now(),
|
|
286
|
+
metadata: { code: error.code },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function fingerprintOf(raw: RawConfigDocument): string {
|
|
293
|
+
const str = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
|
294
|
+
// Cheap non-cryptographic fingerprint — this is a quarantine dedupe key, not a
|
|
295
|
+
// security boundary (signature verification is what actually protects integrity).
|
|
296
|
+
let hash = 0;
|
|
297
|
+
for (let i = 0; i < str.length; i++) {
|
|
298
|
+
hash = (Math.imul(31, hash) + str.charCodeAt(i)) | 0;
|
|
299
|
+
}
|
|
300
|
+
return `${str.length}:${hash}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function stripFields(
|
|
304
|
+
payload: VersionManagerConfigDocument,
|
|
305
|
+
fieldErrors: ReadonlyMap<string, string>
|
|
306
|
+
): VersionManagerConfigDocument {
|
|
307
|
+
const clone: Record<string, unknown> = JSON.parse(JSON.stringify(payload));
|
|
308
|
+
for (const path of fieldErrors.keys()) {
|
|
309
|
+
const segments = path.split('.');
|
|
310
|
+
let cursor: Record<string, unknown> = clone;
|
|
311
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
312
|
+
const next = cursor[segments[i] as string];
|
|
313
|
+
if (typeof next !== 'object' || next === null) break;
|
|
314
|
+
cursor = next as Record<string, unknown>;
|
|
315
|
+
}
|
|
316
|
+
delete cursor[segments[segments.length - 1] as string];
|
|
317
|
+
}
|
|
318
|
+
return clone as VersionManagerConfigDocument;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function firstDefined<T>(...values: (T | undefined | null)[]): T | undefined {
|
|
322
|
+
for (const v of values) {
|
|
323
|
+
if (v !== undefined && v !== null) return v;
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Doc 03 §4.2 precedence matrix — per-field deep merge, honoring which tiers each field
|
|
330
|
+
* group is eligible to appear in (not every field accepts every tier; e.g. env never
|
|
331
|
+
* overrides cache.storage or stores.*, and security.* is tier-4-only).
|
|
332
|
+
*/
|
|
333
|
+
function mergeConfig(
|
|
334
|
+
tier4: ResolvedVersionManagerConfig,
|
|
335
|
+
tier3: VersionManagerConfigDocument | null,
|
|
336
|
+
tier2: VersionManagerConfigDocument | null,
|
|
337
|
+
tier1: Partial<VersionManagerConfigDocument>
|
|
338
|
+
): ResolvedVersionManagerConfig {
|
|
339
|
+
return {
|
|
340
|
+
appVersion:
|
|
341
|
+
firstDefined(tier1.localVersionIdentifier, tier4.appVersion) ?? null,
|
|
342
|
+
logging: tier4.logging,
|
|
343
|
+
cache: {
|
|
344
|
+
ttlMs:
|
|
345
|
+
firstDefined(
|
|
346
|
+
tier1.cache?.ttlMs,
|
|
347
|
+
tier2?.cache?.ttlMs,
|
|
348
|
+
tier3?.cache?.ttlMs,
|
|
349
|
+
tier4.cache.ttlMs
|
|
350
|
+
) ?? 21_600_000,
|
|
351
|
+
storage:
|
|
352
|
+
firstDefined(
|
|
353
|
+
tier2?.cache?.storage,
|
|
354
|
+
tier3?.cache?.storage,
|
|
355
|
+
tier4.cache.storage
|
|
356
|
+
) ?? 'persistent',
|
|
357
|
+
bustOnManualCheck: tier4.cache.bustOnManualCheck,
|
|
358
|
+
},
|
|
359
|
+
fallback: {
|
|
360
|
+
onNetworkError: tier4.fallback.onNetworkError,
|
|
361
|
+
defaultConfig: tier4.fallback.defaultConfig,
|
|
362
|
+
requestTimeoutMs:
|
|
363
|
+
firstDefined(
|
|
364
|
+
tier1.network?.requestTimeoutMs,
|
|
365
|
+
tier2?.network?.requestTimeoutMs,
|
|
366
|
+
tier3?.network?.requestTimeoutMs,
|
|
367
|
+
tier4.fallback.requestTimeoutMs
|
|
368
|
+
) ?? 8_000,
|
|
369
|
+
retry: {
|
|
370
|
+
maxAttempts:
|
|
371
|
+
firstDefined(
|
|
372
|
+
tier1.network?.retry?.maxAttempts,
|
|
373
|
+
tier2?.network?.retry?.maxAttempts,
|
|
374
|
+
tier3?.network?.retry?.maxAttempts,
|
|
375
|
+
tier4.fallback.retry.maxAttempts
|
|
376
|
+
) ?? 3,
|
|
377
|
+
backoff:
|
|
378
|
+
firstDefined(
|
|
379
|
+
tier2?.network?.retry?.backoff,
|
|
380
|
+
tier3?.network?.retry?.backoff,
|
|
381
|
+
tier4.fallback.retry.backoff
|
|
382
|
+
) ?? 'exponential-jitter',
|
|
383
|
+
baseDelayMs:
|
|
384
|
+
firstDefined(
|
|
385
|
+
tier1.network?.retry?.baseDelayMs,
|
|
386
|
+
tier2?.network?.retry?.baseDelayMs,
|
|
387
|
+
tier3?.network?.retry?.baseDelayMs,
|
|
388
|
+
tier4.fallback.retry.baseDelayMs
|
|
389
|
+
) ?? 500,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
stores: firstDefined(tier2?.stores, tier3?.stores) ?? tier4.stores,
|
|
393
|
+
policy: {
|
|
394
|
+
forceUpdateBelow:
|
|
395
|
+
firstDefined(
|
|
396
|
+
tier1.alerts?.forceUpdateBelow,
|
|
397
|
+
tier2?.alerts?.forceUpdateBelow,
|
|
398
|
+
tier3?.alerts?.forceUpdateBelow,
|
|
399
|
+
tier4.policy.forceUpdateBelow ?? undefined
|
|
400
|
+
) ?? null,
|
|
401
|
+
reminderIntervalMs:
|
|
402
|
+
firstDefined(
|
|
403
|
+
tier1.alerts?.reminderDelayMs,
|
|
404
|
+
tier2?.alerts?.reminderDelayMs,
|
|
405
|
+
tier3?.alerts?.reminderDelayMs,
|
|
406
|
+
tier4.policy.reminderIntervalMs
|
|
407
|
+
) ?? 259_200_000,
|
|
408
|
+
rolloutPercentage: tier4.policy.rolloutPercentage,
|
|
409
|
+
},
|
|
410
|
+
strictReadiness: tier4.strictReadiness,
|
|
411
|
+
remoteConfigUrl:
|
|
412
|
+
firstDefined(tier1.remoteConfigUrl, tier4.remoteConfigUrl ?? undefined) ??
|
|
413
|
+
null,
|
|
414
|
+
// security.* is tier-4-only, never overridden by a loaded document (§4.2).
|
|
415
|
+
security: tier4.security,
|
|
416
|
+
schemaVersion:
|
|
417
|
+
firstDefined(
|
|
418
|
+
tier1.schemaVersion,
|
|
419
|
+
tier2?.schemaVersion,
|
|
420
|
+
tier3?.schemaVersion,
|
|
421
|
+
tier4.schemaVersion
|
|
422
|
+
) ?? '1.0',
|
|
423
|
+
};
|
|
424
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { parseVersion } from '../../domain/engines/semver/SemVerEngine';
|
|
2
|
+
import { InvalidConfigException } from '../../domain/errors/VersionManagerException';
|
|
3
|
+
import type { VersionManagerOptions } from '../../domain/models/VersionManagerOptions';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Boundary validation (Prompt 3 §2.2) for the "default fallback" config tier — the
|
|
7
|
+
* VersionManagerOptions object passed to configure(). Full JSON-Schema structural
|
|
8
|
+
* validation and signature verification for local/remote config documents (Prompt 3
|
|
9
|
+
* §2.1, §3) are scoped to the not-yet-built config-document pipeline; see
|
|
10
|
+
* docs/architecture/03-configuration-system.md.
|
|
11
|
+
*/
|
|
12
|
+
export function validateOptions(options: VersionManagerOptions): void {
|
|
13
|
+
if (!options.stores || Object.keys(options.stores).length === 0) {
|
|
14
|
+
throw new InvalidConfigException({
|
|
15
|
+
message:
|
|
16
|
+
'VersionManagerOptions.stores must configure at least one store (ios/android/huawei/amazon/custom)',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (options.appVersion !== undefined) {
|
|
21
|
+
try {
|
|
22
|
+
parseVersion(options.appVersion);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new InvalidConfigException({
|
|
25
|
+
message: `options.appVersion "${options.appVersion}" is not a valid SemVer string`,
|
|
26
|
+
cause: error,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.policy?.forceUpdateBelow !== undefined) {
|
|
32
|
+
try {
|
|
33
|
+
parseVersion(options.policy.forceUpdateBelow);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new InvalidConfigException({
|
|
36
|
+
message: `options.policy.forceUpdateBelow "${options.policy.forceUpdateBelow}" is not a valid SemVer string`,
|
|
37
|
+
cause: error,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const timeoutMs = options.fallback?.requestTimeoutMs;
|
|
43
|
+
if (timeoutMs !== undefined && timeoutMs <= 0) {
|
|
44
|
+
throw new InvalidConfigException({
|
|
45
|
+
message: 'options.fallback.requestTimeoutMs must be greater than 0',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ttlMs = options.cache?.ttlMs;
|
|
50
|
+
if (ttlMs !== undefined && ttlMs < 0) {
|
|
51
|
+
throw new InvalidConfigException({
|
|
52
|
+
message: 'options.cache.ttlMs must be >= 0',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const maxAttempts = options.fallback?.retry?.maxAttempts;
|
|
57
|
+
if (maxAttempts !== undefined && (maxAttempts < 0 || maxAttempts > 10)) {
|
|
58
|
+
throw new InvalidConfigException({
|
|
59
|
+
message: 'options.fallback.retry.maxAttempts must be between 0 and 10',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const baseDelayMs = options.fallback?.retry?.baseDelayMs;
|
|
64
|
+
if (baseDelayMs !== undefined && baseDelayMs <= 0) {
|
|
65
|
+
throw new InvalidConfigException({
|
|
66
|
+
message: 'options.fallback.retry.baseDelayMs must be greater than 0',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rolloutPercentage = options.policy?.rolloutPercentage;
|
|
71
|
+
if (
|
|
72
|
+
rolloutPercentage !== undefined &&
|
|
73
|
+
(rolloutPercentage < 0 || rolloutPercentage > 100)
|
|
74
|
+
) {
|
|
75
|
+
throw new InvalidConfigException({
|
|
76
|
+
message: 'options.policy.rolloutPercentage must be between 0 and 100',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { canonicalize, utf8ToBase64 } from './canonicalize';
|
|
2
|
+
import type {
|
|
3
|
+
SignatureVerificationResult,
|
|
4
|
+
SignedConfigEnvelope,
|
|
5
|
+
TrustedKeySet,
|
|
6
|
+
} from '../../domain/models/ConfigDocument';
|
|
7
|
+
import type { ICryptoProvider } from '../../domain/ports/IPlatformBridge';
|
|
8
|
+
import type { ISignatureVerifier } from '../../domain/ports/IConfigSources';
|
|
9
|
+
|
|
10
|
+
declare const __DEV__: boolean | undefined;
|
|
11
|
+
|
|
12
|
+
function isDev(): boolean {
|
|
13
|
+
return typeof __DEV__ !== 'undefined' && __DEV__ === true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Doc 03 §3.2/§3.4 — verification calls are implemented using OS-provided cryptography
|
|
18
|
+
* exclusively (iOS CryptoKit/Security.framework, Android java.security), reached through
|
|
19
|
+
* IPlatformBridge.crypto (ICryptoProvider) so this class never itself hand-rolls
|
|
20
|
+
* cryptographic primitives (Prompt 1 §9.3's zero-external-dependency constraint extends
|
|
21
|
+
* to "don't implement crypto in JS either" — only OS APIs are trusted for this).
|
|
22
|
+
*/
|
|
23
|
+
export class SignatureVerifier implements ISignatureVerifier {
|
|
24
|
+
constructor(private readonly crypto?: ICryptoProvider) {}
|
|
25
|
+
|
|
26
|
+
async verify(
|
|
27
|
+
envelope: SignedConfigEnvelope,
|
|
28
|
+
trustedKeys: TrustedKeySet
|
|
29
|
+
): Promise<SignatureVerificationResult> {
|
|
30
|
+
const { algorithm, keyId, value } = envelope.signature;
|
|
31
|
+
|
|
32
|
+
// §3.1: algorithm "none" is permitted only for the local bundled document in
|
|
33
|
+
// development builds — rejected for any remote fetch and for release builds,
|
|
34
|
+
// enforced here regardless of what security.signatureAlgorithm claims (a
|
|
35
|
+
// compromised document cannot downgrade its own algorithm to "none" and be
|
|
36
|
+
// believed). The pipeline is responsible for only calling verify() with a "none"
|
|
37
|
+
// envelope on the local-dev path in the first place; this is a defense-in-depth
|
|
38
|
+
// second check.
|
|
39
|
+
if (algorithm === 'none') {
|
|
40
|
+
if (!isDev()) {
|
|
41
|
+
return {
|
|
42
|
+
valid: false,
|
|
43
|
+
reason:
|
|
44
|
+
'signature.algorithm "none" is rejected outside development builds',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { valid: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (trustedKeys.algorithm !== algorithm) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
reason: `Envelope algorithm "${algorithm}" does not match configured security.signatureAlgorithm "${trustedKeys.algorithm}"`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!trustedKeys.keys.has(keyId)) {
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
reason: `keyId "${keyId}" is not present in security.trustedKeyIds`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!this.crypto) {
|
|
65
|
+
return {
|
|
66
|
+
valid: false,
|
|
67
|
+
reason:
|
|
68
|
+
'No ICryptoProvider available on the active IPlatformBridge — cannot verify signatures on this target',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const canonicalPayload = canonicalize(envelope.payload);
|
|
73
|
+
const messageBase64 = utf8ToBase64(canonicalPayload);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const valid =
|
|
77
|
+
algorithm === 'ed25519'
|
|
78
|
+
? await this.crypto.verifyEd25519(keyId, messageBase64, value)
|
|
79
|
+
: await this.crypto.verifyHmacSha256(keyId, messageBase64, value);
|
|
80
|
+
return valid
|
|
81
|
+
? { valid: true }
|
|
82
|
+
: { valid: false, reason: 'Signature mismatch' };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return {
|
|
85
|
+
valid: false,
|
|
86
|
+
reason: `Native signature verification threw: ${String(error)}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-rolled canonicalizer (Prompt 1 §9.3 zero-dependency constraint), implementing the
|
|
3
|
+
* same principles as RFC 8785 (JSON Canonicalization Scheme) that doc 03 §3.3 calls for:
|
|
4
|
+
* recursive key sorting by UTF-16 code unit order, fixed number formatting, no
|
|
5
|
+
* insignificant whitespace. Signatures are computed/verified over this canonical byte
|
|
6
|
+
* form so that whitespace/key-order differences in transit never invalidate a
|
|
7
|
+
* legitimately signed document.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type JsonValue =
|
|
11
|
+
null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
12
|
+
|
|
13
|
+
function canonicalizeValue(value: JsonValue): string {
|
|
14
|
+
if (value === null) return 'null';
|
|
15
|
+
|
|
16
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
17
|
+
|
|
18
|
+
if (typeof value === 'number') return canonicalizeNumber(value);
|
|
19
|
+
|
|
20
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
21
|
+
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return `[${value.map(canonicalizeValue).join(',')}]`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Recursive key sorting, UTF-16 code unit order (default JS string `<` comparison).
|
|
27
|
+
const keys = Object.keys(value).sort();
|
|
28
|
+
const members = keys.map(
|
|
29
|
+
(key) =>
|
|
30
|
+
`${JSON.stringify(key)}:${canonicalizeValue(value[key] as JsonValue)}`
|
|
31
|
+
);
|
|
32
|
+
return `{${members.join(',')}}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fixed number formatting per RFC 8785 §3.2.2.3 (simplified): integers render without a
|
|
37
|
+
* decimal point/exponent; this codebase's config schema (doc 03 §6) only ever contains
|
|
38
|
+
* integers and no fractional/exponential numeric fields, so the full ECMA-262
|
|
39
|
+
* Number::toString algorithm RFC 8785 mandates is unnecessary here.
|
|
40
|
+
*/
|
|
41
|
+
function canonicalizeNumber(value: number): string {
|
|
42
|
+
if (!Number.isFinite(value)) {
|
|
43
|
+
throw new Error('Cannot canonicalize a non-finite number');
|
|
44
|
+
}
|
|
45
|
+
if (Number.isInteger(value)) return String(value);
|
|
46
|
+
return String(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Canonical byte sequence (as a UTF-8 string) for the given JSON-compatible value. */
|
|
50
|
+
export function canonicalize(value: unknown): string {
|
|
51
|
+
return canonicalizeValue(value as JsonValue);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Base64 encoding of the UTF-8 bytes of a string, without relying on Node's Buffer. */
|
|
55
|
+
export function utf8ToBase64(input: string): string {
|
|
56
|
+
if (typeof btoa === 'function' && typeof TextEncoder !== 'undefined') {
|
|
57
|
+
const bytes = new TextEncoder().encode(input);
|
|
58
|
+
let binary = '';
|
|
59
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
60
|
+
binary += String.fromCharCode(bytes[i] as number);
|
|
61
|
+
}
|
|
62
|
+
return btoa(binary);
|
|
63
|
+
}
|
|
64
|
+
// React Native's Hermes engine provides global Buffer via a polyfill in most RN
|
|
65
|
+
// setups, but not guaranteed — fall back to a minimal hand-rolled base64 encoder.
|
|
66
|
+
return manualUtf8ToBase64(input);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const BASE64_CHARS =
|
|
70
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
71
|
+
|
|
72
|
+
function manualUtf8ToBase64(input: string): string {
|
|
73
|
+
const bytes: number[] = [];
|
|
74
|
+
for (let i = 0; i < input.length; i++) {
|
|
75
|
+
let code = input.codePointAt(i) as number;
|
|
76
|
+
if (code > 0xffff) i++; // consumed a surrogate pair
|
|
77
|
+
if (code < 0x80) {
|
|
78
|
+
bytes.push(code);
|
|
79
|
+
} else if (code < 0x800) {
|
|
80
|
+
bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
|
|
81
|
+
} else if (code < 0x10000) {
|
|
82
|
+
bytes.push(
|
|
83
|
+
0xe0 | (code >> 12),
|
|
84
|
+
0x80 | ((code >> 6) & 0x3f),
|
|
85
|
+
0x80 | (code & 0x3f)
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
bytes.push(
|
|
89
|
+
0xf0 | (code >> 18),
|
|
90
|
+
0x80 | ((code >> 12) & 0x3f),
|
|
91
|
+
0x80 | ((code >> 6) & 0x3f),
|
|
92
|
+
0x80 | (code & 0x3f)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let output = '';
|
|
98
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
99
|
+
const b0 = bytes[i] as number;
|
|
100
|
+
const b1 = bytes[i + 1];
|
|
101
|
+
const b2 = bytes[i + 2];
|
|
102
|
+
output += BASE64_CHARS[b0 >> 2];
|
|
103
|
+
output += BASE64_CHARS[((b0 & 0x03) << 4) | ((b1 ?? 0) >> 4)];
|
|
104
|
+
output +=
|
|
105
|
+
b1 === undefined
|
|
106
|
+
? '='
|
|
107
|
+
: BASE64_CHARS[((b1 & 0x0f) << 2) | ((b2 ?? 0) >> 6)];
|
|
108
|
+
output += b2 === undefined ? '=' : BASE64_CHARS[b2 & 0x3f];
|
|
109
|
+
}
|
|
110
|
+
return output;
|
|
111
|
+
}
|