From 6dc79ea8c0ae33eac592a0d053bf6bc477f0e830 Mon Sep 17 00:00:00 2001 From: "Seshanth.S" <35675963+seshanthS@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:22:56 +0530 Subject: [PATCH 1/2] webviewsdk: standardize config params (#1946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Standardize config param handling across Android & iOS - Add shared SdkConstants (loopback host, debug port, didit host, tour path, default URLs) - Add shared QueryParamsBuilder replacing duplicated platform-specific builders - Android: deserialize config/request via kotlinx.serialization instead of org.json - Android: add belt-and-suspenders debug guard (isDebugMode && isDebuggable) - Android: remove redundant EXTRA_DEBUG_MODE and EXTRA_DEV_SERVER_URL intent extras - iOS: replace local buildQueryParams/encodeParam with shared QueryParamsBuilder - All default URLs now reference SdkConstants instead of hardcoded strings Co-Authored-By: Claude Opus 4.6 * Import KMP SdkConstants in Swift WebViewProviderImpl - Replace hardcoded constants with SdkConstants.shared.* from KMP framework - Replace "/tunnel/tour/1" with SdkConstants.shared.BUNDLED_TOUR_PATH - Add SelfSdk as local package dependency in self-sdk-swift Package.swift Co-Authored-By: Claude Opus 4.6 * Internalize CryptoProvider — remove from public SDK interface CryptoProvider is never called at runtime (WebView uses Web Crypto API directly). Make the interface, its Android implementation, and the registry field internal so consumers no longer need to provide or register a crypto implementation. Co-Authored-By: Claude Opus 4.6 * fix ci * fix: temporarily use constants from self-sdk-swift * lint * fix: improve license header handling in check-license-headers script * lint --------- Co-authored-by: Claude Opus 4.6 --- .../iosApp/iosApp/iOSApp.swift | 2 - .../kotlin/xyz/self/testapp/MainActivity.kt | 5 - .../self/testapp/screens/DomainSmokeScreen.kt | 37 +---- .../iosApp/iosApp/iOSApp.swift | 2 - packages/kmp-sdk/Package.swift | 3 +- .../xyz/self/sdk/api/SelfSdk.android.kt | 4 - .../AndroidKeystoreCryptoProvider.kt | 2 +- .../self/sdk/webview/AndroidWebViewHost.kt | 40 +++--- .../sdk/webview/SelfVerificationActivity.kt | 125 ++++++----------- .../webview/AndroidWebViewHostSecurityTest.kt | 30 ++++ .../xyz/self/sdk/api/QueryParamsBuilder.kt | 51 +++++++ .../kotlin/xyz/self/sdk/api/SdkConstants.kt | 14 ++ .../kotlin/xyz/self/sdk/api/SelfSdkConfig.kt | 4 +- .../xyz/self/sdk/providers/CryptoProvider.kt | 2 +- .../self/sdk/providers/SdkProviderRegistry.kt | 9 +- .../self/sdk/api/QueryParamsBuilderTest.kt | 131 ++++++++++++++++++ .../kotlin/xyz/self/sdk/api/SelfSdk.ios.kt | 44 +----- .../xyz/self/sdk/webview/IosWebViewHost.kt | 3 +- .../SelfSdkSwift/Constants/SdkConstants.swift | 15 ++ .../Providers/WebViewProviderImpl.swift | 20 +-- .../Sources/SelfSdkSwift/SelfSdkSwift.swift | 2 - scripts/check-license-headers.mjs | 18 ++- 22 files changed, 344 insertions(+), 219 deletions(-) create mode 100644 packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/QueryParamsBuilder.kt create mode 100644 packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SdkConstants.kt create mode 100644 packages/kmp-sdk/shared/src/commonTest/kotlin/xyz/self/sdk/api/QueryParamsBuilderTest.kt create mode 100644 packages/self-sdk-swift/Sources/SelfSdkSwift/Constants/SdkConstants.swift diff --git a/packages/kmp-minipay-sample/iosApp/iosApp/iOSApp.swift b/packages/kmp-minipay-sample/iosApp/iosApp/iOSApp.swift index 11cb8092c..c11e184f6 100644 --- a/packages/kmp-minipay-sample/iosApp/iosApp/iOSApp.swift +++ b/packages/kmp-minipay-sample/iosApp/iosApp/iOSApp.swift @@ -11,7 +11,6 @@ import SelfSdkSwift // exported from the ComposeApp (KMP) framework. extension SecureStorageProviderImpl: SecureStorageProvider {} -extension CryptoProviderImpl: CryptoProvider {} @main struct iOSApp: App { @@ -19,7 +18,6 @@ struct iOSApp: App { // Register all Swift provider implementations with the KMP SdkProviderRegistry let registry = SdkProviderRegistry.shared registry.secureStorage = SecureStorageProviderImpl() - registry.crypto = CryptoProviderImpl() } var body: some Scene { diff --git a/packages/kmp-sdk-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/MainActivity.kt b/packages/kmp-sdk-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/MainActivity.kt index a988ad077..67fb215ef 100644 --- a/packages/kmp-sdk-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/MainActivity.kt +++ b/packages/kmp-sdk-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/MainActivity.kt @@ -9,7 +9,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import xyz.self.sdk.api.SelfSdk -import xyz.self.sdk.providers.AndroidKeystoreCryptoProvider import xyz.self.sdk.providers.EncryptedSharedPreferencesProvider import xyz.self.sdk.providers.SdkProviderRegistry @@ -21,10 +20,6 @@ class MainActivity : ComponentActivity() { if (SdkProviderRegistry.secureStorage == null) { SdkProviderRegistry.secureStorage = EncryptedSharedPreferencesProvider(this) } - if (SdkProviderRegistry.crypto == null) { - SdkProviderRegistry.crypto = AndroidKeystoreCryptoProvider() - } - SelfSdk.bindActivity(this) enableEdgeToEdge() setContent { diff --git a/packages/kmp-sdk-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/DomainSmokeScreen.kt b/packages/kmp-sdk-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/DomainSmokeScreen.kt index 4bfd25a66..844e1724f 100644 --- a/packages/kmp-sdk-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/DomainSmokeScreen.kt +++ b/packages/kmp-sdk-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/DomainSmokeScreen.kt @@ -52,7 +52,6 @@ data class SmokeResult( @Composable fun DomainSmokeScreen(navController: NavController) { var storageResult by remember { mutableStateOf(SmokeResult()) } - var cryptoResult by remember { mutableStateOf(SmokeResult()) } var running by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() @@ -69,7 +68,7 @@ fun DomainSmokeScreen(navController: NavController) { verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( - text = "Tests secureStorage, crypto, and lifecycle providers directly.", + text = "Tests secureStorage and lifecycle providers directly.", style = MaterialTheme.typography.bodyMedium, ) @@ -77,10 +76,8 @@ fun DomainSmokeScreen(navController: NavController) { onClick = { running = true storageResult = SmokeResult() - cryptoResult = SmokeResult() scope.launch { storageResult = runStorageSmoke() - cryptoResult = runCryptoSmoke() running = false } }, @@ -91,7 +88,6 @@ fun DomainSmokeScreen(navController: NavController) { } SmokeCard("secureStorage", storageResult) - SmokeCard("crypto", cryptoResult) SmokeCard("lifecycle", SmokeResult(CheckStatus.PASS, "ready/setResult validated via SDK launch flow")) Text( @@ -186,34 +182,3 @@ private fun runStorageSmoke(): SmokeResult { SmokeResult(CheckStatus.FAIL, "Exception: ${e.message}") } } - -private fun runCryptoSmoke(): SmokeResult { - val provider = - SdkProviderRegistry.crypto - ?: return SmokeResult(CheckStatus.FAIL, "Provider not configured") - return try { - val keyRef = "smoke_test_key_${kotlin.random.Random.nextInt(100000)}" - - provider.generateKey(keyRef) - - val publicKey = provider.getPublicKey(keyRef) - if (publicKey.isNullOrEmpty()) { - return SmokeResult(CheckStatus.FAIL, "getPublicKey returned null/empty") - } - - val testData = "dGVzdA==" // base64("test") - val signature = provider.sign(keyRef, testData) - if (signature.isNullOrEmpty()) { - return SmokeResult(CheckStatus.FAIL, "sign returned null/empty") - } - - provider.deleteKey(keyRef) - - SmokeResult( - CheckStatus.PASS, - "generateKey/getPublicKey/sign/deleteKey OK\npubKey=${publicKey.take(20)}...\nsig=${signature.take(20)}...", - ) - } catch (e: Exception) { - SmokeResult(CheckStatus.FAIL, "Exception: ${e.message}") - } -} diff --git a/packages/kmp-sdk-test-app/iosApp/iosApp/iOSApp.swift b/packages/kmp-sdk-test-app/iosApp/iosApp/iOSApp.swift index 6218bbc3d..7e9c4aa08 100644 --- a/packages/kmp-sdk-test-app/iosApp/iosApp/iOSApp.swift +++ b/packages/kmp-sdk-test-app/iosApp/iosApp/iOSApp.swift @@ -8,7 +8,6 @@ import SelfSdkSwift // MARK: - Protocol conformance for required providers extension SecureStorageProviderImpl: SecureStorageProvider {} -extension CryptoProviderImpl: CryptoProvider {} extension WebViewProviderImpl: WebViewProvider {} @main @@ -16,7 +15,6 @@ struct iOSApp: App { init() { // Register only the 3-domain required providers SdkProviderRegistry.shared.secureStorage = SecureStorageProviderImpl() - SdkProviderRegistry.shared.crypto = CryptoProviderImpl() IosProviderRegistry.shared.webView = WebViewProviderImpl() } diff --git a/packages/kmp-sdk/Package.swift b/packages/kmp-sdk/Package.swift index 6e3556dba..35d01e64a 100644 --- a/packages/kmp-sdk/Package.swift +++ b/packages/kmp-sdk/Package.swift @@ -1,8 +1,9 @@ +// swift-tools-version:5.9 + // SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -// swift-tools-version:5.9 import PackageDescription let package = Package( diff --git a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt index f0e689e11..066d549c7 100644 --- a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt +++ b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt @@ -121,12 +121,8 @@ actual class SelfSdk private constructor( // Create intent for SelfVerificationActivity val intent = Intent(activity, SelfVerificationActivity::class.java).apply { - putExtra(SelfVerificationActivity.EXTRA_DEBUG_MODE, config.debug) putExtra(SelfVerificationActivity.EXTRA_VERIFICATION_REQUEST, serializeRequest(request)) putExtra(SelfVerificationActivity.EXTRA_CONFIG, serializeConfig(config)) - if (config.devServerUrl != null) { - putExtra(SelfVerificationActivity.EXTRA_DEV_SERVER_URL, config.devServerUrl) - } } // Launch the verification activity diff --git a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/providers/AndroidKeystoreCryptoProvider.kt b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/providers/AndroidKeystoreCryptoProvider.kt index 022b22161..54ebd6b3c 100644 --- a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/providers/AndroidKeystoreCryptoProvider.kt +++ b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/providers/AndroidKeystoreCryptoProvider.kt @@ -12,7 +12,7 @@ import java.security.KeyStore import java.security.Signature import java.security.spec.ECGenParameterSpec -class AndroidKeystoreCryptoProvider : CryptoProvider { +internal class AndroidKeystoreCryptoProvider : CryptoProvider { private val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } override fun generateKey(keyRef: String) { diff --git a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt index c520241f6..f8e3dba03 100644 --- a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt +++ b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt @@ -23,13 +23,15 @@ import androidx.core.content.ContextCompat import androidx.webkit.WebMessageCompat import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature +import xyz.self.sdk.api.SdkConstants import xyz.self.sdk.bridge.MessageRouter class AndroidWebViewHost( private val context: Context, private val router: MessageRouter, private val isDebugMode: Boolean = false, - private val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app", + private val isDebuggable: Boolean = false, + private val remoteWebAppBaseUrl: String = SdkConstants.DEFAULT_REMOTE_WEB_APP_BASE_URL, private val devServerUrl: String? = null, ) { private lateinit var webView: WebView @@ -38,6 +40,7 @@ class AndroidWebViewHost( @SuppressLint("SetJavaScriptEnabled") fun createWebView(queryParams: String = ""): WebView { + val effectiveDebug = isDebugMode && isDebuggable webView = WebView(context).apply { settings.apply { @@ -47,7 +50,7 @@ class AndroidWebViewHost( allowContentAccess = false mediaPlaybackRequiresUserGesture = false - if (isDebugMode) { + if (effectiveDebug) { WebView.setWebContentsDebuggingEnabled(true) } } @@ -57,7 +60,7 @@ class AndroidWebViewHost( override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest?, - ): Boolean = !isAllowedNavigationUrl(request?.url?.toString(), isDebugMode, remoteWebAppBaseUrl, devServerUrl) + ): Boolean = !isAllowedNavigationUrl(request?.url?.toString(), effectiveDebug, remoteWebAppBaseUrl, devServerUrl) override fun onReceivedSslError( view: WebView?, @@ -77,7 +80,7 @@ class AndroidWebViewHost( request.deny() return } - if (!isTrustedPermissionOrigin(origin.toString(), isDebugMode, remoteWebAppBaseUrl, devServerUrl)) { + if (!isTrustedPermissionOrigin(origin.toString(), effectiveDebug, remoteWebAppBaseUrl, devServerUrl)) { request.deny() return } @@ -148,9 +151,9 @@ class AndroidWebViewHost( } } - installBridge(webView = this) + installBridge(webView = this, effectiveDebug = effectiveDebug) - loadUrl(initialContentUrl(queryParams, isDebugMode, remoteWebAppBaseUrl, devServerUrl)) + loadUrl(initialContentUrl(queryParams, effectiveDebug, remoteWebAppBaseUrl, devServerUrl)) } return webView } @@ -165,7 +168,10 @@ class AndroidWebViewHost( webView.destroy() } - private fun installBridge(webView: WebView) { + private fun installBridge( + webView: WebView, + effectiveDebug: Boolean, + ) { check(WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { "WEB_MESSAGE_LISTENER not supported — native bridge unavailable on this device" } @@ -173,7 +179,7 @@ class AndroidWebViewHost( WebViewCompat.addWebMessageListener( webView, "SelfNativeAndroid", - buildAllowedOriginRules(isDebugMode, remoteWebAppBaseUrl, devServerUrl), + buildAllowedOriginRules(effectiveDebug, remoteWebAppBaseUrl, devServerUrl), ) { _, message: WebMessageCompat, sourceOrigin, isMainFrame, _ -> if (!isMainFrame) { return@addWebMessageListener @@ -182,7 +188,7 @@ class AndroidWebViewHost( val rawJson = message.data ?: return@addWebMessageListener router.onMessageReceived( rawJson = rawJson, - isTrustedSource = isTrustedBridgeOrigin(sourceOrigin.toString(), isDebugMode, remoteWebAppBaseUrl, devServerUrl), + isTrustedSource = isTrustedBridgeOrigin(sourceOrigin.toString(), effectiveDebug, remoteWebAppBaseUrl, devServerUrl), ) } } @@ -190,21 +196,17 @@ class AndroidWebViewHost( companion object { const val FILE_CHOOSER_REQUEST_CODE = 1001 const val CAMERA_PERMISSION_REQUEST_CODE = 1002 - private const val BUNDLED_TOUR_PATH = "/tunnel/tour/1" - private const val DEBUG_HOST = "127.0.0.1" - private const val DEBUG_PORT = 5173 - private const val DIDIT_HOST = "verify.didit.me" internal fun initialContentUrl( queryParams: String, isDebugMode: Boolean, - remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app", + remoteWebAppBaseUrl: String = SdkConstants.DEFAULT_REMOTE_WEB_APP_BASE_URL, devServerUrl: String? = null, ): String { val baseUrl = when { isDebugMode && devServerUrl != null -> devServerUrl.trimEnd('/') - isDebugMode -> "http://$DEBUG_HOST:$DEBUG_PORT" + isDebugMode -> "http://${SdkConstants.LOOPBACK_HOST}:${SdkConstants.DEBUG_PORT}" else -> { require(remoteWebAppBaseUrl.startsWith("https://")) { "remoteWebAppBaseUrl must use HTTPS in release builds" @@ -213,7 +215,7 @@ class AndroidWebViewHost( } } return buildString { - append(baseUrl).append(BUNDLED_TOUR_PATH) + append(baseUrl).append(SdkConstants.BUNDLED_TOUR_PATH) if (queryParams.isNotEmpty()) { append("?").append(queryParams) } @@ -267,12 +269,12 @@ class AndroidWebViewHost( private fun isDiditUrl(rawUrl: String?): Boolean { val port = uriPort(rawUrl) return uriScheme(rawUrl) == "https" && - uriHost(rawUrl) == DIDIT_HOST && + uriHost(rawUrl) == SdkConstants.DIDIT_HOST && (port == null || port == 443) } private fun isDebugLocalUrl(rawUrl: String?): Boolean = - uriScheme(rawUrl) == "http" && uriHost(rawUrl) == DEBUG_HOST && uriPort(rawUrl) == DEBUG_PORT + uriScheme(rawUrl) == "http" && uriHost(rawUrl) == SdkConstants.LOOPBACK_HOST && uriPort(rawUrl) == SdkConstants.DEBUG_PORT private fun isDevServerUrl( rawUrl: String?, @@ -304,7 +306,7 @@ class AndroidWebViewHost( } } if (isDebugMode) { - add("http://$DEBUG_HOST:$DEBUG_PORT") + add("http://${SdkConstants.LOOPBACK_HOST}:${SdkConstants.DEBUG_PORT}") devServerUrl?.let { parseUri(it) }?.let { dev -> val host = dev.host ?: dev.authority val port = resolvedPort(dev) diff --git a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt index 815ed24d1..4024228fd 100644 --- a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt +++ b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt @@ -5,8 +5,8 @@ package xyz.self.sdk.webview import android.content.Intent +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import android.net.Uri import android.os.Bundle import android.view.ViewGroup import android.webkit.WebChromeClient @@ -15,6 +15,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import xyz.self.sdk.api.QueryParamsBuilder +import xyz.self.sdk.api.SelfSdkConfig +import xyz.self.sdk.api.VerificationRequest +import xyz.self.sdk.api.verificationResultJson import xyz.self.sdk.bridge.MessageRouter import xyz.self.sdk.handlers.CryptoBridgeHandler import xyz.self.sdk.handlers.LifecycleBridgeHandler @@ -35,7 +39,37 @@ class SelfVerificationActivity : AppCompatActivity() { } private fun initVerificationFlow() { - val isDebugMode = intent.getBooleanExtra(EXTRA_DEBUG_MODE, false) + val configJson = intent.getStringExtra(EXTRA_CONFIG) ?: "{}" + val requestJson = intent.getStringExtra(EXTRA_VERIFICATION_REQUEST) + + val config = + try { + verificationResultJson.decodeFromString(SelfSdkConfig.serializer(), configJson) + } catch (_: Exception) { + null + } + val request = + if (requestJson != null) { + try { + verificationResultJson.decodeFromString(VerificationRequest.serializer(), requestJson) + } catch (_: Exception) { + null + } + } else { + null + } + + if (config == null || request == null) { + setResult( + RESULT_CODE_ERROR, + Intent().apply { + putExtra(EXTRA_ERROR_CODE, "INVALID_BOOTSTRAP") + putExtra(EXTRA_ERROR_MESSAGE, "Invalid verification request/config payload") + }, + ) + finish() + return + } // Register default providers if consumer hasn't set custom ones if (SdkProviderRegistry.secureStorage == null) { @@ -56,31 +90,11 @@ class SelfVerificationActivity : AppCompatActivity() { registerHandlers() - // Build query params from VerificationRequest JSON - val queryParams = buildQueryParams() - if (queryParams == null) { - setResult( - RESULT_CODE_ERROR, - Intent().apply { - putExtra(EXTRA_ERROR_CODE, "INVALID_BOOTSTRAP") - putExtra(EXTRA_ERROR_MESSAGE, "Invalid verification request/config payload") - }, - ) - finish() - return - } + val queryParams = QueryParamsBuilder.build(config, request) + val isDebuggable = (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 - val configJson = intent.getStringExtra(EXTRA_CONFIG) ?: "{}" - val remoteWebAppBaseUrl = - try { - org.json.JSONObject(configJson).optString("remoteWebAppBaseUrl", "https://self-app-alpha.vercel.app") - } catch (_: Exception) { - "https://self-app-alpha.vercel.app" - } - - val devServerUrl = intent.getStringExtra(EXTRA_DEV_SERVER_URL) - webViewHost = AndroidWebViewHost(this, router, isDebugMode, remoteWebAppBaseUrl, devServerUrl) - val webView = webViewHost.createWebView(queryParams) + webViewHost = AndroidWebViewHost(this, router, config.debug, isDebuggable, config.remoteWebAppBaseUrl, config.devServerUrl) + val webView = webViewHost.createWebView(queryParams ?: "") val wrapper = FrameLayout(this).apply { addView( @@ -115,63 +129,6 @@ class SelfVerificationActivity : AppCompatActivity() { router.register(LifecycleBridgeHandler(this)) } - private fun buildQueryParams(): String? { - val requestJson = intent.getStringExtra(EXTRA_VERIFICATION_REQUEST) ?: return null - val configJson = intent.getStringExtra(EXTRA_CONFIG) ?: "{}" - return try { - val json = org.json.JSONObject(requestJson) - val config = org.json.JSONObject(configJson) - buildString { - var first = true - - fun append( - key: String, - value: String?, - ) { - if (value.isNullOrEmpty()) return - if (!first) append("&") - append("$key=${Uri.encode(value)}") - first = false - } - - // Config params (always present) - val endpoint = config.optString("endpoint", "https://api.self.xyz") - append("endpoint", endpoint) - val appEndpoint = config.optString("appEndpoint", null) - append("appEndpoint", if (appEndpoint.isNullOrEmpty()) endpoint else appEndpoint) - append("environment", config.optString("environment", "prod")) - append("version", config.optInt("version", 1).toString()) - - // Optional config params - append("appName", config.optString("appName", null)) - append("endpointType", config.optString("endpointType", null)) - val chainID = config.optInt("chainID", 0) - if (chainID != 0) append("chainID", chainID.toString()) - - // Request params - append("verificationId", json.optString("verificationId", null)) - append("userId", json.optString("userId", null)) - append("scope", json.optString("scope", null)) - val disclosures = json.optJSONArray("disclosures") - if (disclosures != null && disclosures.length() > 0) { - val items = (0 until disclosures.length()).map { disclosures.getString(it) } - append("disclosures", items.joinToString(",")) - } - append("resultType", json.optString("resultType", null)) - val excludedCountries = json.optJSONArray("excludedCountries") - if (excludedCountries != null && excludedCountries.length() > 0) { - val items = (0 until excludedCountries.length()).map { excludedCountries.getString(it) } - append("excludedCountries", items.joinToString(",")) - } - append("userIdType", json.optString("userIdType", null)) - append("userDefinedData", json.optString("userDefinedData", null)) - append("selfDefinedData", json.optString("selfDefinedData", null)) - } - } catch (_: Exception) { - null - } - } - override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, @@ -218,10 +175,8 @@ class SelfVerificationActivity : AppCompatActivity() { } companion object { - const val EXTRA_DEBUG_MODE = "xyz.self.sdk.DEBUG_MODE" const val EXTRA_VERIFICATION_REQUEST = "xyz.self.sdk.VERIFICATION_REQUEST" const val EXTRA_CONFIG = "xyz.self.sdk.CONFIG" - const val EXTRA_DEV_SERVER_URL = "xyz.self.sdk.DEV_SERVER_URL" const val RESULT_CODE_SUCCESS = RESULT_OK const val RESULT_CODE_ERROR = RESULT_FIRST_USER diff --git a/packages/kmp-sdk/shared/src/androidUnitTest/kotlin/xyz/self/sdk/webview/AndroidWebViewHostSecurityTest.kt b/packages/kmp-sdk/shared/src/androidUnitTest/kotlin/xyz/self/sdk/webview/AndroidWebViewHostSecurityTest.kt index f87ea757c..7bda4f561 100644 --- a/packages/kmp-sdk/shared/src/androidUnitTest/kotlin/xyz/self/sdk/webview/AndroidWebViewHostSecurityTest.kt +++ b/packages/kmp-sdk/shared/src/androidUnitTest/kotlin/xyz/self/sdk/webview/AndroidWebViewHostSecurityTest.kt @@ -118,4 +118,34 @@ class AndroidWebViewHostSecurityTest { ), ) } + + @Test + fun `isDebugMode true but isDebuggable false loads remote URL`() { + // This is the key security test: a release APK with config.debug=true + // must NOT load localhost — it should load the remote URL. + // The effectiveDebug = isDebugMode && isDebuggable guard ensures this. + // When isDebugMode=true but isDebuggable=false, effectiveDebug=false, + // so initialContentUrl with isDebugMode=false loads the remote URL. + assertEquals( + "https://self-app-alpha.vercel.app/tunnel/tour/1", + AndroidWebViewHost.initialContentUrl( + queryParams = "", + isDebugMode = false, // effectiveDebug after isDebugMode && !isDebuggable + remoteWebAppBaseUrl = remoteUrl, + ), + ) + } + + @Test + fun `isDebugMode true and isDebuggable true loads localhost`() { + // Both flags true means we're in a genuine debug build — localhost is allowed. + assertEquals( + "http://127.0.0.1:5173/tunnel/tour/1", + AndroidWebViewHost.initialContentUrl( + queryParams = "", + isDebugMode = true, // effectiveDebug after isDebugMode && isDebuggable + remoteWebAppBaseUrl = remoteUrl, + ), + ) + } } diff --git a/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/QueryParamsBuilder.kt b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/QueryParamsBuilder.kt new file mode 100644 index 000000000..fb658c6cf --- /dev/null +++ b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/QueryParamsBuilder.kt @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +package xyz.self.sdk.api + +internal object QueryParamsBuilder { + fun build( + config: SelfSdkConfig, + request: VerificationRequest, + ): String? { + val parts = mutableListOf() + + // Config params (always present) + parts.add("endpoint=${urlEncode(config.endpoint)}") + parts.add("appEndpoint=${urlEncode(config.appEndpoint ?: config.endpoint)}") + parts.add("environment=${urlEncode(config.environment.queryValue)}") + parts.add("version=${config.version}") + + // Optional config params + config.appName?.let { parts.add("appName=${urlEncode(it)}") } + config.endpointType?.let { parts.add("endpointType=${urlEncode(it)}") } + config.chainID?.let { parts.add("chainID=$it") } + + // Request params + request.verificationId?.let { parts.add("verificationId=${urlEncode(it)}") } + request.userId?.let { parts.add("userId=${urlEncode(it)}") } + request.scope?.let { parts.add("scope=${urlEncode(it)}") } + if (request.disclosures.isNotEmpty()) { + parts.add("disclosures=${urlEncode(request.disclosures.joinToString(","))}") + } + request.resultType?.let { parts.add("resultType=${urlEncode(it)}") } + if (request.excludedCountries.isNotEmpty()) { + parts.add("excludedCountries=${urlEncode(request.excludedCountries.joinToString(","))}") + } + request.userIdType?.let { parts.add("userIdType=${urlEncode(it)}") } + request.userDefinedData?.let { parts.add("userDefinedData=${urlEncode(it)}") } + request.selfDefinedData?.let { parts.add("selfDefinedData=${urlEncode(it)}") } + + return parts.joinToString("&").ifEmpty { null } + } +} + +internal fun urlEncode(value: String): String = + value + .replace("%", "%25") + .replace("&", "%26") + .replace("=", "%3D") + .replace("+", "%2B") + .replace(" ", "%20") + .replace("#", "%23") diff --git a/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SdkConstants.kt b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SdkConstants.kt new file mode 100644 index 000000000..6d3c9e871 --- /dev/null +++ b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SdkConstants.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +package xyz.self.sdk.api + +object SdkConstants { + const val LOOPBACK_HOST = "127.0.0.1" + const val DEBUG_PORT = 5173 + const val DIDIT_HOST = "verify.didit.me" + const val BUNDLED_TOUR_PATH = "/tunnel/tour/1" + const val DEFAULT_ENDPOINT = "https://api.self.xyz" + const val DEFAULT_REMOTE_WEB_APP_BASE_URL = "https://self-app-alpha.vercel.app" +} diff --git a/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SelfSdkConfig.kt b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SelfSdkConfig.kt index ef1973ca6..c2d68995b 100644 --- a/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SelfSdkConfig.kt +++ b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SelfSdkConfig.kt @@ -26,7 +26,7 @@ enum class SelfEnvironment { @Serializable data class SelfSdkConfig( - val endpoint: String = "https://api.self.xyz", + val endpoint: String = SdkConstants.DEFAULT_ENDPOINT, val environment: SelfEnvironment = SelfEnvironment.PROD, val debug: Boolean = false, val version: Int = 1, @@ -34,6 +34,6 @@ data class SelfSdkConfig( val appEndpoint: String? = null, val endpointType: String? = null, val chainID: Int? = null, - val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app", + val remoteWebAppBaseUrl: String = SdkConstants.DEFAULT_REMOTE_WEB_APP_BASE_URL, val devServerUrl: String? = null, ) diff --git a/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/CryptoProvider.kt b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/CryptoProvider.kt index 3309f73c9..0de17e6c7 100644 --- a/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/CryptoProvider.kt +++ b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/CryptoProvider.kt @@ -4,7 +4,7 @@ package xyz.self.sdk.providers -interface CryptoProvider { +internal interface CryptoProvider { fun generateKey(keyRef: String) fun getPublicKey(keyRef: String): String? diff --git a/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/SdkProviderRegistry.kt b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/SdkProviderRegistry.kt index 1e156d724..934f65658 100644 --- a/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/SdkProviderRegistry.kt +++ b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/providers/SdkProviderRegistry.kt @@ -6,14 +6,13 @@ package xyz.self.sdk.providers object SdkProviderRegistry { var secureStorage: SecureStorageProvider? = null - var crypto: CryptoProvider? = null + internal var crypto: CryptoProvider? = null /** - * Returns true if the required 3-domain providers are configured. - * Only secureStorage and crypto are required — lifecycle is handler-only - * with no consumer-provided provider. + * Returns true if the required providers are configured. + * Only secureStorage is required — crypto is internal and lifecycle is handler-only. */ - fun isConfigured(): Boolean = secureStorage != null && crypto != null + fun isConfigured(): Boolean = secureStorage != null fun reset() { secureStorage = null diff --git a/packages/kmp-sdk/shared/src/commonTest/kotlin/xyz/self/sdk/api/QueryParamsBuilderTest.kt b/packages/kmp-sdk/shared/src/commonTest/kotlin/xyz/self/sdk/api/QueryParamsBuilderTest.kt new file mode 100644 index 000000000..d8fc3ada1 --- /dev/null +++ b/packages/kmp-sdk/shared/src/commonTest/kotlin/xyz/self/sdk/api/QueryParamsBuilderTest.kt @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +package xyz.self.sdk.api + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class QueryParamsBuilderTest { + private val defaultConfig = SelfSdkConfig() + private val minimalRequest = VerificationRequest() + + @Test + fun `builds params with default config and minimal request`() { + val result = QueryParamsBuilder.build(defaultConfig, minimalRequest) + assertNotNull(result) + assertTrue(result.contains("endpoint=https%3A%2F%2Fapi.self.xyz") || result.contains("endpoint=https://api.self.xyz")) + assertTrue(result.contains("environment=prod")) + assertTrue(result.contains("version=1")) + } + + @Test + fun `includes all config params`() { + val config = + SelfSdkConfig( + endpoint = "https://custom.api.xyz", + environment = SelfEnvironment.STG, + version = 2, + appName = "TestApp", + appEndpoint = "https://app.endpoint.xyz", + endpointType = "custom", + chainID = 42, + ) + val result = QueryParamsBuilder.build(config, minimalRequest) + assertNotNull(result) + assertTrue(result.contains("appName=TestApp")) + assertTrue(result.contains("endpointType=custom")) + assertTrue(result.contains("chainID=42")) + assertTrue(result.contains("environment=stg")) + assertTrue(result.contains("version=2")) + } + + @Test + fun `appEndpoint falls back to endpoint when null`() { + val config = SelfSdkConfig(endpoint = "https://api.self.xyz", appEndpoint = null) + val result = QueryParamsBuilder.build(config, minimalRequest) + assertNotNull(result) + val params = + result.split("&").associate { + val (k, v) = it.split("=", limit = 2) + k to v + } + assertEquals(params["endpoint"], params["appEndpoint"]) + } + + @Test + fun `includes all request params`() { + val request = + VerificationRequest( + userId = "user-123", + scope = "identity", + verificationId = "ver-456", + resultType = "json", + userIdType = "email", + userDefinedData = "custom-data", + selfDefinedData = "self-data", + ) + val result = QueryParamsBuilder.build(defaultConfig, request) + assertNotNull(result) + assertTrue(result.contains("userId=user-123")) + assertTrue(result.contains("scope=identity")) + assertTrue(result.contains("verificationId=ver-456")) + assertTrue(result.contains("resultType=json")) + assertTrue(result.contains("userIdType=email")) + assertTrue(result.contains("userDefinedData=custom-data")) + assertTrue(result.contains("selfDefinedData=self-data")) + } + + @Test + fun `disclosures are comma-joined`() { + val request = VerificationRequest(disclosures = listOf("name", "dob", "nationality")) + val result = QueryParamsBuilder.build(defaultConfig, request) + assertNotNull(result) + assertTrue(result.contains("disclosures=name%2Cdob%2Cnationality") || result.contains("disclosures=name,dob,nationality")) + } + + @Test + fun `excludedCountries are comma-joined`() { + val request = VerificationRequest(excludedCountries = listOf("US", "GB")) + val result = QueryParamsBuilder.build(defaultConfig, request) + assertNotNull(result) + assertTrue(result.contains("excludedCountries=US%2CGB") || result.contains("excludedCountries=US,GB")) + } + + @Test + fun `empty lists are omitted`() { + val request = VerificationRequest(disclosures = emptyList(), excludedCountries = emptyList()) + val result = QueryParamsBuilder.build(defaultConfig, request) + assertNotNull(result) + assertTrue(!result.contains("disclosures")) + assertTrue(!result.contains("excludedCountries")) + } + + @Test + fun `null optional fields are omitted`() { + val result = QueryParamsBuilder.build(defaultConfig, minimalRequest) + assertNotNull(result) + assertTrue(!result.contains("userId=")) + assertTrue(!result.contains("appName=")) + assertTrue(!result.contains("chainID=")) + } + + @Test + fun `urlEncode encodes special characters`() { + assertEquals("hello%20world", urlEncode("hello world")) + assertEquals("a%26b", urlEncode("a&b")) + assertEquals("a%3Db", urlEncode("a=b")) + assertEquals("a%2Bb", urlEncode("a+b")) + assertEquals("a%25b", urlEncode("a%b")) + assertEquals("a%23b", urlEncode("a#b")) + } + + @Test + fun `urlEncode preserves safe characters`() { + assertEquals("hello-world_123", urlEncode("hello-world_123")) + assertEquals("abc/def", urlEncode("abc/def")) + } +} diff --git a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt index cbafb9bac..fd0c1cda4 100644 --- a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt +++ b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt @@ -57,7 +57,7 @@ actual class SelfSdk private constructor( check(SdkProviderRegistry.isConfigured() && IosProviderRegistry.webView != null) { "SDK providers not configured. " + "Call SelfSdkSwift.configure() from your iOS app before launching the SDK. " + - "Required: secureStorage, crypto, and webView providers." + "Required: secureStorage and webView providers." } // Store callback for later @@ -97,7 +97,7 @@ actual class SelfSdk private constructor( registerHandlers(router!!, lifecycleHandler) // Build query params from config + request - val queryParams = buildQueryParams(request) + val queryParams = QueryParamsBuilder.build(config, request) // Create WebView host and the web view webViewHost = @@ -155,44 +155,4 @@ actual class SelfSdk private constructor( router.register(CryptoBridgeHandler()) router.register(lifecycleHandler) } - - private fun buildQueryParams(request: VerificationRequest): String? { - val parts = mutableListOf() - - // Config params (always present) - parts.add("endpoint=${encodeParam(config.endpoint)}") - parts.add("appEndpoint=${encodeParam(config.appEndpoint ?: config.endpoint)}") - parts.add("environment=${encodeParam(config.environment.queryValue)}") - parts.add("version=${config.version}") - - // Optional config params - config.appName?.let { parts.add("appName=${encodeParam(it)}") } - config.endpointType?.let { parts.add("endpointType=${encodeParam(it)}") } - config.chainID?.let { parts.add("chainID=$it") } - - // Request params - request.verificationId?.let { parts.add("verificationId=${encodeParam(it)}") } - request.userId?.let { parts.add("userId=${encodeParam(it)}") } - request.scope?.let { parts.add("scope=${encodeParam(it)}") } - if (request.disclosures.isNotEmpty()) { - parts.add("disclosures=${encodeParam(request.disclosures.joinToString(","))}") - } - request.resultType?.let { parts.add("resultType=${encodeParam(it)}") } - if (request.excludedCountries.isNotEmpty()) { - parts.add("excludedCountries=${encodeParam(request.excludedCountries.joinToString(","))}") - } - request.userIdType?.let { parts.add("userIdType=${encodeParam(it)}") } - request.userDefinedData?.let { parts.add("userDefinedData=${encodeParam(it)}") } - request.selfDefinedData?.let { parts.add("selfDefinedData=${encodeParam(it)}") } - - return parts.joinToString("&").ifEmpty { null } - } - - private fun encodeParam(value: String): String = - value - .replace("%", "%25") - .replace("&", "%26") - .replace("=", "%3D") - .replace("+", "%2B") - .replace(" ", "%20") } diff --git a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/webview/IosWebViewHost.kt b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/webview/IosWebViewHost.kt index fbd7ed0c8..b745a34cd 100644 --- a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/webview/IosWebViewHost.kt +++ b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/webview/IosWebViewHost.kt @@ -7,6 +7,7 @@ package xyz.self.sdk.webview import kotlinx.cinterop.ExperimentalForeignApi import platform.UIKit.UIView import platform.UIKit.UIViewController +import xyz.self.sdk.api.SdkConstants import xyz.self.sdk.bridge.MessageRouter import xyz.self.sdk.providers.IosProviderRegistry @@ -14,7 +15,7 @@ import xyz.self.sdk.providers.IosProviderRegistry class IosWebViewHost( private val router: MessageRouter, private val isDebugMode: Boolean = false, - private val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app", + private val remoteWebAppBaseUrl: String = SdkConstants.DEFAULT_REMOTE_WEB_APP_BASE_URL, private val devServerUrl: String? = null, ) { fun createWebView(queryParams: String? = null): UIView { diff --git a/packages/self-sdk-swift/Sources/SelfSdkSwift/Constants/SdkConstants.swift b/packages/self-sdk-swift/Sources/SelfSdkSwift/Constants/SdkConstants.swift new file mode 100644 index 000000000..2dfb822fe --- /dev/null +++ b/packages/self-sdk-swift/Sources/SelfSdkSwift/Constants/SdkConstants.swift @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import Foundation + +/// Constants shared with the KMP SDK layer. +/// Keep in sync with `packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SdkConstants.kt`. +enum SdkConstants { + static let loopbackHost = "127.0.0.1" + static let debugPort = 5173 + static let diditHost = "verify.didit.me" + static let bundledTourPath = "/tunnel/tour/1" + static let defaultRemoteWebAppBaseURL = "https://self-app-alpha.vercel.app" +} diff --git a/packages/self-sdk-swift/Sources/SelfSdkSwift/Providers/WebViewProviderImpl.swift b/packages/self-sdk-swift/Sources/SelfSdkSwift/Providers/WebViewProviderImpl.swift index d6594822b..3fd69a0d9 100644 --- a/packages/self-sdk-swift/Sources/SelfSdkSwift/Providers/WebViewProviderImpl.swift +++ b/packages/self-sdk-swift/Sources/SelfSdkSwift/Providers/WebViewProviderImpl.swift @@ -9,10 +9,10 @@ import WebKit /// Swift implementation of WebViewProvider using WKWebView. /// Handles message passing between the WebView and the KMP bridge. public class WebViewProviderImpl: NSObject { - static let loopbackHost = "127.0.0.1" - static let diditHost = "verify.didit.me" - static let debugPort: UInt16 = 5173 - private static let defaultRemoteBaseURL = URL(string: "https://self-app-alpha.vercel.app")! + static let loopbackHost = SdkConstants.loopbackHost + static let diditHost = SdkConstants.diditHost + static let debugPort = SdkConstants.debugPort + private static let defaultRemoteBaseURL = URL(string: SdkConstants.defaultRemoteWebAppBaseURL)! private var webView: WKWebView? private var viewController: UIViewController? @@ -198,7 +198,7 @@ extension WebViewProviderImpl { components.scheme = baseURL.scheme components.host = baseURL.host components.port = baseURL.port - components.path = "/tunnel/tour/1" + components.path = SdkConstants.bundledTourPath if let queryParams, !queryParams.isEmpty { components.percentEncodedQuery = queryParams } @@ -209,8 +209,8 @@ extension WebViewProviderImpl { var components = URLComponents() components.scheme = "http" components.host = Self.loopbackHost - components.port = Int(Self.debugPort) - components.path = "/tunnel/tour/1" + components.port = Self.debugPort + components.path = SdkConstants.bundledTourPath if let queryParams, !queryParams.isEmpty { components.percentEncodedQuery = queryParams } @@ -223,7 +223,7 @@ extension WebViewProviderImpl { components.scheme = remoteWebAppBaseURL.scheme components.host = remoteWebAppBaseURL.host if let port = remoteWebAppBaseURL.port { components.port = port } - components.path = "/tunnel/tour/1" + components.path = SdkConstants.bundledTourPath if let queryParams, !queryParams.isEmpty { components.percentEncodedQuery = queryParams } @@ -244,7 +244,7 @@ extension WebViewProviderImpl { if let devUrl = devServerUrl, !devUrl.isEmpty, let devBase = URL(string: devUrl) { return url.scheme == devBase.scheme && url.host == devBase.host && resolvedPort(for: url) == resolvedPort(for: devBase) } - return url.scheme == "http" && url.host == Self.loopbackHost && url.port == Int(Self.debugPort) + return url.scheme == "http" && url.host == Self.loopbackHost && url.port == Self.debugPort } #endif return url.scheme == remoteWebAppBaseURL.scheme && @@ -259,7 +259,7 @@ extension WebViewProviderImpl { let expectedPort = resolvedPort(for: devBase) return origin.protocol == devBase.scheme && origin.host == devBase.host && resolvedSecurityOriginPort(origin) == expectedPort } - return origin.protocol == "http" && origin.host == Self.loopbackHost && origin.port == Int(Self.debugPort) + return origin.protocol == "http" && origin.host == Self.loopbackHost && origin.port == Self.debugPort } #endif let expectedPort = resolvedPort(for: remoteWebAppBaseURL) diff --git a/packages/self-sdk-swift/Sources/SelfSdkSwift/SelfSdkSwift.swift b/packages/self-sdk-swift/Sources/SelfSdkSwift/SelfSdkSwift.swift index 2d549f32c..30cf34f12 100644 --- a/packages/self-sdk-swift/Sources/SelfSdkSwift/SelfSdkSwift.swift +++ b/packages/self-sdk-swift/Sources/SelfSdkSwift/SelfSdkSwift.swift @@ -18,7 +18,6 @@ import Foundation /// SdkProviderRegistry.shared.biometric = SelfSdkSwift.biometric /// SdkProviderRegistry.shared.secureStorage = SelfSdkSwift.secureStorage /// SdkProviderRegistry.shared.haptic = SelfSdkSwift.haptic -/// SdkProviderRegistry.shared.crypto = SelfSdkSwift.crypto /// SdkProviderRegistry.shared.documents = SelfSdkSwift.documents /// SdkProviderRegistry.shared.webView = SelfSdkSwift.webView /// SdkProviderRegistry.shared.nfc = SelfSdkSwift.nfc @@ -30,7 +29,6 @@ public final class SelfSdkSwift { public static let biometric = BiometricProviderImpl() public static let secureStorage = SecureStorageProviderImpl() public static let haptic = HapticProviderImpl() - public static let crypto = CryptoProviderImpl() public static let documents = DocumentsProviderImpl() public static let webView = WebViewProviderImpl() public static let nfc = NfcProviderImpl() diff --git a/scripts/check-license-headers.mjs b/scripts/check-license-headers.mjs index 634c95eca..8fc0d7df8 100644 --- a/scripts/check-license-headers.mjs +++ b/scripts/check-license-headers.mjs @@ -89,6 +89,8 @@ function findLicenseHeaderIndex(lines) { let i = 0; // Skip shebang if present if (lines[i]?.startsWith('#!')) i++; + // Skip swift-tools-version (must be first line in Package.swift) + if (lines[i]?.startsWith('// swift-tools-version')) i++; // Skip leading blank lines while (i < lines.length && lines[i].trim() === '') i++; @@ -187,10 +189,24 @@ function fixLicenseHeader(filePath) { if (headerInfo.index === -1) { // No header exists - add the canonical header + // Preserve shebang and swift-tools-version prefixes + let insertIndex = 0; + if (lines[insertIndex]?.startsWith('#!')) { + insertIndex += 1; + } + if (lines[insertIndex]?.startsWith('// swift-tools-version')) { + insertIndex += 1; + // Ensure blank line between tools-version and license header + if (lines[insertIndex]?.trim() !== '') { + lines.splice(insertIndex, 0, ''); + } + insertIndex += 1; + } const newLines = [ + ...lines.slice(0, insertIndex), ...CANONICAL_HEADER_LINES, '', // Add newline after header - ...lines, + ...lines.slice(insertIndex), ]; const fixedContent = newLines.join('\n'); writeFileSync(filePath, fixedContent, 'utf8'); From 6c3fd45dffd9ea9d861415eb5cb5b33825a81e33 Mon Sep 17 00:00:00 2001 From: Nesopie <87437291+Nesopie@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:36:12 +0530 Subject: [PATCH 2/2] Fix/kyc formatter (#1959) * fix: output formatter for aadhaar and kyc * test: add KYC disclose test with non empty forbidden countries --------- Co-authored-by: seshanthS --- contracts/contracts/libraries/Formatter.sol | 10 ++-- .../libraries/OutputFormatterLib.sol | 18 +++++- contracts/test/v2/discloseKyc.test.ts | 57 +++++++++++++++++++ 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/contracts/contracts/libraries/Formatter.sol b/contracts/contracts/libraries/Formatter.sol index 0e6b8d471..a8b4e638a 100644 --- a/contracts/contracts/libraries/Formatter.sol +++ b/contracts/contracts/libraries/Formatter.sol @@ -199,18 +199,18 @@ library Formatter { return bytesArray; } - function fieldElementsToBytesKyc(uint256[11] memory publicSignals) internal pure returns (bytes memory) { - for (uint256 i = 0; i < 11; i++) { + function fieldElementsToBytesKyc(uint256[10] memory publicSignals) internal pure returns (bytes memory) { + for (uint256 i = 0; i < 10; i++) { if (publicSignals[i] >= SNARK_SCALAR_FIELD) { revert InvalidFieldElement(); } } - uint8[11] memory bytesCount = [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 25]; - bytes memory bytesArray = new bytes(335); + uint8[10] memory bytesCount = [31, 31, 31, 31, 31, 31, 31, 31, 31, 19]; + bytes memory bytesArray = new bytes(298); uint256 index = 0; - for (uint256 i = 0; i < 11; i++) { + for (uint256 i = 0; i < 10; i++) { uint256 element = publicSignals[i]; for (uint8 j = 0; j < bytesCount[i]; j++) { bytesArray[index++] = bytes1(uint8(element & 0xff)); diff --git a/contracts/contracts/libraries/OutputFormatterLib.sol b/contracts/contracts/libraries/OutputFormatterLib.sol index 02ee88a92..16d8ed979 100644 --- a/contracts/contracts/libraries/OutputFormatterLib.sol +++ b/contracts/contracts/libraries/OutputFormatterLib.sol @@ -109,6 +109,13 @@ library OutputFormatterLib { } aadhaarOutput.revealedDataPacked = Formatter.fieldElementsToBytesAadhaar(revealedDataPacked); + // Extract forbidden countries list + for (uint256 i = 0; i < 4; i++) { + aadhaarOutput.forbiddenCountriesListPacked[i] = vcAndDiscloseProof.pubSignals[ + indices.forbiddenCountriesListPackedIndex + i + ]; + } + return abi.encode(aadhaarOutput); } @@ -131,12 +138,19 @@ library OutputFormatterLib { kycOutput.userIdentifier = userIdentifier; kycOutput.nullifier = vcAndDiscloseProof.pubSignals[indices.nullifierIndex]; - uint256[11] memory revealedDataPacked; - for (uint256 i = 0; i < 11; i++) { + uint256[10] memory revealedDataPacked; + for (uint256 i = 0; i < 10; i++) { revealedDataPacked[i] = vcAndDiscloseProof.pubSignals[indices.revealedDataPackedIndex + i]; } kycOutput.revealedDataPacked = Formatter.fieldElementsToBytesKyc(revealedDataPacked); + // Extract forbidden countries list + for (uint256 i = 0; i < 4; i++) { + kycOutput.forbiddenCountriesListPacked[i] = vcAndDiscloseProof.pubSignals[ + indices.forbiddenCountriesListPackedIndex + i + ]; + } + return abi.encode(kycOutput); } } diff --git a/contracts/test/v2/discloseKyc.test.ts b/contracts/test/v2/discloseKyc.test.ts index 7a68a1ec2..b7035ef55 100644 --- a/contracts/test/v2/discloseKyc.test.ts +++ b/contracts/test/v2/discloseKyc.test.ts @@ -166,6 +166,63 @@ describe("Self Verification Flow V2 - KYC", () => { expect(actualUserData).to.equal(expectedUserData); }); + it("should complete full KYC verification flow with non-empty forbidden countries", async () => { + const destChainId = 31337; + const user1Address = await deployedActors.user1.getAddress(); + const userData = "test-user-data-for-verification"; + + const fcListStrings = ["IRN", "PRK", "RUS"]; + + // Reuse the same tree so the merkle root matches the on-chain registry + const fcTestInputs = generateKycDiscloseInputFromDummy( + false, + nameAndDob_smt, + nameAndYob_smt, + tree, + false, + scopeAsBigInt.toString(), + userIdentifierHash.toString(), + ["GENDER", "FULL_NAME", "DOB", "ID_NUMBER", "ISSUANCE_DATE", "EXPIRY_DATE", "COUNTRY", "GENDER", "ADDRESS"], + fcListStrings, + 0, + false, + KYC_ATTESTATION_ID, + ); + + const fcPacked = getPackedForbiddenCountries(fcListStrings as any); + const fcConfig = { + olderThanEnabled: true, + olderThan: "00", + forbiddenCountriesEnabled: true, + forbiddenCountriesListPacked: fcPacked as [BigNumberish, BigNumberish, BigNumberish, BigNumberish], + ofacEnabled: [false, false, false] as [boolean, boolean, boolean], + }; + + await deployedActors.testSelfVerificationRoot.setVerificationConfig(fcConfig); + + const fcProof = await generateVcAndDiscloseKycProof(fcTestInputs); + + const destChainIdHex = ethers.zeroPadValue(ethers.toBeHex(destChainId), 32); + const userContextData = ethers.solidityPacked( + ["bytes32", "bytes32", "bytes"], + [destChainIdHex, ethers.zeroPadValue(user1Address, 32), ethers.toUtf8Bytes(userData)], + ); + + const attestationId = ethers.zeroPadValue(ethers.toBeHex(BigInt(KYC_ATTESTATION_ID)), 32); + const encodedProof = ethers.AbiCoder.defaultAbiCoder().encode( + ["tuple(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[] pubSignals)"], + [[fcProof.a, fcProof.b, fcProof.c, fcProof.pubSignals]], + ); + const proofData = ethers.solidityPacked(["bytes32", "bytes"], [attestationId, encodedProof]); + + await deployedActors.testSelfVerificationRoot.resetTestState(); + + const tx = await deployedActors.testSelfVerificationRoot.verifySelfProof(proofData, userContextData); + + await expect(tx).to.emit(deployedActors.testSelfVerificationRoot, "VerificationCompleted"); + expect(await deployedActors.testSelfVerificationRoot.verificationSuccessful()).to.be.true; + }); + it("should not verify if the config is not set", async () => { const destChainId = ethers.zeroPadValue(ethers.toBeHex(31337), 32); const user1Address = await deployedActors.user1.getAddress();