diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..b7c06c5 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,35 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: en-US + +reviews: + request_changes_workflow: false + high_level_summary: true + poem: false + + path_instructions: + - path: "src/**/*.kt" + instructions: | + Flag any hardcoded occurrence of the literal `thunderid`/`ThunderID`/`THUNDERID` (any casing) used to + build a runtime key or name — `EncryptedSharedPreferences` keys, log tags, and similar. + + Do not flag the package namespace or a type whose purpose IS to represent the SDK itself (e.g. + `ThunderIDClient`, `ThunderIDProvider`). That's a fixed identity, not a per-tenant value. + + For everything else, ask: **"Should this read from `ThunderIDConfig.vendor` instead of hardcoding the + vendor name?"** The best fix is avoiding the vendor name entirely when the brand prefix isn't + load-bearing. When a brand-scoped namespace is genuinely required, it should resolve through + `config.vendor` (which already defaults to `"thunderid"`) rather than a literal string. If the same + default-resolution logic starts appearing in more than one place, ask for it to be extracted into a + shared helper instead of repeated inline. + + auto_review: + enabled: true + ignore_title_keywords: + - "WIP" + - "DO NOT MERGE" + base_branches: + - main + + path_filters: + - "!**/build/**" + - "!**/.gradle/**" diff --git a/AGENTS.md b/AGENTS.md index be44332..6c644a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,15 @@ Kotlin Android library providing the ThunderID authentication SDK (`dev.thunderid.android`) and a Jetpack Compose component library (`dev.thunderid.compose`). The `samples/quickstart` directory contains a standalone demo app. +## Vendor naming rules + +The SDK is white-labelable: a consuming app can override the brand/vendor namespace via `ThunderIDConfig.vendor`, so storage keys, log tags, and similar runtime names shouldn't be pinned to one brand. + +- Do not hardcode the literal `thunderid`/`ThunderID`/`THUNDERID` (any casing) when building a runtime key/name that a consumer's `vendor` override should control — `EncryptedSharedPreferences` keys, log tags, and the like. +- It's fine for the **entry point** — the package namespace or a class whose purpose IS to represent the SDK itself (e.g. `ThunderIDClient`, `ThunderIDProvider`) — to carry the name. That's a fixed identity, not a per-tenant value. Don't flag those. +- Avoiding the vendor name entirely is the best outcome, when the brand prefix isn't actually load-bearing. +- When a brand-scoped namespace is genuinely required, resolve it from `config.vendor` (which already defaults to `"thunderid"`) instead of hardcoding a literal. If the same default-resolution logic starts appearing in more than one place, extract a small shared helper rather than repeating the literal default. + ## Build & test ```bash diff --git a/src/main/kotlin/dev/thunderid/android/StorageAdapter.kt b/src/main/kotlin/dev/thunderid/android/StorageAdapter.kt index 1873a10..7c25f8f 100644 --- a/src/main/kotlin/dev/thunderid/android/StorageAdapter.kt +++ b/src/main/kotlin/dev/thunderid/android/StorageAdapter.kt @@ -41,7 +41,10 @@ interface StorageAdapter { /** * Default storage using Android EncryptedSharedPreferences backed by the Android Keystore (spec §11.1). */ -class EncryptedStorageAdapter(context: Context) : StorageAdapter { +class EncryptedStorageAdapter( + context: Context, + prefsName: String = "dev.thunderid.sdk.prefs", +) : StorageAdapter { private val prefs = run { val masterKey = @@ -50,7 +53,7 @@ class EncryptedStorageAdapter(context: Context) : StorageAdapter { .build() EncryptedSharedPreferences.create( context, - "dev.thunderid.sdk.prefs", + prefsName, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, diff --git a/src/main/kotlin/dev/thunderid/android/ThunderIDConfig.kt b/src/main/kotlin/dev/thunderid/android/ThunderIDConfig.kt index 9479e49..5e47756 100644 --- a/src/main/kotlin/dev/thunderid/android/ThunderIDConfig.kt +++ b/src/main/kotlin/dev/thunderid/android/ThunderIDConfig.kt @@ -46,7 +46,17 @@ data class ThunderIDConfig( val instanceId: Int? = null, // Development only — bypasses TLS certificate verification. Never use in production. val allowInsecureConnections: Boolean = false, -) + /** + * Vendor/brand namespace used to derive default storage identifiers (e.g. EncryptedSharedPreferences file + * name). Override this when white-labeling the SDK under a different brand. Defaults to + * [DEFAULT_VENDOR]. + */ + val vendor: String = DEFAULT_VENDOR, +) { + companion object { + const val DEFAULT_VENDOR: String = "thunderid" + } +} data class TokenValidationConfig( val validate: Boolean = true, diff --git a/src/main/kotlin/dev/thunderid/compose/ThunderIDProvider.kt b/src/main/kotlin/dev/thunderid/compose/ThunderIDProvider.kt index 2f7c3b0..0adcc9f 100644 --- a/src/main/kotlin/dev/thunderid/compose/ThunderIDProvider.kt +++ b/src/main/kotlin/dev/thunderid/compose/ThunderIDProvider.kt @@ -40,10 +40,12 @@ import dev.thunderid.compose.i18n.ThunderIDI18n fun ThunderIDProvider( config: ThunderIDConfig, client: ThunderIDClient = remember { ThunderIDClient() }, - i18n: ThunderIDI18n = remember { ThunderIDI18n() }, + i18n: ThunderIDI18n? = null, content: @Composable () -> Unit, ) { - val state = remember(client, i18n) { ThunderIDState(client, i18n) } + val resolvedI18n = + i18n ?: remember(config.vendor) { ThunderIDI18n(storageKey = "${config.vendor}_locale") } + val state = remember(client, resolvedI18n) { ThunderIDState(client, resolvedI18n) } val scope = rememberCoroutineScope() LaunchedEffect(config) { diff --git a/src/main/kotlin/dev/thunderid/compose/components/presentation/auth/SignIn.kt b/src/main/kotlin/dev/thunderid/compose/components/presentation/auth/SignIn.kt index 65566cb..33dae2f 100644 --- a/src/main/kotlin/dev/thunderid/compose/components/presentation/auth/SignIn.kt +++ b/src/main/kotlin/dev/thunderid/compose/components/presentation/auth/SignIn.kt @@ -164,7 +164,7 @@ fun BaseSignIn( val response = thunderState.client.signIn(payload = payload, request = request) handleSignInResponse(response, signInState, thunderState, onComplete, onError) } catch (e: Exception) { - android.util.Log.e("ThunderID:SignIn", "Flow submit failed", e) + android.util.Log.e("SignInFlow", "Flow submit failed", e) signInState.error = e.message onError?.invoke(e.message ?: "Sign-in failed") } finally { @@ -180,13 +180,13 @@ fun BaseSignIn( val payload = EmbeddedSignInPayload(actionId = "__initiate__") val response = thunderState.client.signIn(payload = payload, request = request) android.util.Log.d( - "ThunderID:SignIn", + "SignInFlow", "status=${response.flowStatus} inputs=${response.data?.inputs?.size} " + "actions=${response.data?.actions?.size} data=${response.data}", ) handleSignInResponse(response, signInState, thunderState, onComplete, onError) } catch (e: Exception) { - android.util.Log.e("ThunderID:SignIn", "Flow initiation failed", e) + android.util.Log.e("SignInFlow", "Flow initiation failed", e) signInState.error = e.message onError?.invoke(e.message ?: "Sign-in failed") } finally {