Rename the custom recovery passphrase well-known to match the merged schema

The merged element-enterprise#261 well-known schema advertises the feature
under custom_recovery_passphrase (no _settings/_requirements suffix), so the
parser previously looking for custom_recovery_passphrase_settings would never
activate the flow. Update the @SerialName and drop the narrow "Requirements"
suffix from the type — the block is now general, extensible feature settings —
to mirror the .pkl CustomRecoveryPassphrase class. Bumps the enterprise
submodule to the matching consumer change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jenna Vassar
2026-06-11 15:03:24 -07:00
parent cc9d787602
commit fdac92a9ea
8 changed files with 41 additions and 41 deletions
@@ -8,11 +8,11 @@
package io.element.android.libraries.wellknown.api package io.element.android.libraries.wellknown.api
/** /**
* Server-driven requirements for a user-chosen recovery passphrase. Today the only rule * Server-driven settings for a user-chosen recovery passphrase, advertised under the well-known
* is a minimum character count; additional rules can be added here as the well-known * `custom_recovery_passphrase` key. Today the only rule is a minimum character count; additional
* schema (`custom_recovery_passphrase_settings`) grows. * settings can be added here as the schema grows.
*/ */
data class CustomRecoveryPassphraseRequirements( data class CustomRecoveryPassphrase(
val minCharacterCount: Int, val minCharacterCount: Int,
) { ) {
/** True when [input] meets every active rule. */ /** True when [input] meets every active rule. */
@@ -15,5 +15,5 @@ data class ElementWellKnown(
val brandColor: String?, val brandColor: String?,
val notificationSound: String?, val notificationSound: String?,
val identityProviderAppScheme: String?, val identityProviderAppScheme: String?,
val customRecoveryPassphraseRequirements: CustomRecoveryPassphraseRequirements?, val customRecoveryPassphrase: CustomRecoveryPassphrase?,
) )
@@ -21,7 +21,7 @@ import kotlinx.serialization.Serializable
* "brand_color": "#FF0000", * "brand_color": "#FF0000",
* "notification_sound": "ring.flac", * "notification_sound": "ring.flac",
* "idp_app_scheme": "io.element.app", * "idp_app_scheme": "io.element.app",
* "custom_recovery_passphrase_settings": { * "custom_recovery_passphrase": {
* "min_character_count": 8 * "min_character_count": 8
* } * }
* } * }
@@ -42,12 +42,12 @@ data class InternalElementWellKnown(
val notificationSound: String? = null, val notificationSound: String? = null,
@SerialName("idp_app_scheme") @SerialName("idp_app_scheme")
val identityProviderAppScheme: String? = null, val identityProviderAppScheme: String? = null,
@SerialName("custom_recovery_passphrase_settings") @SerialName("custom_recovery_passphrase")
val customRecoveryPassphraseRequirements: InternalCustomRecoveryPassphraseRequirements? = null, val customRecoveryPassphrase: InternalCustomRecoveryPassphrase? = null,
) )
@Serializable @Serializable
data class InternalCustomRecoveryPassphraseRequirements( data class InternalCustomRecoveryPassphrase(
@SerialName("min_character_count") @SerialName("min_character_count")
val minCharacterCount: Int? = null, val minCharacterCount: Int? = null,
) )
@@ -9,7 +9,7 @@
package io.element.android.libraries.wellknown.impl package io.element.android.libraries.wellknown.impl
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphraseRequirements import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.ElementWellKnown
import timber.log.Timber import timber.log.Timber
@@ -29,20 +29,20 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown(
brandColor = brandColor, brandColor = brandColor,
notificationSound = notificationSound, notificationSound = notificationSound,
identityProviderAppScheme = identityProviderAppScheme, identityProviderAppScheme = identityProviderAppScheme,
customRecoveryPassphraseRequirements = customRecoveryPassphraseRequirements?.toPublic(), customRecoveryPassphrase = customRecoveryPassphrase?.toPublic(),
) )
private fun InternalCustomRecoveryPassphraseRequirements.toPublic(): CustomRecoveryPassphraseRequirements { private fun InternalCustomRecoveryPassphrase.toPublic(): CustomRecoveryPassphrase {
// Whenever the homeserver advertises the settings block we run the custom passphrase flow, flooring // Whenever the homeserver advertises the settings block we run the custom passphrase flow, flooring
// the minimum at 1 so the passphrase can never be empty even if the server omits min_character_count // the minimum at 1 so the passphrase can never be empty even if the server omits min_character_count
// or advertises a non-positive value. The operator owns any stronger minimum. // or advertises a non-positive value. The operator owns any stronger minimum.
val min = (minCharacterCount ?: MINIMUM_PASSPHRASE_CHARACTER_COUNT).coerceAtLeast(MINIMUM_PASSPHRASE_CHARACTER_COUNT) val min = (minCharacterCount ?: MINIMUM_PASSPHRASE_CHARACTER_COUNT).coerceAtLeast(MINIMUM_PASSPHRASE_CHARACTER_COUNT)
if (min != minCharacterCount) { if (min != minCharacterCount) {
Timber.tag(loggerTag.value).w( Timber.tag(loggerTag.value).w(
"custom_recovery_passphrase_settings.min_character_count was %s; flooring to %d", "custom_recovery_passphrase.min_character_count was %s; flooring to %d",
minCharacterCount, minCharacterCount,
min, min,
) )
} }
return CustomRecoveryPassphraseRequirements(minCharacterCount = min) return CustomRecoveryPassphrase(minCharacterCount = min)
} }
@@ -10,24 +10,24 @@ package io.element.android.libraries.wellknown.api
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Test import org.junit.Test
class CustomRecoveryPassphraseRequirementsTest { class CustomRecoveryPassphraseTest {
@Test @Test
fun `empty input never satisfies a positive minimum`() { fun `empty input never satisfies a positive minimum`() {
assertThat(CustomRecoveryPassphraseRequirements(minCharacterCount = 1).isSatisfiedBy("")).isFalse() assertThat(CustomRecoveryPassphrase(minCharacterCount = 1).isSatisfiedBy("")).isFalse()
} }
@Test @Test
fun `input shorter than minimum is not satisfied`() { fun `input shorter than minimum is not satisfied`() {
assertThat(CustomRecoveryPassphraseRequirements(minCharacterCount = 8).isSatisfiedBy("abc")).isFalse() assertThat(CustomRecoveryPassphrase(minCharacterCount = 8).isSatisfiedBy("abc")).isFalse()
} }
@Test @Test
fun `input exactly at minimum is satisfied`() { fun `input exactly at minimum is satisfied`() {
assertThat(CustomRecoveryPassphraseRequirements(minCharacterCount = 8).isSatisfiedBy("abcdefgh")).isTrue() assertThat(CustomRecoveryPassphrase(minCharacterCount = 8).isSatisfiedBy("abcdefgh")).isTrue()
} }
@Test @Test
fun `input longer than minimum is satisfied`() { fun `input longer than minimum is satisfied`() {
assertThat(CustomRecoveryPassphraseRequirements(minCharacterCount = 4).isSatisfiedBy("abcdefgh")).isTrue() assertThat(CustomRecoveryPassphrase(minCharacterCount = 4).isSatisfiedBy("abcdefgh")).isTrue()
} }
} }
@@ -19,7 +19,7 @@ import io.element.android.libraries.cachestore.api.CacheStore
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphraseRequirements import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
@@ -52,7 +52,7 @@ class DefaultSessionWellknownRetrieverTest {
brandColor = null, brandColor = null,
notificationSound = null, notificationSound = null,
identityProviderAppScheme = null, identityProviderAppScheme = null,
customRecoveryPassphraseRequirements = null, customRecoveryPassphrase = null,
) )
) )
) )
@@ -78,7 +78,7 @@ class DefaultSessionWellknownRetrieverTest {
brandColor = "#FF0000", brandColor = "#FF0000",
notificationSound = "a_notification_sound.flac", notificationSound = "a_notification_sound.flac",
identityProviderAppScheme = "an_app_scheme", identityProviderAppScheme = "an_app_scheme",
customRecoveryPassphraseRequirements = null, customRecoveryPassphrase = null,
) )
) )
) )
@@ -108,19 +108,19 @@ class DefaultSessionWellknownRetrieverTest {
brandColor = null, brandColor = null,
notificationSound = null, notificationSound = null,
identityProviderAppScheme = null, identityProviderAppScheme = null,
customRecoveryPassphraseRequirements = null, customRecoveryPassphrase = null,
) )
) )
) )
} }
@Test @Test
fun `get element wellknown with custom recovery passphrase requirements`() = runTest { fun `get element wellknown with custom recovery passphrase settings`() = runTest {
val sut = createDefaultSessionWellknownRetriever( val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = { getUrlLambda = {
Result.success( Result.success(
"""{ """{
"custom_recovery_passphrase_settings": { "custom_recovery_passphrase": {
"min_character_count": 8 "min_character_count": 8
} }
}""".trimIndent().toByteArray() }""".trimIndent().toByteArray()
@@ -130,19 +130,19 @@ class DefaultSessionWellknownRetrieverTest {
assertThat(sut.getElementWellKnown()).isEqualTo( assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success( WellknownRetrieverResult.Success(
anElementWellKnown( anElementWellKnown(
customRecoveryPassphraseRequirements = CustomRecoveryPassphraseRequirements(minCharacterCount = 8) customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 8)
) )
) )
) )
} }
@Test @Test
fun `get element wellknown with custom recovery passphrase requirements missing min character count floors to 1`() = runTest { fun `get element wellknown with custom recovery passphrase settings missing min character count floors to 1`() = runTest {
val sut = createDefaultSessionWellknownRetriever( val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = { getUrlLambda = {
Result.success( Result.success(
"""{ """{
"custom_recovery_passphrase_settings": {} "custom_recovery_passphrase": {}
}""".trimIndent().toByteArray() }""".trimIndent().toByteArray()
) )
}, },
@@ -150,7 +150,7 @@ class DefaultSessionWellknownRetrieverTest {
assertThat(sut.getElementWellKnown()).isEqualTo( assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success( WellknownRetrieverResult.Success(
anElementWellKnown( anElementWellKnown(
customRecoveryPassphraseRequirements = CustomRecoveryPassphraseRequirements(minCharacterCount = 1) customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1)
) )
) )
) )
@@ -162,7 +162,7 @@ class DefaultSessionWellknownRetrieverTest {
getUrlLambda = { getUrlLambda = {
Result.success( Result.success(
"""{ """{
"custom_recovery_passphrase_settings": { "custom_recovery_passphrase": {
"min_character_count": 0 "min_character_count": 0
} }
}""".trimIndent().toByteArray() }""".trimIndent().toByteArray()
@@ -172,7 +172,7 @@ class DefaultSessionWellknownRetrieverTest {
assertThat(sut.getElementWellKnown()).isEqualTo( assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success( WellknownRetrieverResult.Success(
anElementWellKnown( anElementWellKnown(
customRecoveryPassphraseRequirements = CustomRecoveryPassphraseRequirements(minCharacterCount = 1) customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1)
) )
) )
) )
@@ -184,7 +184,7 @@ class DefaultSessionWellknownRetrieverTest {
getUrlLambda = { getUrlLambda = {
Result.success( Result.success(
"""{ """{
"custom_recovery_passphrase_settings": { "custom_recovery_passphrase": {
"min_character_count": -5 "min_character_count": -5
} }
}""".trimIndent().toByteArray() }""".trimIndent().toByteArray()
@@ -194,7 +194,7 @@ class DefaultSessionWellknownRetrieverTest {
assertThat(sut.getElementWellKnown()).isEqualTo( assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success( WellknownRetrieverResult.Success(
anElementWellKnown( anElementWellKnown(
customRecoveryPassphraseRequirements = CustomRecoveryPassphraseRequirements(minCharacterCount = 1) customRecoveryPassphrase = CustomRecoveryPassphrase(minCharacterCount = 1)
) )
) )
) )
@@ -247,7 +247,7 @@ class DefaultSessionWellknownRetrieverTest {
brandColor = "#FF0000", brandColor = "#FF0000",
notificationSound = "a_notification_sound.flac", notificationSound = "a_notification_sound.flac",
identityProviderAppScheme = "an_app_scheme", identityProviderAppScheme = "an_app_scheme",
customRecoveryPassphraseRequirements = null, customRecoveryPassphrase = null,
) )
) )
) )
@@ -301,7 +301,7 @@ class DefaultSessionWellknownRetrieverTest {
brandColor = "#FF0000", brandColor = "#FF0000",
notificationSound = "a_notification_sound.flac", notificationSound = "a_notification_sound.flac",
identityProviderAppScheme = "an_app_scheme", identityProviderAppScheme = "an_app_scheme",
customRecoveryPassphraseRequirements = null, customRecoveryPassphrase = null,
) )
) )
) )
@@ -8,7 +8,7 @@
package io.element.android.features.wellknown.test package io.element.android.features.wellknown.test
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphraseRequirements import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.ElementWellKnown
fun anElementWellKnown( fun anElementWellKnown(
@@ -18,7 +18,7 @@ fun anElementWellKnown(
brandColor: String? = null, brandColor: String? = null,
notificationSound: String? = null, notificationSound: String? = null,
identityProviderAppScheme: String? = null, identityProviderAppScheme: String? = null,
customRecoveryPassphraseRequirements: CustomRecoveryPassphraseRequirements? = null, customRecoveryPassphrase: CustomRecoveryPassphrase? = null,
) = ElementWellKnown( ) = ElementWellKnown(
registrationHelperUrl = registrationHelperUrl, registrationHelperUrl = registrationHelperUrl,
enforceElementPro = enforceElementPro, enforceElementPro = enforceElementPro,
@@ -26,11 +26,11 @@ fun anElementWellKnown(
brandColor = brandColor, brandColor = brandColor,
notificationSound = notificationSound, notificationSound = notificationSound,
identityProviderAppScheme = identityProviderAppScheme, identityProviderAppScheme = identityProviderAppScheme,
customRecoveryPassphraseRequirements = customRecoveryPassphraseRequirements, customRecoveryPassphrase = customRecoveryPassphrase,
) )
fun aCustomRecoveryPassphraseRequirements( fun aCustomRecoveryPassphrase(
minCharacterCount: Int = 8, minCharacterCount: Int = 8,
) = CustomRecoveryPassphraseRequirements( ) = CustomRecoveryPassphrase(
minCharacterCount = minCharacterCount, minCharacterCount = minCharacterCount,
) )