catalyst-core 0.1.0-canary.4 → 0.1.0-canary.6

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 (55) hide show
  1. package/dist/native/androidProject/app/build.gradle.kts +20 -0
  2. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
  3. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +2 -10
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +4 -1
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +21 -18
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +19 -0
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +104 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/URLWhitelistManager.kt +231 -0
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/Utils.kt +0 -71
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +12 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +7 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DownloadUtils.kt +4 -3
  13. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +286 -0
  14. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +296 -0
  15. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +594 -0
  16. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +466 -0
  17. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +187 -0
  18. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +304 -0
  19. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +424 -0
  20. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +469 -0
  21. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/SecurityBridgeTest.kt +199 -0
  22. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/URLWhitelistManagerTest.kt +462 -0
  23. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +391 -0
  24. package/dist/native/bridge/constants/NativeInterfaces.js +5 -3
  25. package/dist/native/bridge/hooks.js +29 -3
  26. package/dist/native/bridge/useBaseHook.js +6 -7
  27. package/dist/native/bridge/utils/NativeBridge.js +15 -4
  28. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -6
  29. package/dist/native/iosnativeWebView/Package.resolved +0 -126
  30. package/dist/native/iosnativeWebView/Package.swift +8 -0
  31. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +9 -2
  32. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +41 -6
  33. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +13 -2
  34. package/dist/native/iosnativeWebView/Sources/Core/Utils/ScreenSecureManager.swift +131 -0
  35. package/dist/native/iosnativeWebView/Sources/Core/Utils/URLWhitelistManager.swift +173 -187
  36. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +83 -0
  37. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +7 -1
  38. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +76 -7
  39. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +15 -105
  40. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/xcshareddata/xcschemes/iosnativeWebView.xcscheme +9 -4
  41. package/dist/native/iosnativeWebView/iosnativeWebView.xctestplan +47 -0
  42. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +236 -0
  43. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeCommandHandlerSecurityTests.swift +212 -0
  44. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +418 -0
  45. package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +390 -0
  46. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +299 -0
  47. package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +383 -0
  48. package/dist/native/iosnativeWebView/iosnativeWebViewTests/FrameworkServerUtilsTests.swift +391 -0
  49. package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +581 -0
  50. package/dist/native/iosnativeWebView/iosnativeWebViewTests/NotificationHandlerTests.swift +430 -0
  51. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ScreenSecureManagerTests.swift +121 -0
  52. package/dist/native/iosnativeWebView/iosnativeWebViewTests/URLWhitelistManagerTests.swift +367 -0
  53. package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +440 -0
  54. package/dist/scripts/loadEnvironmentVariables.js +3 -1
  55. package/package.json +1 -1
@@ -15,6 +15,18 @@ val keystorePassword: String? by project.properties // Changed from keyStorePas
15
15
  val keyAlias: String? by project.properties
16
16
  val keyPassword: String? by project.properties
17
17
 
18
+ fun isAllowBackupEnabled(): Boolean {
19
+ return try {
20
+ val propsFile = File("${project.projectDir}/src/main/assets/webview_config.properties")
21
+ if (!propsFile.exists()) return false
22
+ val props = Properties()
23
+ props.load(propsFile.inputStream())
24
+ props.getProperty("android.security.allowBackup", "false").trim().lowercase() == "true"
25
+ } catch (e: Exception) {
26
+ false
27
+ }
28
+ }
29
+
18
30
  fun isNotificationsEnabled(): Boolean {
19
31
  return try {
20
32
  if (configPath == null) return false
@@ -56,6 +68,7 @@ android {
56
68
  versionName = "1.1"
57
69
  testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
58
70
  buildConfigField("String", "LOCAL_IP", "\"${getLocalIpAddress()}\"")
71
+ manifestPlaceholders["allowBackup"] = isAllowBackupEnabled()
59
72
  }
60
73
 
61
74
  // Add signing configuration for app bundle
@@ -148,6 +161,13 @@ android {
148
161
  abortOnError = true
149
162
  }
150
163
 
164
+ // Configure test options for unit tests
165
+ testOptions {
166
+ unitTests {
167
+ isReturnDefaultValues = true
168
+ }
169
+ }
170
+
151
171
  // Conditional source sets based on notifications config
152
172
  sourceSets {
153
173
  getByName("main") {
@@ -12,7 +12,7 @@
12
12
  </intent>
13
13
  </queries>
14
14
  <application
15
- android:allowBackup="true"
15
+ android:allowBackup="${allowBackup}"
16
16
  android:dataExtractionRules="@xml/data_extraction_rules"
17
17
  android:fullBackupContent="@xml/backup_rules"
18
18
  android:hardwareAccelerated="true"
@@ -62,16 +62,8 @@ object BridgeMessageValidator {
62
62
  private const val TAG = "BridgeMessageValidator"
63
63
  private const val MAX_MESSAGE_SIZE = 10 * 1024 * 1024 // 10MB (match iOS)
64
64
 
65
- // Valid commands (should match iOS CatalystConstants.Bridge.validCommands)
66
- private val validCommands = setOf(
67
- "openCamera",
68
- "requestCameraPermission",
69
- "pickFile",
70
- "requestHapticFeedback",
71
- "openFileWithIntent",
72
- "getDeviceInfo",
73
- "logger"
74
- )
65
+ // Valid commands — single source of truth from CatalystConstants
66
+ private val validCommands = CatalystConstants.Bridge.VALID_COMMANDS
75
67
 
76
68
  // MARK: - Schema Definitions (mirror iOS schemas)
77
69
 
@@ -70,7 +70,10 @@ object CatalystConstants {
70
70
  "logger",
71
71
  "pickFile",
72
72
  "openFileWithIntent",
73
- "requestHapticFeedback"
73
+ "requestHapticFeedback",
74
+ "setScreenSecure",
75
+ "getScreenSecure",
76
+ "clearWebData"
74
77
  )
75
78
  }
76
79
 
@@ -15,8 +15,7 @@ import androidx.webkit.WebResourceRequestCompat
15
15
  import android.widget.ProgressBar
16
16
  import androidx.webkit.WebViewAssetLoader
17
17
  import io.yourname.androidproject.WebCacheManager
18
- import io.yourname.androidproject.isUrlAllowed
19
- import io.yourname.androidproject.isExternalDomain
18
+ import io.yourname.androidproject.URLWhitelistManager
20
19
  import io.yourname.androidproject.matchesCachePattern
21
20
  import io.yourname.androidproject.utils.CameraUtils
22
21
  import io.yourname.androidproject.utils.NetworkUtils
@@ -100,6 +99,9 @@ class CustomWebView(
100
99
  .getProperty("accessControl.enabled", "true")
101
100
  .equals("true", ignoreCase = true)
102
101
 
102
+ // Initialize URLWhitelistManager with access control configuration
103
+ URLWhitelistManager.initialize(accessControlEnabled, allowedUrls)
104
+
103
105
  // Set initial flags based on buildOptimisation
104
106
  if (buildOptimisation) {
105
107
  isInitialApiCalled = false
@@ -187,6 +189,10 @@ class CustomWebView(
187
189
  webView.clearHistory()
188
190
  }
189
191
 
192
+ fun clearAllCache() {
193
+ cacheManager.clearAll()
194
+ }
195
+
190
196
  fun canGoBack(): Boolean = webView.canGoBack()
191
197
 
192
198
  fun goBack() {
@@ -480,7 +486,7 @@ class CustomWebView(
480
486
  }
481
487
 
482
488
  // Check if URL is allowed
483
- if (accessControlEnabled && !isUrlAllowed(url, allowedUrls)) {
489
+ if (!URLWhitelistManager.isUrlAllowed(url)) {
484
490
  if (BuildConfig.DEBUG) {
485
491
  Log.w(TAG, "🙫 SERVICE_WORKER: Request blocked by access control: $url")
486
492
  }
@@ -624,7 +630,7 @@ class CustomWebView(
624
630
  }
625
631
 
626
632
  // Check if URL is an external domain
627
- if (url.scheme in listOf("http", "https") && isExternalDomain(urlString, allowedUrls)) {
633
+ if (url.scheme in listOf("http", "https") && URLWhitelistManager.isExternalDomain(urlString)) {
628
634
  if (BuildConfig.DEBUG) {
629
635
  Log.d(TAG, "🌍 External domain detected, opening in in-app browser: $urlString")
630
636
  }
@@ -633,7 +639,7 @@ class CustomWebView(
633
639
  }
634
640
 
635
641
  // Check if URL is allowed for internal navigation
636
- if (accessControlEnabled && !isUrlAllowed(urlString, allowedUrls)) {
642
+ if (!URLWhitelistManager.isUrlAllowed(urlString)) {
637
643
  if (BuildConfig.DEBUG) {
638
644
  Log.w(TAG, "🚫 URL blocked by access control: $urlString")
639
645
  }
@@ -666,21 +672,18 @@ class CustomWebView(
666
672
  Log.d(TAG, "🔄 INTERCEPT: isInitialPageLoaded: $isInitialPageLoaded")
667
673
  }
668
674
 
669
- if (accessControlEnabled) {
670
- if (!isUrlAllowed(url, allowedUrls)) {
671
- if (BuildConfig.DEBUG) {
672
- Log.w(TAG, "🚫 STEP_1_FAIL: Network request blocked by access control: $url")
673
- Log.w(TAG, "🚫 STEP_1_FAIL: Allowed URLs: $allowedUrls")
674
- }
675
- // Return an empty response to block the request
676
- return WebResourceResponse("text/plain", "utf-8", null)
677
- } else {
678
- if (BuildConfig.DEBUG) {
679
- Log.d(TAG, "✅ STEP_1_PASS: URL allowed by access control: $url")
680
- }
675
+ if (!URLWhitelistManager.isUrlAllowed(url)) {
676
+ if (BuildConfig.DEBUG) {
677
+ Log.w(TAG, "🚫 STEP_1_FAIL: Network request blocked by access control: $url")
681
678
  }
679
+ // Return an empty response to block the request
680
+ return WebResourceResponse("text/plain", "utf-8", null)
682
681
  } else if (BuildConfig.DEBUG) {
683
- Log.d(TAG, "⚙️ STEP_1_SKIP: Access control disabled, allowing URL: $url")
682
+ if (URLWhitelistManager.isAccessControlEnabled()) {
683
+ Log.d(TAG, "✅ STEP_1_PASS: URL allowed by access control: $url")
684
+ } else {
685
+ Log.d(TAG, "⚙️ STEP_1_SKIP: Access control disabled, allowing URL: $url")
686
+ }
684
687
  }
685
688
 
686
689
  // TODO: Add HTML file workflow - index.html not available yet
@@ -141,6 +141,25 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
141
141
  // Public method for NativeBridge to get current safe area insets
142
142
  fun getCurrentSafeAreaInsets(): SafeAreaInsets = latestSafeAreaInsets
143
143
 
144
+ fun clearWebCache() {
145
+ customWebView.clearAllCache()
146
+ }
147
+
148
+ fun setScreenSecure(enable: Boolean) {
149
+ if (enable) {
150
+ window.setFlags(
151
+ WindowManager.LayoutParams.FLAG_SECURE,
152
+ WindowManager.LayoutParams.FLAG_SECURE
153
+ )
154
+ } else {
155
+ window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
156
+ }
157
+ }
158
+
159
+ fun isScreenSecure(): Boolean {
160
+ return (window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0
161
+ }
162
+
144
163
  /**
145
164
  * Notify WebView when safe area insets are updated
146
165
  * Called when deferred inset calculation completes
@@ -1029,6 +1029,110 @@ class NativeBridge(
1029
1029
  }
1030
1030
  }
1031
1031
 
1032
+ @JavascriptInterface
1033
+ fun setScreenSecure(params: String?) {
1034
+ mainActivity.runOnUiThread {
1035
+ try {
1036
+ val enable = try {
1037
+ org.json.JSONObject(params ?: "{}").optBoolean("enable", true)
1038
+ } catch (e: Exception) {
1039
+ true
1040
+ }
1041
+ mainActivity.setScreenSecure(enable)
1042
+ BridgeUtils.notifyWeb(
1043
+ webView,
1044
+ BridgeUtils.WebEvents.ON_SCREEN_SECURE_SET,
1045
+ """{"secure": $enable, "success": true}"""
1046
+ )
1047
+ } catch (e: Exception) {
1048
+ // Use notifyWebJson to prevent JSON injection from e.message
1049
+ BridgeUtils.notifyWebJson(
1050
+ webView,
1051
+ BridgeUtils.WebEvents.ON_SCREEN_SECURE_ERROR,
1052
+ org.json.JSONObject().apply {
1053
+ put("success", false)
1054
+ put("error", e.message ?: "setScreenSecure failed")
1055
+ }
1056
+ )
1057
+ }
1058
+ }
1059
+ }
1060
+
1061
+ @JavascriptInterface
1062
+ fun getScreenSecure(params: String?) {
1063
+ mainActivity.runOnUiThread {
1064
+ try {
1065
+ val isSecure = mainActivity.isScreenSecure()
1066
+ BridgeUtils.notifyWebJson(
1067
+ webView,
1068
+ BridgeUtils.WebEvents.ON_SCREEN_SECURE_STATUS,
1069
+ org.json.JSONObject().apply {
1070
+ put("secure", isSecure)
1071
+ put("success", true)
1072
+ }
1073
+ )
1074
+ } catch (e: Exception) {
1075
+ BridgeUtils.notifyWebJson(
1076
+ webView,
1077
+ BridgeUtils.WebEvents.ON_SCREEN_SECURE_ERROR,
1078
+ org.json.JSONObject().apply {
1079
+ put("success", false)
1080
+ put("error", e.message ?: "getScreenSecure failed")
1081
+ }
1082
+ )
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ @JavascriptInterface
1088
+ fun clearWebData(params: String?) {
1089
+ mainActivity.runOnUiThread {
1090
+ try {
1091
+ webView.clearCache(true)
1092
+ webView.clearHistory()
1093
+ android.webkit.WebStorage.getInstance().deleteAllData()
1094
+ mainActivity.clearWebCache()
1095
+ // CookieManager.removeAllCookies is async — fire success only from its callback
1096
+ android.webkit.CookieManager.getInstance().removeAllCookies { cookiesRemoved ->
1097
+ android.webkit.CookieManager.getInstance().flush()
1098
+ mainActivity.runOnUiThread {
1099
+ try {
1100
+ // Use notifyWebJson to prevent JSON injection; cookiesRemoved is a
1101
+ // Boolean so string interpolation is safe, but keep consistent pattern
1102
+ BridgeUtils.notifyWebJson(
1103
+ webView,
1104
+ BridgeUtils.WebEvents.ON_WEB_DATA_CLEARED,
1105
+ org.json.JSONObject().apply {
1106
+ put("success", true)
1107
+ put("cookiesRemoved", cookiesRemoved)
1108
+ }
1109
+ )
1110
+ } catch (e: Exception) {
1111
+ BridgeUtils.notifyWebJson(
1112
+ webView,
1113
+ BridgeUtils.WebEvents.ON_WEB_DATA_CLEAR_ERROR,
1114
+ org.json.JSONObject().apply {
1115
+ put("success", false)
1116
+ put("error", e.message ?: "clearWebData cookie flush failed")
1117
+ }
1118
+ )
1119
+ }
1120
+ }
1121
+ }
1122
+ } catch (e: Exception) {
1123
+ // Use notifyWebJson to prevent JSON injection from e.message
1124
+ BridgeUtils.notifyWebJson(
1125
+ webView,
1126
+ BridgeUtils.WebEvents.ON_WEB_DATA_CLEAR_ERROR,
1127
+ org.json.JSONObject().apply {
1128
+ put("success", false)
1129
+ put("error", e.message ?: "clearWebData failed")
1130
+ }
1131
+ )
1132
+ }
1133
+ }
1134
+ }
1135
+
1032
1136
  @JavascriptInterface
1033
1137
  fun getSafeArea(data: String? = null) {
1034
1138
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_SAFE_AREA_INSETS_UPDATED, "get safe area") {
@@ -0,0 +1,231 @@
1
+ package io.yourname.androidproject
2
+
3
+ import android.util.Log
4
+
5
+ /**
6
+ * Manager for URL whitelisting and access control
7
+ * Provides centralized URL validation with pattern matching support
8
+ *
9
+ * Pattern matching rules:
10
+ * - *text* or text (no wildcard) -> checks if URL contains "text"
11
+ * - text* -> checks if URL starts with "text"
12
+ * - *text -> checks if URL ends with "text"
13
+ *
14
+ * URL processing:
15
+ * - Query parameters and hash fragments are stripped before matching
16
+ * - Matching is case-insensitive
17
+ * - Ports are kept as-is (user must include port in pattern if URL has explicit port)
18
+ */
19
+ object URLWhitelistManager {
20
+
21
+ private const val TAG = "URLWhitelistManager"
22
+
23
+ /**
24
+ * Data class to hold categorized URL patterns for efficient matching
25
+ */
26
+ private data class CategorizedPatterns(
27
+ val contains: List<String>,
28
+ val prefix: List<String>,
29
+ val suffix: List<String>
30
+ )
31
+
32
+ @Volatile
33
+ private var accessControlEnabled: Boolean = false
34
+
35
+ @Volatile
36
+ private var patterns: CategorizedPatterns? = null
37
+
38
+ /**
39
+ * Initialize the whitelist manager with configuration
40
+ * Should be called once during app initialization
41
+ */
42
+ @Synchronized
43
+ fun initialize(enabled: Boolean, allowedUrls: List<String>) {
44
+ this.accessControlEnabled = enabled
45
+ this.patterns = categorizePatterns(allowedUrls)
46
+
47
+ try {
48
+ if (BuildConfig.DEBUG) {
49
+ Log.d(TAG, "URLWhitelistManager initialized")
50
+ Log.d(TAG, "Access Control Enabled: $enabled")
51
+ Log.d(TAG, "Allowed URLs: $allowedUrls")
52
+ patterns?.let {
53
+ Log.d(TAG, "Categorized - Contains: ${it.contains}")
54
+ Log.d(TAG, "Categorized - Prefix: ${it.prefix}")
55
+ Log.d(TAG, "Categorized - Suffix: ${it.suffix}")
56
+ }
57
+ }
58
+ } catch (e: RuntimeException) {
59
+ // Log not available in unit tests - silently ignore
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Check if access control is enabled
65
+ */
66
+ fun isAccessControlEnabled(): Boolean {
67
+ return accessControlEnabled
68
+ }
69
+
70
+ /**
71
+ * Check if a URL is allowed according to the whitelist patterns
72
+ * If access control is disabled, all URLs are allowed
73
+ */
74
+ fun isUrlAllowed(url: String): Boolean {
75
+ // If access control is disabled, allow everything
76
+ if (!accessControlEnabled) {
77
+ return true
78
+ }
79
+
80
+ // Always allow framework server URLs (localhost with /framework- pattern)
81
+ // These are internal infrastructure URLs used for large file handling
82
+ val frameworkUrlPattern = Regex("^https?://(?:localhost|127\\.0\\.0\\.1)(?::\\d+)?/framework-[a-zA-Z0-9_-]+", RegexOption.IGNORE_CASE)
83
+ if (frameworkUrlPattern.containsMatchIn(url)) {
84
+ return true
85
+ }
86
+
87
+ val patterns = this.patterns ?: return false
88
+
89
+ // If all pattern lists are empty, block everything
90
+ if (patterns.contains.isEmpty() && patterns.prefix.isEmpty() && patterns.suffix.isEmpty()) {
91
+ return false
92
+ }
93
+
94
+ val cleanUrl = cleanUrl(url)
95
+
96
+ // Check contains patterns
97
+ if (patterns.contains.any { cleanUrl.contains(it) }) {
98
+ try {
99
+ if (BuildConfig.DEBUG) {
100
+ Log.d(TAG, "✅ URL allowed (contains match): $url")
101
+ }
102
+ } catch (e: RuntimeException) { /* Ignore in tests */ }
103
+ return true
104
+ }
105
+
106
+ // Check prefix patterns
107
+ if (patterns.prefix.any { cleanUrl.startsWith(it) }) {
108
+ try {
109
+ if (BuildConfig.DEBUG) {
110
+ Log.d(TAG, "✅ URL allowed (prefix match): $url")
111
+ }
112
+ } catch (e: RuntimeException) { /* Ignore in tests */ }
113
+ return true
114
+ }
115
+
116
+ // Check suffix patterns
117
+ if (patterns.suffix.any { cleanUrl.endsWith(it) }) {
118
+ try {
119
+ if (BuildConfig.DEBUG) {
120
+ Log.d(TAG, "✅ URL allowed (suffix match): $url")
121
+ }
122
+ } catch (e: RuntimeException) { /* Ignore in tests */ }
123
+ return true
124
+ }
125
+
126
+ try {
127
+ if (BuildConfig.DEBUG) {
128
+ Log.w(TAG, "🚫 URL blocked by access control: $url")
129
+ Log.w(TAG, "🚫 Clean URL: $cleanUrl")
130
+ }
131
+ } catch (e: RuntimeException) { /* Ignore in tests */ }
132
+
133
+ return false
134
+ }
135
+
136
+ /**
137
+ * Check if a URL is from an external domain (not in whitelist)
138
+ * If access control is disabled, nothing is considered external
139
+ */
140
+ fun isExternalDomain(url: String): Boolean {
141
+ // If access control is disabled, nothing is external
142
+ if (!accessControlEnabled) {
143
+ return false
144
+ }
145
+
146
+ // Simply the inverse of isUrlAllowed
147
+ return !isUrlAllowed(url)
148
+ }
149
+
150
+ /**
151
+ * Clean URL by removing query parameters and hash, then lowercase for case-insensitive matching
152
+ * Also decodes URL-encoded characters to prevent bypass attempts
153
+ */
154
+ private fun cleanUrl(url: String): String {
155
+ // Decode URL-encoded characters first to prevent bypass (e.g., %3F for ?, %23 for #)
156
+ val decoded = try {
157
+ java.net.URLDecoder.decode(url, "UTF-8")
158
+ } catch (e: Exception) {
159
+ // If decode fails, use original URL
160
+ url
161
+ }
162
+
163
+ var cleanUrl = decoded
164
+
165
+ // Remove hash fragment (# and everything after)
166
+ val hashIndex = cleanUrl.indexOf('#')
167
+ if (hashIndex != -1) {
168
+ cleanUrl = cleanUrl.substring(0, hashIndex)
169
+ }
170
+
171
+ // Remove query parameters (? and everything after)
172
+ val queryIndex = cleanUrl.indexOf('?')
173
+ if (queryIndex != -1) {
174
+ cleanUrl = cleanUrl.substring(0, queryIndex)
175
+ }
176
+
177
+ return cleanUrl.lowercase()
178
+ }
179
+
180
+ /**
181
+ * Categorize patterns into contains, prefix, and suffix for efficient matching
182
+ */
183
+ private fun categorizePatterns(allowedUrls: List<String>): CategorizedPatterns {
184
+ val contains = mutableListOf<String>()
185
+ val prefix = mutableListOf<String>()
186
+ val suffix = mutableListOf<String>()
187
+
188
+ allowedUrls.forEach { pattern ->
189
+ if (pattern.isEmpty()) return@forEach
190
+
191
+ val startsWithWildcard = pattern.startsWith('*')
192
+ val endsWithWildcard = pattern.endsWith('*')
193
+
194
+ when {
195
+ startsWithWildcard && endsWithWildcard -> {
196
+ // *text* -> contains check
197
+ if (pattern.length <= 2) return@forEach // Pattern is just * or ** - ignore
198
+ val extracted = pattern.substring(1, pattern.length - 1).lowercase()
199
+ if (extracted.isNotEmpty()) {
200
+ contains.add(extracted)
201
+ }
202
+ }
203
+ endsWithWildcard -> {
204
+ // text* -> prefix check
205
+ if (pattern.length <= 1) return@forEach // Pattern is just * - ignore
206
+ val extracted = pattern.substring(0, pattern.length - 1).lowercase()
207
+ if (extracted.isNotEmpty()) {
208
+ prefix.add(extracted)
209
+ }
210
+ }
211
+ startsWithWildcard -> {
212
+ // *text -> suffix check
213
+ if (pattern.length <= 1) return@forEach // Pattern is just * - ignore
214
+ val extracted = pattern.substring(1).lowercase()
215
+ if (extracted.isNotEmpty()) {
216
+ suffix.add(extracted)
217
+ }
218
+ }
219
+ else -> {
220
+ // No wildcard -> treat as contains check
221
+ val extracted = pattern.lowercase()
222
+ if (extracted.isNotEmpty()) {
223
+ contains.add(extracted)
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ return CategorizedPatterns(contains, prefix, suffix)
230
+ }
231
+ }
@@ -2,43 +2,6 @@ package io.yourname.androidproject
2
2
 
3
3
  import android.net.Uri
4
4
 
5
- fun isUrlAllowed(url: String, allowedUrls: List<String>): Boolean {
6
- // TODO: temp
7
- return true
8
- // Always allow framework server URLs (localhost with /framework- pattern)
9
- // These are internal infrastructure URLs used for large file handling
10
- // Use strict regex to prevent bypass attacks
11
- val frameworkUrlPattern = Regex("^https?://(?:localhost|127\\.0\\.0\\.1)(?::\\d+)?/framework-[a-zA-Z0-9_-]+", RegexOption.IGNORE_CASE)
12
- if (frameworkUrlPattern.containsMatchIn(url)) {
13
- return true
14
- }
15
-
16
- if (allowedUrls.isEmpty()) return false
17
-
18
- val parsedUrl = Uri.parse(url) ?: return false
19
- val urlHost = "${parsedUrl.scheme}://${parsedUrl.host}${if (parsedUrl.port != -1) ":${parsedUrl.port}" else ""}"
20
-
21
- return allowedUrls.any { pattern ->
22
- when {
23
- pattern.contains("*") -> {
24
- val regex = pattern
25
- .replace(".", "\\.")
26
- .replace("*", ".*")
27
- .toRegex(RegexOption.IGNORE_CASE)
28
- regex.matches(url) || regex.matches(urlHost)
29
- }
30
- else -> {
31
- val patternUri = Uri.parse(pattern)
32
- val patternHost = "${patternUri.scheme}://${patternUri.host}${if (patternUri.port != -1) ":${patternUri.port}" else ""}"
33
-
34
- // Exact match or pattern without port matches URL with any port
35
- urlHost.equals(patternHost, ignoreCase = true) ||
36
- (patternUri.port == -1 && urlHost.startsWith("${patternUri.scheme}://${patternUri.host}", ignoreCase = true))
37
- }
38
- }
39
- }
40
- }
41
-
42
5
  /**
43
6
  * Checks if a URL matches any of the cache patterns
44
7
  * Handles query parameters, hashes, and version strings
@@ -92,38 +55,4 @@ fun matchesCachePattern(url: String, cachePatterns: List<String>): Boolean {
92
55
  // If parsing fails, return false
93
56
  return false
94
57
  }
95
- }
96
-
97
- fun isExternalDomain(url: String, allowedUrls: List<String>): Boolean {
98
- if (allowedUrls.isEmpty()) return true
99
-
100
- val parsedUrl = Uri.parse(url)
101
- val urlHost = parsedUrl.host ?: return true
102
- val urlScheme = parsedUrl.scheme ?: return true
103
- val urlPort = parsedUrl.port
104
-
105
- return !allowedUrls.any { pattern ->
106
- if (pattern.startsWith("*.")) {
107
- val domain = pattern.substring(2)
108
- urlHost.equals(domain, ignoreCase = true) || urlHost.endsWith(".$domain", ignoreCase = true)
109
- } else {
110
- val patternUri = Uri.parse(pattern)
111
- val patternHost = patternUri?.host ?: return@any false
112
- val patternScheme = patternUri?.scheme ?: return@any false
113
- val patternPort = patternUri?.port ?: -1
114
-
115
- val schemeMatches = urlScheme.equals(patternScheme, ignoreCase = true)
116
- val hostMatches = if (patternHost.startsWith("*.")) {
117
- val domain = patternHost.substring(2)
118
- urlHost.equals(domain, ignoreCase = true) || urlHost.endsWith(".$domain", ignoreCase = true)
119
- } else {
120
- urlHost.equals(patternHost, ignoreCase = true)
121
- }
122
- val portMatches = patternPort == -1 || urlPort == patternPort ||
123
- (urlPort == -1 && ((patternPort == 443 && patternScheme.equals("https", ignoreCase = true)) ||
124
- (patternPort == 80 && patternScheme.equals("http", ignoreCase = true))))
125
-
126
- schemeMatches && hostMatches && portMatches
127
- }
128
- }
129
58
  }
@@ -178,6 +178,18 @@ class WebCacheManager(private val context: Context, private val properties: java
178
178
  }
179
179
  }
180
180
 
181
+ fun clearAll() {
182
+ try {
183
+ memoryCache.evictAll()
184
+ cacheDir.listFiles()?.forEach { it.delete() }
185
+ if (BuildConfig.DEBUG) {
186
+ Log.d(TAG, "clearAll: memory cache and disk cache wiped")
187
+ }
188
+ } catch (e: Exception) {
189
+ Log.e(TAG, "clearAll failed: ${e.message}")
190
+ }
191
+ }
192
+
181
193
  suspend fun cleanup() {
182
194
  withContext(Dispatchers.IO) {
183
195
  try {
@@ -75,6 +75,13 @@ object BridgeUtils {
75
75
  NETWORK_STATUS_CHANGED("NETWORK_STATUS_CHANGED"),
76
76
  // Safe area events
77
77
  ON_SAFE_AREA_INSETS_UPDATED("ON_SAFE_AREA_INSETS_UPDATED"),
78
+
79
+ // Security events
80
+ ON_SCREEN_SECURE_SET("ON_SCREEN_SECURE_SET"),
81
+ ON_SCREEN_SECURE_STATUS("ON_SCREEN_SECURE_STATUS"),
82
+ ON_SCREEN_SECURE_ERROR("ON_SCREEN_SECURE_ERROR"),
83
+ ON_WEB_DATA_CLEARED("ON_WEB_DATA_CLEARED"),
84
+ ON_WEB_DATA_CLEAR_ERROR("ON_WEB_DATA_CLEAR_ERROR"),
78
85
  }
79
86
 
80
87
  /**