From 0c81e17964228e3fa042412b2ebed66f7f9b80e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 00:24:53 +0000 Subject: [PATCH] Add per-relay permissions for kind 22242 client authentication (NIP-42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kind 22242 events contain a ["relay", "wss://..."] tag identifying which relay is requesting authentication. This change makes permissions for these events per-relay URL rather than global: - DB: Add `relay` field to ApplicationPermissionsEntity (migration 16→17) with a new unique index on (pkKey, type, kind, relay). Non-22242 permissions keep relay="" to maintain backward compatibility. - DAO: Add getPermissionForRelay() and getWildcardRelayPermission() for relay-specific lookups, plus deletePermissions(key, type, kind, relay) and deletePermissionsForKind() helpers. - AmberUtils: acceptOrRejectPermission() and acceptPermission() accept an optional relay parameter. Wildcard "*" clears all relay-specific entries for a kind; a specific URL only clears its own entry. - UI: New BunkerRelayAuthScreen composable shows the relay URL prominently and presents a 2-option "Authentication scope" toggle: · "This relay only" — stores permission for the specific relay URL · "All relays" — stores permission with wildcard "*" Both options still include the RememberMyChoice time-based memory control. - BunkerSingleEventHomeScreen & IntentSingleEventHomeScreen: detect kind 22242 requests and show BunkerRelayAuthScreen instead of the generic BunkerEventData. Permission lookup checks specific relay first, then the "*" wildcard fallback. - SignerProvider (ContentProvider path): kind 22242 checks relay-specific permission first, then wildcard, before auto-approving. - AmberToggles: add optional segmentWidth parameter (default 55.dp) so the relay scope toggle can use wider 120dp segments. https://claude.ai/code/session_01Dm9zoUbbzdJzuXTNdAVhfW --- .../greenart7c3/nostrsigner/SignerProvider.kt | 11 +- .../nostrsigner/database/AppDatabase.kt | 11 +- .../nostrsigner/database/ApplicationDao.kt | 48 +++++- .../database/ApplicationPermissionsEntity.kt | 3 +- .../nostrsigner/service/AmberUtils.kt | 26 ++- .../nostrsigner/service/BunkerRequestUtils.kt | 4 + .../nostrsigner/service/IntentUtils.kt | 4 + .../nostrsigner/ui/components/AmberToggles.kt | 4 +- .../ui/components/BunkerRelayAuthScreen.kt | 149 ++++++++++++++++++ .../components/BunkerSingleEventHomeScreen.kt | 77 +++++++++ .../components/IntentSingleEventHomeScreen.kt | 70 ++++++++ app/src/main/res/values/strings.xml | 5 + 12 files changed, 402 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt index 3c6eba50..a74de970 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt @@ -12,6 +12,7 @@ import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.kindToNip import com.greenart7c3.nostrsigner.service.AmberUtils import com.greenart7c3.nostrsigner.service.IntentUtils +import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.utils.Hex @@ -175,7 +176,12 @@ class SignerProvider : ContentProvider() { val database = Amber.instance.getDatabase(account.npub) val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) - var permission = + var permission = if (event.kind == 22242) { + // Kind 22242 = relay client auth (NIP-42): check relay-specific permission first + val relayUrl = AmberEvent.relay(event) ?: "" + database.dao().getPermissionForRelay(packageName, "SIGN_EVENT", 22242, relayUrl) + ?: database.dao().getWildcardRelayPermission(packageName, "SIGN_EVENT", 22242) + } else { database .dao() .getPermission( @@ -183,7 +189,8 @@ class SignerProvider : ContentProvider() { "SIGN_EVENT", event.kind, ) - if (permission == null) { + } + if (permission == null && event.kind != 22242) { event.kind.kindToNip()?.let { val nipNumber = it.toIntOrNull() permission = if (nipNumber == null) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt index f1e9e7f8..eeacb1e0 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt @@ -148,12 +148,20 @@ val MIGRATION_15_16 = object : Migration(15, 16) { } } +val MIGRATION_16_17 = object : Migration(16, 17) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `applicationPermission` ADD COLUMN `relay` TEXT NOT NULL DEFAULT ''") + db.execSQL("DROP INDEX IF EXISTS `permissions_unique`") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `permissions_unique` ON `applicationPermission` (`pkKey`, `type`, `kind`, `relay`)") + } +} + @Database( entities = [ ApplicationEntity::class, ApplicationPermissionsEntity::class, ], - version = 16, + version = 17, ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { @@ -190,6 +198,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(MIGRATION_13_14) .addMigrations(MIGRATION_14_15) .addMigrations(MIGRATION_15_16) + .addMigrations(MIGRATION_16_17) .build() instance diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt index 5df4295e..d776994b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt @@ -59,7 +59,7 @@ interface ApplicationDao { @Transaction fun getAllAcceptedPermissions(): List - @Query("SELECT * FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind") + @Query("SELECT * FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = ''") fun getPermission( key: String, type: String, @@ -72,6 +72,21 @@ interface ApplicationDao { type: String, ): ApplicationPermissionsEntity? + @Query("SELECT * FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = :relay LIMIT 1") + fun getPermissionForRelay( + key: String, + type: String, + kind: Int, + relay: String, + ): ApplicationPermissionsEntity? + + @Query("SELECT * FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = '*' LIMIT 1") + fun getWildcardRelayPermission( + key: String, + type: String, + kind: Int, + ): ApplicationPermissionsEntity? + @Insert(onConflict = OnConflictStrategy.REPLACE) @Transaction suspend fun insertApplication(event: ApplicationEntity): Long? @@ -84,7 +99,17 @@ interface ApplicationDao { suspend fun insertPermissions(permissions: List): List? { permissions.forEach { if (it.kind != null) { - deletePermissions(it.pkKey, it.type, it.kind) + if (it.relay.isNotEmpty()) { + // For relay-specific permissions (kind 22242): wildcard "*" clears all relay entries, + // specific relay only clears its own entry + if (it.relay == "*") { + deletePermissionsForKind(it.pkKey, it.type, it.kind) + } else { + deletePermissions(it.pkKey, it.type, it.kind, it.relay) + } + } else { + deletePermissions(it.pkKey, it.type, it.kind) + } } else { deletePermissions(it.pkKey, it.type) } @@ -107,7 +132,7 @@ interface ApplicationDao { type: String, ) - @Query("DELETE FROM applicationPermission WHERE pkKey = :key AND type = :type and kind = :kind") + @Query("DELETE FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = ''") @Transaction suspend fun deletePermissions( key: String, @@ -115,6 +140,23 @@ interface ApplicationDao { kind: Int, ) + @Query("DELETE FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = :relay") + @Transaction + suspend fun deletePermissions( + key: String, + type: String, + kind: Int, + relay: String, + ) + + @Query("DELETE FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind") + @Transaction + suspend fun deletePermissionsForKind( + key: String, + type: String, + kind: Int, + ) + @Insert(onConflict = OnConflictStrategy.IGNORE) @Transaction suspend fun insertApplicationWithPermissions(application: ApplicationWithPermissions) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationPermissionsEntity.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationPermissionsEntity.kt index 73fd7292..7228c7c6 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationPermissionsEntity.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationPermissionsEntity.kt @@ -21,7 +21,7 @@ import androidx.room.PrimaryKey name = "permissions_by_pk_key", ), Index( - value = ["pkKey", "type", "kind"], + value = ["pkKey", "type", "kind", "relay"], name = "permissions_unique", unique = true, ), @@ -37,4 +37,5 @@ data class ApplicationPermissionsEntity( val rememberType: Int, var acceptUntil: Long, var rejectUntil: Long, + val relay: String = "", ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt index c4596200..7e28e66f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt @@ -81,6 +81,7 @@ object AmberUtils { value: Boolean, rememberType: RememberType, account: Account, + relay: String = "", ) { val until = when (rememberType) { RememberType.ALWAYS -> Long.MAX_VALUE / 1000 @@ -91,7 +92,17 @@ object AmberUtils { } if (kind != null) { - application.permissions.removeIf { it.kind == kind && it.type == signerType.toString() } + if (relay.isNotEmpty()) { + // For relay-specific permissions: wildcard "*" removes all relay entries for this kind, + // specific relay removes only its own entry + if (relay == "*") { + application.permissions.removeIf { it.kind == kind && it.type == signerType.toString() } + } else { + application.permissions.removeIf { it.kind == kind && it.type == signerType.toString() && it.relay == relay } + } + } else { + application.permissions.removeIf { it.kind == kind && it.type == signerType.toString() && it.relay.isEmpty() } + } } else { application.permissions.removeIf { it.type == signerType.toString() && it.type != "SIGN_EVENT" } } @@ -106,6 +117,7 @@ object AmberUtils { rememberType.screenCode, if (value) until else 0L, if (!value) until else 0L, + relay, ), ) @@ -173,6 +185,7 @@ object AmberUtils { type: SignerType, kind: Int?, rememberType: RememberType, + relay: String = "", ) { val until = when (rememberType) { RememberType.ALWAYS -> Long.MAX_VALUE / 1000 @@ -183,7 +196,15 @@ object AmberUtils { } if (kind != null) { - application.permissions.removeIf { it.kind == kind && it.type == type.toString() } + if (relay.isNotEmpty()) { + if (relay == "*") { + application.permissions.removeIf { it.kind == kind && it.type == type.toString() } + } else { + application.permissions.removeIf { it.kind == kind && it.type == type.toString() && it.relay == relay } + } + } else { + application.permissions.removeIf { it.kind == kind && it.type == type.toString() && it.relay.isEmpty() } + } } else { application.permissions.removeIf { it.type == type.toString() && it.type != "SIGN_EVENT" } } @@ -198,6 +219,7 @@ object AmberUtils { rememberType.screenCode, until, 0, + relay, ), ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt index 243849b4..e64f5f2b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt @@ -240,6 +240,7 @@ object BunkerRequestUtils { rememberType: RememberType, oldKey: String = "", deleteAfter: Long = 0L, + relay: String = "", ) { onLoading(true) Amber.instance.applicationIOScope.launch { @@ -319,6 +320,7 @@ object BunkerRequestUtils { type = type, kind = kind, rememberType = rememberType, + relay = relay, ) } @@ -434,6 +436,7 @@ object BunkerRequestUtils { signerType: SignerType, kind: Int?, onLoading: (Boolean) -> Unit, + relay: String = "", ) { onLoading(true) Amber.instance.applicationIOScope.launch(Dispatchers.IO) { @@ -481,6 +484,7 @@ object BunkerRequestUtils { false, rememberType, account, + relay, ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt index cec35f36..a8444639 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt @@ -681,6 +681,7 @@ object IntentUtils { shouldCloseApplication: Boolean? = null, rememberType: RememberType, deleteAfter: Long = 0L, + relay: String = "", ) { onLoading(true) Amber.instance.applicationIOScope.launch { @@ -732,6 +733,7 @@ object IntentUtils { type = intentData.type, kind = kind, rememberType = rememberType, + relay = relay, ) } @@ -864,6 +866,7 @@ object IntentUtils { kind: Int?, onLoading: (Boolean) -> Unit, onRemoveIntentData: (List, IntentResultType) -> Unit, + relay: String = "", ) { Amber.instance.applicationIOScope.launch(Dispatchers.IO) { if (key == "null") { @@ -901,6 +904,7 @@ object IntentUtils { false, rememberType, account, + relay, ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt index 7be4bc72..2d6aac92 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt @@ -20,15 +20,17 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @Composable fun AmberToggles( selectedIndex: Int, count: Int, + segmentWidth: Dp = 55.dp, content: @Composable RowScope.() -> Unit, ) { - val fixedSegmentWidth = 55.dp + val fixedSegmentWidth = segmentWidth val padding = 2.dp val totalWidth = (fixedSegmentWidth * count) + (padding * 2) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt new file mode 100644 index 00000000..d4c769a5 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt @@ -0,0 +1,149 @@ +package com.greenart7c3.nostrsigner.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.models.Account +import com.greenart7c3.nostrsigner.ui.RememberType + +/** + * Scope for a kind 22242 relay authentication permission. + * [SPECIFIC] means the permission applies only to the specific relay URL in the event. + * [ALL] means the permission applies to all relays (wildcard "*"). + */ +enum class RelayAuthScope { + SPECIFIC, + ALL, +} + +@Composable +fun BunkerRelayAuthScreen( + modifier: Modifier, + appName: String, + relayUrl: String, + shouldAcceptOrReject: Boolean?, + defaultScope: RelayAuthScope, + account: Account, + onAccept: (RememberType, RelayAuthScope) -> Unit, + onReject: (RememberType, RelayAuthScope) -> Unit, +) { + var rememberType by remember { mutableStateOf(RememberType.NEVER) } + var scope by remember { mutableStateOf(defaultScope) } + val scopeIndex by remember(scope) { mutableIntStateOf(if (scope == RelayAuthScope.SPECIFIC) 0 else 1) } + + if (shouldAcceptOrReject != null) { + LaunchedEffect(Unit) { + if (shouldAcceptOrReject) { + onAccept(RememberType.entries[0], scope) + } else { + onReject(RememberType.entries[0], scope) + } + } + } + + Column( + modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.size(16.dp)) + + Text( + buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(appName) + } + append(" ") + append(stringResource(R.string.relay_auth_request, "")) + }, + fontSize = 18.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + Spacer(Modifier.size(8.dp)) + + Text( + relayUrl, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + Spacer(Modifier.size(16.dp)) + + SigningAs(account) + + Spacer(modifier = Modifier.weight(1f)) + + LabeledBorderBox( + label = stringResource(R.string.relay_auth_scope), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + AmberToggles( + selectedIndex = scopeIndex, + count = 2, + segmentWidth = 120.dp, + content = { + ToggleOption( + modifier = Modifier.width(120.dp), + text = stringResource(R.string.for_this_relay_only), + isSelected = scope == RelayAuthScope.SPECIFIC, + onClick = { scope = RelayAuthScope.SPECIFIC }, + ) + ToggleOption( + modifier = Modifier.width(120.dp), + text = stringResource(R.string.for_all_relays), + isSelected = scope == RelayAuthScope.ALL, + onClick = { scope = RelayAuthScope.ALL }, + ) + }, + ) + } + + Spacer(Modifier.size(8.dp)) + + RememberMyChoice( + shouldRunAcceptOrReject = null, + packageName = null, + alwaysShow = true, + onAccept = { onAccept(it, scope) }, + onReject = { onReject(it, scope) }, + ) { + rememberType = it + } + + AcceptRejectButtons( + onAccept = { onAccept(rememberType, scope) }, + onReject = { onReject(rememberType, scope) }, + ) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt index 01cbb92b..40e8c13f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt @@ -35,6 +35,7 @@ import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.kindToNip import com.greenart7c3.nostrsigner.service.BunkerRequestUtils import com.greenart7c3.nostrsigner.service.isPrivateEvent +import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.RememberType import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -616,6 +617,82 @@ fun BunkerSingleEventHomeScreen( Spacer(Modifier.size(8.dp)) Text("Not logged in") } + } else if (event.kind == 22242) { + // Kind 22242 = relay client authentication (NIP-42) + // Permission is per-relay URL extracted from the event's "relay" tag + val relayUrl = AmberEvent.relay(event) ?: "" + + // Check for a relay-specific permission first, then wildcard "*" (all relays) + val permission = applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == type.toString() && it.kind == 22242 && it.relay == relayUrl + } ?: applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == type.toString() && it.kind == 22242 && it.relay == "*" + } + + val acceptUntil = permission?.acceptUntil ?: 0 + val rejectUntil = permission?.rejectUntil ?: 0 + + val acceptOrReject = if (rejectUntil == 0L && acceptUntil == 0L) { + null + } else if (rejectUntil > TimeUtils.now() && rejectUntil > 0 && permission?.acceptable == false) { + false + } else if (acceptUntil > TimeUtils.now() && acceptUntil > 0 && permission?.acceptable == true) { + true + } else { + null + } + + BunkerRelayAuthScreen( + modifier = modifier, + appName = appName, + relayUrl = relayUrl, + shouldAcceptOrReject = acceptOrReject, + defaultScope = RelayAuthScope.SPECIFIC, + account = account, + onAccept = { rememberType, scope -> + if (event.pubKey != account.hexKey && !isPrivateEvent(event.kind, event.tags)) { + coroutineScope.launch { + Toast.makeText( + context, + context.getString(R.string.event_pubkey_is_not_equal_to_current_logged_in_user), + Toast.LENGTH_SHORT, + ).show() + } + return@BunkerRelayAuthScreen + } + + val relayPermission = if (scope == RelayAuthScope.ALL) "*" else relayUrl + BunkerRequestUtils.sendResult( + context = context, + account = account, + key = key, + response = event.toJson(), + bunkerRequest = bunkerRequest, + kind = event.kind, + onLoading = onLoading, + permissions = null, + appName = appName, + signPolicy = null, + shouldCloseApplication = bunkerRequest.closeApplication, + rememberType = rememberType, + relay = relayPermission, + ) + }, + onReject = { rememberType, scope -> + val relayPermission = if (scope == RelayAuthScope.ALL) "*" else relayUrl + BunkerRequestUtils.sendRejection( + key = key, + account = account, + bunkerRequest = bunkerRequest, + appName = appName, + rememberType = rememberType, + signerType = type, + kind = event.kind, + onLoading = onLoading, + relay = relayPermission, + ) + }, + ) } else { val permission = applicationEntity?.permissions?.firstOrNull { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt index 17e4187b..164eedbe 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt @@ -30,6 +30,7 @@ import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.kindToNip import com.greenart7c3.nostrsigner.service.IntentUtils import com.greenart7c3.nostrsigner.service.isPrivateEvent +import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.ToastManager import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -242,6 +243,75 @@ fun IntentSingleEventHomeScreen( Spacer(Modifier.size(8.dp)) Text("Not logged in") } + } else if (event.kind == 22242) { + // Kind 22242 = relay client authentication (NIP-42) + // Permission is per-relay URL extracted from the event's "relay" tag + val relayUrl = AmberEvent.relay(event) ?: "" + + // Check for relay-specific permission first, then wildcard "*" (all relays) + val permission = applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == intentData.type.toString() && it.kind == 22242 && it.relay == relayUrl + } ?: applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == intentData.type.toString() && it.kind == 22242 && it.relay == "*" + } + + val acceptOrReject = if (permission == null) { + IntentUtils.isRemembered(applicationEntity?.application?.signPolicy, null) + } else { + IntentUtils.isRemembered(applicationEntity?.application?.signPolicy, permission) + } + + BunkerRelayAuthScreen( + modifier = modifier, + appName = appName, + relayUrl = relayUrl, + shouldAcceptOrReject = acceptOrReject, + defaultScope = RelayAuthScope.SPECIFIC, + account = account, + onAccept = { rememberType, scope -> + if (intentData.unsignedEventKey.isNotBlank() && intentData.unsignedEventKey != account.hexKey && !isPrivateEvent(event.kind, event.tags)) { + ToastManager.toast( + title = context.getString(R.string.warning), + message = context.getString(R.string.event_pubkey_is_not_equal_to_current_logged_in_user), + ) + return@BunkerRelayAuthScreen + } + + val relayPermission = if (scope == RelayAuthScope.ALL) "*" else relayUrl + IntentUtils.sendResult( + context = context, + packageName = packageName, + account = account, + key = key, + clipboardManager = clipboardManager, + event = event.toJson(), + value = event.sig, + intentData = intentData, + kind = event.kind, + onLoading = onLoading, + onRemoveIntentData = onRemoveIntentData, + signPolicy = null, + appName = applicationName ?: appName, + permissions = null, + rememberType = rememberType, + relay = relayPermission, + ) + }, + onReject = { rememberType, scope -> + val relayPermission = if (scope == RelayAuthScope.ALL) "*" else relayUrl + IntentUtils.sendRejection( + key = key, + account = account, + intentData = intentData, + appName = appName, + rememberType = rememberType, + onLoading = onLoading, + onRemoveIntentData = onRemoveIntentData, + kind = event.kind, + relay = relayPermission, + ) + }, + ) } else { val permission = applicationEntity?.permissions?.firstOrNull { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bdedc7fe..40cac538 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -643,4 +643,9 @@ Account picture Encrypt and copy to clipboard Encrypt and show QR Code + Relay authentication + %1$s wants to authenticate to relay + This relay only + All relays + Authentication scope