catalyst-core 0.0.3-canary.9 → 0.1.0-beta.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.
Files changed (182) hide show
  1. package/README.md +4 -5
  2. package/bin/catalyst.js +28 -15
  3. package/changelog.md +139 -30
  4. package/dist/native/androidProject/app/build.gradle.kts +125 -15
  5. package/dist/native/androidProject/app/proguard-rules.pro +85 -14
  6. package/dist/native/androidProject/app/src/debug/res/xml/network_security_config.xml +8 -0
  7. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +10 -3
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +552 -0
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +109 -0
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +605 -124
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +405 -47
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +341 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +954 -71
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +111 -0
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/URLWhitelistManager.kt +231 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/Utils.kt +49 -55
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +81 -8
  18. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/design/DesignTokens.kt +307 -0
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/EmulatorDetector.kt +203 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +187 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertHandler.kt +170 -0
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertUI.kt +366 -0
  23. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityBottomSheet.kt +138 -0
  24. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckManager.kt +511 -0
  25. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckScheduler.kt +68 -0
  26. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +47 -11
  27. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DeviceInfoUtils.kt +38 -13
  28. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DownloadUtils.kt +36 -14
  29. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FileUtils.kt +3 -2
  30. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +57 -8
  31. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/IntentUtils.kt +31 -6
  32. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +82 -0
  33. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NetworkUtils.kt +105 -0
  34. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationConstants.kt +28 -0
  35. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationUtils.kt +639 -0
  36. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/SafeAreaUtils.kt +62 -0
  37. package/dist/native/androidProject/app/src/main/res/drawable/splashscreen_fallback.png +0 -0
  38. package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +1 -0
  39. package/dist/native/androidProject/app/src/main/res/layout/activity_splash.xml +20 -0
  40. package/dist/native/androidProject/app/src/main/res/values/themes.xml +23 -0
  41. package/dist/native/androidProject/app/src/main/res/values-night/themes.xml +11 -0
  42. package/dist/native/androidProject/app/src/main/res/xml/network_security_config.xml +12 -0
  43. package/dist/native/androidProject/app/src/noFcm/java/io/yourname/androidproject/utils/NotificationManager.kt +166 -0
  44. package/dist/native/androidProject/app/src/noFcm/java/io/yourname/androidproject/utils/PushNotificationUtils.kt +94 -0
  45. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeMessageValidatorTest.kt +267 -0
  46. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +286 -0
  47. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +296 -0
  48. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +594 -0
  49. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +466 -0
  50. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +187 -0
  51. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +304 -0
  52. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +424 -0
  53. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +469 -0
  54. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/SecurityBridgeTest.kt +199 -0
  55. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/URLWhitelistManagerTest.kt +462 -0
  56. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +391 -0
  57. package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/NotificationManager.kt +224 -0
  58. package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/PushNotificationUtils.kt +399 -0
  59. package/dist/native/androidProject/build.gradle.kts +6 -0
  60. package/dist/native/androidProject/gradle/libs.versions.toml +8 -2
  61. package/dist/native/androidSetup.js +4 -3
  62. package/dist/native/assets/catalyst.png +0 -0
  63. package/dist/native/bridge/WebBridge.js +16 -3
  64. package/dist/native/bridge/constants/NativeInterfaces.js +28 -10
  65. package/dist/native/bridge/errors.js +1 -1
  66. package/dist/native/bridge/hooks.js +76 -16
  67. package/dist/native/bridge/safeArea.js +5 -0
  68. package/dist/native/bridge/standardizedHooks.js +23 -5
  69. package/dist/native/bridge/useBaseHook.js +7 -9
  70. package/dist/native/bridge/utils/CameraUtils.js +58 -0
  71. package/dist/native/bridge/utils/FileObjectConverter.js +40 -0
  72. package/dist/native/bridge/utils/NativeBridge.js +57 -8
  73. package/dist/native/buildAppAndroid.js +115 -43
  74. package/dist/native/buildAppIos.js +291 -26
  75. package/dist/native/iosnativeWebView/Configurations/Debug.xcconfig +21 -0
  76. package/dist/native/iosnativeWebView/Configurations/Release.xcconfig +24 -0
  77. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +50 -0
  78. package/dist/native/iosnativeWebView/Package.resolved +32 -0
  79. package/dist/native/iosnativeWebView/Package.swift +37 -0
  80. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/LocalNotificationHandler.swift +293 -0
  81. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +299 -0
  82. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/PushNotificationHandler.swift +199 -0
  83. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +125 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +70 -0
  85. package/dist/native/iosnativeWebView/Sources/Core/Notifications/NotificationModels.swift +156 -0
  86. package/dist/native/iosnativeWebView/Sources/Core/Protocols/NotificationHandlerProtocol.swift +35 -0
  87. package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +26 -0
  88. package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/CacheManager.swift +69 -36
  89. package/dist/native/iosnativeWebView/Sources/Core/Utils/CameraTransportUtils.swift +363 -0
  90. package/dist/native/iosnativeWebView/Sources/Core/Utils/DeviceInfoUtils.swift +67 -0
  91. package/dist/native/iosnativeWebView/Sources/Core/Utils/FilePickerHandler.swift +1053 -0
  92. package/dist/native/iosnativeWebView/Sources/Core/Utils/FrameworkServerUtils.swift +901 -0
  93. package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/ImageHandler.swift +206 -20
  94. package/dist/native/iosnativeWebView/Sources/Core/Utils/NetworkMonitor.swift +112 -0
  95. package/dist/native/iosnativeWebView/Sources/Core/Utils/NotificationHandlerProvider.swift +20 -0
  96. package/dist/native/iosnativeWebView/Sources/Core/Utils/NullNotificationHandler.swift +85 -0
  97. package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/ResourceURLProtocol.swift +60 -25
  98. package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +132 -0
  99. package/dist/native/iosnativeWebView/Sources/Core/Utils/ScreenSecureManager.swift +131 -0
  100. package/dist/native/iosnativeWebView/Sources/Core/Utils/URLWhitelistManager.swift +224 -0
  101. package/dist/native/iosnativeWebView/Sources/Core/Utils/WebKitConfig.swift +43 -0
  102. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +991 -0
  103. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeDelegateHandler.swift +166 -0
  104. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +340 -0
  105. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeJavaScriptInterface.swift +149 -0
  106. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +611 -0
  107. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +334 -0
  108. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +95 -0
  109. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +86 -0
  110. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +181 -0
  111. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +182 -0
  112. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +395 -0
  113. package/dist/native/iosnativeWebView/iosnativeWebView/AppDelegate.swift +84 -0
  114. package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/AccentColor.colorset/Contents.json +8 -8
  115. package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/AppIcon.appiconset/Contents.json +10 -10
  116. package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/Contents.json +4 -4
  117. package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +65 -10
  118. package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +21 -0
  119. package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +13 -0
  120. package/dist/native/iosnativeWebView/iosnativeWebView/Preview Content/Preview Assets.xcassets/Contents.json +4 -4
  121. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +12 -0
  122. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebViewApp.swift +80 -0
  123. package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
  124. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +118 -100
  125. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +69 -0
  126. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/xcshareddata/xcschemes/iosnativeWebView.xcscheme +9 -4
  127. package/dist/native/iosnativeWebView/iosnativeWebView.xctestplan +47 -0
  128. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +236 -0
  129. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeCommandHandlerSecurityTests.swift +212 -0
  130. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +418 -0
  131. package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +390 -0
  132. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +299 -0
  133. package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +383 -0
  134. package/dist/native/iosnativeWebView/iosnativeWebViewTests/FrameworkServerUtilsTests.swift +391 -0
  135. package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +581 -0
  136. package/dist/native/iosnativeWebView/iosnativeWebViewTests/NotificationHandlerTests.swift +430 -0
  137. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ScreenSecureManagerTests.swift +121 -0
  138. package/dist/native/iosnativeWebView/iosnativeWebViewTests/URLWhitelistManagerTests.swift +367 -0
  139. package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +440 -0
  140. package/dist/native/renameAndroidProject.js +80 -70
  141. package/dist/native/setupEmulatorIos.js +22 -5
  142. package/dist/native/setupServer.js +11 -10
  143. package/dist/native/terminalProgress.js +10 -10
  144. package/dist/native/utils.js +4 -5
  145. package/dist/otel.js +4 -0
  146. package/dist/scripts/build.js +2 -2
  147. package/dist/scripts/devBuild.js +2 -2
  148. package/dist/scripts/loadEnvironmentVariables.js +3 -1
  149. package/dist/scripts/scriptUtils.js +2 -2
  150. package/dist/sentry.js +4 -1
  151. package/dist/server/renderer/document/Body.js +4 -4
  152. package/dist/server/renderer/handler.js +7 -5
  153. package/dist/webpack/development.client.babel.js +1 -1
  154. package/mcp_v2/conversion-tasks.json +326 -0
  155. package/mcp_v2/knowledge-base.json +1068 -0
  156. package/mcp_v2/lib/helpers.js +165 -0
  157. package/mcp_v2/mcp.js +563 -0
  158. package/mcp_v2/package.json +13 -0
  159. package/mcp_v2/schema.sql +88 -0
  160. package/mcp_v2/setup.js +278 -0
  161. package/mcp_v2/tools/build.js +686 -0
  162. package/mcp_v2/tools/config.js +453 -0
  163. package/mcp_v2/tools/conversion.js +799 -0
  164. package/mcp_v2/tools/debug.js +113 -0
  165. package/mcp_v2/tools/knowledge.js +219 -0
  166. package/mcp_v2/tools/sync.js +23 -0
  167. package/mcp_v2/tools/tasks.js +945 -0
  168. package/package.json +11 -15
  169. package/dist/native/androidProject/.idea/.name +0 -1
  170. package/dist/native/androidProject/.idea/AndroidProjectSystem.xml +0 -6
  171. package/dist/native/androidProject/.idea/compiler.xml +0 -6
  172. package/dist/native/androidProject/.idea/deploymentTargetSelector.xml +0 -10
  173. package/dist/native/androidProject/.idea/gradle.xml +0 -18
  174. package/dist/native/androidProject/.idea/migrations.xml +0 -10
  175. package/dist/native/androidProject/.idea/misc.xml +0 -10
  176. package/dist/native/androidProject/.idea/runConfigurations.xml +0 -17
  177. package/dist/native/androidProject/.idea/vcs.xml +0 -6
  178. package/dist/native/iosnativeWebView/iosnativeWebView/NativeBridge.swift +0 -217
  179. package/dist/native/iosnativeWebView/iosnativeWebView/WebView.swift +0 -96
  180. package/dist/native/iosnativeWebView/iosnativeWebView/WebViewModel.swift +0 -47
  181. package/dist/native/iosnativeWebView/iosnativeWebView/WebViewNavigationDelegate.swift +0 -140
  182. package/license +0 -10
@@ -0,0 +1,132 @@
1
+ //
2
+ // SafeAreaUtils.swift
3
+ // iosnativeWebView
4
+ //
5
+ // Safe area inset calculation utilities for iOS
6
+ // Equivalent to Android's SafeAreaUtils.kt
7
+ //
8
+
9
+ import UIKit
10
+ import os
11
+
12
+ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "SafeAreaUtils")
13
+
14
+ // MARK: - SafeAreaInsets Data Structure
15
+
16
+ /// Represents safe area insets for all edges of the screen
17
+ /// Matches Android's SafeAreaInsets data class
18
+ public struct SafeAreaInsets: Codable, Equatable, CustomStringConvertible {
19
+ public let top: CGFloat
20
+ public let right: CGFloat
21
+ public let bottom: CGFloat
22
+ public let left: CGFloat
23
+
24
+ /// Zero insets (no safe area)
25
+ public static let zero = SafeAreaInsets(top: 0, right: 0, bottom: 0, left: 0)
26
+
27
+ /// Public initializer
28
+ public init(top: CGFloat, right: CGFloat, bottom: CGFloat, left: CGFloat) {
29
+ self.top = top
30
+ self.right = right
31
+ self.bottom = bottom
32
+ self.left = left
33
+ }
34
+
35
+ /// Convert to dictionary for JSON serialization
36
+ public func toDict() -> [String: Any] {
37
+ return [
38
+ "top": Int(top),
39
+ "right": Int(right),
40
+ "bottom": Int(bottom),
41
+ "left": Int(left)
42
+ ]
43
+ }
44
+
45
+ /// Convert to header dictionary for HTTP requests
46
+ public func toHeaders() -> [String: String] {
47
+ return [
48
+ "X-Safe-Area-Top": String(Int(top)),
49
+ "X-Safe-Area-Right": String(Int(right)),
50
+ "X-Safe-Area-Bottom": String(Int(bottom)),
51
+ "X-Safe-Area-Left": String(Int(left)),
52
+ // Prevent caching of SSR response so updated headers are always used
53
+ "Cache-Control": "no-cache, no-store, must-revalidate",
54
+ "Pragma": "no-cache"
55
+ ]
56
+ }
57
+
58
+ /// Check if all insets are zero
59
+ public var isZero: Bool {
60
+ return top == 0 && right == 0 && bottom == 0 && left == 0
61
+ }
62
+
63
+ /// String representation for logging and debugging
64
+ public var description: String {
65
+ return "SafeAreaInsets(top: \(Int(top)), right: \(Int(right)), bottom: \(Int(bottom)), left: \(Int(left)))"
66
+ }
67
+ }
68
+
69
+ // MARK: - SafeAreaUtils
70
+
71
+ public class SafeAreaUtils {
72
+ public static func getSafeAreaInsets(from window: UIWindow?, edgeToEdgeEnabled: Bool) -> SafeAreaInsets {
73
+ guard let window = window else {
74
+ #if DEBUG
75
+ logger.debug("⚠️ No window available, returning zero insets")
76
+ #endif
77
+ return .zero
78
+ }
79
+
80
+ // If edge-to-edge is disabled, SwiftUI is already respecting safe areas
81
+ // The system handles padding automatically, so return zero
82
+ // (app doesn't need to apply manual padding)
83
+ guard edgeToEdgeEnabled else {
84
+ #if DEBUG
85
+ logger.debug("🎨 Edge-to-edge disabled - SwiftUI respects safe areas, returning zero")
86
+ #endif
87
+ return .zero
88
+ }
89
+
90
+ // Edge-to-edge enabled: Content draws behind system UI
91
+ // Return actual insets so the app can apply padding manually
92
+ let safeArea = window.safeAreaInsets
93
+
94
+ #if DEBUG
95
+ logger.debug("📐 Window safe area insets - top: \(safeArea.top), right: \(safeArea.right), bottom: \(safeArea.bottom), left: \(safeArea.left)")
96
+ logger.debug("🎨 Edge-to-edge enabled - returning insets for manual padding")
97
+ #endif
98
+
99
+ // On iOS, safeAreaInsets already includes:
100
+ // - Status bar
101
+ // - Home indicator
102
+ // - Notch/Dynamic Island
103
+ // - Any other screen intrusions
104
+ //
105
+ // Unlike Android, we don't need to manually combine system bars and cutouts
106
+
107
+ let insets = SafeAreaInsets(
108
+ top: max(0, safeArea.top),
109
+ right: max(0, safeArea.right),
110
+ bottom: max(0, safeArea.bottom),
111
+ left: max(0, safeArea.left)
112
+ )
113
+
114
+ #if DEBUG
115
+ logger.debug("✅ Computed safe area insets: \(insets)")
116
+ #endif
117
+
118
+ return insets
119
+ }
120
+
121
+ /// Get safe area insets from the key window of a scene
122
+ /// Convenience method that finds the active window automatically
123
+ public static func getSafeAreaInsetsFromKeyWindow(edgeToEdgeEnabled: Bool) -> SafeAreaInsets {
124
+ // Get the key window from active scene
125
+ let window = UIApplication.shared.connectedScenes
126
+ .compactMap { $0 as? UIWindowScene }
127
+ .flatMap { $0.windows }
128
+ .first { $0.isKeyWindow }
129
+
130
+ return getSafeAreaInsets(from: window, edgeToEdgeEnabled: edgeToEdgeEnabled)
131
+ }
132
+ }
@@ -0,0 +1,131 @@
1
+ // ScreenSecureManager.swift
2
+ // Manages screen security on iOS by overlaying a blank view when the app
3
+ // moves to the background — preventing the app switcher thumbnail from
4
+ // capturing sensitive content.
5
+ //
6
+ // iOS limitation: unlike Android FLAG_SECURE, iOS cannot block system or
7
+ // third-party screenshots. This only protects the app switcher snapshot.
8
+
9
+ import UIKit
10
+ import os
11
+
12
+ private let secureLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "ScreenSecureManager")
13
+
14
+ final class ScreenSecureManager {
15
+
16
+ public static let shared = ScreenSecureManager()
17
+ private init() {
18
+ setupSceneObservers()
19
+ }
20
+
21
+ // Track whether screen security is requested by the web layer
22
+ private(set) var isScreenSecure: Bool = false
23
+
24
+ // The overlay view installed over the window when backgrounding
25
+ private var overlayWindow: UIWindow?
26
+ // Guard flag set before async dispatch to prevent double-install race
27
+ private var overlayInstalling: Bool = false
28
+
29
+ // MARK: - Public API
30
+
31
+ /// Enable or disable screen-secure mode.
32
+ /// When enabled, a blank overlay is shown whenever the app moves to background
33
+ /// so the app-switcher snapshot does not capture sensitive content.
34
+ func setScreenSecure(_ enable: Bool) {
35
+ isScreenSecure = enable
36
+ secureLogger.debug("setScreenSecure called: enable=\(enable)")
37
+ if !enable {
38
+ removeOverlay()
39
+ }
40
+ // If enabling while already in background, install immediately
41
+ if enable && UIApplication.shared.applicationState == .background {
42
+ secureLogger.debug("setScreenSecure: app already in background, installing overlay immediately")
43
+ installOverlay()
44
+ }
45
+ }
46
+
47
+ // MARK: - Scene lifecycle
48
+
49
+ private func setupSceneObservers() {
50
+ NotificationCenter.default.addObserver(
51
+ self,
52
+ selector: #selector(sceneWillDeactivate),
53
+ name: UIScene.willDeactivateNotification,
54
+ object: nil
55
+ )
56
+ NotificationCenter.default.addObserver(
57
+ self,
58
+ selector: #selector(sceneDidActivate),
59
+ name: UIScene.didActivateNotification,
60
+ object: nil
61
+ )
62
+ }
63
+
64
+ @objc private func sceneWillDeactivate() {
65
+ secureLogger.debug("sceneWillDeactivate fired: isScreenSecure=\(self.isScreenSecure)")
66
+ guard isScreenSecure else { return }
67
+ installOverlay()
68
+ }
69
+
70
+ @objc private func sceneDidActivate() {
71
+ secureLogger.debug("sceneDidActivate fired — removing overlay")
72
+ removeOverlay()
73
+ }
74
+
75
+ // MARK: - Overlay management
76
+
77
+ private func installOverlay() {
78
+ // Set flag before async dispatch to prevent double-install race
79
+ guard overlayWindow == nil && !overlayInstalling else {
80
+ secureLogger.debug("installOverlay: skipped (already installing or installed)")
81
+ return
82
+ }
83
+ overlayInstalling = true
84
+ secureLogger.debug("installOverlay: dispatching to main thread")
85
+
86
+ DispatchQueue.main.async { [weak self] in
87
+ guard let self else { return }
88
+ defer { self.overlayInstalling = false }
89
+
90
+ // Accept foregroundInactive too — that's the state during the
91
+ // willDeactivate transition on both simulator and device.
92
+ guard let scene = UIApplication.shared.connectedScenes
93
+ .first(where: {
94
+ $0.activationState == .foregroundActive ||
95
+ $0.activationState == .foregroundInactive ||
96
+ $0.activationState == .background
97
+ })
98
+ as? UIWindowScene else {
99
+ secureLogger.error("installOverlay: no suitable UIWindowScene found — overlay NOT installed")
100
+ return
101
+ }
102
+
103
+ secureLogger.debug("installOverlay: scene found (state=\(String(describing: scene.activationState.rawValue))), creating overlay window")
104
+
105
+ let window = UIWindow(windowScene: scene)
106
+ window.windowLevel = .alert + 1
107
+ window.backgroundColor = .black
108
+ window.rootViewController = UIViewController()
109
+ window.rootViewController?.view.backgroundColor = .black
110
+ window.isHidden = false
111
+
112
+ self.overlayWindow = window
113
+ secureLogger.debug("installOverlay: overlay window installed successfully")
114
+ }
115
+ }
116
+
117
+ private func removeOverlay() {
118
+ DispatchQueue.main.async { [weak self] in
119
+ guard let self else { return }
120
+ if self.overlayWindow != nil {
121
+ secureLogger.debug("removeOverlay: hiding and releasing overlay window")
122
+ }
123
+ self.overlayWindow?.isHidden = true
124
+ self.overlayWindow = nil
125
+ }
126
+ }
127
+
128
+ deinit {
129
+ NotificationCenter.default.removeObserver(self)
130
+ }
131
+ }
@@ -0,0 +1,224 @@
1
+ import Foundation
2
+ import os
3
+
4
+ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "URLWhitelistManager")
5
+
6
+ /// Data structure to hold categorized URL patterns for efficient matching
7
+ private struct CategorizedPatterns {
8
+ let contains: [String]
9
+ let prefix: [String]
10
+ let suffix: [String]
11
+ }
12
+
13
+ /// Manager for URL whitelisting and access control
14
+ /// Provides centralized URL validation with pattern matching support
15
+ ///
16
+ /// Pattern matching rules:
17
+ /// - *text* or text (no wildcard) -> checks if URL contains "text"
18
+ /// - text* -> checks if URL starts with "text"
19
+ /// - *text -> checks if URL ends with "text"
20
+ ///
21
+ /// URL processing:
22
+ /// - Query parameters and hash fragments are stripped before matching
23
+ /// - Matching is case-insensitive
24
+ /// - Ports are kept as-is (user must include port in pattern if URL has explicit port)
25
+ class URLWhitelistManager {
26
+ static let shared = URLWhitelistManager()
27
+
28
+ private var accessControlEnabled: Bool = false
29
+ private var patterns: CategorizedPatterns?
30
+ private let syncQueue = DispatchQueue(label: "com.app.urlwhitelist", attributes: .concurrent)
31
+
32
+ private init() {
33
+ loadConfiguration()
34
+ }
35
+
36
+ #if DEBUG
37
+ /// Test-only initialization method to override configuration
38
+ /// This allows unit tests to set custom whitelist configurations
39
+ /// - Parameters:
40
+ /// - enabled: Whether access control is enabled
41
+ /// - allowedUrls: Array of URL patterns to allow
42
+ func testInitialize(enabled: Bool, allowedUrls: [String]) {
43
+ syncQueue.sync(flags: .barrier) {
44
+ self.accessControlEnabled = enabled
45
+ self.patterns = categorizePatterns(allowedUrls)
46
+ }
47
+ }
48
+ #endif
49
+
50
+ /// Load URL whitelist configuration from ConfigConstants
51
+ private func loadConfiguration() {
52
+ syncQueue.sync(flags: .barrier) {
53
+ self.accessControlEnabled = ConfigConstants.accessControlEnabled
54
+ let allowedUrls = ConfigConstants.allowedUrls
55
+ self.patterns = categorizePatterns(allowedUrls)
56
+
57
+ logger.info("URLWhitelistManager initialized")
58
+ logger.info("Access Control Enabled: \(self.accessControlEnabled)")
59
+ logger.info("Allowed URLs: \(allowedUrls)")
60
+ if let patterns = self.patterns {
61
+ logger.info("Categorized - Contains: \(patterns.contains)")
62
+ logger.info("Categorized - Prefix: \(patterns.prefix)")
63
+ logger.info("Categorized - Suffix: \(patterns.suffix)")
64
+ }
65
+ }
66
+ }
67
+
68
+ /// Check if URL whitelisting is enabled
69
+ var isAccessControlEnabled: Bool {
70
+ return syncQueue.sync { self.accessControlEnabled }
71
+ }
72
+
73
+ /// Check if a URL is allowed according to the whitelist patterns
74
+ /// If access control is disabled, all URLs are allowed
75
+ /// - Parameter url: The URL to check
76
+ /// - Returns: true if URL is allowed, false otherwise
77
+ func isUrlAllowed(_ url: URL) -> Bool {
78
+ return isUrlAllowed(url.absoluteString)
79
+ }
80
+
81
+ /// Check if a URL string is allowed according to the whitelist patterns
82
+ /// If access control is disabled, all URLs are allowed
83
+ /// - Parameter urlString: The URL string to check
84
+ /// - Returns: true if URL is allowed, false otherwise
85
+ func isUrlAllowed(_ urlString: String) -> Bool {
86
+ return syncQueue.sync {
87
+ // If access control is disabled, allow everything
88
+ if !self.accessControlEnabled {
89
+ return true
90
+ }
91
+
92
+ // Always allow framework server URLs (localhost with /framework- pattern)
93
+ // These are internal infrastructure URLs used for large file handling
94
+ let frameworkPattern = "^https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?/framework-[a-zA-Z0-9_-]+"
95
+ if let regex = try? NSRegularExpression(pattern: frameworkPattern, options: .caseInsensitive) {
96
+ let range = NSRange(location: 0, length: urlString.utf16.count)
97
+ if regex.firstMatch(in: urlString, options: [], range: range) != nil {
98
+ return true
99
+ }
100
+ }
101
+
102
+ guard let patterns = self.patterns else {
103
+ return false
104
+ }
105
+
106
+ // If all pattern lists are empty, block everything
107
+ if patterns.contains.isEmpty && patterns.prefix.isEmpty && patterns.suffix.isEmpty {
108
+ return false
109
+ }
110
+
111
+ let cleanUrl = cleanUrl(urlString)
112
+
113
+ // Check contains patterns
114
+ if patterns.contains.contains(where: { cleanUrl.contains($0) }) {
115
+ logger.info("✅ URL allowed (contains match): \(urlString)")
116
+ return true
117
+ }
118
+
119
+ // Check prefix patterns
120
+ if patterns.prefix.contains(where: { cleanUrl.hasPrefix($0) }) {
121
+ logger.info("✅ URL allowed (prefix match): \(urlString)")
122
+ return true
123
+ }
124
+
125
+ // Check suffix patterns
126
+ if patterns.suffix.contains(where: { cleanUrl.hasSuffix($0) }) {
127
+ logger.info("✅ URL allowed (suffix match): \(urlString)")
128
+ return true
129
+ }
130
+
131
+ logger.warning("🚫 URL blocked by access control: \(urlString)")
132
+ logger.warning("🚫 Clean URL: \(cleanUrl)")
133
+
134
+ return false
135
+ }
136
+ }
137
+
138
+ /// Check if a URL is from an external domain (not in whitelist)
139
+ /// If access control is disabled, nothing is considered external
140
+ /// - Parameter url: The URL to check
141
+ /// - Returns: true if URL is external, false if it's whitelisted
142
+ func isExternalDomain(_ url: URL) -> Bool {
143
+ return isExternalDomain(url.absoluteString)
144
+ }
145
+
146
+ /// Check if a URL string is from an external domain (not in whitelist)
147
+ /// If access control is disabled, nothing is considered external
148
+ /// - Parameter urlString: The URL string to check
149
+ /// - Returns: true if URL is external, false if it's whitelisted
150
+ func isExternalDomain(_ urlString: String) -> Bool {
151
+ // If access control is disabled, nothing is external
152
+ if !self.accessControlEnabled {
153
+ return false
154
+ }
155
+
156
+ // Simply the inverse of isUrlAllowed
157
+ return !isUrlAllowed(urlString)
158
+ }
159
+
160
+ /// Clean URL by removing query parameters and hash, then lowercase for case-insensitive matching
161
+ /// Also decodes URL-encoded characters to prevent bypass attempts
162
+ /// - Parameter url: The URL string to clean
163
+ /// - Returns: Cleaned and lowercased URL string
164
+ private func cleanUrl(_ url: String) -> String {
165
+ // Decode URL-encoded characters first to prevent bypass (e.g., %3F for ?, %23 for #)
166
+ let decoded = url.removingPercentEncoding ?? url
167
+
168
+ var cleanUrl = decoded
169
+
170
+ // Remove hash fragment (# and everything after)
171
+ if let hashIndex = cleanUrl.firstIndex(of: "#") {
172
+ cleanUrl = String(cleanUrl[..<hashIndex])
173
+ }
174
+
175
+ // Remove query parameters (? and everything after)
176
+ if let queryIndex = cleanUrl.firstIndex(of: "?") {
177
+ cleanUrl = String(cleanUrl[..<queryIndex])
178
+ }
179
+
180
+ return cleanUrl.lowercased()
181
+ }
182
+
183
+ /// Categorize patterns into contains, prefix, and suffix for efficient matching
184
+ /// - Parameter allowedUrls: Array of URL pattern strings
185
+ /// - Returns: Categorized patterns structure
186
+ private func categorizePatterns(_ allowedUrls: [String]) -> CategorizedPatterns {
187
+ var contains: [String] = []
188
+ var prefix: [String] = []
189
+ var suffix: [String] = []
190
+
191
+ for pattern in allowedUrls {
192
+ let startsWithWildcard = pattern.hasPrefix("*")
193
+ let endsWithWildcard = pattern.hasSuffix("*")
194
+
195
+ if startsWithWildcard && endsWithWildcard {
196
+ // *text* -> contains check
197
+ let extracted = String(pattern.dropFirst().dropLast()).lowercased()
198
+ if !extracted.isEmpty {
199
+ contains.append(extracted)
200
+ }
201
+ } else if endsWithWildcard {
202
+ // text* -> prefix check
203
+ let extracted = String(pattern.dropLast()).lowercased()
204
+ if !extracted.isEmpty {
205
+ prefix.append(extracted)
206
+ }
207
+ } else if startsWithWildcard {
208
+ // *text -> suffix check
209
+ let extracted = String(pattern.dropFirst()).lowercased()
210
+ if !extracted.isEmpty {
211
+ suffix.append(extracted)
212
+ }
213
+ } else {
214
+ // No wildcard -> treat as contains check
215
+ let extracted = pattern.lowercased()
216
+ if !extracted.isEmpty {
217
+ contains.append(extracted)
218
+ }
219
+ }
220
+ }
221
+
222
+ return CategorizedPatterns(contains: contains, prefix: prefix, suffix: suffix)
223
+ }
224
+ }
@@ -0,0 +1,43 @@
1
+ //
2
+ // WebKitConfig.swift
3
+ // CatalystCore
4
+ //
5
+ // Lightweight shim that keeps legacy APIs compiling without triggering
6
+ // WKProcessPool deprecation warnings on iOS 15+.
7
+ //
8
+
9
+ import WebKit
10
+
11
+ public enum WebKitConfig {
12
+ @available(iOS, introduced: 11.0, deprecated: 15.0, message: "Custom WKProcessPool instances have no effect on iOS 15+.")
13
+ public static let sharedProcessPool = WKProcessPool()
14
+
15
+ private static var legacyPrewarmedWebView: WKWebView?
16
+
17
+ /// Recreate the legacy shared process pool behavior for iOS versions where it still matters.
18
+ @discardableResult
19
+ public static func prewarmProcessPoolIfNeeded() -> WKWebView? {
20
+ if #available(iOS 15, *) {
21
+ logWithTimestamp("🔥 WebKit process pool prewarm skipped (ignored by iOS 15+)")
22
+ legacyPrewarmedWebView = nil
23
+ return nil
24
+ } else {
25
+ if legacyPrewarmedWebView == nil {
26
+ let configuration = WKWebViewConfiguration()
27
+ configuration.processPool = sharedProcessPool
28
+ legacyPrewarmedWebView = WKWebView(frame: .zero, configuration: configuration)
29
+ logWithTimestamp("🔥 WebKit process pool prewarmed for legacy iOS versions")
30
+ }
31
+ return legacyPrewarmedWebView
32
+ }
33
+ }
34
+
35
+ /// Apply the shared process pool only on OS versions where Apple still honors it.
36
+ public static func applySharedProcessPoolIfNeeded(to configuration: WKWebViewConfiguration) {
37
+ if #available(iOS 15, *) {
38
+ return
39
+ } else {
40
+ configuration.processPool = sharedProcessPool
41
+ }
42
+ }
43
+ }