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(); 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');