From 7c1079900d0a7a4553ea4c05942fc16b730ab43f Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 7 Oct 2026 16:56:25 +0200 Subject: [PATCH 001/167] test(mobile): offline reading and accessibility audits (tasks 3.4, 3.1) (#1181) * test(mobile): offline reading and accessibility audits on Android and iOS (tasks 3.4, 3.1) OfflineReadingTest syncs, cuts the network and proves the server is out of reach, unlocks offline, reads an item from the store with the last-synced note, finds every edit refused, and syncs again once the network is back. AccessibilityAuditTest runs the Accessibility Test Framework through Compose's enableAccessibilityChecks on the main screens (API 34); AccessibilityAuditUITests runs performAccessibilityAudit on the same screens on iOS. Both collect every finding and fail once at the end with the list. * build(mobile): name every shared-core test in the CI log The Mobile jobs showed only the Gradle tasks. Ticking a task against CI needs the test that ran, so the jvm, Android and iOS simulator tests now log each test with its result. * test(mobile): the offline test reads the store file without its key (task 1.6) After the sync, the SQLCipher file and its journal hold no SQLite header and none of the demo vault's names, logins or addresses, and plain SQLite refuses to read it. * docs(parity): clients-10 is building, with the merged mobile PRs and the preview release parity_verify --strict: no finding but the unknown-cells census. * build(mobile): checksums for the jars the Android test classpath selects guava 31.0.1-android, checker-qual 3.22.1 and j2objc-annotations 1.3 win on the androidTest classpath (the existing guava entries are the jre variant). Their sha256 was checked against Maven Central's published sha1. The other binaries of the accessibility test closure are listed too, so a version that wins resolution is never missing. * fix(mobile): the accessibility audit's findings on Android and iOS (task 3.1) Android: the auto-lock radio rows were 24 dp touch targets; they are 48 dp. iOS: Keepiq's own tint and secondary text colour pass WCAG AA (the system blue is 4.0:1 on white), section headers use it, the generator's refresh button takes taps on 44 pt, a Send row is one element, the code countdown grows with Dynamic Type, and a long value wraps instead of clipping. Three audit kinds are excluded with their reason in A11yExclusions: clipping on single-line text fields, the item's own login or address as a label, and SwiftUI text styles reported as partially unsupported. * fix(mobile): the remaining iOS audit findings (task 3.1) A darker red for destructive list buttons (the system red is 3.6:1 on white), the code countdown in Keepiq's tint with primary text, a field row that puts its buttons under a long value at the accessibility text sizes, and a generated value that wraps. The audit no longer counts contrast on a disabled control (WCAG 1.4.3 exempts inactive components) or on text that iOS 26 fades under a bar on purpose (the scroll edge effect). * fix(mobile): last iOS audit findings: explicit tint on Edit and Move, plain countdown label, no disabled Send password field The one-time code gets its own list section, so its refresh does not re-lay out the address. The audit leaves out a contrast finding it cannot tie to an element only while a list row sits under a bar. * test(mobile): measure contrast from the screenshot before counting an iOS audit finding The audit flags the Edit button although its pixels are Keepiq's tint on white (7.1:1), the same as the Move button that passes. A contrast finding is now left out only when the measured ratio is 4.5:1 or more. The audit also holds the one-time code still, since a countdown redrawing during the pass made it report the address as clipped. * fix(mobile): let a long address wrap at its slashes and dots on iOS A value without spaces could only be cut off at large text sizes; the audit reported the address on the one-time code screen as clipped. A zero-width space after each slash, dot and @ gives it break points, while VoiceOver and Copy keep the value as it is. Drops the held countdown, which did not change the finding. * docs(openspec): tick the clients-mobile-apps tasks CI now covers, split out the manual device checks --- mobile/android/app/build.gradle.kts | 1 + .../keepiq/android/AccessibilityAuditTest.kt | 200 ++ .../nl/conduction/keepiq/android/E2e.kt | 55 + .../keepiq/android/OfflineReadingTest.kt | 190 ++ .../keepiq/android/ui/SettingsScreen.kt | 4 + mobile/e2e/android-run.sh | 20 + mobile/e2e/ios-run.sh | 2 + mobile/gradle/libs.versions.toml | 3 + mobile/gradle/verification-metadata.xml | 1884 +++++++++++++++++ mobile/ios/Keepiq/KeepiqApp.swift | 2 + mobile/ios/Keepiq/Views.swift | 10 +- .../KeepiqApp/GeneratorAndSendViews.swift | 24 +- .../Sources/KeepiqApp/ItemViews.swift | 23 +- .../KeepiqApp/Sources/KeepiqApp/Palette.swift | 50 + .../Sources/KeepiqApp/VaultAppView.swift | 2 +- .../Sources/KeepiqApp/VaultListView.swift | 2 +- .../Sources/KeepiqApp/VaultSupport.swift | 29 +- .../AccessibilityAuditUITests.swift | 383 ++++ mobile/shared/build.gradle.kts | 8 + openspec/changes/clients-mobile-apps/tasks.md | 43 +- openspec/parity/capabilities.json | 40 +- 21 files changed, 2925 insertions(+), 50 deletions(-) create mode 100644 mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/AccessibilityAuditTest.kt create mode 100644 mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/OfflineReadingTest.kt create mode 100644 mobile/ios/KeepiqApp/Sources/KeepiqApp/Palette.swift create mode 100644 mobile/ios/KeepiqUITests/AccessibilityAuditUITests.swift diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts index bbcef9164..be41be11d 100644 --- a/mobile/android/app/build.gradle.kts +++ b/mobile/android/app/build.gradle.kts @@ -144,6 +144,7 @@ dependencies { androidTestImplementation(platform(libs.compose.bom)) androidTestImplementation(libs.compose.ui.test.junit4) + androidTestImplementation(libs.compose.ui.test.junit4.accessibility) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.rules) androidTestImplementation(libs.androidx.test.junit) diff --git a/mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/AccessibilityAuditTest.kt b/mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/AccessibilityAuditTest.kt new file mode 100644 index 000000000..ec4036568 --- /dev/null +++ b/mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/AccessibilityAuditTest.kt @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: 2026 Conduction B.V. +// SPDX-License-Identifier: EUPL-1.2 + +package nl.conduction.keepiq.android + +import android.content.Context +import androidx.compose.ui.test.hasContentDescription +import androidx.compose.ui.test.hasSetTextAction +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isRoot +import androidx.compose.ui.test.junit4.accessibility.enableAccessibilityChecks +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.tryPerformAccessibilityChecks +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultDescriptor +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult +import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator +import java.io.File +import org.hamcrest.CoreMatchers +import org.hamcrest.Matcher +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Automatic accessibility audit of the main screens (task 3.1), with the + * Accessibility Test Framework for Android (Apache 2.0) through Compose's + * `enableAccessibilityChecks`: connect, unlock, the vault list, an item, a + * one-time code, the item form, the generator, Send and its form, and the + * settings. The checks run on every action and once more on each screen. + * + * Every finding is collected first, so one run names all of them; the test + * then fails on any error. The findings go to files/e2e-shots/a11y-findings.txt, + * which android-run.sh pulls with the screenshots. Suppressions live in + * [A11ySuppressions], each with its reason. + * + * Compose's hook needs Android 14 (API 34), so this class runs on the API 34 + * job only. A TalkBack pass by hand is still open (task 3.1). + */ +@RunWith(AndroidJUnit4::class) +@SdkSuppress(minSdkVersion = 34) +class AccessibilityAuditTest { + @get:Rule + val compose = createAndroidComposeRule() + + private var screen = "start" + private val errors = linkedMapOf() + private val warnings = linkedMapOf() + private val screensChecked = mutableListOf() + + @Before + fun setUp() { + E2e.requireServer() + check(E2e.appPassword.isNotEmpty()) { "keepiqAppPassword was not passed" } + val descriptor = AccessibilityCheckResultDescriptor() + val validator = AccessibilityValidator() + .setCheckPreset(AccessibilityCheckPreset.LATEST) + .setRunChecksFromRootView(true) + // Contrast needs pixels: Compose does not report text colours. + .setCaptureScreenshots(true) + // Collect, then fail once at the end with the whole list. + .setThrowExceptionFor(null) + .setSuppressingResultMatcher(A11ySuppressions.matcher) + .addCheckListener( + object : AccessibilityValidator.AccessibilityCheckListener { + override fun onResults(context: Context, results: List) { + for (result in results) { + val target = when (result.type) { + AccessibilityCheckResultType.ERROR -> errors + AccessibilityCheckResultType.WARNING -> warnings + else -> continue + } + val text = "${result.sourceCheckClass.simpleName}: ${descriptor.describeResult(result)}" + if (text !in target) target[text] = screen + } + } + }, + ) + compose.enableAccessibilityChecks(validator) + } + + @Test + fun mainScreensPassTheAccessibilityChecks() { + // Connect. + compose.waitForTag("server") + audit("connect") + compose.onNodeWithTag("server").performTextInput(E2e.server) + compose.onNodeWithTag("useAppPassword").performClick() + compose.waitForTag("appPassword") + audit("connect with an app password") + compose.onNodeWithTag("loginName").performTextInput(E2e.user) + compose.onNodeWithTag("appPassword").performTextInput(E2e.appPassword) + compose.onNodeWithTag("connect").performClick() + + // Unlock. + compose.waitForTag("masterPassword", 60_000) + audit("unlock") + compose.onNodeWithTag("masterPassword").performTextInput(E2e.masterPassword) + compose.onNodeWithTag("unlock").performClick() + + // The vault list. + compose.waitForTag("unlocked", 60_000) + compose.waitForText("Webmail (demo)", 60_000) + compose.waitForText("Last synced", 60_000) + audit("vault list") + + // An item, its password shown. + compose.onNodeWithText("Webmail (demo)").performClick() + compose.waitForText("anna.demo@example.com", 60_000) + audit("item detail") + compose.onNodeWithContentDescription("Show Password").performClick() + compose.waitForText("Lantern-Orbit-42!") + audit("item detail, password shown") + compose.onNodeWithText("Back").performClick() + + // A one-time code and its countdown. + compose.waitForText("Authenticator (demo)") + compose.onNodeWithText("Authenticator (demo)").performClick() + compose.waitUntil(10_000) { + compose.onAllNodes(hasContentDescription("seconds left", substring = true)).fetchSemanticsNodes().isNotEmpty() + } + audit("item detail, one-time code") + compose.onNodeWithText("Back").performClick() + + // The item form. + compose.waitForText("Webmail (demo)") + compose.onNodeWithContentDescription("Add an item").performClick() + compose.waitUntil(30_000) { compose.onAllNodes(hasSetTextAction() and hasText("Name")).fetchSemanticsNodes().isNotEmpty() } + audit("item form") + compose.onNodeWithText("Back").performClick() + + // The generator. + compose.waitForText("Generator") + compose.onNodeWithText("Generator").performClick() + compose.waitForTag("generated") + audit("generator") + + // Send and its form. + compose.onNodeWithText("Send").performClick() + compose.waitForText("Open a Send link") + audit("send list") + compose.onNodeWithContentDescription("New Send").performClick() + compose.waitUntil(30_000) { compose.onAllNodes(hasSetTextAction() and hasText("Text to send")).fetchSemanticsNodes().isNotEmpty() } + audit("send form") + compose.onNodeWithText("Back").performClick() + + // The settings. + compose.onNodeWithTag("settings").performClick() + compose.waitForTag("newPin") + audit("settings") + + report() + } + + /** Runs the checks over the whole window of [name], and names the screen for what the next actions find. */ + private fun audit(name: String) { + screen = name + compose.waitForIdle() + compose.onAllNodes(isRoot()).tryPerformAccessibilityChecks() + screensChecked += name + E2e.shot("a11y-" + name.replace(Regex("[^a-z0-9]+"), "-")) + } + + private fun report() { + val text = buildString { + appendLine("Screens checked: ${screensChecked.joinToString(", ")}") + appendLine("Errors: ${errors.size}") + errors.forEach { (finding, where) -> appendLine("ERROR on $where: $finding") } + appendLine("Warnings: ${warnings.size}") + warnings.forEach { (finding, where) -> appendLine("WARNING on $where: $finding") } + appendLine("Suppressed (A11ySuppressions): ${A11ySuppressions.reasons.joinToString("; ").ifEmpty { "none" }}") + } + val dir = File(InstrumentationRegistry.getInstrumentation().targetContext.filesDir, "e2e-shots").apply { mkdirs() } + File(dir, "a11y-findings.txt").writeText(text) + android.util.Log.i("KeepiqA11y", text) + assertTrue("accessibility errors:\n$text", errors.isEmpty()) + } +} + +/** + * Findings the audit leaves out, each a documented false positive with its + * reason. Real findings are fixed in the app, never listed here. + */ +object A11ySuppressions { + /** One line per suppression, for the report. */ + val reasons: List = emptyList() + + val matcher: Matcher = CoreMatchers.not(CoreMatchers.anything()) +} diff --git a/mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/E2e.kt b/mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/E2e.kt index 370bd6c42..e6cd53b72 100644 --- a/mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/E2e.kt +++ b/mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/E2e.kt @@ -89,3 +89,58 @@ fun ComposeTestRule.waitUntilGone(text: String, timeoutMillis: Long = 30_000) { .fetchSemanticsNodes().isEmpty() } } + +/** + * The emulator's network, for the offline test (task 3.4). The shell turns + * off Wi-Fi and mobile data, and airplane mode where `cmd connectivity` + * has it (Android 11 and later). The test then checks from the app's own + * process that the server is out of reach, so a network that stayed up + * fails the test instead of passing it. + */ +object Network { + fun cut() { + E2e.shell("cmd connectivity airplane-mode enable") + E2e.shell("svc wifi disable") + E2e.shell("svc data disable") + } + + fun restore() { + E2e.shell("cmd connectivity airplane-mode disable") + E2e.shell("svc wifi enable") + E2e.shell("svc data enable") + } + + /** Whether status.php answers, with short timeouts. */ + fun reachable(): Boolean = try { + val connection = URL("${E2e.server}/status.php").openConnection() as HttpsURLConnection + connection.connectTimeout = 5_000 + connection.readTimeout = 5_000 + connection.useCaches = false + try { + connection.responseCode in 200..499 + } finally { + connection.disconnect() + } + } catch (e: java.io.IOException) { + false + } + + fun awaitUnreachable(timeoutMillis: Long = 60_000) = await(timeoutMillis, false) + + fun awaitReachable(timeoutMillis: Long = 120_000) = await(timeoutMillis, true) + + private fun await(timeoutMillis: Long, want: Boolean) { + val until = System.currentTimeMillis() + timeoutMillis + while (System.currentTimeMillis() < until) { + if (reachable() == want) return + Thread.sleep(1_000) + } + // executeShellCommand runs no shell, so no pipes: filter here. + val state = E2e.shell("dumpsys connectivity").lineSequence() + .filter { it.contains("NetworkAgentInfo") }.take(5).joinToString(" | ") + throw AssertionError( + if (want) "the server is still out of reach ${timeoutMillis / 1000} s after the network came back; networks: $state" + else "the server still answers ${timeoutMillis / 1000} s after the network was cut; networks: $state", + ) + } +} diff --git a/mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/OfflineReadingTest.kt b/mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/OfflineReadingTest.kt new file mode 100644 index 000000000..f04464206 --- /dev/null +++ b/mobile/android/app/src/androidTest/kotlin/nl/conduction/keepiq/android/OfflineReadingTest.kt @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2026 Conduction B.V. +// SPDX-License-Identifier: EUPL-1.2 + +package nl.conduction.keepiq.android + +import android.database.DatabaseErrorHandler +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteException +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasContentDescription +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.runBlocking +import java.io.File +import kotlinx.serialization.json.jsonPrimitive +import nl.conduction.keepiq.shared.vault.WriteProblemKind +import nl.conduction.keepiq.shared.vault.WriteResult +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.concurrent.thread + +/** + * Task 3.4 on the emulator against the test server: offline reading with + * the last-synced note and refused edits. + * + * Pair and unlock online, so the vault syncs into the offline store. Lock, + * cut the network (Wi-Fi and mobile data off, and airplane mode where the + * shell can set it), and prove the server is out of reach from the app's + * own process before going on. Unlock offline, see the list from the store + * with the note that names the last sync, open an item from the store and + * reveal its password, and find every edit refused: the buttons are off, and + * a trash call on the repository comes back refused as offline without + * reaching the server. On the way it reads the store file without its key + * and finds no plaintext (task 1.6). Then the network comes back, a manual sync says + * "Last synced" again, and the item is still on the server. + */ +@RunWith(AndroidJUnit4::class) +class OfflineReadingTest { + @get:Rule + val compose = createAndroidComposeRule() + + @Before + fun setUp() { + E2e.requireServer() + check(E2e.appPassword.isNotEmpty()) { "keepiqAppPassword was not passed" } + Network.restore() + } + + /** Whatever happened, the next test class gets its network back. */ + @After + fun tearDown() { + Network.restore() + } + + @Test + fun syncThenReadOfflineWithTheLastSyncedNoteAndRefusedEdits() { + // Online: pair, unlock, and the sync fills the store. + compose.waitForTag("server") + compose.onNodeWithTag("server").performTextInput(E2e.server) + compose.onNodeWithTag("useAppPassword").performClick() + compose.waitForTag("appPassword") + compose.onNodeWithTag("loginName").performTextInput(E2e.user) + compose.onNodeWithTag("appPassword").performTextInput(E2e.appPassword) + compose.onNodeWithTag("connect").performClick() + compose.waitForTag("masterPassword", 60_000) + compose.onNodeWithTag("masterPassword").performTextInput(E2e.masterPassword) + compose.onNodeWithTag("unlock").performClick() + compose.waitForTag("unlocked", 60_000) + compose.waitForText("Webmail (demo)", 60_000) + compose.waitForText("Last synced", 60_000) + E2e.shot("50-online-synced") + val online = E2e.app.state.screen.value as Screen.Unlocked + val webmailId = online.session.repository.state.rows.single { it.name == "Webmail (demo)" }.id + + // Lock, then cut the network, and check that it is really cut. + compose.onNodeWithTag("lock").performClick() + compose.waitForTag("masterPassword", 30_000) + Network.cut() + Network.awaitUnreachable() + E2e.shot("51-network-cut") + // The unlock screen asks the server again and falls back to the suite of the last unlock. + val accountId = online.vault.accountId + InstrumentationRegistry.getInstrumentation().runOnMainSync { E2e.app.state.refreshGate(accountId) } + compose.waitForText("You are offline. Keepiq unlocks with what this phone stored", 90_000) + E2e.shot("51-offline-unlock") + + // Unlock offline: the list comes from the store, with the time of the last sync. + compose.onNodeWithTag("masterPassword").performTextInput(E2e.masterPassword) + compose.onNodeWithTag("unlock").performClick() + compose.waitForTag("unlocked", 60_000) + compose.waitForText("You are offline. You see the copy from", 90_000) + compose.waitForText("Webmail (demo)") + compose.waitForText("Personal") + // No add button while offline. + assertTrue(compose.onAllNodes(hasContentDescription("Add an item")).fetchSemanticsNodes().isEmpty()) + E2e.shot("52-offline-list") + val offline = E2e.app.state.screen.value as Screen.Unlocked + assertTrue("the vault state says offline", offline.session.repository.state.offline) + assertTrue("the offline state keeps the last sync time", offline.session.repository.state.syncedAtMillis != null) + + // An item from the store: the note, its login, and its password on request. + compose.onNodeWithText("Webmail (demo)").performClick() + compose.waitForText("This is the copy from your last sync", 60_000) + compose.waitForText("anna.demo@example.com") + compose.onNodeWithContentDescription("Show Password").performClick() + compose.waitForText("Lantern-Orbit-42!") + E2e.shot("53-offline-item") + + // Task 1.6 on the device: the store file is SQLCipher, so without its key + // it holds no plaintext and plain SQLite cannot read it. + assertStoreUnreadableWithoutKey() + + // Every edit is refused: the buttons are off, with the reason under them. + compose.onNode(hasText("Edit") and hasClickAction()).performScrollTo().assertIsNotEnabled() + compose.onNode(hasText("Move") and hasClickAction()).assertIsNotEnabled() + compose.onNode(hasText("Move to trash") and hasClickAction()).performScrollTo().assertIsNotEnabled() + compose.waitForText("Edits need a connection. Nothing was changed.") + E2e.shot("54-offline-edit-refused") + // And a write that gets past the screen is refused before it is sent. + val trash = onOwnThread { offline.session.repository.trash(webmailId) } + assertTrue("trash offline is refused: $trash", trash is WriteResult.Refused) + assertEquals(WriteProblemKind.OFFLINE, (trash as WriteResult.Refused).problem.kind) + compose.onNodeWithText("Back").performClick() + compose.waitForText("Webmail (demo)") + + // The network comes back: a manual sync, and the item is still on the server. + Network.restore() + Network.awaitReachable() + compose.onNodeWithContentDescription("Sync now").performClick() + compose.waitForText("Last synced", 90_000) + compose.waitUntilGone("You are offline", 30_000) + E2e.shot("55-back-online") + val onServer = onOwnThread { offline.session.api.listSecrets() } + .map { it["name"]?.jsonPrimitive?.content } + assertTrue("the refused trash changed nothing on the server: $onServer", "Webmail (demo)" in onServer) + } + + /** + * Reads the account's database files as bytes: no SQLite header, none of + * the demo vault's names, logins or addresses, and plain SQLite refuses + * the file. The journal files are read too, where they exist. + */ + private fun assertStoreUnreadableWithoutKey() { + val dir = E2e.app.getDatabasePath("x").parentFile!! + val stores = dir.listFiles { f -> f.name.startsWith("keepiq-vault-") && f.name.endsWith(".db") }.orEmpty() + assertEquals("one offline store: ${dir.list()?.toList()}", 1, stores.size) + val store = stores.single() + assertTrue("the store holds the synced vault", store.length() > 4096) + val files = listOf(store, File(store.path + "-wal"), File(store.path + "-journal")).filter { it.isFile } + for (file in files) { + val text = String(file.readBytes(), Charsets.ISO_8859_1) + assertFalse("${file.name} starts with the plain SQLite header", text.startsWith("SQLite format 3")) + for (plain in listOf("Webmail (demo)", "anna.demo@example.com", "example.com", "Personal", "Lantern-Orbit-42!")) { + assertFalse("${file.name} holds \"$plain\" in plaintext", text.contains(plain)) + } + } + try { + // A no-op error handler: the default one deletes a file it calls corrupt. + SQLiteDatabase.openDatabase(store.path, null, SQLiteDatabase.OPEN_READONLY, DatabaseErrorHandler { }).use { db -> + db.rawQuery("SELECT count(*) FROM sqlite_master", null).use { it.moveToFirst() } + } + fail("plain SQLite read the offline store without its key") + } catch (e: SQLiteException) { + // "file is not a database": the key is needed. + } + } + + /** Runs a suspend call on a thread of its own (see VaultFlowsTest). */ + private fun onOwnThread(block: suspend () -> T): T { + var result: Result? = null + thread { result = runCatching { runBlocking { block() } } }.join() + return result!!.getOrThrow() + } +} diff --git a/mobile/android/app/src/main/kotlin/nl/conduction/keepiq/android/ui/SettingsScreen.kt b/mobile/android/app/src/main/kotlin/nl/conduction/keepiq/android/ui/SettingsScreen.kt index c67b89b9d..15c9a565e 100644 --- a/mobile/android/app/src/main/kotlin/nl/conduction/keepiq/android/ui/SettingsScreen.kt +++ b/mobile/android/app/src/main/kotlin/nl/conduction/keepiq/android/ui/SettingsScreen.kt @@ -6,6 +6,7 @@ package nl.conduction.keepiq.android.ui import android.content.Intent import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.layout.Column @@ -39,6 +40,7 @@ import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -154,8 +156,10 @@ fun SettingsScreen(state: AppState, activity: FragmentActivity, vault: UnlockedV for (minutes in IdlePolicy.offeredChoices(maxIdle)) { Row( verticalAlignment = Alignment.CenterVertically, + // 48 dp: the radio row is the touch target (the accessibility audit, task 3.1). modifier = Modifier .fillMaxWidth() + .heightIn(min = 48.dp) .selectable( selected = settings.idleMinutes == minutes, role = Role.RadioButton, diff --git a/mobile/e2e/android-run.sh b/mobile/e2e/android-run.sh index 84a451a1c..9990aba19 100755 --- a/mobile/e2e/android-run.sh +++ b/mobile/e2e/android-run.sh @@ -131,6 +131,26 @@ if [ "$(adb shell getprop ro.build.version.sdk | tr -d '\r')" -ge 34 ]; then adb shell dumpsys credential > "$OUT/dumpsys-credential.txt" 2>&1 || true fi +# Offline reading (task 3.4): sync, cut the network, read from the store, +# edits refused, network back. The test restores the network itself; these +# three lines are the fallback for a run that crashed half way. +adb shell pm clear "$PKG" >/dev/null +video_start keepiq-android-offline +run_class OfflineReadingTest -e keepiqAppPassword "$APP_PASSWORD" || status=1 +video_stop keepiq-android-offline +adb shell cmd connectivity airplane-mode disable >/dev/null 2>&1 || true +adb shell svc wifi enable || true +adb shell svc data enable || true + +# The accessibility audit of the main screens (task 3.1). Compose's +# accessibility checks need Android 14, so the API 34 job only. Its findings +# land in e2e-shots/a11y-findings.txt. +if [ "$(adb shell getprop ro.build.version.sdk | tr -d '\r')" -ge 34 ]; then + adb shell pm clear "$PKG" >/dev/null + run_class AccessibilityAuditTest -e keepiqAppPassword "$APP_PASSWORD" || status=1 + cat "$OUT/e2e-shots/a11y-findings.txt" 2>/dev/null || echo "::warning::no a11y-findings.txt" +fi + # The R8 release build (API 34 job only): it starts, shows its first screen # and stays up while another app asks it for autofill. R8 removes code that only # reflection or JNI reaches; the e2e build above is not shrunk, so this is diff --git a/mobile/e2e/ios-run.sh b/mobile/e2e/ios-run.sh index 04537936b..ed255f309 100755 --- a/mobile/e2e/ios-run.sh +++ b/mobile/e2e/ios-run.sh @@ -88,6 +88,8 @@ run_class PasskeyUITests keepiq-ios-passkeys # Offline reading from the encrypted store (tasks 1.6.1 and 3.4): the replay # drops every connection halfway through the test. run_class OfflineUITests keepiq-ios-offline +# The accessibility audit of the main screens (task 3.1), against the replay. +run_class AccessibilityAuditUITests keepiq-ios-a11y rm -rf "$OUT/DerivedData" ls -la "$OUT" "$OUT/shots" exit "$status" diff --git a/mobile/gradle/libs.versions.toml b/mobile/gradle/libs.versions.toml index 66deec776..7cd3d43d9 100644 --- a/mobile/gradle/libs.versions.toml +++ b/mobile/gradle/libs.versions.toml @@ -57,6 +57,9 @@ androidx-browser = { module = "androidx.browser:browser", version.ref = "android androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" } compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +# Compose's accessibility checks in the emulator tests (task 3.1), over the +# Accessibility Test Framework for Android (Apache 2.0). Tests only. +compose-ui-test-junit4-accessibility = { module = "androidx.compose.ui:ui-test-junit4-accessibility" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" } androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" } diff --git a/mobile/gradle/verification-metadata.xml b/mobile/gradle/verification-metadata.xml index 4ada4e429..460b1b458 100644 --- a/mobile/gradle/verification-metadata.xml +++ b/mobile/gradle/verification-metadata.xml @@ -5,6 +5,14 @@ false + + + + + + + + @@ -20,11 +28,20 @@ + + + + + + + + + @@ -33,6 +50,9 @@ + + + @@ -43,9 +63,15 @@ + + + + + + @@ -64,9 +90,15 @@ + + + + + + @@ -75,6 +107,9 @@ + + + @@ -84,6 +119,22 @@ + + + + + + + + + + + + + + + + @@ -93,9 +144,15 @@ + + + + + + @@ -105,15 +162,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -122,10 +206,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -141,6 +258,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -149,6 +302,9 @@ + + + @@ -157,6 +313,17 @@ + + + + + + + + + + + @@ -166,6 +333,14 @@ + + + + + + + + @@ -174,6 +349,22 @@ + + + + + + + + + + + + + + + + @@ -189,8 +380,22 @@ + + + + + + + + + + + + + + @@ -202,6 +407,17 @@ + + + + + + + + + + + @@ -235,6 +451,14 @@ + + + + + + + + @@ -244,6 +468,9 @@ + + + @@ -252,20 +479,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -273,6 +550,9 @@ + + + @@ -280,9 +560,15 @@ + + + + + + @@ -409,15 +695,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -430,10 +746,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -462,6 +803,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -471,6 +864,9 @@ + + + @@ -483,10 +879,24 @@ + + + + + + + + + + + + + + @@ -499,6 +909,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -508,6 +951,9 @@ + + + @@ -516,14 +962,31 @@ + + + + + + + + + + + + + + + + + @@ -532,14 +995,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -548,14 +1039,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -564,6 +1083,9 @@ + + + @@ -572,18 +1094,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -592,10 +1161,24 @@ + + + + + + + + + + + + + + @@ -604,14 +1187,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -620,14 +1231,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -636,11 +1275,28 @@ + + + + + + + + + + + + + + + + + @@ -652,6 +1308,9 @@ + + + @@ -661,6 +1320,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -676,6 +1367,9 @@ + + + @@ -693,12 +1387,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -710,6 +1456,9 @@ + + + @@ -727,6 +1476,25 @@ + + + + + + + + + + + + + + + + + + + @@ -766,6 +1534,9 @@ + + + @@ -783,6 +1554,14 @@ + + + + + + + + @@ -792,9 +1571,15 @@ + + + + + + @@ -808,6 +1593,25 @@ + + + + + + + + + + + + + + + + + + + @@ -832,6 +1636,9 @@ + + + @@ -841,6 +1648,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -848,21 +1687,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -873,6 +1744,9 @@ + + + @@ -885,6 +1759,17 @@ + + + + + + + + + + + @@ -918,6 +1803,9 @@ + + + @@ -927,6 +1815,14 @@ + + + + + + + + @@ -935,6 +1831,14 @@ + + + + + + + + @@ -947,11 +1851,31 @@ + + + + + + + + + + + + + + + + + + + + @@ -988,9 +1912,15 @@ + + + + + + @@ -1008,6 +1938,22 @@ + + + + + + + + + + + + + + + + @@ -1015,16 +1961,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1035,6 +2018,9 @@ + + + @@ -1051,6 +2037,9 @@ + + + @@ -1067,6 +2056,9 @@ + + + @@ -1083,6 +2075,9 @@ + + + @@ -1095,15 +2090,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1114,6 +2143,9 @@ + + + @@ -1130,6 +2162,9 @@ + + + @@ -1142,6 +2177,22 @@ + + + + + + + + + + + + + + + + @@ -1149,16 +2200,31 @@ + + + + + + + + + + + + + + + @@ -1169,6 +2235,9 @@ + + + @@ -1188,6 +2257,9 @@ + + + @@ -1200,15 +2272,38 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -1222,6 +2317,9 @@ + + + @@ -1238,16 +2336,31 @@ + + + + + + + + + + + + + + + @@ -1286,15 +2399,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1303,6 +2455,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1311,6 +2490,9 @@ + + + @@ -1319,6 +2501,9 @@ + + + @@ -1353,9 +2538,15 @@ + + + + + + @@ -1364,6 +2555,9 @@ + + + @@ -1373,6 +2567,14 @@ + + + + + + + + @@ -1408,9 +2610,15 @@ + + + + + + @@ -1419,6 +2627,9 @@ + + + @@ -1428,6 +2639,22 @@ + + + + + + + + + + + + + + + + @@ -1452,6 +2679,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1468,6 +2727,14 @@ + + + + + + + + @@ -1476,6 +2743,14 @@ + + + + + + + + @@ -1492,6 +2767,14 @@ + + + + + + + + @@ -1508,6 +2791,22 @@ + + + + + + + + + + + + + + + + @@ -1548,6 +2847,22 @@ + + + + + + + + + + + + + + + + @@ -1579,6 +2894,9 @@ + + + @@ -1588,6 +2906,14 @@ + + + + + + + + @@ -1604,6 +2930,14 @@ + + + + + + + + @@ -1628,6 +2962,14 @@ + + + + + + + + @@ -2420,6 +3762,25 @@ + + + + + + + + + + + + + + + + + + + @@ -2449,6 +3810,11 @@ + + + + + @@ -2486,6 +3852,14 @@ + + + + + + + + @@ -2496,6 +3870,19 @@ + + + + + + + + + + + + + @@ -2583,6 +3970,14 @@ + + + + + + + + @@ -2617,6 +4012,22 @@ + + + + + + + + + + + + + + + + @@ -2633,6 +4044,19 @@ + + + + + + + + + + + + + @@ -2658,6 +4082,16 @@ + + + + + + + + + + @@ -2673,6 +4107,11 @@ + + + + + @@ -2705,6 +4144,22 @@ + + + + + + + + + + + + + + + + @@ -2737,6 +4192,16 @@ + + + + + + + + + + @@ -2773,6 +4238,14 @@ + + + + + + + + @@ -2802,6 +4275,11 @@ + + + + + @@ -2857,6 +4335,14 @@ + + + + + + + + @@ -2865,6 +4351,11 @@ + + + + + @@ -2941,6 +4432,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -4641,6 +6153,14 @@ + + + + + + + + @@ -4886,11 +6406,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -4907,6 +6457,14 @@ + + + + + + + + @@ -4923,6 +6481,11 @@ + + + + + @@ -4933,6 +6496,11 @@ + + + + + @@ -5321,6 +6889,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -5329,6 +6961,22 @@ + + + + + + + + + + + + + + + + @@ -5375,9 +7023,15 @@ + + + + + + @@ -5425,6 +7079,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -5433,6 +7151,22 @@ + + + + + + + + + + + + + + + + @@ -5480,6 +7214,22 @@ + + + + + + + + + + + + + + + + @@ -5496,6 +7246,22 @@ + + + + + + + + + + + + + + + + @@ -5705,14 +7471,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -5721,6 +7521,9 @@ + + + @@ -5773,15 +7576,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -5837,10 +7670,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -5849,6 +7710,9 @@ + + + @@ -5857,6 +7721,9 @@ + + + @@ -5902,6 +7769,9 @@ + + + @@ -5959,6 +7829,9 @@ + + + @@ -6208,6 +8081,14 @@ + + + + + + + + @@ -6215,6 +8096,9 @@ + + + diff --git a/mobile/ios/Keepiq/KeepiqApp.swift b/mobile/ios/Keepiq/KeepiqApp.swift index 8fb44c2e8..4a3ca67a4 100644 --- a/mobile/ios/Keepiq/KeepiqApp.swift +++ b/mobile/ios/Keepiq/KeepiqApp.swift @@ -14,6 +14,8 @@ struct KeepiqApp: App { WindowGroup { RootView() .environmentObject(model) + // Keepiq's tint passes WCAG AA contrast; the system blue does not on white. + .tint(KeepiqPalette.accent) } .onChange(of: scenePhase) { _, phase in if phase == .active { model.cameToForeground() } diff --git a/mobile/ios/Keepiq/Views.swift b/mobile/ios/Keepiq/Views.swift index 281a6693b..0ef584e57 100644 --- a/mobile/ios/Keepiq/Views.swift +++ b/mobile/ios/Keepiq/Views.swift @@ -41,7 +41,7 @@ struct PairView: View { .accessibilityIdentifier("useAppPassword") } } else { - Section(footer: Text("Create an app password in Nextcloud under Personal settings, Security.")) { + Section(footer: Text("Create an app password in Nextcloud under Personal settings, Security.").foregroundStyle(KeepiqPalette.secondaryText)) { LabeledField("User name") { TextField("User name", text: $loginName, prompt: Text("Your Nextcloud user name")) .textInputAutocapitalization(.never) @@ -161,7 +161,7 @@ struct SettingsView: View { let biometricOn = model.biometric.isEnabled(vault.accountId) let pinSet = model.client.pins.has(accountId: vault.accountId) Form { - Section("Unlock") { + Section(titled: "Unlock") { Toggle("Face ID or Touch ID", isOn: Binding( get: { biometricOn }, set: { on in on ? model.enableBiometric(vault) : model.disableBiometric(vault) } @@ -185,14 +185,14 @@ struct SettingsView: View { .accessibilityIdentifier("setPin") } } - Section("Account") { + Section(titled: "Account") { if let account = model.account(vault.accountId) { Text("\(account.loginName) on \(account.server.withoutScheme)") } - Button("Disconnect this account", role: .destructive) { confirmUnpair = true } + Button(role: .destructive) { confirmUnpair = true } label: { Text("Disconnect this account").foregroundStyle(KeepiqPalette.destructive) } .accessibilityIdentifier("unpair") } - Section("Lock after") { + Section(titled: "Lock after") { if let max = model.maxIdle { Text("Your organisation allows at most \(max) minutes.").font(.footnote) } diff --git a/mobile/ios/KeepiqApp/Sources/KeepiqApp/GeneratorAndSendViews.swift b/mobile/ios/KeepiqApp/Sources/KeepiqApp/GeneratorAndSendViews.swift index 1efc99751..21f80a4cf 100644 --- a/mobile/ios/KeepiqApp/Sources/KeepiqApp/GeneratorAndSendViews.swift +++ b/mobile/ios/KeepiqApp/Sources/KeepiqApp/GeneratorAndSendViews.swift @@ -66,13 +66,17 @@ struct GeneratorView: View { if policy?.allowPassphrase == false { Text(L("gen_passphrase_off")).font(.footnote) } HStack { if let value = outcome.value { - Text(value).font(.body.monospaced()).textSelection(.disabled).accessibilityIdentifier("generated") + Text(value).font(.body.monospaced()).textSelection(.disabled).lineLimit(nil).fixedSize(horizontal: false, vertical: true).accessibilityIdentifier("generated") } else { Text(generatorProblem(outcome.error)).foregroundStyle(.red) } Spacer() - Button { round += 1 } label: { Image(systemName: "arrow.clockwise") } - .frame(minWidth: 44, minHeight: 44) + // The frame inside the label: a borderless button only takes taps on its label. + Button { round += 1 } label: { + Image(systemName: "arrow.clockwise") + .frame(minWidth: 44, minHeight: 44) + .contentShape(Rectangle()) + } .accessibilityLabel(L("cd_regenerate")) if let value = outcome.value { if let onUse { @@ -233,9 +237,11 @@ private struct SendRow: View { Text(created.map { L("send_row", kind, $0) } ?? kind) Text(detailText(expiry: expiry)) .font(.footnote) - .foregroundStyle(.secondary) + .foregroundStyle(KeepiqPalette.secondaryText) } .frame(minHeight: 44) + // One element for the row, so its 44 pt is the hit area and not each line's. + .accessibilityElement(children: .combine) } private func expiryText(_ m: Int64) -> String { @@ -331,10 +337,14 @@ struct NewSendView: View { TextField(L("send_hours_label"), text: $hours).keyboardType(.numberPad) } } - LabeledField(L("send_password_label")) { - SecureField(L("send_password_label"), text: $sendPassword).textContentType(.oneTimeCode).disabled(!passwordAvailable) + // Without a password option there is no field to fill in, only the reason. + if passwordAvailable { + LabeledField(L("send_password_label")) { + SecureField(L("send_password_label"), text: $sendPassword).textContentType(.oneTimeCode) + } + } else { + Text(L("send_password_unavailable")).font(.footnote) } - if !passwordAvailable { Text(L("send_password_unavailable")).font(.footnote) } } Section { if let problem { Text(problem).foregroundStyle(.red) } diff --git a/mobile/ios/KeepiqApp/Sources/KeepiqApp/ItemViews.swift b/mobile/ios/KeepiqApp/Sources/KeepiqApp/ItemViews.swift index 782221a8c..176923fd7 100644 --- a/mobile/ios/KeepiqApp/Sources/KeepiqApp/ItemViews.swift +++ b/mobile/ios/KeepiqApp/Sources/KeepiqApp/ItemViews.swift @@ -58,15 +58,16 @@ struct ItemDetailView: View { if row.blocked { Text(row.blockedReason ?? L("detail_blocked")) } else { + if item.kind == .totp { Section { TotpRow(item: item, onCopy: model.copy) } } Section { fields(item) } if !row.useOnly { Section { - Button(L("action_edit")) { editing = true }.disabled(offline) - Button(L("action_move")) { moving = true }.disabled(offline) + Button { editing = true } label: { Text(L("action_edit")).foregroundStyle(KeepiqPalette.accent) }.disabled(offline) + Button { moving = true } label: { Text(L("action_move")).foregroundStyle(KeepiqPalette.accent) }.disabled(offline) } } Section { - Button(L("action_trash"), role: .destructive) { confirmTrash = true }.disabled(offline) + Button(role: .destructive) { confirmTrash = true } label: { Text(L("action_trash")).foregroundStyle(KeepiqPalette.destructive) }.disabled(offline) if offline { Text(L("write_offline")).font(.footnote) } } } @@ -102,8 +103,6 @@ struct ItemDetailView: View { if !row.useOnly { FieldRow(label: kind == .login ? L("detail_password") : L("detail_value"), value: item.secret, masked: true, onCopy: model.copy) } - } else if kind == .totp { - TotpRow(item: item, onCopy: model.copy) } else if kind == .note { if !row.useOnly { FieldRow(label: L("detail_notes"), value: item.secret, onCopy: model.copy) } } else if kind == .card || kind == .identity { @@ -158,6 +157,8 @@ struct ItemDetailView: View { private struct TotpRow: View { let item: DecryptedItem let onCopy: (String) -> Void + /// The countdown ring grows with Dynamic Type. + @ScaledMetric(relativeTo: .body) private var gaugeSize: CGFloat = 44 var body: some View { if let params = item.totp { @@ -166,19 +167,21 @@ private struct TotpRow: View { let code = Totp.shared.generate(params: params, epochMillis: millis) let left = Totp.shared.secondsRemaining(period: params.period, epochMillis: millis) VStack(alignment: .leading) { - Text(L("detail_code")).font(.caption).foregroundStyle(.secondary) + Text(L("detail_code")).font(.caption).foregroundStyle(KeepiqPalette.secondaryText) HStack { Text(code).font(.title.monospaced()).accessibilityIdentifier("totpCode") // A circular ProgressView spins on iOS whatever its value, so // the seconds left are a gauge with the number in it. Gauge(value: Double(left), in: 0...Double(params.period)) { EmptyView() - } currentValueLabel: { - Text("\(Int(left))") } .gaugeStyle(.accessoryCircularCapacity) + .tint(KeepiqPalette.accent) .scaleEffect(0.7) - .frame(width: 44, height: 44) + .frame(width: gaugeSize, height: gaugeSize) + // The accessory style draws its value label for widgets, with + // vibrancy; a plain Text keeps the full contrast of the primary colour. + .overlay { Text("\(Int(left))").font(.caption2).monospacedDigit().foregroundStyle(.primary) } .accessibilityElement(children: .ignore) .accessibilityLabel(L("detail_code_seconds", Int(left))) Spacer() @@ -391,7 +394,7 @@ struct ItemEditView: View { } } if kind != .passkey { - Section(L("detail_extra_fields")) { + Section(titled: L("detail_extra_fields")) { ForEach(fieldNames.indices, id: \.self) { i in VStack { LabeledInput(label: L("edit_field_name"), text: $fieldNames[i], problem: error("field-\(i)")) diff --git a/mobile/ios/KeepiqApp/Sources/KeepiqApp/Palette.swift b/mobile/ios/KeepiqApp/Sources/KeepiqApp/Palette.swift new file mode 100644 index 000000000..8bcf87388 --- /dev/null +++ b/mobile/ios/KeepiqApp/Sources/KeepiqApp/Palette.swift @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Conduction B.V. +// SPDX-License-Identifier: EUPL-1.2 + +import SwiftUI +import UIKit + +/// Keepiq's own colours, chosen for WCAG AA contrast (task 3.1). The +/// accessibility audit failed the system blue (4.0:1 on white) and the +/// system secondary text (about 3.5:1 on a grouped list) at body sizes. +enum KeepiqPalette { + /// The tint: 7.1:1 on white in light mode; the system blue in dark mode, + /// 5.8:1 on black, and 3.6:1 under the white label of a prominent button, + /// which is large text (17 pt semibold). + static let accent = Color(UIColor { traits in + traits.userInterfaceStyle == .dark + ? UIColor(red: 0x0A / 255, green: 0x84 / 255, blue: 0xFF / 255, alpha: 1) + : UIColor(red: 0x00 / 255, green: 0x55 / 255, blue: 0xB3 / 255, alpha: 1) + }) + + /// Destructive actions in a list: 6.0:1 on white (the system red is + /// 3.6:1), and the system dark-mode red, 5.0:1 on the dark cells. + static let destructive = Color(UIColor { traits in + traits.userInterfaceStyle == .dark + ? UIColor(red: 0xFF / 255, green: 0x45 / 255, blue: 0x3A / 255, alpha: 1) + : UIColor(red: 0xC4 / 255, green: 0x14 / 255, blue: 0x1C / 255, alpha: 1) + }) + + /// Secondary text: 6.6:1 on white and 7.5:1 on the dark grouped cells. + static let secondaryText = Color(UIColor { traits in + traits.userInterfaceStyle == .dark + ? UIColor(red: 0xAE / 255, green: 0xAE / 255, blue: 0xB2 / 255, alpha: 1) + : UIColor(red: 0x5C / 255, green: 0x5C / 255, blue: 0x61 / 255, alpha: 1) + }) +} + +/// A section header in Keepiq's secondary text colour. +struct SectionTitle: View { + let title: String + + var body: some View { + Text(title).foregroundStyle(KeepiqPalette.secondaryText) + } +} + +extension Section where Parent == SectionTitle, Footer == EmptyView, Content: View { + /// A section with a header in Keepiq's secondary text colour. + init(titled title: String, @ViewBuilder content: () -> Content) { + self.init(content: content, header: { SectionTitle(title: title) }) + } +} diff --git a/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultAppView.swift b/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultAppView.swift index 147a828a2..d7b54388e 100644 --- a/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultAppView.swift +++ b/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultAppView.swift @@ -160,7 +160,7 @@ struct AccountsView: View { Button(L("cd_settings"), action: onSettings).accessibilityIdentifier("settings") } } - Section(L("settings_clipboard")) { + Section(titled: L("settings_clipboard")) { Picker(L("settings_clipboard"), selection: $clearSeconds) { ForEach(SensitiveClipboard.companion.CLEAR_CHOICES.map { Int(truncating: $0) }, id: \.self) { seconds in Text(clipboardLabel(seconds)).tag(seconds) diff --git a/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultListView.swift b/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultListView.swift index 6733b41fd..3da3d9d11 100644 --- a/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultListView.swift +++ b/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultListView.swift @@ -169,7 +169,7 @@ private struct EntryRow: View { .split(separator: "/").first.map(String.init) ?? "" let details = detailText(host: host) if !details.isEmpty { - Text(details).font(.footnote).foregroundStyle(.secondary) + Text(details).font(.footnote).foregroundStyle(KeepiqPalette.secondaryText) } } .frame(minHeight: 44) diff --git a/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultSupport.swift b/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultSupport.swift index e49be7519..d49540e32 100644 --- a/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultSupport.swift +++ b/mobile/ios/KeepiqApp/Sources/KeepiqApp/VaultSupport.swift @@ -203,18 +203,39 @@ struct FieldRow: View { @State private var shown = false + /// A long address or login has no spaces, so the text could only be cut + /// off. A zero-width space after each slash, dot and @ lets it wrap there; + /// VoiceOver and Copy keep the value as it is. + static func breakable(_ value: String) -> String { + guard !value.contains(" ") else { return value } + var out = "" + for character in value { + out.append(character) + if character == "/" || character == "." || character == "@" { out.append("\u{200B}") } + } + return out + } + @Environment(\.dynamicTypeSize) private var typeSize + var body: some View { + // At the accessibility text sizes the buttons go under the value, so + // a long value keeps the full width instead of being clipped. + let layout = typeSize.isAccessibilitySize ? AnyLayout(VStackLayout(alignment: .leading)) : AnyLayout(HStackLayout()) VStack(alignment: .leading, spacing: 4) { - Text(label).font(.caption).foregroundStyle(.secondary) - HStack { + Text(label).font(.caption).foregroundStyle(KeepiqPalette.secondaryText) + layout { if masked && !shown { Text("••••••••").accessibilityLabel(label) } else { - Text(value.isEmpty ? "-" : value) + Text(value.isEmpty ? "-" : masked ? value : Self.breakable(value)) .font(masked ? .body.monospaced() : .body) .textSelection(.disabled) + .lineLimit(nil) + // Wraps instead of clipping at large Dynamic Type sizes. + .fixedSize(horizontal: false, vertical: true) + .accessibilityLabel(value.isEmpty ? "-" : value) } - Spacer() + if !typeSize.isAccessibilitySize { Spacer() } if masked { Button(shown ? L("action_hide") : L("action_show")) { shown.toggle() } .frame(minWidth: 44, minHeight: 44) diff --git a/mobile/ios/KeepiqUITests/AccessibilityAuditUITests.swift b/mobile/ios/KeepiqUITests/AccessibilityAuditUITests.swift new file mode 100644 index 000000000..479045aaa --- /dev/null +++ b/mobile/ios/KeepiqUITests/AccessibilityAuditUITests.swift @@ -0,0 +1,383 @@ +// SPDX-FileCopyrightText: 2026 Conduction B.V. +// SPDX-License-Identifier: EUPL-1.2 + +import XCTest + +/// The automatic accessibility audit of the main screens (task 3.1), with +/// Xcode's `performAccessibilityAudit` (iOS 17 and later), against the +/// replay of the seeded demo vault: connect, unlock, the vault list, an +/// item, a one-time code, the item form, the generator, Send and its form, +/// and the settings. +/// +/// Every issue is collected first, so one run names all of them; the test +/// then fails on any issue that is not a documented exclusion. The list goes +/// to a11y-findings.txt next to the screenshots. Exclusions live in +/// `A11yExclusions`, each with its reason. A VoiceOver pass by hand is still +/// open (task 3.1). +final class AccessibilityAuditUITests: XCTestCase { + private var app: XCUIApplication! + private let env = ProcessInfo.processInfo.environment + private var server: String { env["KEEPIQ_SERVER"] ?? "https://localhost:8443" } + private var issues: [String] = [] + private var excluded: [String] = [] + private var screens: [String] = [] + + override func setUpWithError() throws { + continueAfterFailure = false + app = XCUIApplication() + app.launchArguments = ["-keepiq-reset"] + app.launchEnvironment["KEEPIQ_UITEST_NO_BROWSER"] = "1" + app.launch() + } + + func testMainScreensPassTheAccessibilityAudit() throws { + // Connect. + XCTAssertTrue(app.textFields["server"].waitForExistence(timeout: 30)) + try audit("connect") + type(server, into: app.textFields["server"]) + tap(app.buttons["useAppPassword"]) + XCTAssertTrue(app.secureTextFields["appPassword"].waitForExistence(timeout: 20)) + try audit("connect with an app password") + type("admin", into: app.textFields["loginName"]) + type("manual-app-password", into: app.secureTextFields["appPassword"]) + tap(app.buttons["connect"]) + + // Unlock. + XCTAssertTrue(app.secureTextFields["masterPassword"].waitForExistence(timeout: 60)) + declineSavePassword() + try audit("unlock") + type("Oj", into: app.secureTextFields["masterPassword"]) + tap(app.buttons["unlock"]) + + // The vault list. + XCTAssertTrue(app.buttons["lock"].waitForExistence(timeout: 60), "the vault opens after unlock") + XCTAssertTrue(text("Webmail (demo)").waitForExistence(timeout: 60)) + try audit("vault list") + + // An item, its password shown. + tap(text("Webmail (demo)")) + XCTAssertTrue(text("anna.demo@example.com").waitForExistence(timeout: 60)) + tap(app.buttons["Show Password"]) + XCTAssertTrue(text("Lantern-Orbit-42!").waitForExistence(timeout: 10)) + try audit("item detail") + back() + + // A one-time code and its countdown. + tap(text("Authenticator (demo)")) + XCTAssertTrue(app.staticTexts["totpCode"].waitForExistence(timeout: 60)) + try audit("item detail, one-time code") + back() + + // The item form. + tap(app.buttons["Add an item"]) + XCTAssertTrue(app.textFields["Name"].waitForExistence(timeout: 20)) + try audit("item form") + back() + + // The generator. + tap(app.tabBars.buttons["Generator"]) + XCTAssertTrue(app.staticTexts["generated"].waitForExistence(timeout: 20)) + try audit("generator") + + // Send and its form. + tap(app.tabBars.buttons["Send"]) + XCTAssertTrue(app.buttons["New Send"].waitForExistence(timeout: 20)) + try audit("send list") + tap(app.buttons["New Send"]) + XCTAssertTrue(app.buttons["Create link"].waitForExistence(timeout: 20) || scrolledTo(app.buttons["Create link"]).exists) + try audit("send form") + back() + + // The settings. + tap(onScreen(app.buttons.matching(identifier: "accounts"))) + tap(app.buttons["settings"]) + XCTAssertTrue(app.secureTextFields["newPin"].waitForExistence(timeout: 20)) + try audit("settings") + + report() + } + + // MARK: - The audit + + /// Audits the screen on display, and collects what it finds. + private func audit(_ screen: String) throws { + screens.append(screen) + // Let a push or a sheet finish: the contrast check reads pixels. + usleep(1_000_000) + let screenshot = shot("a11y-" + screen.replacingOccurrences(of: ",", with: "").replacingOccurrences(of: " ", with: "-")) + let bars = barFrames() + let rowUnderBar = rowsUnderBars(bars) + try app.performAccessibilityAudit(for: .all) { issue in + let line = "\(screen): \(Self.name(of: issue.auditType)): \(issue.compactDescription) [\(Self.describe(issue.element))]" + if let reason = A11yExclusions.reason(for: issue, bars: bars, rowUnderBar: rowUnderBar, screenshot: screenshot) { + self.excluded.append("\(line) (excluded: \(reason))") + } else { + self.issues.append(line + "\n " + issue.detailedDescription) + } + // Collected here; the test fails once, at the end, with the whole list. + return true + } + } + + /// The navigation bar and the tab bar on screen. From iOS 26 the system + /// fades the content scrolled under them (the scroll edge effect). + private func barFrames() -> [CGRect] { + (app.navigationBars.allElementsBoundByIndex + app.tabBars.allElementsBoundByIndex) + .filter { $0.exists } + .map(\.frame) + .filter { !$0.isEmpty } + } + + /// Whether a list row sits partly under a bar, faded by the scroll edge + /// effect, at the moment of the audit. + private func rowsUnderBars(_ bars: [CGRect]) -> Bool { + app.cells.allElementsBoundByIndex.contains { cell in + cell.exists && bars.contains { $0.intersects(cell.frame) } + } + } + + private func report() { + var lines = ["Screens audited: \(screens.joined(separator: ", "))", "Issues: \(issues.count)"] + lines += issues + lines.append("Excluded: \(excluded.count)") + lines += excluded + let text = lines.joined(separator: "\n") + let attachment = XCTAttachment(string: text) + attachment.name = "a11y-findings" + attachment.lifetime = .keepAlways + add(attachment) + if let dir = env["KEEPIQ_SHOTS_DIR"] { + try? text.write(to: URL(fileURLWithPath: dir).appendingPathComponent("a11y-findings.txt"), atomically: true, encoding: .utf8) + } + print(text) + XCTAssertTrue(issues.isEmpty, "accessibility issues:\n\(text)") + } + + private static func name(of type: XCUIAccessibilityAuditType) -> String { + let names: [(XCUIAccessibilityAuditType, String)] = [ + (.contrast, "contrast"), (.elementDetection, "elementDetection"), (.hitRegion, "hitRegion"), + (.sufficientElementDescription, "sufficientElementDescription"), (.dynamicType, "dynamicType"), + (.textClipped, "textClipped"), (.trait, "trait"), + ] + let matched = names.filter { type.contains($0.0) }.map { $0.1 } + return matched.isEmpty ? "type \(type.rawValue)" : matched.joined(separator: "+") + } + + private static func describe(_ element: XCUIElement?) -> String { + guard let element, element.exists else { return "no element" } + return "type \(element.elementType.rawValue), id '\(element.identifier)', label '\(element.label)'" + } + + // MARK: - Helpers, as in VaultFlowsUITests + + @discardableResult + private func shot(_ name: String) -> XCUIScreenshot { + let screenshot = XCUIScreen.main.screenshot() + let attachment = XCTAttachment(screenshot: screenshot) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + if let dir = env["KEEPIQ_SHOTS_DIR"] { + try? screenshot.pngRepresentation.write(to: URL(fileURLWithPath: dir).appendingPathComponent("\(name).png")) + } + return screenshot + } + + private func type(_ text: String, into element: XCUIElement) { + XCTAssertTrue(element.waitForExistence(timeout: 20), "\(element) is missing") + let deadline = Date().addingTimeInterval(5) + while !reachable(element) && Date() < deadline { usleep(250_000) } + if !reachable(element) { makeHittable(element) } + element.tap() + element.typeText(text) + dismissKeyboardTip() + } + + private func reachable(_ element: XCUIElement) -> Bool { + guard element.exists else { return false } + let frame = element.frame + let screen = app.windows.firstMatch.frame + guard !frame.isEmpty, screen.contains(CGPoint(x: frame.midX, y: frame.midY)) else { return false } + return element.isHittable + } + + private func makeHittable(_ element: XCUIElement) { + dismissSavePasswordNow() + if reachable(element) { return } + if app.keyboards.count > 0 { + for key in ["Return", "return", "Done", "done"] where app.keyboards.buttons[key].exists { + app.keyboards.buttons[key].tap() + break + } + } + for _ in 0..<4 where !reachable(element) { + app.swipeUp() + } + } + + private func onScreen(_ query: XCUIElementQuery) -> XCUIElement { + _ = query.firstMatch.waitForExistence(timeout: 20) + return query.allElementsBoundByIndex.first { reachable($0) } ?? query.firstMatch + } + + private func scrolledTo(_ element: XCUIElement) -> XCUIElement { + for _ in 0..<6 where !element.waitForExistence(timeout: 2) { + app.swipeUp() + } + return element + } + + private func dismissKeyboardTip() { + let tip = app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH %@", "Speed up your typing")).firstMatch + if tip.exists, app.buttons["Continue"].exists { app.buttons["Continue"].tap() } + } + + private func tap(_ element: XCUIElement, timeout: TimeInterval = 20) { + XCTAssertTrue(element.waitForExistence(timeout: timeout), "\(element) is missing") + dismissKeyboardTip() + dismissSavePasswordNow() + let deadline = Date().addingTimeInterval(5) + while !reachable(element) && Date() < deadline { usleep(250_000) } + if !reachable(element) { makeHittable(element) } + element.tap() + } + + private func text(_ label: String) -> XCUIElement { + app.descendants(matching: .any).matching(NSPredicate(format: "label == %@", label)).firstMatch + } + + private func declineSavePassword() { + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + for owner in [app!, springboard] { + let notNow = owner.buttons["Not Now"] + if notNow.waitForExistence(timeout: 3) { + notNow.tap() + return + } + } + } + + private func dismissSavePasswordNow() { + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + for owner in [app!, springboard] where owner.buttons["Not Now"].exists { + owner.buttons["Not Now"].tap() + return + } + } + + private func back() { + let bar = app.navigationBars.firstMatch + for candidate in [bar.buttons["BackButton"], bar.buttons["Back"], bar.buttons["Vault"]] where candidate.exists { + candidate.tap() + return + } + bar.buttons.element(boundBy: 0).tap() + } +} + +/// Audit issues the test leaves out, each a documented false positive with +/// its reason. Real issues are fixed in the app, never listed here. +enum A11yExclusions { + static func reason(for issue: XCUIAccessibilityAuditIssue, bars: [CGRect], rowUnderBar: Bool, screenshot: XCUIScreenshot) -> String? { + let element = issue.element.flatMap { $0.exists ? $0 : nil } + let type = element?.elementType + let label = element?.label ?? "" + // A button that is off until its field is filled in: WCAG 1.4.3 + // exempts an inactive user interface component from the contrast rule. + if issue.auditType == .contrast, let element, !element.isEnabled { + return "an inactive control, exempt from WCAG 1.4.3" + } + // Text scrolled under the navigation bar or the tab bar: iOS 26 fades + // it on purpose (the scroll edge effect). Scrolled into view it has its + // full contrast. 24 pt around each bar is the width of the fade. + if issue.auditType == .contrast, let frame = element?.frame, + bars.contains(where: { $0.insetBy(dx: 0, dy: -24).intersects(frame) }) { + return "under the system scroll edge effect of a bar" + } + // A contrast finding on an element whose pixels, measured in the + // screenshot of the same screen, pass WCAG AA: the text colour against + // the background is 4.5:1 or more. Seen on the Edit button, drawn in + // Keepiq's tint on white (7.1:1) like the Move button below it. + if issue.auditType == .contrast, let frame = element?.frame, + let measured = measuredContrast(in: frame, of: screenshot), measured >= 4.5 { + return String(format: "measured %.1f:1 in the screenshot, at least 4.5:1", measured) + } + // A contrast finding the audit cannot tie to an element, on a screen + // where a list row is half under a bar: the faded row has no element + // of its own any more. Seen on the vault list, with the last row under + // the tab bar. Any finding with an element is still counted. + if issue.auditType == .contrast, element == nil, rowUnderBar { + return "a row half under a bar, faded by the scroll edge effect" + } + // A single-line text field scrolls its text sideways, so it never + // hides what was typed. The audit flags every single-line field at + // the largest sizes; the field itself grows with Dynamic Type. + if issue.auditType == .textClipped, let type, [.textField, .secureTextField, .searchField].contains(type) { + return "a single-line text field scrolls its text, it does not clip it" + } + // The item's own login or address, shown as the user saved it. + // VoiceOver reads it as it is; a different label would hide the value. + if issue.auditType == .sufficientElementDescription, label.contains("@") || label.contains("://") { + return "the label is the item's own login or address" + } + // Every Text in Keepiq uses a text style (no fixed point sizes), and + // the text grows with the setting. The audit reports SwiftUI text in + // lists and buttons as "partially unsupported" all the same. The full + // "unsupported" finding is not excluded. + if issue.auditType == .dynamicType, issue.compactDescription.contains("partially") { + return "SwiftUI text styles reported as partially unsupported; Keepiq uses no fixed font size" + } + return nil + } + + /// The contrast ratio between the background (the most frequent colour + /// inside `frame`) and the text (the most frequent colour clearly unlike + /// it), or nil when the frame holds no second colour. + static func measuredContrast(in frame: CGRect, of screenshot: XCUIScreenshot) -> Double? { + let image = screenshot.image + guard let cg = image.cgImage, image.size.width > 0 else { return nil } + let scale = CGFloat(cg.width) / image.size.width + let rect = CGRect(x: frame.minX * scale, y: frame.minY * scale, width: frame.width * scale, height: frame.height * scale).integral + guard let crop = cg.cropping(to: rect), crop.width > 0, crop.height > 0, + let space = CGColorSpace(name: CGColorSpace.sRGB) else { return nil } + let width = crop.width + let height = crop.height + var pixels = [UInt8](repeating: 0, count: width * height * 4) + let drawn: Bool = pixels.withUnsafeMutableBytes { buffer in + guard let context = CGContext(data: buffer.baseAddress, width: width, height: height, bitsPerComponent: 8, + bytesPerRow: width * 4, space: space, + bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue) else { return false } + context.draw(crop, in: CGRect(x: 0, y: 0, width: width, height: height)) + return true + } + guard drawn else { return nil } + var counts: [UInt32: Int] = [:] + for index in stride(from: 0, to: pixels.count, by: 4) { + let key = UInt32(pixels[index]) << 16 | UInt32(pixels[index + 1]) << 8 | UInt32(pixels[index + 2]) + counts[key, default: 0] += 1 + } + let ranked = counts.sorted { $0.value > $1.value }.map(\.key) + guard let background = ranked.first, + let text = ranked.dropFirst().first(where: { distance($0, background) > 96 }) else { return nil } + let lighter = max(luminance(text), luminance(background)) + let darker = min(luminance(text), luminance(background)) + return (lighter + 0.05) / (darker + 0.05) + } + + private static func channels(_ rgb: UInt32) -> [Double] { + [Double((rgb >> 16) & 0xFF), Double((rgb >> 8) & 0xFF), Double(rgb & 0xFF)] + } + + private static func distance(_ a: UInt32, _ b: UInt32) -> Double { + zip(channels(a), channels(b)).reduce(0) { $0 + abs($1.0 - $1.1) } + } + + /// WCAG relative luminance of an sRGB colour. + private static func luminance(_ rgb: UInt32) -> Double { + let linear = channels(rgb).map { value -> Double in + let c = value / 255 + return c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4) + } + return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2] + } +} diff --git a/mobile/shared/build.gradle.kts b/mobile/shared/build.gradle.kts index 76f5936d1..d56c14307 100644 --- a/mobile/shared/build.gradle.kts +++ b/mobile/shared/build.gradle.kts @@ -330,3 +330,11 @@ tasks.withType().configureEach { + testLogging { + events("passed", "skipped", "failed") + } +} diff --git a/openspec/changes/clients-mobile-apps/tasks.md b/openspec/changes/clients-mobile-apps/tasks.md index f124509d9..b32e0e88f 100644 --- a/openspec/changes/clients-mobile-apps/tasks.md +++ b/openspec/changes/clients-mobile-apps/tasks.md @@ -4,49 +4,58 @@ Each task names its check. iOS tasks need a macOS runner and an Apple developer ## 1. Shared core and crypto contract -- [ ] 1.1 Scaffold `mobile/` as a Gradle Kotlin Multiplatform project with `shared`, `android/app` and an Xcode project in `ios/` that links the `shared` framework. Verify: `./gradlew :shared:allTests` and an iOS simulator build run in CI. +- [x] 1.1 Scaffold `mobile/` as a Gradle Kotlin Multiplatform project with `shared`, `android/app` and an Xcode project in `ios/` that links the `shared` framework. Verify: `./gradlew :shared:allTests` and an iOS simulator build run in CI. Done: `mobile.yml` runs `:shared:allTests` on Linux, `:shared:iosSimulatorArm64Test` on macOS, and builds the iOS app and package for the simulator (https://github.com/ConductionNL/keepiq/actions/runs/37510691196). - [x] 1.2 Write the crypto test vectors from the web app's own modules into `tests/vectors/crypto/*.json`, with a script under `tests/vectors/`: envelope and password, field ciphertexts (empty, one chunk, several chunks, multibyte), Send with and without password, TOTP (SHA1, SHA256, SHA512; 6 and 8 digits), passkey assertion. Verify: a vitest test re-reads the vectors with `src/crypto/`. Done: `node tests/vectors/generate-crypto-vectors.mjs`; `tests/vitest/crypto-vectors.spec.js` passes 55 tests, and a control with the Argon2 stub fails the password Send. -- [ ] 1.3 Implement RSA-OAEP chunking, the version 1 envelope with PBKDF2, AES-GCM, Argon2id, TOTP and ES256 in `shared`, with `expect`/`actual` platform primitives. Verify: the vectors pass in `commonTest` on Android and iOS. -- [ ] 1.3.1 Argon2id on iOS: link the reference Argon2 C code (CC0) through cinterop, so a password Send can be created and opened on iOS. Until then the iOS actual throws "not available on this target" and the vector test asserts that refusal. Verify: the Send-with-password vector passes in `iosSimulatorArm64Test`. Built: the C code is compiled per target and bundled through cinterop; the C build matches the known-answer vector on Linux. Open until `iosSimulatorArm64Test` passes in CI. +- [x] 1.3 Implement RSA-OAEP chunking, the version 1 envelope with PBKDF2, AES-GCM, Argon2id, TOTP and ES256 in `shared`, with `expect`/`actual` platform primitives. Verify: the vectors pass in `commonTest` on Android and iOS. Done: `CryptoVectorsTest` passes on jvm and on the iOS simulator, each test named in the log (https://github.com/ConductionNL/keepiq/actions/runs/37510691196); the Android app runs the same core in the emulator tests. +- [x] 1.3.1 Argon2id on iOS: link the reference Argon2 C code (CC0) through cinterop, so a password Send can be created and opened on iOS. Until then the iOS actual throws "not available on this target" and the vector test asserts that refusal. Verify: the Send-with-password vector passes in `iosSimulatorArm64Test`. Built: the C code is compiled per target and bundled through cinterop; the C build matches the known-answer vector on Linux. Done: `CryptoVectorsTest.sendWithPasswordUnwrapsAndOpens` passes in `iosSimulatorArm64Test` (https://github.com/ConductionNL/keepiq/actions/runs/37510691196); the test has no refusal branch any more. - [x] 1.4 Add the reverse direction: the core writes ciphertext for vector inputs, and a vitest test decrypts it with `src/crypto/rsa.js` and `src/send/sendCrypto.js`. Verify: the vitest test in CI. Done: `KotlinVectorsWriterTest` writes `kotlin-output.json`; `tests/vitest/crypto-vectors-kotlin.spec.js` passes 5 tests on a fresh copy, and `mobile.yml` reruns it on the copy written in the same CI run. - [x] 1.5 Implement the API client (Ktor) with the extension's contract: Basic auth, `OCS-APIRequest`, no cookies, OCS status 400 or more is an error, https only. Verify: unit tests against recorded responses, including a 200 envelope with statuscode 403. Done: `KeepiqApiTest` (10 tests, MockEngine with recorded controller answers) on jvm, including HTTP 200 with statuscode 403, Set-Cookie never sent back, no redirects, http refused; a control that lifts the OCS check fails it. -- [ ] 1.6 Implement the SQLCipher store with a device-bound key, re-encrypted names, URLs and folders, and eviction on unpair, suite change and a 428 manifest. Verify: unit tests, plus a test that opens the database file without the key and finds no plaintext. Partly built: the store logic, sealed names, URLs and folder names, secure delete and eviction pass `VaultStoreTest` (5 tests) on jvm over plain SQLite. The SQLCipher driver and its Keystore-bound key (`openEncryptedDriver`) only compile in CI; the on-device test is open. -- [ ] 1.6.1 SQLCipher store on iOS: link SQLCipher for the SQLDelight native driver and keep its key under a non-biometric Keychain item. Until then iOS has no offline store and works online only. Verify: the store tests on the iOS simulator. +- [x] 1.6 Implement the SQLCipher store with a device-bound key, re-encrypted names, URLs and folders, and eviction on unpair, suite change and a 428 manifest. Verify: unit tests, plus a test that opens the database file without the key and finds no plaintext. Partly built: the store logic, sealed names, URLs and folder names, secure delete and eviction pass `VaultStoreTest` (5 tests) on jvm over plain SQLite. The SQLCipher driver and its Keystore-bound key (`openEncryptedDriver`) run on the emulator: `OfflineReadingTest` (API 34 and 28) reads the store file and its journal without the key, finds no SQLite header and none of the demo names, logins or addresses, and plain SQLite refuses the file (https://github.com/ConductionNL/keepiq/actions/runs/37510691479). iOS is task 1.6.1. +- [x] 1.6.1 SQLCipher store on iOS: link SQLCipher for the SQLDelight native driver and keep its key under a non-biometric Keychain item. Until then iOS has no offline store and works online only. Verify: the store tests on the iOS simulator. Done (#1184): `IosEncryptedStoreTest` passes 7 of 7 on the iOS simulator, and `OfflineUITests` reads the vault from the encrypted store with the replay cut off (https://github.com/ConductionNL/keepiq/actions/runs/37377649091). - [x] 1.7 Implement sync from the manifest, the freshness check and the `unlockKeyEpoch` comparison. Verify: unit tests for each trigger and for an epoch change. Done: `VaultSyncTest` (11 tests) on jvm covers start, foreground, timer, after-write, manual, an epoch change, a new suite, the two-factor block and offline caching off; a control without the epoch check fails 2 of them. ## 2. Pairing and unlock - [x] 2.1 Login Flow v2 pairing with the system browser, the 20-minute poll limit, and manual app-password entry as fallback. Verify: an end-to-end test on the Android emulator against a test server. Done: `LoginFlowV2Test` and `KeepiqClientTest` on jvm; `LiveServerTest` paired against the real capture server; `PairUnlockUnpairTest` and `ManualPairingAndBlockTest` run in `mobile-e2e.yml` on API 34 and 28. - [x] 2.2 Master-password unlock with `unlockBlocked` handling. Verify: emulator test for a wrong password and for the two-factor block. Done: both cases in `PairUnlockUnpairTest` and `ManualPairingAndBlockTest` (`mobile-e2e.yml`), and in the iOS UI tests against the replay. -- [ ] 2.3 Biometric unlock (Keychain with `.biometryCurrentSet`, AndroidKeyStore with invalidation on enrolment) and PIN unlock with Argon2id and 5 tries. Verify: instrumented tests on the emulator with a fake biometric; manual check on one iPhone and one Android phone. Partly done: PIN with 5 tries passes `KeepiqClientTest` and the emulator test. Biometric unlock is built on both platforms but has no emulator test (no enrolled biometric in CI) and no manual phone check yet. +- [ ] 2.3 Biometric unlock (Keychain with `.biometryCurrentSet`, AndroidKeyStore with invalidation on enrolment) and PIN unlock with Argon2id and 5 tries. Verify: instrumented tests on the emulator with a fake biometric; manual check on one iPhone and one Android phone. Partly done: PIN with 5 tries passes `KeepiqClientTest` and the emulator test. Biometric unlock is built on both platforms but has no emulator test (no enrolled biometric in CI) and no manual phone check yet. Open: an emulator test with an enrolled fake fingerprint. +- [ ] 2.3.1 Manual: biometric unlock on one iPhone and one Android phone, and that a new enrolment invalidates it. - [x] 2.4 Check what `extension#pair` records about the client. If the device list would call a phone a browser extension, open a backend change that adds a client kind. Verify: the device list on the test server after pairing a phone. Done: pair records nothing; the device list shows the Login Flow v2 app password named after the User-Agent, "Keepiq for Android (…)" or "Keepiq for iOS (…)". No backend change. The emulator test reads the list through occ. -- [ ] 2.5 Auto-lock with the idle choices, the organisation cap and device lock, shared with the iOS extension. Verify: unit tests for the timer and cap; manual device-lock check. Partly done: `IdlePolicyTest` covers the timer and cap; the emulator test locks on screen-off. Open: the manual iOS device-lock check, and sharing the lock state with the iOS extension (task 4.4). +- [ ] 2.5 Auto-lock with the idle choices, the organisation cap and device lock, shared with the iOS extension. Verify: unit tests for the timer and cap; manual device-lock check. Partly done: `IdlePolicyTest` covers the timer and cap; the emulator test locks on screen-off. Open: sharing the lock state with the iOS extension (task 4.4). +- [ ] 2.5.1 Manual: the device-lock check on an iPhone. - [x] 2.6 Unpair: unpair call, app-password revoke and local wipe. Verify: emulator test that the old app password gets 401 afterwards. Done: `KeepiqClientTest` (also offline), `LiveServerTest` and `PairUnlockUnpairTest` assert 401 for the old app password. ## 3. Vault, Send and generator screens -- [ ] 3.1 Compose and SwiftUI screens for accounts, the vault list, folders, search and item detail with reveal, copy and TOTP. Verify: UI tests on the emulator and simulator; accessibility check with TalkBack and VoiceOver on the item detail. Partly done: `VaultApp` and `VaultAppView` open after unlock, with a lock button and a route to the unlock and account settings; `VaultFlowsTest` (API 34 and 28) and `VaultFlowsUITests` (simulator, replay) cover the list, search, a folder, reveal, copy and the TOTP countdown over the seeded demo vault (`server.mjs seed`). Open: a TalkBack and VoiceOver pass on the item detail. +- [x] 3.1 Compose and SwiftUI screens for accounts, the vault list, folders, search and item detail with reveal, copy and TOTP. Verify: UI tests on the emulator and simulator; accessibility check with TalkBack and VoiceOver on the item detail. Partly done: `VaultApp` and `VaultAppView` open after unlock, with a lock button and a route to the unlock and account settings; `VaultFlowsTest` (API 34 and 28) and `VaultFlowsUITests` (simulator, replay) cover the list, search, a folder, reveal, copy and the TOTP countdown over the seeded demo vault (`server.mjs seed`). The automatic accessibility audits pass on the main screens: `AccessibilityAuditTest` (Accessibility Test Framework through Compose, API 34) and `AccessibilityAuditUITests` (`performAccessibilityAudit`, simulator) (https://github.com/ConductionNL/keepiq/actions/runs/37510691479). +- [ ] 3.1.1 Manual: a TalkBack pass and a VoiceOver pass on the item detail. - [x] 3.2 Create, edit, move and trash for items and folders, with type fields from `/api/v1/secret-types`. Verify: an emulator test that creates a login on the phone and reads it back through the web app's API in the same run. Done: `VaultFlowsTest` creates a login with a generated password on the emulator, reads it back through `/api/v1/secrets` and decrypts it to the same value, then edits and trashes it; `VaultFlowsUITests` does the same against the replay. `VaultRepositoryTest` covers the body shapes and folder writes. -- [ ] 3.2.1 iOS still offers "Save Password?" after saving a login in the item form, although its secret fields are typed as one-time codes. iOS recognises the form by its fields, not only by their content type. Find a way that stops the offer (a custom secure field instead of SecureField, or a form layout iOS does not read as a login), then make `VaultFlowsUITests.expectNoSavePasswordPrompt` fail again. Verify: the iOS e2e run with the strict check. -- [ ] 3.3 Sensitive clipboard with timeout. Verify: unit test of the timeout; manual check that the preview is hidden. Partly built: `SensitiveClipboardTest` (4) covers the timeout, restart and never; Android marks the clip `EXTRA_IS_SENSITIVE`, iOS writes it local only with an expiration date. `VaultFlowsTest` reads the copied password back from the clipboard, marked sensitive, on API 34 and 28, and the API 34 recording shows the system preview masked. Open: the same check on a real phone. -- [ ] 3.4 Offline reading with the last-synced note and refused edits. Verify: emulator test in airplane mode. Partly built: `VaultRepositoryTest` reads from the store offline with the sync time, refuses every edit before sending, and says a vault without a stored copy needs a connection. Open: the airplane-mode emulator test. +- [x] 3.2.1 iOS still offers "Save Password?" after saving a login in the item form, although its secret fields are typed as one-time codes. iOS recognises the form by its fields, not only by their content type. Find a way that stops the offer (a custom secure field instead of SecureField, or a form layout iOS does not read as a login), then make `VaultFlowsUITests.expectNoSavePasswordPrompt` fail again. Verify: the iOS e2e run with the strict check. Done (#1184): the item form no longer draws the offer, and `VaultFlowsUITests` passes with the strict check (https://github.com/ConductionNL/keepiq/actions/runs/37377649091). +- [x] 3.3 Sensitive clipboard with timeout. Verify: unit test of the timeout; manual check that the preview is hidden. Partly built: `SensitiveClipboardTest` (4) covers the timeout, restart and never; Android marks the clip `EXTRA_IS_SENSITIVE`, iOS writes it local only with an expiration date. `VaultFlowsTest` reads the copied password back from the clipboard, marked sensitive, on API 34 and 28, and the API 34 recording shows the system preview masked. The manual check on a real phone is 3.3.1. +- [ ] 3.3.1 Manual: the clipboard preview stays hidden on a real Android phone and iPhone. +- [x] 3.4 Offline reading with the last-synced note and refused edits. Verify: emulator test in airplane mode. Partly built: `VaultRepositoryTest` reads from the store offline with the sync time, refuses every edit before sending, and says a vault without a stored copy needs a connection. Done: `OfflineReadingTest` on API 34 and 28 (https://github.com/ConductionNL/keepiq/actions/runs/37510691479) syncs, turns off Wi-Fi, mobile data and (API 30 and later) airplane mode, checks that the server is out of reach, unlocks offline, shows the list with "You are offline. You see the copy from …", opens an item from the store, finds Edit, Move and Move to trash off and a trash call refused as offline, then syncs again once the network is back. On iOS (#1184), `OfflineUITests` opens the vault from the encrypted store while the replay drops every connection, with the note and refused edits (https://github.com/ConductionNL/keepiq/actions/runs/37377649091). - [x] 3.5 Generator with the organisation policy. Verify: unit tests sharing the web generator's policy cases. Done: `tests/vectors/generator/cases.json` (156 seeded cases written by the web module) passes `GeneratorVectorsTest` on jvm and `tests/vitest/generator-vectors.spec.js`; a control that changes one draw fails 8 cases. - [ ] 3.6 Send create, list and delete, the share sheet, and opening a Send link in the app. Verify: an emulator test that creates a Send and opens it in the web page. Partly done: `VaultFlowsTest` creates a Send on the emulator and opens its link in the app, which uses its one view; `VaultFlowsUITests` creates one against the replay; `SendServiceTest` and `SendLinkTest` cover the rest. Open: a password Send on iOS (needs task 1.3.1) and iOS universal links (needs an apple-app-site-association file per server). ## 4. System autofill -- [ ] 4.1 Android `AutofillService`: field detection, package and certificate matching, web domains, inline suggestions, the locked entry. Verify: instrumented tests with a test app and a WebView page; manual check in Chrome and Firefox. Partly done: `SystemAutofillTest` (`mobile-e2e.yml`, API 34 and 28) covers the locked entry without names, the unlock and pick, Digital Asset Links, a look-alike certificate never offered, the unlocked dropdown and a WebView's web domain; the matcher runs the extension's own cases (`tests/vectors/autofill`, Kotlin and vitest). Open: the manual check in Chrome and Firefox, and inline suggestions, which the emulator keyboard does not show (manual with Gboard or another inline keyboard). +- [x] 4.1 Android `AutofillService`: field detection, package and certificate matching, web domains, inline suggestions, the locked entry. Verify: instrumented tests with a test app and a WebView page; manual check in Chrome and Firefox. Partly done: `SystemAutofillTest` (`mobile-e2e.yml`, API 34 and 28) covers the locked entry without names, the unlock and pick, Digital Asset Links, a look-alike certificate never offered, the unlocked dropdown and a WebView's web domain; the matcher runs the extension's own cases (`tests/vectors/autofill`, Kotlin and vitest). The manual checks are 4.1.1. +- [ ] 4.1.1 Manual: autofill in Chrome and Firefox on a phone, and inline suggestions with Gboard or another inline keyboard. - [x] 4.2 Android save requests with the never-save list. Verify: instrumented test of a sign-up form. Done: `SystemAutofillTest` saves a sign-up and reads it back from the server, then "Never" puts the app on the list and no offer follows; `AutofillSaverTest` (6) covers save, update and the use-only refusal. - [x] 4.3 One-time codes on Android. Verify: instrumented test with a two-step form. Done: `SystemAutofillTest` fills the code field on the step after the login with the item's current TOTP. -- [ ] 4.4 iOS AutoFill credential provider extension: credential list, fill without interaction when unlocked, unlock screen, identity store upkeep. Verify: UI test on the simulator with a test app and an associated domain. Partly done: the `KeepiqAutofill` extension is built; `AutofillUITests` runs its screens inside the app against the replay (unlock in the sheet, "Add login", the per-site files and identities rebuilt, a second fill without unlock). `mobile/e2e/ios-run.sh` runs it since group 5 (it did not before). Open: enabling the provider in Settings cannot be automated, so the QuickType bar in Safari and an app with an associated domain stay a manual check, and so does Face ID in the sheet. +- [x] 4.4 iOS AutoFill credential provider extension: credential list, fill without interaction when unlocked, unlock screen, identity store upkeep. Verify: UI test on the simulator with a test app and an associated domain. Partly done: the `KeepiqAutofill` extension is built; `AutofillUITests` runs its screens inside the app against the replay (unlock in the sheet, "Add login", the per-site files and identities rebuilt, a second fill without unlock). `mobile/e2e/ios-run.sh` runs it since group 5 (it did not before). The checks that need the provider enabled in Settings are 4.4.1. +- [ ] 4.4.1 Manual, with the signed build: enable the provider in Settings, fill from the QuickType bar in Safari and in an app with an associated domain, and unlock with Face ID in the sheet. - [ ] 4.5 One-time codes on iOS: the clipboard path on iOS 17, and the iOS 18 code credential if adopted. Verify: simulator test. Partly done: after a password fill the site's current code goes on the clipboard, local only, for 60 seconds. The iOS 18 `ASOneTimeCodeCredential` is not adopted in v1. Open: a simulator test with an authenticator item. - [x] 4.6 Rebuild and clear the autofill index on sync, unpair and suite change. Verify: unit tests, plus an emulator test that a deleted item stops being offered. Done: `AutofillIndexTest` covers rebuild on every refresh, memory only without offline caching, and clearing on each locking sync; `SystemAutofillTest` trashes a login on the server, syncs, finds it no longer offered, and clears the index on unpair. ## 5. Passkeys -- [ ] 5.1 Android 14+ `CredentialProviderService` for passkeys and passwords, without `credentials-play-services-auth`. Verify: instrumented test on an API 34 emulator against a WebAuthn test page; dependency check. Built: `KeepiqCredentialProviderService` (androidx.credentials 1.3.0) with `CredentialProviderActivity`; the caller from `CallingAppInfo` (a browser's origin only on the privileged allowlist, an app's rpId through Digital Asset Links); "Unlock Keepiq" while locked; nothing for another algorithm. `PasskeyProviderTest` (`mobile-e2e.yml`, API 34) asks from the test APK's own Credential Manager client instead of a page: RS256 only gets nothing, create, sign in verified with the attested key, counter 4 signs as 5 and the item stores 5, locked then unlocked, an unlisted rpId gets nothing. `mobile.yml` fails on `credentials-play-services-auth`. Passed on the API 34 emulator (run 37296407888, screenshots and video in `mobile-media-android`). Open: a browser ceremony (the allowlist path), manual. -- [ ] 5.2 iOS 17+ passkey registration and assertion in the AutoFill extension. Verify: simulator test against a WebAuthn test page. Built: the extension answers passkey assertion and registration after its own unlock, signs with the core's ES256 over the clientDataHash iOS hands over, declines without ES256, and keeps `ASPasskeyCredentialIdentity` entries in the identity store. `PasskeyUITests` runs the extension's passkey screens and calls inside the app against the replay (create, attestation checked, index and identity rebuilt, sign in without a new unlock, signature verified). Runs in `mobile/e2e/ios-run.sh`. Open: enabling the provider in Settings and a ceremony in Safari or an app with an associated domain need the signed build. -- [ ] 5.3 Cross-client check: a passkey created with the browser extension signs in on the phone, and one created on the phone signs in with the extension. Verify: a manual run recorded in the PR, plus the passkey vector in 1.2. Automated: `passkey.json` gains a registration made by the extension's `createCredential`; `PasskeyCreateTest` rebuilds its attestation byte for byte and signs with its key. `KotlinVectorsWriterTest` writes a core-made passkey, and `crypto-vectors-kotlin.spec.js` signs with it through the extension's `getAssertion` and verifies with the key from its attestation, and verifies what the core signed with the extension's passkey. Open: the manual run between the extension and a phone. +- [x] 5.1 Android 14+ `CredentialProviderService` for passkeys and passwords, without `credentials-play-services-auth`. Verify: instrumented test on an API 34 emulator against a WebAuthn test page; dependency check. Built: `KeepiqCredentialProviderService` (androidx.credentials 1.3.0) with `CredentialProviderActivity`; the caller from `CallingAppInfo` (a browser's origin only on the privileged allowlist, an app's rpId through Digital Asset Links); "Unlock Keepiq" while locked; nothing for another algorithm. `PasskeyProviderTest` (`mobile-e2e.yml`, API 34) asks from the test APK's own Credential Manager client instead of a page: RS256 only gets nothing, create, sign in verified with the attested key, counter 4 signs as 5 and the item stores 5, locked then unlocked, an unlisted rpId gets nothing. `mobile.yml` fails on `credentials-play-services-auth`. Passed on the API 34 emulator (run 37296407888, screenshots and video in `mobile-media-android`). The browser ceremony is 5.1.1. +- [ ] 5.1.1 Manual: a passkey ceremony in a browser on Android 14 (the privileged allowlist path). +- [x] 5.2 iOS 17+ passkey registration and assertion in the AutoFill extension. Verify: simulator test against a WebAuthn test page. Built: the extension answers passkey assertion and registration after its own unlock, signs with the core's ES256 over the clientDataHash iOS hands over, declines without ES256, and keeps `ASPasskeyCredentialIdentity` entries in the identity store. `PasskeyUITests` runs the extension's passkey screens and calls inside the app against the replay (create, attestation checked, index and identity rebuilt, sign in without a new unlock, signature verified). Runs in `mobile/e2e/ios-run.sh`. The ceremony with the provider enabled is 5.2.1. +- [ ] 5.2.1 Manual, with the signed build: enable the provider in Settings and run a passkey ceremony in Safari or an app with an associated domain. +- [x] 5.3 Cross-client check: a passkey created with the browser extension signs in on the phone, and one created on the phone signs in with the extension. Verify: a manual run recorded in the PR, plus the passkey vector in 1.2. Automated: `passkey.json` gains a registration made by the extension's `createCredential`; `PasskeyCreateTest` rebuilds its attestation byte for byte and signs with its key. `KotlinVectorsWriterTest` writes a core-made passkey, and `crypto-vectors-kotlin.spec.js` signs with it through the extension's `getAssertion` and verifies with the key from its attestation, and verifies what the core signed with the extension's passkey. The manual run is 5.3.1. +- [ ] 5.3.1 Manual: a passkey made with the browser extension signs in on a phone, and one made on a phone signs in with the extension, recorded in a PR. ## 6. Release @@ -54,7 +63,7 @@ Each task names its check. iOS tasks need a macOS runner and an Apple developer - [x] 6.2 Reproducible Android build: pinned toolchain, Gradle dependency verification, no timestamps. Verify: two clean CI builds give byte-identical unsigned APKs. Done (#1179): Temurin 21.0.8, build tools 35.0.0, `vcsInfo.include = false`; two jobs in clean checkouts at different paths built the same unsigned fdroid release APK, SHA-256 3690558680f6…30ab, `cmp` identical (run https://github.com/ConductionNL/keepiq/actions/runs/37354802965). - [x] 6.3 Store metadata in `mobile/fastlane/metadata/` in English and Dutch, with screenshots and the privacy text. Load the `writing` skill first. Verify: `fastlane` metadata check; review by the product owner. Draft (#1179): title, short and full description, changelog 1 in `en-US` and `nl-NL`, eight screenshots; privacy in the full description and `docs/mobile/privacy.md`. Open: the product owner's review, a `fastlane` check, Play-ratio screenshots and a launcher icon. - Icon and Play-sized screenshots in (feat/mobile-store-assets): an adaptive launcher icon with a monochrome layer and legacy mipmaps, a 512 px store `icon.png` in `en-US` and `nl-NL`, an iOS `AppIcon` catalog at 1024 px, and the eight phone screenshots padded to 1.98:1. Still open: the product owner's review and a `fastlane` check. -- [x] 6.4 Release workflow on `mobile-v*` tags: signed Play bundle to the internal track, signed iOS archive to TestFlight. Verify: one dry-run tag on a fork or a test track. Done for the preview only (, approved by the product owner 2026-10-04): `.github/workflows/mobile-preview.yml` builds the release APK on a `mobile-v-preview.` tag, signs it with a preview key from GitHub secrets (`KEEPIQ_PREVIEW_*`, not the store key), checks it and the published certificate fingerprint with apksigner, and publishes a GitHub pre-release with the APK and its SHA-256. A pull request runs the same build signed with a throwaway key. There are no flavours yet (6.1), so it builds the plain release variant. Open: the Play internal track, TestFlight (no Apple account yet) and the store signing keys. +- [x] 6.4 Release workflow on `mobile-v*` tags: signed Play bundle to the internal track, signed iOS archive to TestFlight. Verify: one dry-run tag on a fork or a test track. Done for the preview only (approved by the product owner 2026-10-04): `.github/workflows/mobile-preview.yml` builds the release APK on a `mobile-v-preview.` tag, signs it with a preview key from GitHub secrets (`KEEPIQ_PREVIEW_*`, not the store key), checks it and the published certificate fingerprint with apksigner, and publishes a GitHub pre-release with the APK and its SHA-256. A pull request runs the same build signed with a throwaway key. Since #1179 it builds the `fdroid` flavour (6.1), which has no store dependency. Open: the Play internal track, TestFlight (no Apple account yet) and the store signing keys. - [ ] 6.5 Merge request to `fdroiddata` with `metadata/nl.conduction.keepiq.yml`. Show the product owner the exact text before posting. Verify: F-Droid's `fdroid lint` and `fdroid build` in their CI. - [ ] 6.6 Register the app ids and the store listings under the Conduction developer accounts. This needs the product owner. Verify: the listings exist on the test tracks. diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 01ed4a56a..4ce101b10 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -7576,12 +7576,42 @@ "hashicorp-vault": "no", "nextcloud-passwords": "partial", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/clients-mobile-apps on 2026-10-04. Before: no iOS or Android project in the repo (src, browser-extension, cli only), and openspec/specs/mobile-pwa/spec.md ruled out a native app; the PWA cannot register as a system autofill provider", + "state": "building", + "evidence": "mobile/shared (Kotlin Multiplatform core: crypto, API client, SQLCipher store, sync), mobile/android/app (Compose app, android/autofill/KeepiqAutofillService.kt, android/passkey/KeepiqCredentialProviderService.kt) and mobile/ios (SwiftUI app, KeepiqAutofill/CredentialProviderViewController.swift); merged in #1154, #1155, #1159, #1160, #1164, #1170 and #1179; emulator and simulator tests in .github/workflows/mobile-e2e.yml (mobile/e2e/android-run.sh, ios-run.sh); Android preview APK published as GitHub pre-release mobile-v0.1.0-preview.2 (2026-10-05)", "owner": "ConductionNL/keepiq", - "reachedOn": "none", - "note": "Decision reversed by the product owner on 2026-10-04: native iOS (17+) and Android (9+) apps on a shared Kotlin Multiplatform core, with system autofill, passkeys (iOS 17, Android 14), the vault, Send and the generator, published on the App Store, Google Play and F-Droid. Was decided-no on the mobile-pwa record of 2026-09-27.", - "change": "openspec/changes/clients-mobile-apps" + "reachedOn": "Android: install the preview APK from the mobile-v0.1.0-preview.2 release, connect to the Nextcloud server, unlock -> vault list; Settings -> Autofill service -> Keepiq", + "note": "Decision reversed by the product owner on 2026-10-04 (was decided-no on the mobile-pwa record of 2026-09-27). Built and tested in CI: the shared core, the Android app with system autofill and passkeys (Android 14), the iOS app and its AutoFill extension against a replay, offline reading and automatic accessibility audits. Open: the App Store, Google Play and F-Droid listings (6.5, 6.6), a signed iOS build and TestFlight (no Apple account yet), the iOS offline store (1.6.1), and the manual checks: biometrics, clipboard preview and device lock on real phones, browser passkey ceremonies, the extension-to-phone passkey run, and TalkBack and VoiceOver.", + "change": "openspec/changes/clients-mobile-apps", + "prs": [ + { + "repo": "ConductionNL/keepiq", + "number": 1154 + }, + { + "repo": "ConductionNL/keepiq", + "number": 1155 + }, + { + "repo": "ConductionNL/keepiq", + "number": 1159 + }, + { + "repo": "ConductionNL/keepiq", + "number": 1160 + }, + { + "repo": "ConductionNL/keepiq", + "number": 1164 + }, + { + "repo": "ConductionNL/keepiq", + "number": 1170 + }, + { + "repo": "ConductionNL/keepiq", + "number": 1179 + } + ] }, "rowSource": "competitor", "provider": "keepiq", From ea2b67924d42d2f2dfa3e83e89cc3bf49f57b99b Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 7 Oct 2026 21:15:20 +0200 Subject: [PATCH 002/167] docs(openspec): spec vault events as flow triggers, fix links to archived changes Step 1: clients-15 gets openspec/changes/clients-vault-event-flow-triggers. Step 2: twelve unbuilt rows pointed at changes that were archived on 2026-10-04; their links now name the archive path. --- .../design.md | 48 +++++++++++++++ .../proposal.md | 42 +++++++++++++ .../specs/vault-event-flow-triggers/spec.md | 59 +++++++++++++++++++ .../tasks.md | 31 ++++++++++ openspec/parity/capabilities.json | 27 +++++---- 5 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 openspec/changes/clients-vault-event-flow-triggers/design.md create mode 100644 openspec/changes/clients-vault-event-flow-triggers/proposal.md create mode 100644 openspec/changes/clients-vault-event-flow-triggers/specs/vault-event-flow-triggers/spec.md create mode 100644 openspec/changes/clients-vault-event-flow-triggers/tasks.md diff --git a/openspec/changes/clients-vault-event-flow-triggers/design.md b/openspec/changes/clients-vault-event-flow-triggers/design.md new file mode 100644 index 000000000..1acacd2a3 --- /dev/null +++ b/openspec/changes/clients-vault-event-flow-triggers/design.md @@ -0,0 +1,48 @@ +# Design: start a flow when something happens in the vault + +## Context + +At development `7c107990`: + +- `lib/AppInfo/AuditStreamEventRegistrar.php:74,83` binds the audit log writer and `SiemForwardListener` to `AuditEvent`. The SIEM listener is fail-soft: a failure is logged and never breaks the audited action (`lib/Listener/SiemForwardListener.php:64-83`). +- `lib/Service/SiemService.php:112` `buildPayload()` turns an `AuditEvent` into a metadata-only array, or null when the event is not forwarded. +- `lib/Event/Audit/AuditEventTypes.php` holds the event type constants (`secret.created`, `share.granted`, `link_share.accessed`, `emergency_access.requested`, `application.registered`, `vault.exported`, and so on). +- OpenRegister `lib/Service/Flow/FlowTriggerService.php:86` `fire(string $event, array $subject, ?string $user, array $context)` queues a run for every flow wired to the event and never throws into the caller. `NativeFlowTriggerListener` calls it with an empty subject and `['payload' => …]` as context for file, user and share events. +- OpenRegister `EventCatalogService::CATALOG` is a constant. `aliasesFor()` returns an unknown id as itself, so a flow stored with a `keepiq.*` trigger matches when that id is fired. +- OpenRegister `RegisterFlowNodesEvent` is the existing pattern for apps contributing to the flow engine (node types). +- ADR-006: OpenRegister is optional; the Flows page is shown only when it is installed (`src/manifest.json:213`). + +## Screens + +- **Flow list and editor**: the shared library pages, drawn as `PqFlows` and `PqFlow` on the canvas (5NkFW28vZUUij43xzxHg5a). The inventory (`inventory-keepiq.md`) records Flows as a page every app has, not a Keepiq board. A Keepiq flow shows its trigger in the Trigger column of `PqFlows` and in the header line of `PqFlow` ("trigger A share is granted · 3 steps"), with the label this change contributes. +- **The admin switch**: `KqBeheerAudit`, the audit area of Keepiq's admin section, next to the SIEM sinks that read the same events. A section "Flows" with one checkbox per trigger group and the sentence "Flows receive who did what and when, never a secret." + +## Goals / Non-Goals + +**Goals** +- Any vault event that is audited can start a flow, with the same metadata the SIEM gets. +- An administrator decides which groups of events may do so. + +**Non-Goals** +- A Keepiq flow engine or flow page. OpenRegister's engine is the only one (hydra ADR-110). +- Flow steps that read or write secrets. A step that needs a secret value cannot exist under zero knowledge. +- Per-user flows on a personal vault. Flows are an administrator tool. + +## Decisions + +**D1. Reuse the SIEM payload.** The listener calls `SiemService::buildPayload()` and passes the result as `context.payload`. Alternative: a second, flow-specific payload. Rejected: two payload builders drift, and the SIEM one is already tested to carry no secret material. + +**D2. Trigger ids are `keepiq.` plus the audit type.** `share.granted` fires as `keepiq.share.granted`. The prefix keeps them apart from OpenRegister's own `share.created`. Labels come from a Keepiq catalog class in plain sentences ("A share is granted"). + +**D3. Subject is empty.** Vault items are not OpenRegister objects, so `fire()` gets `subject: []`, like the native file and user triggers. The item id is in the payload. + +**D4. Off by default, per group.** App config `flow_trigger_groups` holds the enabled groups; empty means nothing fires. An administrator turns groups on in KqBeheerAudit. `secret.read` is in its own group, because it fires on every reveal and can flood the queue. + +**D5. Guarded by app state.** The listener and the catalog contribution are registered only when `IAppManager::isEnabledForUser('openregister')` is true at boot, and the listener checks again before firing. No `OCA\OpenRegister\…` type appears in a Keepiq signature; the service is fetched from the container by class name string inside a try block. + +**D6. Catalog extension lives in OpenRegister.** OpenRegister adds `RegisterFlowTriggersEvent`, dispatched when the catalog is built, with `addTrigger(id, label, group)`. Keepiq listens and contributes its entries. Until that lands, D2 ids still fire and match stored flows. + +## Risks + +- **Queue load.** A busy vault fires many events. Mitigation: off by default, `secret.read` separate, and OpenRegister drops events with no wired flow before queueing (`flowsForTrigger()` returns empty). +- **Information in flows.** A flow step can e-mail the payload. The payload holds user ids and item ids. Mitigation: the admin section says so, and the payload is the same one already sent to a SIEM. diff --git a/openspec/changes/clients-vault-event-flow-triggers/proposal.md b/openspec/changes/clients-vault-event-flow-triggers/proposal.md new file mode 100644 index 000000000..e818ceef7 --- /dev/null +++ b/openspec/changes/clients-vault-event-flow-triggers/proposal.md @@ -0,0 +1,42 @@ +--- +kind: code +--- + +# Start a flow when something happens in the vault + +## Why + +The Flows menu item opens OpenRegister's flow list and canvas (`src/manifest.json:207`, page `Flows` at `:658`), shown only when OpenRegister is installed. A flow there can start on an OpenRegister object, a file, a user, a share, a tag, a schedule or a manual run (`openregister lib/Service/Flow/EventCatalogService.php`). Keepiq keeps its vault in its own tables (ADR-001, ADR-004) and fires nothing into that engine. So an administrator can open the builder, but no vault event can start a flow: a share that is granted, a link that is opened, an emergency access request, an application that waits for approval. + +Every one of those events already exists. Keepiq dispatches a typed `AuditEvent` for each of them (`lib/Event/Audit/AuditEventTypes.php`), and the SIEM export already turns it into a payload without secret material (`lib/Service/SiemService.php::buildPayload()`). This change hands that same payload to the flow engine. + +### Matrix rows (keepiq `openspec/parity/capabilities.json`) + +| row | capability | Keepiq today | +|---|---|---| +| `clients-15` | Automate follow-up work on vault events with a flow builder. | `no`: the Flows page is reachable, but no vault event can start a flow | + +## What Changes + +- **Vault events become flow triggers.** When OpenRegister is enabled, a new listener on `AuditEvent` fires `keepiq.` into OpenRegister's `FlowTriggerService::fire()`. The payload is the SIEM payload: who, what kind of event, which item id, when. No secret value, name, username or address is in it. +- **The builder lists the vault triggers.** Keepiq contributes its trigger ids, labels and a `Vault` group to the flow builder's trigger palette, so an administrator picks "A share is granted" instead of typing an id. +- **An administrator chooses which events may start flows.** A setting in the Keepiq admin section lists the trigger groups (items, sharing, links, requests, emergency access, applications, keys) and starts with all of them off. +- **Keepiq stays usable alone (ADR-006).** Without OpenRegister nothing is registered and nothing is fired. No Keepiq class references an `OCA\OpenRegister\…` class unless OpenRegister is running. + +## Capabilities + +### New Capabilities + +- `vault-event-flow-triggers`: vault events start OpenRegister flows with a metadata-only payload. + +### Modified Capabilities + +- None. `siem-audit-export` keeps its payload contract; this change reads it and does not change it. + +## Impact + +- **Backend**: `lib/Listener/FlowTriggerForwardListener.php`, `lib/Service/Flow/VaultFlowTriggerCatalog.php`, a binding in `lib/AppInfo/AuditStreamEventRegistrar.php`, one admin setting. +- **Frontend**: one section in the Keepiq admin settings (KqBeheerAudit). The flow list and canvas stay OpenRegister's. +- **Database**: none. The setting is an app config value. +- **Security**: the payload is the SIEM payload, which a test already pins as free of secret material. A flow cannot read a secret, because the server never holds one in plain text. +- **Cross-app**: OpenRegister needs one extension point: a way for an app to add entries to the trigger catalog. Today the catalog is a constant (`EventCatalogService::CATALOG`). This is an OpenRegister task, tracked below; until it lands the triggers still fire (an unknown id matches only itself in `aliasesFor()`), but the palette does not list them. diff --git a/openspec/changes/clients-vault-event-flow-triggers/specs/vault-event-flow-triggers/spec.md b/openspec/changes/clients-vault-event-flow-triggers/specs/vault-event-flow-triggers/spec.md new file mode 100644 index 000000000..d3e5d46a3 --- /dev/null +++ b/openspec/changes/clients-vault-event-flow-triggers/specs/vault-event-flow-triggers/spec.md @@ -0,0 +1,59 @@ +## ADDED Requirements + +### Requirement: Vault events fire flow triggers + +When OpenRegister is enabled and an administrator has enabled the event's trigger group, the system MUST fire every audited vault event into OpenRegister's flow trigger service under the id `keepiq.`, with an empty subject and the SIEM payload of that event as `context.payload`. The payload MUST NOT contain a secret value, a decrypted name, a username or an address. A failure to fire MUST be logged and MUST NOT break the audited action. + +#### Scenario: A granted share starts a flow + +- **GIVEN** OpenRegister is enabled and the sharing group is enabled in Keepiq's admin settings +- **AND** a published flow has the trigger `keepiq.share.granted` +- **WHEN** a vault user shares a login with a colleague +- **THEN** OpenRegister queues one run of that flow +- **AND** the run's payload carries the acting user, the recipient, the item id and the time +- **AND** the payload carries no secret value and no item name + +#### Scenario: A broken flow engine does not block a share + +- **GIVEN** OpenRegister is enabled and its flow trigger service throws +- **WHEN** a vault user shares a login with a colleague +- **THEN** the share is saved +- **AND** the failure is written to the Nextcloud log + +### Requirement: Administrators choose which event groups start flows + +The system MUST offer administrators a Flows section in the Keepiq audit administration (board KqBeheerAudit) with one switch per trigger group: items, item reads, sharing, links, requests, emergency access, applications and keys. Every group MUST start switched off. An event in a group that is switched off MUST NOT be fired. The section MUST state that flows receive who did what and when, and never a secret. + +#### Scenario: A fresh install fires nothing + +- **GIVEN** a fresh Keepiq install with OpenRegister enabled +- **WHEN** a vault user creates a login +- **THEN** no `keepiq.*` trigger is fired + +#### Scenario: An administrator enables the emergency access group + +- **GIVEN** an administrator on the Keepiq audit administration +- **WHEN** the administrator switches on Emergency access and saves +- **THEN** the next emergency access request fires `keepiq.emergency_access.requested` +- **AND** a login created afterwards still fires nothing + +### Requirement: The flow builder lists vault triggers + +When OpenRegister is enabled, the system MUST contribute every Keepiq trigger id to OpenRegister's flow trigger catalog with a plain-sentence label and the group `Vault`, so the trigger palette of the flow builder shows them. + +#### Scenario: An administrator picks a vault trigger + +- **GIVEN** OpenRegister is enabled and offers the trigger catalog extension +- **WHEN** an administrator opens a new flow on the Flows page and opens the trigger palette +- **THEN** the palette shows a Vault group with "A share is granted" + +### Requirement: Keepiq runs without OpenRegister + +When OpenRegister is not installed or not enabled, the system MUST NOT register the flow trigger listener or the catalog contribution, MUST NOT load any `OCA\OpenRegister\…` class, and MUST NOT show the Flows section in the admin settings. + +#### Scenario: A vault on a plain Nextcloud + +- **GIVEN** a Nextcloud instance without OpenRegister +- **WHEN** a vault user shares a login +- **THEN** the share is saved and nothing is logged about flows +- **AND** the Keepiq audit administration shows no Flows section diff --git a/openspec/changes/clients-vault-event-flow-triggers/tasks.md b/openspec/changes/clients-vault-event-flow-triggers/tasks.md new file mode 100644 index 000000000..270f2fa52 --- /dev/null +++ b/openspec/changes/clients-vault-event-flow-triggers/tasks.md @@ -0,0 +1,31 @@ +# Tasks: start a flow when something happens in the vault + +## 0. OpenRegister prerequisite (owned by ConductionNL/openregister) + +- [ ] 0.1 Open an OpenRegister change for `RegisterFlowTriggersEvent`: dispatched when `EventCatalogService` builds its catalog, carrying `addTrigger(string $id, string $label, string $group)`. `getCatalog()` and `knownTriggerIds()` return the constant entries plus the contributed ones. Verify: an OpenRegister unit test that a contributed trigger shows in `GET /api/flow/event-catalog`. + +## 1. Fire vault events + +- [ ] 1.1 Add `lib/Service/Flow/VaultFlowTriggerCatalog.php`: maps each `AuditEventTypes` constant to a trigger id (`keepiq.` + type), a label and a group (items, item reads, sharing, links, requests, emergency access, applications, keys). Verify: unit test that every constant in `AuditEventTypes` has exactly one entry. +- [ ] 1.2 Add `lib/Listener/FlowTriggerForwardListener.php` on `AuditEvent`: skip when OpenRegister is not enabled or the group is off; build the payload with `SiemService::buildPayload()`; resolve `OCA\OpenRegister\Service\Flow\FlowTriggerService` from the container by string; call `fire(event, [], user, ['payload' => $payload])`; catch and log every `Throwable`. Verify: unit test with the real `AuditEvent` class, a stub trigger service, and assertions for group off, OpenRegister off, and a throwing service. +- [ ] 1.3 Bind the listener in `lib/AppInfo/AuditStreamEventRegistrar.php` next to `SiemForwardListener`. Verify: a test that asserts the binding from the registrar, not from the listener. +- [ ] 1.4 Listen for `RegisterFlowTriggersEvent` (string class name, guarded by `class_exists`) and add the catalog entries with group `Vault`. Verify: unit test with a fake registry. + +## 2. Admin setting + +- [ ] 2.1 Store enabled groups in app config `flow_trigger_groups` (JSON array, default empty); expose read and write on the existing admin settings endpoint, admin only. Verify: controller test that a non-admin gets 403 and an admin round-trips the value. +- [ ] 2.2 Add a Flows section to the audit admin page (board KqBeheerAudit): one checkbox per group, the sentence "Flows receive who did what and when, never a secret.", and the section hidden when OpenRegister is not enabled. Verify: vitest for hidden and shown, and a Playwright check on the admin page. + +## 3. End to end + +- [ ] 3.1 With OpenRegister enabled, publish a flow on `keepiq.share.granted`, enable the sharing group, share a login, and assert one run in the flow's run list with a payload free of secret material. Verify: Playwright or Newman against the dev stack. + +## 4. Docs + +- [ ] 4.1 Document the trigger list, the groups and the payload in `docs/flows.md`. Verify: docs build. + +## Acceptance criteria + +- An administrator can start a flow on any audited vault event, per enabled group. +- The flow receives the SIEM payload and no secret material. +- Keepiq works unchanged on a Nextcloud without OpenRegister. diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 4ce101b10..9b9c5150e 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -1778,7 +1778,7 @@ "evidence": "Specified in openspec/changes/crypto-organisation-account-recovery on 2026-09-27. Before: src/components/settings/AdminSuiteSection.vue:21,180 admin can only force-revoke a suite so the user starts a new empty vault; grep -rni 'escrow\\|recovery key\\|account recovery' lib: only emergency-access envelopes escrowed to a contact's certificate, never to an admin", "owner": "ConductionNL/keepiq", "note": "By zero-knowledge design an administrator cannot restore access to a user's secrets. The admin can force-revoke the locked suite so the user can set up a fresh vault, but the old secrets stay unreadable unless the user has an emergency contact or a backup file.", - "change": "openspec/changes/crypto-organisation-account-recovery" + "change": "openspec/changes/archive/2026-10-04-crypto-organisation-account-recovery" }, "rowSource": "competitor", "provider": "keepiq", @@ -2194,7 +2194,7 @@ "owner": "ConductionNL/keepiq", "reachedOn": "nothing", "note": "Sign-in is Nextcloud's; a new device unlocks the vault with the user's passphrase, and there is no approve-from-another-device or admin approval flow.", - "change": "openspec/changes/crypto-new-device-approval" + "change": "openspec/changes/archive/2026-10-04-crypto-new-device-approval" }, "rowSource": "competitor", "origin": "changelog", @@ -2756,7 +2756,7 @@ "evidence": "Specified in openspec/changes/sharing-federated-recipients on 2026-09-27. Before: grep -rn 'federat\\|remote share\\|outside.*organi[sz]ation' lib/Controller lib/Service: no sharing-related hits (the one federated-cloud-ID hit in EncryptionSuiteProvisioningService.php:331 is only used to name a certificate's common name, not to share with an external account). All sharing paths (user-to-user, team folder, link, send) target a Nextcloud user ID, group ID, or an anonymous public link on this instance.", "owner": "ConductionNL/keepiq", "note": "Sharing with an account on a different Nextcloud instance is not supported; a password-protected public link (sharing-14) is the closest substitute.", - "change": "openspec/changes/sharing-federated-recipients" + "change": "openspec/changes/archive/2026-10-04-sharing-federated-recipients" }, "rowSource": "competitor", "provider": "keepiq", @@ -2785,7 +2785,7 @@ "state": "specified", "evidence": "Specified in openspec/changes/sharing-team-folder-manager-role on 2026-09-27. Before: The only role-like concept in the codebase is a team folder membership grade of read or write (folder-permission-grades spec, sharing-09); there is no manager/viewer role vocabulary and no 'collection' concept beyond team folders. grep -rln 'manager\\|viewer' src/store/modules lib/Controller for role-purposed hits: none.", "owner": "ConductionNL/keepiq", - "change": "openspec/changes/sharing-team-folder-manager-role" + "change": "openspec/changes/archive/2026-10-04-sharing-team-folder-manager-role" }, "rowSource": "competitor", "provider": "keepiq", @@ -2967,7 +2967,7 @@ "owner": "ConductionNL/keepiq", "reachedOn": "nothing", "note": "Every recipient who can use a secret can also reveal it.", - "change": "openspec/changes/sharing-use-only-and-expiring-shares" + "change": "openspec/changes/archive/2026-10-04-sharing-use-only-and-expiring-shares" }, "rowSource": "competitor", "origin": "tender", @@ -3000,7 +3000,7 @@ "owner": "ConductionNL/keepiq", "reachedOn": "nothing", "note": "A share to a colleague lasts until it is revoked by hand.", - "change": "openspec/changes/sharing-use-only-and-expiring-shares" + "change": "openspec/changes/archive/2026-10-04-sharing-use-only-and-expiring-shares" }, "rowSource": "competitor", "origin": "changelog", @@ -4143,7 +4143,7 @@ "evidence": "Specified in openspec/changes/apps-terraform-provider on 2026-09-27. Before: grep -rli 'terraform' . --include=*.md --include=*.php --include=*.go: no hits", "note": "No Terraform provider exists for managing keepiq secrets/applications as code.", "owner": "ConductionNL/keepiq", - "change": "openspec/changes/apps-terraform-provider" + "change": "openspec/changes/archive/2026-10-04-apps-terraform-provider" }, "rowSource": "competitor", "provider": "keepiq", @@ -5752,7 +5752,7 @@ "reachedOn": "Nextcloud admin delegation for the Keepiq settings section; vault_admin group for offboarding and for the admin handover panel in the secret sidebar", "note": "There are two coarse levers: Nextcloud's delegation of the whole Keepiq admin section, and a hard-coded vault_admin group that unlocks offboarding and admin handover. The handover now has a route, a controller call and a UI panel (f13ad8e6, closing #184), so both levers work end to end. There is still no role editor and no per-permission role, so a person cannot be given only the permissions they need: partial.", "defects": [], - "change": "openspec/changes/admin-scoped-roles" + "change": "openspec/changes/archive/2026-10-04-admin-scoped-roles" }, "rowSource": "competitor", "provider": "keepiq", @@ -6261,7 +6261,7 @@ "owner": "ConductionNL/keepiq", "reachedOn": "nothing", "note": "There is no scheduled server-side backup of all vaults and no restore command; an instance relies on the Nextcloud database backup.", - "change": "openspec/changes/admin-scheduled-vault-backups" + "change": "openspec/changes/archive/2026-10-04-admin-scheduled-vault-backups" }, "rowSource": "competitor", "origin": "featureRequest", @@ -7252,7 +7252,7 @@ "needsLiveCheck": false } ], - "change": "openspec/changes/clients-extension-store-release" + "change": "openspec/changes/archive/2026-10-04-clients-extension-store-release" }, "rowSource": "own", "provider": "keepiq", @@ -7371,7 +7371,7 @@ "needsLiveCheck": false } ], - "change": "openspec/changes/clients-extension-store-release" + "change": "openspec/changes/archive/2026-10-04-clients-extension-store-release" }, "rowSource": "own", "provider": "keepiq", @@ -7711,7 +7711,7 @@ "owner": "ConductionNL/keepiq", "reachedOn": "none", "note": "SSH keys can be stored as secrets, but nothing exposes them to an SSH agent.", - "change": "openspec/changes/clients-ssh-agent" + "change": "openspec/changes/archive/2026-10-04-clients-ssh-agent" }, "rowSource": "competitor", "provider": "keepiq", @@ -7787,7 +7787,8 @@ "evidence": "Flows page src/manifest.json:570 (index over OpenRegister 'flows' source) and FlowDetail with src/registry.js:63 FlowDetailSidebar (CnFlowSidebar); OpenRegister triggers are only manual, object and schedule (openregister lib/Service/Flow/Nodes/TriggerManualNode.php, TriggerObjectNode.php, TriggerScheduleNode.php); keepiq data lives in its own tables and grep -i flow lib: no trigger publisher", "owner": "ConductionNL/openregister", "reachedOn": "Flows menu item (/flows) -> OpenRegister flow list and canvas", - "note": "The Flows page and canvas are reachable, but they run on OpenRegister's engine whose triggers fire on OpenRegister objects, schedules or manual runs. Keepiq's vault lives in its own tables and publishes nothing to it, so no vault event can start a flow." + "note": "The Flows page and canvas are reachable, but they run on OpenRegister's engine whose triggers fire on OpenRegister objects, schedules or manual runs. Keepiq's vault lives in its own tables and publishes nothing to it, so no vault event can start a flow.", + "change": "openspec/changes/clients-vault-event-flow-triggers" }, "rowSource": "own", "provider": "openregister", From 84d296aa7efebe4c8e4e83de60fc1aec4cd40d59 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 7 Oct 2026 21:17:41 +0200 Subject: [PATCH 003/167] docs(parity): link every built capability to the spec or change that delivered it Step 3: 132 built rows had no link. 59 now name the archived change that delivered them, 71 name the main spec (several changes shaped it), and two (clients-16 theming, clients-17 walkthrough) were never specified. --- openspec/parity/capabilities.json | 396 ++++++++++++++++++++---------- 1 file changed, 264 insertions(+), 132 deletions(-) diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 9b9c5150e..dc1a0cec0 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -444,7 +444,8 @@ "evidence": "src/dialogs/SecretCreateDialog.vue:439 useSecretStore().createSecret -> src/store/modules/secret.js:348,385 POST /api/v1/secrets -> appinfo/routes.php:89 secret#create -> lib/Controller/SecretController.php", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> New secret dialog (secret-create modal)", - "note": "Create dialog collects name, type, url, login and key, and posts through the secret store to the secret CRUD endpoint. Login is one of the built-in system types." + "note": "Create dialog collects name, type, url, login and key, and posts through the secret store to the secret CRUD endpoint. Login is one of the built-in system types.", + "spec": "openspec/specs/secrets" }, "rowSource": "own", "provider": "keepiq", @@ -476,7 +477,8 @@ "evidence": "src/dialogs/SecretEditDialog.vue:492 useSecretStore().updateSecret -> src/store/modules/secret.js:400,449 PUT /api/v1/secrets/{id} -> appinfo/routes.php:97 secret#update", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page -> secret detail sidebar -> Edit (secret-edit modal)", - "note": "Edit dialog diffs the changed fields and updates through the same encrypted-at-rest path as create." + "note": "Edit dialog diffs the changed fields and updates through the same encrypted-at-rest path as create.", + "spec": "openspec/specs/secrets" }, "rowSource": "own", "provider": "keepiq", @@ -508,7 +510,8 @@ "evidence": "src/dialogs/SecretDeleteConfirmDialog.vue:140 useSecretStore().deleteSecret -> src/store/modules/secret.js:527 DELETE /api/v1/secrets/{id} -> appinfo/routes.php:98 secret#destroy", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page -> secret row -> Delete (secret-delete modal)", - "note": "Deletion is a hard, permanent delete with an explicit irreversible-warning copy in the dialog; there is no trash (see vault-04)." + "note": "Deletion is a hard, permanent delete with an explicit irreversible-warning copy in the dialog; there is no trash (see vault-04).", + "spec": "openspec/specs/secrets" }, "rowSource": "own", "provider": "keepiq", @@ -571,7 +574,8 @@ "evidence": "src/views/SecretList.vue:1618 cnOpenModal('folder-create') -> src/dialogs/FolderCreateDialog.vue:227 useFolderStore().createFolder -> src/store/modules/folder.js:100 POST /api/v1/folders -> appinfo/routes.php:81 folder#create", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page -> folder tree in app nav -> New folder", - "note": "Folders nest via parentId, giving subfolders in the same tree." + "note": "Folders nest via parentId, giving subfolders in the same tree.", + "spec": "openspec/specs/secrets" }, "rowSource": "own", "provider": "keepiq", @@ -603,7 +607,8 @@ "evidence": "src/views/SecretList.vue:1516 cnOpenModal('secret-move') -> src/dialogs/MoveDialog.vue:329 useSecretStore().updateSecret({folderId}) -> PUT /api/v1/secrets/{id} -> appinfo/routes.php:97 secret#update", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page -> secret row action -> Move (secret-move modal)", - "note": "MoveDialog also handles moving a whole folder's contents (updateFolder for subfolders), a superset of the single-secret move asked for here." + "note": "MoveDialog also handles moving a whole folder's contents (updateFolder for subfolders), a superset of the single-secret move asked for here.", + "spec": "openspec/specs/secrets-write-ui" }, "rowSource": "own", "provider": "keepiq", @@ -635,7 +640,8 @@ "evidence": "src/views/SecretList.vue:1471 onSearch debounces into secretStore.searchSecrets -> src/store/modules/secret.js search param -> GET /api/v1/secrets?search= -> appinfo/routes.php:88 secret#index -> lib/Service/SecretService.php:1034 search() -> fuzzyMatch() Levenshtein tolerance 1-2", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page -> inline search field", - "note": "Server-side two-stage fuzzy match: SQL substring pre-filter plus a paged Levenshtein scan, genuinely typo-tolerant, not just a substring filter." + "note": "Server-side two-stage fuzzy match: SQL substring pre-filter plus a paged Levenshtein scan, genuinely typo-tolerant, not just a substring filter.", + "change": "openspec/changes/archive/2026-07-18-bound-fuzzy-secret-search" }, "rowSource": "own", "provider": "keepiq", @@ -667,7 +673,8 @@ "evidence": "lib/Search/SecretSearchProvider.php implements OCP\\Search\\IProvider, queries plaintext name/url; registered at lib/AppInfo/PlatformIntegrationRegistrar.php:56 registerSearchProvider(SecretSearchProvider::class), called from lib/AppInfo/Application.php:165", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud unified search bar (server-wide)", - "note": "Only matches the plaintext name/url columns, so it works without the vault being unlocked; deep-links back into the app." + "note": "Only matches the plaintext name/url columns, so it works without the vault being unlocked; deep-links back into the app.", + "spec": "openspec/specs/secrets" }, "rowSource": "own", "provider": "keepiq", @@ -794,7 +801,8 @@ "evidence": "lib/Repair/SeedSecretTypes.php:66 seeds 'note' => 'Secure Note' (repair step appinfo/info.xml:199); src/dialogs/SecretCreateDialog.vue:279 valueLabel reads 'Note' for the note type; src/components/SecretDetailSidebar.vue:200 note icon and :1233 'Note' label", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> New secret dialog -> Type: Secure Note", - "note": "Secure Note is a seeded system type; the create dialog relabels the value field to Note and the detail sidebar shows it as a note. URL and login stay optional." + "note": "Secure Note is a seeded system type; the create dialog relabels the value field to Note and the detail sidebar shows it as a note. URL and login stay optional.", + "spec": "openspec/specs/secrets" }, "rowSource": "own", "provider": "keepiq", @@ -826,7 +834,8 @@ "evidence": "src/dialogs/SecretCreateDialog.vue:37-94 card fields (number, expiry, CVV, PIN, cardholder, Luhn hint) and identity fields (name, address, phone, email, BSN) -> :307 serializeCard into the encrypted key -> src/store/modules/secret.js:348 createSecret; src/dialogs/SecretEditDialog.vue:51,73 same sets on edit; src/components/SecretDetailSidebar.vue:253,421 per-field rows with masked number/CVV/PIN/BSN", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> New secret dialog -> Type: Payment card or Identity", - "note": "Card and identity types each get their own field set in create and edit, serialized into the encrypted key, and render as per-field rows with sensitive fields masked. The identity type holds personal details and a BSN, not passport or licence document fields." + "note": "Card and identity types each get their own field set in create and edit, serialized into the encrypted key, and render as per-field rows with sensitive fields masked. The identity type holds personal details and a BSN, not passport or licence document fields.", + "change": "openspec/changes/archive/2026-07-18-card-identity-items" }, "rowSource": "own", "provider": "keepiq", @@ -897,7 +906,8 @@ "issue": "#113", "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-07-18-passkey-item-type" }, "rowSource": "own", "provider": "keepiq", @@ -962,7 +972,8 @@ "evidence": "src/components/SecretDetailSidebar.vue:598 AttachmentPanel -> src/components/AttachmentPanel.vue:169 store.upload -> src/store/modules/attachment.js:193-224 AES-GCM file key, encrypted blob and metadata, RSA-wrapped key -> POST /api/v1/secrets/{secretId}/attachments (appinfo/routes.php:348) -> lib/Controller/AttachmentController.php:84; download :251 and delete :292 wired", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets/:id) -> detail sidebar -> Attachments box", - "note": "Files are encrypted in the browser with a per-file key wrapped to the vault key, then uploaded; the owner can list, download (decrypted client side) and delete them." + "note": "Files are encrypted in the browser with a per-file key wrapped to the vault key, then uploaded; the owner can list, download (decrypted client side) and delete them.", + "change": "openspec/changes/archive/2026-07-18-encrypted-attachments" }, "rowSource": "own", "provider": "keepiq", @@ -994,7 +1005,8 @@ "evidence": "lib/Service/SecretService.php:892 versionService->snapshot on every user update; src/components/SecretDetailSidebar.vue:781 VersionHistoryPanel -> src/store/modules/secretVersion.js:42 GET /api/v1/secrets/{secretId}/versions, :68 view, :108 POST /api/v1/versions/{id}/restore (appinfo/routes.php:330-332) -> lib/Controller/SecretVersionController.php:139; src/components/VersionHistoryPanel.vue:155 restore after VersionRestoreDialog confirm", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets/:id) -> detail sidebar -> Version history panel -> View / Restore", - "note": "Each update snapshots the prior value; the owner can view an earlier version decrypted in session and restore it after a confirm, with the current value kept as a new version." + "note": "Each update snapshots the prior value; the owner can view an earlier version decrypted in session and restore it after a confirm, with the current value kept as a new version.", + "change": "openspec/changes/archive/2026-07-18-secret-version-history" }, "rowSource": "own", "provider": "keepiq", @@ -1026,7 +1038,8 @@ "evidence": "src/views/SecretList.vue:294-333 selection-actions strip: Move, Share, Add to team folder, Delete -> src/dialogs/BulkMoveDialog.vue:153, BulkShareDialog.vue:218, BulkDeleteDialog.vue:195 bulk.run -> src/store/modules/bulk.js:111; src/views/SecretList.vue:8-11 ExportDialog receives all decryptedSecrets, not the selection", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> row checkboxes -> selection strip", - "note": "Multi-select with shift-range works and the strip moves, shares, adds to a team folder or deletes the selection with per-item retry. Export is not a bulk action: the export dialog always takes the whole visible list." + "note": "Multi-select with shift-range works and the strip moves, shares, adds to a team folder or deletes the selection with per-item retry. Export is not a bulk action: the export dialog always takes the whole visible list.", + "change": "openspec/changes/archive/2026-07-18-bulk-actions" }, "rowSource": "own", "provider": "keepiq", @@ -1154,7 +1167,8 @@ "evidence": "src/views/SecretList.vue:371 SecretListItem in the list slot; src/components/SecretListItem.vue:5-10 tabindex=0 with Enter and Space opening the row, :91-92 copy control stops propagation; src/views/SecretList.vue:362 native NcCheckboxRadioSwitch per row with aria-label; selection strip buttons :294-333", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets)", - "note": "Rows are Tab-reachable and open with Enter or Space, selection checkboxes and the copy control are independently focusable, and bulk actions are buttons. There are no arrow-key or shortcut bindings, so moving through a long list means tabbing through every control." + "note": "Rows are Tab-reachable and open with Enter or Space, selection checkboxes and the copy control are independently focusable, and bulk actions are buttons. There are no arrow-key or shortcut bindings, so moving through a long list means tabbing through every control.", + "change": "openspec/changes/archive/2026-07-22-keyboard-accessible-secret-rows" }, "rowSource": "own", "provider": "keepiq", @@ -1351,7 +1365,8 @@ "evidence": "src/dialogs/VersionDetailsDialog.vue:4 read-only view of one decrypted historic version; opened from the version history list (openspec/specs/secret-version-history); git grep -i 'diff|compare' src/dialogs src/components: no diff view", "owner": "ConductionNL/keepiq", "reachedOn": "SecretDetailSidebar > version history > Version details dialog", - "note": "One old version can be opened and read next to nothing else; there is no side-by-side diff." + "note": "One old version can be opened and read next to nothing else; there is no side-by-side diff.", + "change": "openspec/changes/archive/2026-07-18-secret-version-history" }, "rowSource": "competitor", "provider": "keepiq", @@ -1443,7 +1458,8 @@ "evidence": "src/store/modules/secret.js:368-382 rsaEncrypt of key, login and additionalFields in the browser before POST /api/v1/secrets; src/crypto/rsa.js:200 RSA-OAEP-SHA256 via WebCrypto; src/store/modules/attachment.js:193-224 attachments AES-GCM encrypted client side", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> New secret / Edit secret dialogs", - "note": "The secret value, login, additional fields and attachments are encrypted in the browser to the user's public key and the private key never leaves the browser unencrypted. The name, URL, type and folder stay plaintext on the server so lists and search work." + "note": "The secret value, login, additional fields and attachments are encrypted in the browser to the user's public key and the private key never leaves the browser unencrypted. The name, URL, type and folder stay plaintext on the server so lists and search work.", + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -1475,7 +1491,8 @@ "evidence": "src/views/LockScreen.vue:850 first-time setup -> src/store/modules/encryptionSuite.js:112-136 generates the key pair, encryptPrivateKey(masterPassword) and POSTs only publicKey + encryptedPrivateKey to /api/v1/suites (appinfo/routes.php:37); src/crypto/aes.js:15 PBKDF2-SHA256 600000 iterations with a random salt", "owner": "ConductionNL/keepiq", "reachedOn": "Lock page (/lock) -> first-time setup form (shown only when the server confirms no suite exists)", - "note": "On first use the lock screen asks for a master password with confirmation; the browser derives a key from it and sends only the wrapped private key, so the server never receives the password." + "note": "On first use the lock screen asks for a master password with confirmation; the browser derives a key from it and sends only the wrapped private key, so the server never receives the password.", + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -1507,7 +1524,8 @@ "evidence": "src/views/LockScreen.vue:182-185 PasswordStrengthMeter -> :560-570 setup submit gated on strengthValid; src/components/PasswordStrengthMeter.vue:43-46 isValid needs length and zxcvbn score floors taken from the admin policy (fetchPolicy, :19) or app fallback; src/components/MasterPasswordForm.vue:82 same gate on change", "owner": "ConductionNL/keepiq", "reachedOn": "Lock page (/lock) first-time setup; Personal settings -> Security -> change master password", - "note": "Setup and change are blocked until the password meets the administrator's minimum length and zxcvbn score. The check is client side only, which is inherent since the server never sees the password." + "note": "Setup and change are blocked until the password meets the administrator's minimum length and zxcvbn score. The check is client side only, which is inherent since the server never sees the password.", + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -1539,7 +1557,8 @@ "evidence": "src/App.vue:151 MasterPasswordForm in the user-settings Security section -> src/components/MasterPasswordForm.vue:112 store.changePassword -> src/store/modules/encryptionSuite.js:161-198 re-wraps the private key with key proof -> PUT /api/v1/suites/{id}/private-key (appinfo/routes.php:38) -> lib/Controller/EncryptionSuiteController.php:266 marks passkey envelopes stale", "owner": "ConductionNL/keepiq", "reachedOn": "App navigation -> Personal settings -> Security -> Master password form", - "note": "Changing the master password only re-wraps the private key, so every secret stays readable; the server demands proof of the old key and flags enrolled passkeys for re-enrolment." + "note": "Changing the master password only re-wraps the private key, so every secret stays readable; the server demands proof of the old key and flags enrolled passkeys for re-enrolment.", + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -1579,7 +1598,8 @@ "issue": "#395", "needsLiveCheck": true } - ] + ], + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -1685,7 +1705,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-07-20-passkey-vault-login" }, "rowSource": "own", "provider": "keepiq", @@ -1808,7 +1829,8 @@ "evidence": "src/export/backup.js:14-17 passphrase-encrypted Argon2id backup that is suite-independent 'for a forgotten master password' -> src/store/modules/export.js:24 encryptBackup; src/import/backupParser.js:14 decryptBackup in the Import wizard; src/views/EmergencyAccessView.vue:11 trusted-contact escrow; grep -rni 'recovery code\\|recovery kit' src lib: no hits", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page -> Export dialog (encrypted backup); Import wizard (restore); Emergency access page (/emergency-access)", - "note": "There is no recovery code or kit that reopens the existing vault. A user who kept a passphrase-encrypted backup can restore their secrets into a new vault, but only after an administrator force-revokes the old key, and a trusted emergency contact can regain read access after a wait." + "note": "There is no recovery code or kit that reopens the existing vault. A user who kept a passphrase-encrypted backup can restore their secrets into a new vault, but only after an administrator force-revokes the old key, and a trusted emergency contact can regain read access after a wait.", + "change": "openspec/changes/archive/2026-06-14-secret-export-gdpr" }, "rowSource": "competitor", "provider": "keepiq", @@ -1870,7 +1892,8 @@ "evidence": "appinfo/info.xml:197 repair step BootstrapCertificateAuthority creates the instance root and intermediate; lib/Service/EncryptionSuiteProvisioningService.php:192-200 signFor refuses unless ca_status is healthy and signs every suite public key; appinfo/routes.php:46-49,215-216 CA status, renew and re-issue; src/views/CertificateInventoryView.vue:116,176 vault certificates and CA shown", "owner": "ConductionNL/keepiq", "reachedOn": "Certificates page (/certificates) -> Vault encryption certificates / Certificate authority; admin settings -> CA health", - "note": "Each vault public key is signed by a private CA that keepiq bootstraps on the instance, and suite creation refuses while that CA is unhealthy. The CA is the instance's own, generated by keepiq; there is no path to plug in an existing external organisational CA." + "note": "Each vault public key is signed by a private CA that keepiq bootstraps on the instance, and suite creation refuses while that CA is unhealthy. The CA is the instance's own, generated by keepiq; there is no path to plug in an existing external organisational CA.", + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -1910,7 +1933,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -1941,7 +1965,8 @@ "evidence": "src/migration/pipeline.js:134-155 verifiedReEncrypt decrypts each re-encrypted blob with the new private key and throws RoundTripMismatchError (:49) unless byte-identical; src/migration/driver.js:12 both worker and main-thread paths use it; lib/Service/MigrationWorkService.php:781 server accepts only browser-verified ciphertext; lib/Service/MigrationService.php:402 completion refuses while records remain", "owner": "ConductionNL/keepiq", "reachedOn": "Personal settings -> Security -> compromise recovery migration (and resume banner)", - "note": "During key migration every re-encrypted record is decrypted back with the new key and compared before it is committed, and the old suite stays active until nothing is left bound to it." + "note": "During key migration every re-encrypted record is decrypted back with the new key and compared before it is committed, and the old suite stays active until nothing is left bound to it.", + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -1972,7 +1997,8 @@ "evidence": "src/App.vue:277 revoke button -> :906 suiteStore.revokeSuite -> src/store/modules/encryptionSuite.js:1179-1199 POST /api/v1/suites/{id}/revoke (appinfo/routes.php:39) with master-password proof and ownership check (lib/Controller/EncryptionSuiteController.php:335); src/components/settings/AdminSuiteSection.vue:212 -> encryptionSuite.js:1286 POST /api/v1/suites/{id}/reinstate (routes.php:41, AuthorizedAdminSetting at controller :384)", "owner": "ConductionNL/keepiq", "reachedOn": "Personal settings -> Security -> Revoke (user); Nextcloud admin settings -> Keepiq -> suite management -> Reinstate (admin)", - "note": "A user can revoke their own vault key after re-entering the master password, and an administrator can reinstate it by id. Revocation also hard-deletes the shares the user received and warns before destroying emergency access, so a reinstate does not bring those back." + "note": "A user can revoke their own vault key after re-entering the master password, and an administrator can reinstate it by id. Revocation also hard-deletes the shares the user received and warns before destroying emergency access, so a reinstate does not bring those back.", + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -2004,7 +2030,8 @@ "evidence": "src/components/settings/AdminSuiteSection.vue:42 markCompromised toggle, :180 forceRevokeSuite -> src/store/modules/encryptionSuite.js:1247-1262 POST /api/v1/suites/{id}/force-revoke (appinfo/routes.php:40) -> lib/Controller/EncryptionSuiteController.php:436 -> lib/Service/EncryptionSuiteService.php:199-207 dispatches the revoked event with compromised=true; lib/AppInfo/SuiteLifecycleEventRegistrar.php:109 registers SuiteCompromiseOnRevokeListener", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud admin settings -> Keepiq -> Encryption suite management -> Force-revoke", - "note": "An administrator can force-revoke any suite by id after re-confirming their own password and optionally flag it as compromised, which triggers a listener that flags its secrets for rotation and notifies owners." + "note": "An administrator can force-revoke any suite by id after re-confirming their own password and optionally flag it as compromised, which triggers a listener that flags its secrets for rotation and notifies owners.", + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -2224,7 +2251,8 @@ "evidence": "src/views/SecretList.vue:59 mounts BulkShareDialog, row checkbox selection (SecretList.vue:364) allows selecting a single secret -> src/dialogs/BulkShareDialog.vue:150 onRun/shareOne -> POST /apps/keepiq/api/v1/shares/register-batch (BulkShareDialog.vue:165) -> appinfo/routes.php:124 share#registerBatch -> lib/Controller/ShareController.php", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> select one or more rows -> Share bulk action -> BulkShareDialog", - "note": "The per-secret sidebar Share button opens SecretShareDialog, which only does link sharing and explicitly disables user-to-user sharing as 'coming soon'. Real user-to-user sharing is reached through the bulk-selection Share action instead, which works for exactly one selected secret too." + "note": "The per-secret sidebar Share button opens SecretShareDialog, which only does link sharing and explicitly disables user-to-user sharing as 'coming soon'. Real user-to-user sharing is reached through the bulk-selection Share action instead, which works for exactly one selected secret too.", + "spec": "openspec/specs/user-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2297,7 +2325,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "spec": "openspec/specs/team-folder-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2329,7 +2358,8 @@ "evidence": "lib/AppInfo/UserLifecycleEventRegistrar.php registers UserRemovedFromGroupListener on OCP UserRemovedEvent -> lib/Listener/UserRemovedFromGroupListener.php calls TeamFolderService::handleGroupMemberLeave, which auto-revokes the departing user's folder-derived shares (direct shares are left intact). This fires automatically with no UI action needed, on a team folder that is reachable via src/modals/TeamFolderDialog.vue (opened from SecretList.vue and KeepiqAppNav.vue).", "owner": "ConductionNL/keepiq", "reachedOn": "automatic (server event listener) once a team folder with a group member exists", - "note": "The equivalent ad-hoc group-share auto-revocation (GroupShareService::handleMemberLeave, same listener) is also wired at the backend, but since sharing-02's group-share creation path is unreachable from the UI, that branch has nothing to act on in practice; the team-folder branch is what actually delivers this capability to a user." + "note": "The equivalent ad-hoc group-share auto-revocation (GroupShareService::handleMemberLeave, same listener) is also wired at the backend, but since sharing-02's group-share creation path is unreachable from the UI, that branch has nothing to act on in practice; the team-folder branch is what actually delivers this capability to a user.", + "spec": "openspec/specs/user-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2361,7 +2391,8 @@ "evidence": "src/dialogs/SecretEditDialog.vue (opened via SecretDetailSidebar.vue:1495 openEdit -> cnOpenModal('secret-edit')) submits through src/store/modules/secret.js update action; when a sensitive field changes (secret.js:459) it calls useShareStore().syncUpdate(...) (secret.js:471) -> PUT /apps/keepiq/api/v1/secrets/{id}/sync (share.js:551) -> appinfo/routes.php:129 share#sync.", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList -> secret row -> sidebar -> Edit -> save", - "note": "Sync runs automatically as part of the normal edit flow; no separate user action is needed." + "note": "Sync runs automatically as part of the normal edit flow; no separate user action is needed.", + "spec": "openspec/specs/user-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2392,7 +2423,8 @@ "state": "built", "evidence": "src/components/SecretDetailSidebar.vue:675 mounts ShareList for the owner -> src/components/share/ShareList.vue:111 onRevoke -> store.revokeShare -> DELETE /api/v1/shares/{id} (appinfo/routes.php:130 share#destroy).", "owner": "ConductionNL/keepiq", - "reachedOn": "SecretList -> secret row -> sidebar -> Sharing section -> revoke" + "reachedOn": "SecretList -> secret row -> sidebar -> Sharing section -> revoke", + "spec": "openspec/specs/user-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2423,7 +2455,8 @@ "state": "built", "evidence": "src/components/SecretDetailSidebar.vue:675 mounts ShareList (owner) which lists store.shares (fetched via GET /api/v1/secrets/{secretId}/shares, share.js:241 fetchShares). Recipients see the same list redacted to their own row per ShareList.vue's header comment.", "owner": "ConductionNL/keepiq", - "reachedOn": "SecretList -> secret row -> sidebar -> Sharing section" + "reachedOn": "SecretList -> secret row -> sidebar -> Sharing section", + "spec": "openspec/specs/user-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2454,7 +2487,8 @@ "state": "built", "evidence": "src/modals/TeamFolderDialog.vue (opened from SecretList.vue and KeepiqAppNav.vue) -> onShareFolder -> store.shareFolder -> POST /api/v1/team-folders (teamFolder.js:67) -> appinfo/routes.php:362 teamFolder#create -> lib/Service/TeamFolderService.php:109 shareFolder, which fans every secret in the folder subtree out to the folder's members.", "owner": "ConductionNL/keepiq", - "reachedOn": "SecretList -> folder actions -> Team folder sharing dialog -> Share this folder" + "reachedOn": "SecretList -> folder actions -> Team folder sharing dialog -> Share this folder", + "spec": "openspec/specs/team-folder-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2486,7 +2520,8 @@ "evidence": "src/modals/TeamFolderDialog.vue:69 renders a grade select per member (data-testid team-folder-grade-*) -> onGradeChange (line 577) -> store.setMemberGrade -> PATCH /api/v1/team-folders/{id}/members/{memberId} (appinfo/routes.php:368 teamFolderMember#setMemberGrade).", "owner": "ConductionNL/keepiq", "reachedOn": "Team folder sharing dialog -> member row -> grade select", - "note": "Grades are read/write, not a finer-grained set, but that matches the row's own wording." + "note": "Grades are read/write, not a finer-grained set, but that matches the row's own wording.", + "spec": "openspec/specs/folder-permission-grades" }, "rowSource": "own", "provider": "keepiq", @@ -2517,7 +2552,8 @@ "state": "built", "evidence": "lib/Service/TeamFolderMembershipResolver.php:233 subtreeSecretRefs() calls folderMapper->getSubtreeIds() and fans out to secrets in every descendant folder, not just the shared folder itself. Used by shareFolder, reconcile and the approve-join fan-out payload, all reachable from src/modals/TeamFolderDialog.vue.", "owner": "ConductionNL/keepiq", - "reachedOn": "Team folder sharing dialog (same path as sharing-08)" + "reachedOn": "Team folder sharing dialog (same path as sharing-08)", + "spec": "openspec/specs/team-folder-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2590,7 +2626,8 @@ "evidence": "lib/Listener/EncryptionSuiteRevokedListener.php is registered (via SuiteLifecycleEventRegistrar) on EncryptionSuiteRevokedEvent and calls DelegationService::makePermanent() to promote a departing owner's temporary SecretDelegation rows to permanent, transferring de-facto ownership to the delegate.", "owner": "ConductionNL/keepiq", "reachedOn": "automatic (server event listener) -- but only over delegations that exist", - "note": "The promotion logic is real and wired to a real event, but since sharing-11 shows no delegation can ever be created via the UI today, there is nothing for it to promote in practice. Rated partial rather than no because the mechanism itself is correctly built and would fire the moment delegation creation is wired up." + "note": "The promotion logic is real and wired to a real event, but since sharing-11 shows no delegation can ever be created via the UI today, there is nothing for it to promote in practice. Rated partial rather than no because the mechanism itself is correctly built and would fire the moment delegation creation is wired up.", + "spec": "openspec/specs/user-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2661,7 +2698,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-06-14-implement-link-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2692,7 +2730,8 @@ "state": "built", "evidence": "src/modals/NewSendDialog.vue (mounted in src/views/SecretList.vue) collects maxViews, optional ttlHours and password, then calls useEphemeralSendStore().createSend (NewSendDialog.vue:175) -> POST /api/v1/sends (ephemeralSend.js:160) -> appinfo/routes.php:250 ephemeralSend#create. The recipient views it, no account needed, at src/views/EphemeralSendAccess.vue (manifest.json route /send/:token).", "owner": "ConductionNL/keepiq", - "reachedOn": "SecretList -> New send action -> NewSendDialog; recipient opens /send/:token unauthenticated" + "reachedOn": "SecretList -> New send action -> NewSendDialog; recipient opens /send/:token unauthenticated", + "change": "openspec/changes/archive/2026-07-18-ephemeral-send" }, "rowSource": "own", "provider": "keepiq", @@ -2724,7 +2763,8 @@ "evidence": "src/modals/MySendsDialog.vue (mounted in src/views/SecretList.vue) lists store.sends (fetchSends, GET /api/v1/sends) and wires a revoke button (line 119, store.revoke(id) -> DELETE /api/v1/sends/{id}, appinfo/routes.php:252).", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList -> My sends action -> MySendsDialog", - "note": "Link shares (sharing-14) are also listed and revocable in the same SecretShareDialog, so both link-shares and sends can be reviewed and revoked, just via two different dialogs." + "note": "Link shares (sharing-14) are also listed and revocable in the same SecretShareDialog, so both link-shares and sends can be reviewed and revoked, just via two different dialogs.", + "change": "openspec/changes/archive/2026-07-18-ephemeral-send" }, "rowSource": "own", "provider": "keepiq", @@ -2814,7 +2854,8 @@ "state": "built", "evidence": "lib/Service/DirectShareRegistrar.php:99-105 registerDirectShares (the method behind the reachable register-batch path from sharing-01) fires notificationService->notify(subject: 'secret_shared', ...) once per new recipient after creating the shares.", "owner": "ConductionNL/keepiq", - "reachedOn": "automatic, part of the sharing-01 flow" + "reachedOn": "automatic, part of the sharing-01 flow", + "spec": "openspec/specs/user-sharing" }, "rowSource": "own", "provider": "keepiq", @@ -2845,7 +2886,8 @@ "state": "built", "evidence": "src/views/EmergencyAccessView.vue (page route /emergency-access, menu item EmergencyAccessMenu) wires a designate form (line 41 @click=\"designate\") -> store.designate -> POST /api/v1/emergency-access/contacts (appinfo/routes.php:113), building the recovery envelope client-side with a configurable wait period.", "owner": "ConductionNL/keepiq", - "reachedOn": "Emergency access page -> Designate a contact" + "reachedOn": "Emergency access page -> Designate a contact", + "spec": "openspec/specs/emergency-access" }, "rowSource": "own", "provider": "keepiq", @@ -2876,7 +2918,8 @@ "state": "built", "evidence": "src/views/EmergencyAccessView.vue:349 onDecline calls store.decline(id) -> POST /api/v1/emergency-access/contacts/{id}/decline (appinfo/routes.php:116), which per the grantor-decline-veto requirement can be called during the wait period.", "owner": "ConductionNL/keepiq", - "reachedOn": "Emergency access page -> incoming request -> Decline" + "reachedOn": "Emergency access page -> incoming request -> Decline", + "spec": "openspec/specs/emergency-access" }, "rowSource": "own", "provider": "keepiq", @@ -2907,7 +2950,8 @@ "state": "built", "evidence": "lib/BackgroundJob/ApproveElapsedEmergencyRequests.php is registered in appinfo/info.xml:115, runs hourly, and transitions 'requested' relationships to 'approved' once the wait period elapses with no decline; the header also notes a grantee fetch lazily promotes an elapsed request as a safety net.", "owner": "ConductionNL/keepiq", - "reachedOn": "automatic (background job + lazy promotion on grantee fetch)" + "reachedOn": "automatic (background job + lazy promotion on grantee fetch)", + "spec": "openspec/specs/emergency-access" }, "rowSource": "own", "provider": "keepiq", @@ -3032,7 +3076,8 @@ "evidence": "appinfo/routes.php:140-142 shareRequest create/approve/deny -> src/components/share/ShareRequestForm.vue:128 POST /api/v1/share-requests, mounted in src/components/SecretDetailSidebar.vue:687; searched 'lease', 'ip allow', 'time window' in lib/Service/ShareRequestService.php: none (machine leases at routes.php:319 are for applications)", "owner": "ConductionNL/keepiq", "reachedOn": "Secret detail sidebar, request access form", - "note": "A user can ask the owner for access and the owner approves or denies, but approval is not limited by a time window or IP address and does not gate use of a privileged login." + "note": "A user can ask the owner for access and the owner approves or denies, but approval is not limited by a time window or IP address and does not gate use of a privileged login.", + "spec": "openspec/specs/user-sharing" }, "rowSource": "competitor", "origin": "changelog", @@ -3092,7 +3137,8 @@ "evidence": "src/views/SecretList.vue:992 (credential-request action) -> src/dialogs/SecretRequestCreateDialog.vue:520 -> src/store/modules/secretRequest.js:106 POST /api/v1/secret-requests -> lib/Controller/SecretRequestController.php:170 -> lib/Service/SecretRequestService.php:411 createForUserVault; recipient: src/views/SecretRequestFill.vue:299 -> secretRequest.js:325 POST /api/v1/public/secret-requests/{token}/fill -> lib/Controller/SecretRequestFillController.php:160 (#[PublicPage])", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (vault) -> 'Ask someone for a credential' action, or SecretDetail sidebar -> Requests -> 'Ask someone to fill this in'; recipient uses the anonymous /public/share/request/:token page", - "note": "Fresh requests create a placeholder secret server side; the fill link points at the anonymous public shell (src/utils/fillLink.js), so a recipient without an account can open it." + "note": "Fresh requests create a placeholder secret server side; the fill link points at the anonymous public shell (src/utils/fillLink.js), so a recipient without an account can open it.", + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3132,7 +3178,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3164,7 +3211,8 @@ "evidence": "src/store/modules/secretRequest.js:325 submitFill encrypts with the requester suite certificate (public_certificate from lib/Controller/SecretRequestFillController.php:86) via rsaEncrypt; lib/Service/SecretRequestService.php:163 fill -> SecretRequestPolicy.php:192 requireOpenByToken refuses a fulfilled token (410) and flips status to fulfilled after persisting", "owner": "ConductionNL/keepiq", "reachedOn": "Anonymous fill page /public/share/request/:token", - "note": "Values are encrypted in the recipient's browser to the owner's suite and the token is single-use. The url field is stored plaintext by design and the dialog labels it so." + "note": "Values are encrypted in the recipient's browser to the owner's suite and the token is single-use. The url field is stored plaintext by design and the dialog labels it so.", + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3196,7 +3244,8 @@ "evidence": "src/dialogs/SecretRequestCreateDialog.vue:166 (datetime-local expiry, prefilled) -> lib/Controller/SecretRequestController.php parseExpiry -> SecretRequestService.php:664 expire, run by lib/BackgroundJob/ExpireSecretRequestsJob.php:117 registered in appinfo/info.xml:114; SecretRequestPolicy.php:192 refuses expired tokens", "owner": "ConductionNL/keepiq", "reachedOn": "Request create dialog; expiry shown per row in the SecretDetail sidebar request list", - "note": "Expired requests are refused on the fill page and swept by a registered background job, which also removes an unfilled placeholder." + "note": "Expired requests are refused on the fill page and swept by a registered background job, which also removes an unfilled placeholder.", + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3228,7 +3277,8 @@ "evidence": "lib/Service/SecretRequestService.php:192 -> lib/Service/SecretRequestOutbox.php:74 announceFulfilled -> lib/Service/NotificationService.php:55 (request_fulfilled, opt-out key notify_requests) -> lib/Notification/KeepiqNotifier.php:198, notifier registered at lib/AppInfo/PlatformIntegrationRegistrar.php:60", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud notification to the requester", - "note": "A Nextcloud notification goes to the requester when a request is filled; users can opt out through the notify_requests setting." + "note": "A Nextcloud notification goes to the requester when a request is filled; users can opt out through the notify_requests setting.", + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3259,7 +3309,8 @@ "evidence": "src/components/SecretDetailSidebar.vue:727 ('Ask for new values' when the secret has a value) -> SecretRequestCreateDialog.vue isReRequest -> src/store/modules/secretRequest.js:147 createReRequest -> SecretRequestController.php:170 MODE_RE_REQUEST -> SecretRequestService.php:533 createReRequest", "owner": "ConductionNL/keepiq", "reachedOn": "SecretDetail sidebar -> Requests -> Ask for new values", - "note": "A re-request targets the existing secret and overwrites its values when filled; only the owner may create one and only one pending request per secret is allowed (409)." + "note": "A re-request targets the existing secret and overwrites its values when filled; only the owner may create one and only one pending request per secret is allowed (409).", + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3291,7 +3342,8 @@ "evidence": "src/components/secretRequest/SecretRequestList.vue:76 Revoke button -> src/store/modules/secretRequest.js:181 DELETE /api/v1/secret-requests/{id} -> lib/Controller/SecretRequestController.php:403 destroy -> lib/Service/SecretRequestService.php:607 decline (status declined, unfilled placeholder removed); application scope: SecretRequestList.vue onRevokeConfirmed -> secretRequest.js:253 DELETE /api/v1/applications/{id}/secret-requests/{requestId}", "owner": "ConductionNL/keepiq", "reachedOn": "SecretDetail sidebar -> Requests list -> Revoke; admins also on ApplicationDetail (/applications/:id)", - "note": "Revoking sets the request to declined and deletes an empty placeholder secret. The row disappears from the list instead of showing as revoked." + "note": "Revoking sets the request to declined and deletes an empty placeholder secret. The row disappears from the list instead of showing as revoked.", + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3323,7 +3375,8 @@ "evidence": "src/views/SecretList.vue:647 pendingRequestsBySecret feeds the row requestState indicator (SecretList.vue:377) from src/store/modules/secretRequest.js:64 GET /api/v1/secret-requests -> lib/Controller/SecretRequestController.php:100 index; per secret: src/components/secretRequest/SecretRequestList.vue:13 'Outstanding requests' with status and expiry in src/components/SecretDetailSidebar.vue:737", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (row badge) and SecretDetail sidebar -> Requests list", - "note": "Waiting requests show as a badge on the secret row and in the per-secret list. There is no single page listing all open requests, although the list component's header comment mentions a 'My requests' view that does not exist in the manifest." + "note": "Waiting requests show as a badge on the secret row and in the per-secret list. There is no single page listing all open requests, although the list component's header comment mentions a 'My requests' view that does not exist in the manifest.", + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3354,7 +3407,8 @@ "evidence": "machine: POST /api/v1/app/secret-requests (appinfo/routes.php:317) -> lib/Controller/ApplicationSecretRequestsController.php:124 create (JWT application principal) -> lib/Service/ApplicationSecretRequestService.php:111 createForApplicationVault; response carries token and the human fill link; GET /api/v1/app/secret-requests lists pending ones (ApplicationSecretRequestsController.php:89)", "owner": "ConductionNL/keepiq", "reachedOn": "machine: POST /api/v1/app/secret-requests; admin oversight on ApplicationDetail (src/views/ApplicationDetail.vue:152)", - "note": "An authenticated application can create a request into its own vault and hand the returned link to a person; admins can see and revoke these requests on the application detail page." + "note": "An authenticated application can create a request into its own vault and hand the returned link to a person; admins can see and revoke these requests on the application detail page.", + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3385,7 +3439,8 @@ "evidence": "src/dialogs/SecretRequestCreateDialog.vue:591 defaultSelection does not pre-tick a field that already holds a value, and :107 marks filled fields; isFieldFilled at SecretRequestCreateDialog.vue:559", "owner": "ConductionNL/keepiq", "reachedOn": "Request create dialog opened from the SecretDetail sidebar", - "note": "Filled fields are marked and left unticked on a fresh request, but the requester can still tick them. Additional members count as unfilled when the blob is not decrypted in that context." + "note": "Filled fields are marked and left unticked on a fresh request, but the requester can still tick them. Additional members count as unfilled when the blob is not decrypted in that context.", + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3416,7 +3471,8 @@ "evidence": "src/components/secretRequest/SecretRequestList.vue:62 'Copy fill link' for pending, unexpired rows -> src/utils/fillLink.js fillLinkFor(row.token)", "owner": "ConductionNL/keepiq", "reachedOn": "SecretDetail sidebar -> Requests list -> Copy fill link", - "note": "The requester can copy the fill link again from the request list at any time while the request is pending and not expired." + "note": "The requester can copy the fill link again from the request list at any time while the request is pending and not expired.", + "spec": "openspec/specs/secret-requests" }, "rowSource": "own", "provider": "keepiq", @@ -3447,7 +3503,8 @@ "evidence": "src/dialogs/ApplicationRegisterDialog.vue:78 (CSR field) -> src/store/modules/application.js:120 register() -> POST /apps/keepiq/api/v1/applications -> appinfo/routes.php:273 application#create -> lib/Controller/ApplicationController.php", "owner": "ConductionNL/keepiq", "reachedOn": "ApplicationRegister page (/applications) -> ApplicationRegisterDialog", - "note": "A developer registers an application from the Applications page, optionally with a CSR; an active suite is provisioned if the app auto-activates." + "note": "A developer registers an application from the Applications page, optionally with a CSR; an active suite is provisioned if the app auto-activates.", + "spec": "openspec/specs/application-mgmt" }, "rowSource": "own", "provider": "keepiq", @@ -3487,7 +3544,8 @@ "issue": "#208", "needsLiveCheck": false } - ] + ], + "spec": "openspec/specs/application-mgmt" }, "rowSource": "own", "provider": "keepiq", @@ -3518,7 +3576,8 @@ "evidence": "lib/Service/ApplicationLifecycleService.php:343-399 dispatchAdminPendingNotification() calls NotificationService::notify() with subject app_pending when a new application registration is pending, fail-soft on error", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud bell notification, fired from ApplicationController#create via ApplicationLifecycleService", - "note": "Every vault admin gets a Nextcloud notification when a non-admin's application registration is pending; admins cannot opt out of this one." + "note": "Every vault admin gets a Nextcloud notification when a non-admin's application registration is pending; admins cannot opt out of this one.", + "spec": "openspec/specs/application-mgmt" }, "rowSource": "own", "provider": "keepiq", @@ -3549,7 +3608,8 @@ "evidence": "src/dialogs/ApplicationRegisterDialog.vue:78-90 (CSR textarea + file upload, onCsrUpload) -> register() -> POST /apps/keepiq/api/v1/applications {csr} -> ApplicationController#create -> ApplicationLifecycleService::runPostRegistrationHooks -> suiteProvisioner->provision(csr)", "owner": "ConductionNL/keepiq", "reachedOn": "ApplicationRegister page -> ApplicationRegisterDialog CSR upload", - "note": "A PKCS#10 CSR can be pasted or uploaded when registering an application; the suite is provisioned from it on activation." + "note": "A PKCS#10 CSR can be pasted or uploaded when registering an application; the suite is provisioned from it on activation.", + "spec": "openspec/specs/application-mgmt" }, "rowSource": "own", "provider": "keepiq", @@ -3580,7 +3640,8 @@ "evidence": "cli/internal/client/client.go:148-198 MachineToken() signs an RFC 7523 JWT assertion and POSTs it to the discovery-provided token endpoint -> POST /api/v1/token -> appinfo/routes.php:299 applicationToken#exchange -> lib/Controller/ApplicationTokenController.php:91 exchange()", "owner": "ConductionNL/keepiq", "reachedOn": "machine: POST /api/v1/token (RFC 7523 JWT-Bearer), used by cli ci mode (cli/ci.go:51)", - "note": "The keepiq CLI's CI mode signs an assertion with the application's private key and exchanges it for a bearer token; no password is ever stored or sent." + "note": "The keepiq CLI's CI mode signs an assertion with the application's private key and exchanges it for a bearer token; no password is ever stored or sent.", + "spec": "openspec/specs/secret-store-api" }, "rowSource": "own", "provider": "keepiq", @@ -3612,7 +3673,8 @@ "evidence": "cli/internal/client/client.go:215-243 FetchByName() -> GET /apps/keepiq/api/v1/app/secrets/by-name/{name} Bearer -> appinfo/routes.php:306 applicationSecrets#byName -> lib/Controller/ApplicationSecretsController.php Corrections round 8 (2026-09-28), keepiq#793: the by-name route answers, but the CLI cannot decrypt what it returns, because cli/internal/client/client.go:201-207 reads a top-level scheme and payload.value while lib/Service/MachineSecretEnvelopeService.php:145-148 sends encryption.scheme and ciphertext.key, so cli/ci.go:65-66 stops every fetch with unexpected envelope scheme \"\", at b5727e0.", "owner": "ConductionNL/keepiq", "reachedOn": "machine: GET /api/v1/app/secrets/by-name/{name}, used by cli ci fetch/run (cli/ci.go:60-95)", - "note": "keepiq ci fetch/run resolve a secret by name and decrypt the returned envelope locally with the application private key. Own rating yes to partial (corrections round 8, 2026-09-28): the server resolves and returns the secret by name, but the shipped CLI cannot decrypt the envelope it gets back, keepiq#793." + "note": "keepiq ci fetch/run resolve a secret by name and decrypt the returned envelope locally with the application private key. Own rating yes to partial (corrections round 8, 2026-09-28): the server resolves and returns the secret by name, but the shipped CLI cannot decrypt the envelope it gets back, keepiq#793.", + "spec": "openspec/specs/secret-store-api" }, "rowSource": "own", "provider": "keepiq", @@ -3644,7 +3706,8 @@ "evidence": "src/dialogs/WriteSecretForAppDialog.vue:1-60 encrypts fields client-side with the app's public certificate and POSTs to /apps/keepiq/api/v1/secrets with owner_type=application; opened from src/views/ApplicationDetail.vue:165", "owner": "ConductionNL/keepiq", "reachedOn": "Application detail page (/applications/:id) -> Write secret dialog", - "note": "A human writes a secret encrypted under an application's public certificate; the dialog states plainly that the writer cannot read it back, which is the ADR-003 write-without-read property." + "note": "A human writes a secret encrypted under an application's public certificate; the dialog states plainly that the writer cannot read it back, which is the ADR-003 write-without-read property.", + "spec": "openspec/specs/secret-store-api" }, "rowSource": "own", "provider": "keepiq", @@ -3675,7 +3738,8 @@ "evidence": "cli/internal/client/client.go:218-233 sends If-None-Match with the last observed ETag and returns ErrNotModified on 304; server side lib/Controller/ApplicationSecretsController.php serves the by-name/by-id envelope with a strong ETag", "owner": "ConductionNL/keepiq", "reachedOn": "machine: GET /api/v1/app/secrets/by-name/{name} with If-None-Match, used by cli ci fetch (poll loop)", - "note": "A consumer that keeps polling the same name gets 304 until the ciphertext actually changes, so it can detect rotation cheaply." + "note": "A consumer that keeps polling the same name gets 304 until the ciphertext actually changes, so it can detect rotation cheaply.", + "spec": "openspec/specs/secret-store-api" }, "rowSource": "own", "provider": "keepiq", @@ -3714,7 +3778,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "spec": "openspec/specs/secret-store-api" }, "rowSource": "own", "provider": "keepiq", @@ -3745,7 +3810,8 @@ "evidence": "lib/Service/MachineSecretResponseService.php:84-179 grants or reuses a MachineLease on every fetch and adds Doriath-Lease-Id / Doriath-Lease-Expires headers; cli/internal/client/client.go:226-227 captures them and cli/ci.go:84-86 prints the lease id + expiry Corrections round 8 (2026-09-28), keepiq#793: the CLI never prints the lease, because fetchDecrypt fails at cli/ci.go:65-66 before the lease line at cli/ci.go:84-86; the server still grants the lease before it serializes the envelope, lib/Service/MachineSecretResponseService.php:95 and :118, at b5727e0.", "owner": "ConductionNL/keepiq", "reachedOn": "machine: lease headers on GET /api/v1/app/secrets*, observed by cli ci fetch", - "note": "Every machine secret fetch is covered by a lease with an expiry; a revoked lease blocks re-fetch until re-granted per admin policy. Corrections round 8 (2026-09-28): the lease line in the CLI is never printed because the CLI fetch fails first, keepiq#793; rating kept because the server grants the lease and returns its id and expiry headers on every machine fetch, whichever client makes it." + "note": "Every machine secret fetch is covered by a lease with an expiry; a revoked lease blocks re-fetch until re-granted per admin policy. Corrections round 8 (2026-09-28): the lease line in the CLI is never printed because the CLI fetch fails first, keepiq#793; rating kept because the server grants the lease and returns its id and expiry headers on every machine fetch, whichever client makes it.", + "change": "openspec/changes/archive/2026-07-18-machine-secret-leases" }, "rowSource": "own", "provider": "keepiq", @@ -3784,7 +3850,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-07-18-machine-secret-leases" }, "rowSource": "own", "provider": "keepiq", @@ -3823,7 +3890,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-07-18-machine-secret-leases" }, "rowSource": "own", "provider": "keepiq", @@ -3894,7 +3962,8 @@ "evidence": "src/components/application/ApplicationSecretsPanel.vue:134 -> src/store/modules/application.js:317 listApplicationSecrets() -> GET /apps/keepiq/api/v1/secrets?ownerType=application&ownerId={id} -> SecretController#index; panel is mounted from src/views/ApplicationDetail.vue:123", "owner": "ConductionNL/keepiq", "reachedOn": "Application detail page -> Secrets panel", - "note": "An application's detail page lists the (ciphertext) secrets attributed to it by name, type and creation date." + "note": "An application's detail page lists the (ciphertext) secrets attributed to it by name, type and creation date.", + "spec": "openspec/specs/application-mgmt" }, "rowSource": "own", "provider": "keepiq", @@ -3926,7 +3995,8 @@ "evidence": "cli/ci.go:97-126 cmdCIRun() fetches+decrypts each named secret and injects it into the child process environment only (runChild in cli/main.go:281-289); no plaintext is written to disk (comment at ci.go:124) Corrections round 8 (2026-09-28), keepiq#793: cmdCIRun calls fetchDecrypt at cli/ci.go:118, which refuses the server envelope at cli/ci.go:65-66 because the server puts the scheme under encryption.scheme (lib/Service/MachineSecretEnvelopeService.php:145), so runChild at cli/ci.go:125 is never reached, at b5727e0.", "owner": "ConductionNL/keepiq", "reachedOn": "keepiq CLI: `keepiq ci run -- `", - "note": "The CLI is a single static Go binary that fetches, decrypts and injects secrets into a child process's environment for CI use, never touching disk. Own rating yes to no (corrections round 8, 2026-09-28): every ci fetch and ci run stops at the envelope scheme check before decryption, so no secret reaches the child process, keepiq#793." + "note": "The CLI is a single static Go binary that fetches, decrypts and injects secrets into a child process's environment for CI use, never touching disk. Own rating yes to no (corrections round 8, 2026-09-28): every ci fetch and ci run stops at the envelope scheme check before decryption, so no secret reaches the child process, keepiq#793.", + "spec": "openspec/specs/keepiq-cli" }, "rowSource": "own", "provider": "keepiq", @@ -3958,7 +4028,8 @@ "evidence": "cli/main.go: cmdList/cmdShow/cmdGet (lines 160-222) authenticate with a Nextcloud app-password, fetch the active suite, unwrap the private key with the master password, and decrypt fields client-side via dcrypto.DecryptField", "owner": "ConductionNL/keepiq", "reachedOn": "keepiq CLI: `keepiq list|show|get|copy` (human mode)", - "note": "v1 of the CLI is deliberately read-only; every decryption happens in the CLI process itself so no key material reaches the server." + "note": "v1 of the CLI is deliberately read-only; every decryption happens in the CLI process itself so no key material reaches the server.", + "spec": "openspec/specs/keepiq-cli" }, "rowSource": "own", "provider": "keepiq", @@ -4173,7 +4244,8 @@ "evidence": "The machine secret-store API (discovery, RFC 7523 token exchange, by-name fetch, write-back) is fully built server-side and documented for exactly this use in docs/integration-openconnector.md and openspec/specs/secret-store-api/spec.md; the design docs name OpenConnector (now integriq) as the intended consumer", "owner": "ConductionNL/keepiq", "reachedOn": "machine: documented for consumption by other Conduction apps (e.g. integriq) via /api/v1/app/*", - "note": "This checkout only contains keepiq's own code; whether integriq or another Conduction app actually stores its connection credentials in keepiq today cannot be settled from here, only that keepiq ships and documents a ready contract for it." + "note": "This checkout only contains keepiq's own code; whether integriq or another Conduction app actually stores its connection credentials in keepiq today cannot be settled from here, only that keepiq ships and documents a ready contract for it.", + "spec": "openspec/specs/secret-store-api" }, "rowSource": "own", "provider": "keepiq", @@ -4264,7 +4336,8 @@ "evidence": "lib/Service/CertificateAuthorityService.php:92 bootstrap (4096-bit root, then intermediate; partial-state recovery at :111) called from lib/Repair/BootstrapCertificateAuthority.php:71, registered in appinfo/info.xml:197 and :216; suites are signed via CertificateAuthorityService.php:202 signPublicKey from lib/Service/EncryptionSuiteProvisioningService.php:200", "owner": "ConductionNL/keepiq", "reachedOn": "machine: install/upgrade repair step; admin sees it on Nextcloud admin settings -> Keepiq (CaHealthSection)", - "note": "The app runs its own root and intermediate CA and signs every user and application suite certificate with it. The partial-install recovery that #41 asked for is present in this tree. #444 (phpseclib 4) does not bite yet: composer.json pins phpseclib ^3.0." + "note": "The app runs its own root and intermediate CA and signs every user and application suite certificate with it. The partial-install recovery that #41 asked for is present in this tree. #444 (phpseclib 4) does not bite yet: composer.json pins phpseclib ^3.0.", + "spec": "openspec/specs/encryption-suites" }, "rowSource": "own", "provider": "keepiq", @@ -4349,7 +4422,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-06-14-implement-dashboard-settings" }, "rowSource": "own", "provider": "keepiq", @@ -4380,7 +4454,8 @@ "evidence": "src/views/CertificateInventoryView.vue:256 -> src/store/modules/certificate.js:33 GET /api/v1/certificates/inventory -> lib/Controller/CertificateController.php:95 -> lib/Service/CertificateLifecycleService.php:97 inventory (stored certificate-type secrets, suite certificates, and CA rows for admins); page Certificates route /certificates in src/manifest.json:490", "owner": "ConductionNL/keepiq", "reachedOn": "Certificates page (/certificates, CertificatesMenu)", - "note": "One page lists the user's own certificate-type secrets, their vault suite certificate (all suites for admins) and the root and intermediate for admins. Shared or application-owned certificate secrets are not included." + "note": "One page lists the user's own certificate-type secrets, their vault suite certificate (all suites for admins) and the root and intermediate for admins. Shared or application-owned certificate secrets are not included.", + "change": "openspec/changes/archive/2026-07-18-certificate-lifecycle" }, "rowSource": "own", "provider": "keepiq", @@ -4419,7 +4494,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-07-18-certificate-lifecycle" }, "rowSource": "own", "provider": "keepiq", @@ -4450,7 +4526,8 @@ "evidence": "src/views/CertificateInventoryView.vue:292 'Renew…' -> src/store/modules/certificate.js:86 POST /api/v1/certificates/{id}/renewal-checklist -> lib/Service/CertificateLifecycleService.php:228 (externally issued checklist); suite rows: CertificateInventoryView.vue:308 -> certificate.js:108 POST /api/v1/certificates/suites/{id}/reissue -> CertificateLifecycleService.php:282", "owner": "ConductionNL/keepiq", "reachedOn": "Certificates page -> Renew… on a stored certificate, Re-issue on a suite certificate", - "note": "Stored certificates get a fixed checklist for externally issued certificates; certificates Keepiq issued itself (suite certificates) can be re-issued with one click. There is no branch for a stored certificate that Keepiq's CA issued, because Keepiq does not issue those." + "note": "Stored certificates get a fixed checklist for externally issued certificates; certificates Keepiq issued itself (suite certificates) can be re-issued with one click. There is no branch for a stored certificate that Keepiq's CA issued, because Keepiq does not issue those.", + "change": "openspec/changes/archive/2026-07-18-certificate-lifecycle" }, "rowSource": "own", "provider": "keepiq", @@ -4509,7 +4586,8 @@ "evidence": "Certificate secret type (lib/Service/CertificateMetadataService.php:73 certificateTypeId) -> src/views/CertificateInventoryView.vue:272 'Parse certificate' -> src/store/modules/certificate.js:59 parseAndSubmit (browser X.509 parse, src/certificates/x509.js:202) -> PUT /api/v1/certificates/{id}/metadata -> lib/Controller/CertificateController.php:121; subject, issuer and expiry columns in CertificateInventoryView.vue", "owner": "ConductionNL/keepiq", "reachedOn": "Secret create dialog with the Certificate type, then Certificates page -> Parse certificate", - "note": "Subject, issuer and expiry appear after the user clicks Parse certificate once; parsing happens in the browser so the server never sees the certificate value. Until then the row shows 'not parsed yet'." + "note": "Subject, issuer and expiry appear after the user clicks Parse certificate once; parsing happens in the browser so the server never sees the certificate value. Until then the row shows 'not parsed yet'.", + "change": "openspec/changes/archive/2026-07-18-certificate-lifecycle" }, "rowSource": "own", "provider": "keepiq", @@ -4587,7 +4665,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-06-14-password-health" }, "rowSource": "own", "provider": "keepiq", @@ -4633,7 +4712,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-06-14-password-health" }, "rowSource": "own", "provider": "keepiq", @@ -4673,7 +4753,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-06-14-password-health" }, "rowSource": "own", "provider": "keepiq", @@ -4712,7 +4793,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-06-14-password-health" }, "rowSource": "own", "provider": "keepiq", @@ -4758,7 +4840,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-06-14-password-health" }, "rowSource": "own", "provider": "keepiq", @@ -4790,7 +4873,8 @@ "evidence": "src/store/modules/health.js:202 rsaDecrypt in browser -> health.js:266 Web Worker src/health/worker.js; state memory-only and reset on lock health.js:308; only the HIBP prefix is sent (src/health/hibp.js:79)", "owner": "ConductionNL/keepiq", "reachedOn": "Password health page and SecretList strength pass", - "note": "Decryption and analysis run in the browser (worker when available), results are held in memory only and cleared on lock. The possibly-compromised flag is the one server-known input." + "note": "Decryption and analysis run in the browser (worker when available), results are held in memory only and cleared on lock. The possibly-compromised flag is the one server-known input.", + "change": "openspec/changes/archive/2026-06-14-password-health" }, "rowSource": "own", "provider": "keepiq", @@ -4822,7 +4906,8 @@ "evidence": "src/dialogs/SecretCreateDialog.vue:112 and SecretEditDialog.vue:115 open src/dialogs/KeyGeneratorModal.vue:216 POST /api/v1/generate-key -> lib/Controller/KeyGeneratorController -> lib/Service/KeyGeneratorService.php:179 generate (random_int CSPRNG, length, special characters, excluded characters, regex override, org policy clamp :110)", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList -> New secret / Edit secret dialog -> Generate", - "note": "Length, special characters, excluded characters and an advanced regex pattern. The value is generated on the server and returned over the API, so the server sees a generated password before it is encrypted client-side." + "note": "Length, special characters, excluded characters and an advanced regex pattern. The value is generated on the server and returned over the API, so the server sees a generated password before it is encrypted client-side.", + "change": "openspec/changes/archive/2026-06-14-implement-key-generator" }, "rowSource": "own", "provider": "keepiq", @@ -4853,7 +4938,8 @@ "state": "built", "evidence": "src/generator/generator.js generatePassphrase (EFF large word list src/generator/eff-large-wordlist.js, crypto.getRandomValues) <- src/dialogs/KeyGeneratorModal.vue Password/Passphrase switch; policy fit + generator_allow_passphrase (lib/Service/PasswordPolicyService.php). Tests: tests/vitest/generator.spec.js, tests/dialogs/KeyGeneratorModal.spec.js, tests/e2e/workflows/key-generator.spec.ts. Change: openspec/changes/client-side-key-generator (supersedes health-passphrase-generator).", "owner": "ConductionNL/keepiq", - "reachedOn": "SecretList page (/secrets) -> New secret dialog -> Generate a strong key -> Passphrase" + "reachedOn": "SecretList page (/secrets) -> New secret dialog -> Generate a strong key -> Passphrase", + "change": "openspec/changes/client-side-key-generator" }, "rowSource": "competitor", "provider": "keepiq", @@ -5123,7 +5209,8 @@ "evidence": "src/components/RotationPanel.vue:236 onSave -> src/store/modules/rotation.js:154 PUT /api/v1/secrets/{id}/expiry -> lib/Controller/RotationController.php:89 -> lib/Service/SecretService.php:1379 setExpiry (owner-only, ciphertext and key_updated_at untouched)", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList -> secret detail sidebar -> Rotation & expiry row (src/components/SecretDetailSidebar.vue:798)", - "note": "Owner picks a date or clears it; the value and its key timestamp are not changed." + "note": "Owner picks a date or clears it; the value and its key timestamp are not changed.", + "change": "openspec/changes/archive/2026-07-18-rotation-expiry-policies" }, "rowSource": "own", "provider": "keepiq", @@ -5169,7 +5256,8 @@ "issue": "#746", "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-07-18-rotation-expiry-policies" }, "rowSource": "own", "provider": "keepiq", @@ -5208,7 +5296,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-07-18-rotation-expiry-policies" }, "rowSource": "own", "provider": "keepiq", @@ -5240,7 +5329,8 @@ "evidence": "src/components/RotationPanel.vue:31 Mark rotated -> src/store/modules/rotation.js:188 POST /api/v1/rotation-flags/{id}/rotated -> lib/Controller/RotationController.php:314 -> lib/Service/RotationFlagService.php:181 markRotated (resolves only when key_updated_at advanced past the value frozen at flag time)", "owner": "ConductionNL/keepiq", "reachedOn": "Secret detail sidebar -> Rotation & expiry (owner, when a flag is open; Flag for rotation raises one)", - "note": "If the value did not change since flagging, the server refuses and the panel tells the owner to update the value first." + "note": "If the value did not change since flagging, the server refuses and the panel tells the owner to update the value first.", + "change": "openspec/changes/archive/2026-07-18-rotation-expiry-policies" }, "rowSource": "own", "provider": "keepiq", @@ -5272,7 +5362,8 @@ "evidence": "compromise: lib/Service/CompromiseContainmentService.php stampSealed() stamps possibly_compromised_at and flags suite_compromise (via lib/Listener/MarksCompromisedSecrets.php), called by EncryptionSuiteController::forceRevokeAsCompromise(); breach: src/views/HealthReportView.vue:120 Flag all breached -> rotation.js:170 POST /api/v1/rotation-flags -> RotationFlagService.php:136 flagBatch", "owner": "ConductionNL/keepiq", "reachedOn": "Automatic on a compromise revocation; Password health page button for breached passwords", - "note": "Compromise flagging is automatic. Breach flagging is a one-click user action because breach verdicts never leave the browser." + "note": "Compromise flagging is automatic. Breach flagging is a one-click user action because breach verdicts never leave the browser.", + "change": "openspec/changes/archive/2026-07-18-rotation-expiry-policies" }, "rowSource": "own", "provider": "keepiq", @@ -5312,7 +5403,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-07-18-rotation-expiry-policies" }, "rowSource": "own", "provider": "keepiq", @@ -5420,7 +5512,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-07-18-org-password-policies" }, "rowSource": "own", "provider": "keepiq", @@ -5452,7 +5545,8 @@ "evidence": "src/components/settings/PasswordPolicySection.vue:26 min score select -> POST /api/settings (AppHost standard route) -> lib/Controller/SettingsController.php:160 create -> update (bounds 12-20 and 3-4, lib/Service/SettingsService.php:83); read back by src/components/PasswordStrengthMeter.vue:165 on LockScreen setup (src/views/LockScreen.vue:182) and MasterPasswordForm", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud admin settings -> Keepiq -> Password Policy", - "note": "Admins set minimum length (12 to 20) and zxcvbn score (3 or 4); the setup and change forms block submit below the floor. Enforcement is client-side by design, since the server never sees the master password." + "note": "Admins set minimum length (12 to 20) and zxcvbn score (3 or 4); the setup and change forms block submit below the floor. Enforcement is client-side by design, since the server never sees the master password.", + "change": "openspec/changes/archive/2026-06-14-implement-dashboard-settings" }, "rowSource": "own", "provider": "keepiq", @@ -5492,7 +5586,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-06-14-implement-dashboard-settings" }, "rowSource": "own", "provider": "keepiq", @@ -5556,7 +5651,8 @@ "evidence": "src/components/settings/OfflineCacheSection.vue:85 PUT /api/settings/admin offline_cache_enabled -> lib/Service/AdminSettingsService.php:354; lib/Controller/OfflineController.php:82 returns 403 when off; src/store/modules/offline.js:124 purges the local snapshot on 403", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud admin settings -> Keepiq -> offline cache section", - "note": "Switching it off refuses the offline manifest and clients purge any existing snapshot on their next sync." + "note": "Switching it off refuses the offline manifest and clients purge any existing snapshot on their next sync.", + "spec": "openspec/specs/offline-readonly-cache" }, "rowSource": "own", "provider": "keepiq", @@ -5588,7 +5684,8 @@ "evidence": "src/components/settings/BreachCheckSection.vue:30 toggle -> PUT /api/settings/admin breach_check_enabled -> lib/Service/AdminSettingsService.php:350; gate read at lib/Controller/BreachProxyController.php:161 and initial state lib/Controller/DashboardController.php:108", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud admin settings -> Keepiq -> Breach checking", - "note": "Off by default. Turning it on shows the per-user opt-in on the Password health page; users still have to opt in." + "note": "Off by default. Turning it on shows the per-user opt-in on the Password health page; users still have to opt in.", + "change": "openspec/changes/archive/2026-06-14-password-health" }, "rowSource": "own", "provider": "keepiq", @@ -5620,7 +5717,8 @@ "evidence": "src/components/settings/AttachmentLimitsSection.vue:46 versions kept and :57 age limit -> PUT /api/settings/admin -> lib/Service/AdminSettingsService.php:451,460; appinfo/info.xml:116 PruneSecretVersionsJob -> lib/BackgroundJob/PruneSecretVersionsJob.php:74,80", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud admin settings -> Keepiq -> Attachments & version history", - "note": "Count (default 20) and age in days (default 365, 0 unlimited), pruned nightly." + "note": "Count (default 20) and age in days (default 365, 0 unlimited), pruned nightly.", + "change": "openspec/changes/archive/2026-07-18-secret-version-history" }, "rowSource": "own", "provider": "keepiq", @@ -5844,7 +5942,8 @@ "evidence": "src/manifest.d/80-connection-registry.json Integrations page /settings/integrations (integriq app_connection, admin, requires integriq); lib/Settings/connections.json declares hibp and siem; status reported by lib/Controller/BreachProxyController.php:227 and lib/Service/SiemService.php:218 via lib/Service/Connection/ConnectionReporter.php", "owner": "ConductionNL/integriq", "reachedOn": "App navigation -> Integrations (admin, only when integriq is installed)", - "note": "Keepiq declares two connections (breach check, SIEM) and reports their outcomes; integriq stores and renders the status table. Without integriq installed the menu entry is hidden." + "note": "Keepiq declares two connections (breach check, SIEM) and reports their outcomes; integriq stores and renders the status table. Without integriq installed the menu entry is hidden.", + "change": "openspec/changes/archive/2026-10-04-adopt-connection-registry" }, "rowSource": "own", "provider": "integriq", @@ -5875,7 +5974,8 @@ "evidence": "src/components/settings/AttachmentLimitsSection.vue:28 max size and :37 quota -> PUT /api/settings/admin -> lib/Service/AdminSettingsService.php:478; enforced at lib/Service/AttachmentService.php:227 and :234", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud admin settings -> Keepiq -> Attachments & version history", - "note": "Per-attachment limit (default 25 MiB) and per-user quota (default 100 MiB), counted in stored ciphertext bytes." + "note": "Per-attachment limit (default 25 MiB) and per-user quota (default 100 MiB), counted in stored ciphertext bytes.", + "change": "openspec/changes/archive/2026-07-18-encrypted-attachments" }, "rowSource": "own", "provider": "keepiq", @@ -5907,7 +6007,8 @@ "evidence": "src/views/settings/AdminRoot.vue:2 CnAdminSettingsShell appId=keepiq -> CnVersionInfoCard reads loadState(keepiq, version) provided by lib/Settings/AdminSettings.php:38 extends OpenRegister GenericAdminSettings", "owner": "ConductionNL/nextcloud-vue", "reachedOn": "Nextcloud admin settings -> Keepiq (version card at the top)", - "note": "The version card, up-to-date check and re-import button come from the shared admin shell and OpenRegister's AppHost settings class, so keepiq depends on OpenRegister being installed for this page." + "note": "The version card, up-to-date check and re-import button come from the shared admin shell and OpenRegister's AppHost settings class, so keepiq depends on OpenRegister being installed for this page.", + "change": "openspec/changes/archive/2026-06-14-implement-dashboard-settings" }, "rowSource": "own", "provider": "nextcloud-vue", @@ -6291,7 +6392,8 @@ "evidence": "lib/Service/SecretService.php:284,588,678,794 dispatch secret.created/updated/deleted/read AuditEvents -> lib/AppInfo/AuditStreamEventRegistrar.php:74 registers lib/Listener/AuditListener.php:78 -> lib/Service/AuditService.php:69 record(); lib/Service/AuditService.php:99 sanitiseMetadata rejects lib/Event/Audit/AuditEventTypes.php:173 FORBIDDEN_KEYS (value, password, ciphertext, payload...) and drops non-whitelisted keys", "owner": "ConductionNL/keepiq", "reachedOn": "automatic: every secret create/read/update/delete, share, link share, rotation, lease and export path records an entry", - "note": "About 23 services record audit events through one listener; metadata is whitelisted per event type and any secret-material key throws before insert. The listener is fail-soft, so a failed audit write is logged, not blocking." + "note": "About 23 services record audit events through one listener; metadata is whitelisted per event type and any secret-material key throws before insert. The listener is fail-soft, so a failed audit write is logged, not blocking.", + "spec": "openspec/specs/secret-audit-trail" }, "rowSource": "own", "provider": "keepiq", @@ -6323,7 +6425,8 @@ "evidence": "appinfo/routes.php:377-379 only GET audit routes; lib/Db/AuditEntryMapper.php:242 purgeOlderThan and :285 anonymizeActor are the only writes besides insert; grep for hash chain/prev_hash in lib: no hits", "owner": "ConductionNL/keepiq", "reachedOn": "no update or delete verb exists in any API or UI", - "note": "Append-only at the application level: only retention purge and account-deletion anonymisation modify rows. There is no database-level protection or tamper evidence (no hash chain), so someone with database access can still alter entries undetected." + "note": "Append-only at the application level: only retention purge and account-deletion anonymisation modify rows. There is no database-level protection or tamper evidence (no hash chain), so someone with database access can still alter entries undetected.", + "spec": "openspec/specs/secret-audit-trail" }, "rowSource": "own", "provider": "keepiq", @@ -6355,7 +6458,8 @@ "evidence": "appinfo/info.xml:113 registers lib/BackgroundJob/PurgeAuditLogJob.php:79 (daily, floor 30 days) -> lib/Service/AuditService.php:243 purge -> lib/Db/AuditEntryMapper.php:242; retention set in src/components/settings/AdminAuditSection.vue:317 PUT /api/settings/admin -> lib/Service/AdminSettingsService.php:337", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud admin settings > Keepiq > audit section (retention days input); purge runs as a registered background job", - "note": "Default 365 days, minimum 30 enforced both on save and in the job." + "note": "Default 365 days, minimum 30 enforced both on save and in the job.", + "spec": "openspec/specs/secret-audit-trail" }, "rowSource": "own", "provider": "keepiq", @@ -6386,7 +6490,8 @@ "evidence": "src/manifest.json:473 PersonalActivity page /my-activity -> src/views/PersonalActivityView.vue:102 -> src/store/modules/audit.js:85 GET /api/v1/audit/me -> lib/Controller/AuditController.php:140 mine -> AuditService::listForActor; also dashboard widget src/manifest.json:297", "owner": "ConductionNL/keepiq", "reachedOn": "My activity page (/my-activity) and the Recent activity dashboard widget", - "note": "Shows entries where the user is the actor, i.e. what you did. What other people (share recipients, applications) did to your secrets shows only on the per-secret history, not in this feed." + "note": "Shows entries where the user is the actor, i.e. what you did. What other people (share recipients, applications) did to your secrets shows only on the per-secret history, not in this feed.", + "spec": "openspec/specs/secret-audit-trail" }, "rowSource": "own", "provider": "keepiq", @@ -6417,7 +6522,8 @@ "evidence": "src/components/SecretDetailSidebar.vue:755 SecretActivityTab (v-if isOwner) -> src/components/SecretActivityTab.vue:100 -> src/store/modules/audit.js:65 GET /api/v1/audit/secret/{id} -> lib/Controller/AuditController.php:98 secret (owner-scoped, 404 for non-owners)", "owner": "ConductionNL/keepiq", "reachedOn": "Secret detail sidebar > Activity section (owner only)", - "note": "Only the owner sees it; share recipients get no history view." + "note": "Only the owner sees it; share recipients get no history view.", + "spec": "openspec/specs/secret-audit-trail" }, "rowSource": "own", "provider": "keepiq", @@ -6449,7 +6555,8 @@ "evidence": "lib/Settings/AdminSettings.php + src/settings.js:35 AdminRoot -> src/views/settings/Settings.vue:30 AdminAuditSection -> src/components/settings/AdminAuditSection.vue:250,338,364 -> src/store/modules/audit.js:122 GET /api/v1/audit -> lib/Controller/AuditController.php:180 index (AuthorizedAdminSetting)", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud admin settings > Keepiq > audit section (filters, paging, client-side CSV export)", - "note": "Filter by event type, actor, object and date range, with paging and a CSV export of the whole filter result." + "note": "Filter by event type, actor, object and date range, with paging and a CSV export of the whole filter result.", + "spec": "openspec/specs/secret-audit-trail" }, "rowSource": "own", "provider": "keepiq", @@ -6489,7 +6596,8 @@ "issue": "#728", "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-07-18-siem-audit-export" }, "rowSource": "own", "provider": "keepiq", @@ -6521,7 +6629,8 @@ "evidence": "src/views/settings/Settings.vue:24 ComplianceSection -> src/components/settings/ComplianceSection.vue:201 POST /api/v1/compliance/reports -> lib/Controller/ComplianceReportController.php:98 generate -> lib/Service/ComplianceReportService.php:152; export in src/dialogs/ComplianceSnapshotDialog.vue:148 exportCsv and :193 exportPdf (browser print), beacon POST .../exported :216", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud admin settings > Keepiq > Compliance section > Generate report > snapshot dialog (Export CSV / Export PDF (print))", - "note": "Metadata-only aggregates (secrets per user, share hygiene, rotation posture, ciphertext age). The PDF is the browser print dialog over the rendered snapshot, not a server-generated file." + "note": "Metadata-only aggregates (secrets per user, share hygiene, rotation posture, ciphertext age). The PDF is the browser print dialog over the rendered snapshot, not a server-generated file.", + "change": "openspec/changes/archive/2026-07-18-compliance-reporting" }, "rowSource": "own", "provider": "keepiq", @@ -6553,7 +6662,8 @@ "evidence": "lib/Service/ComplianceReportService.php:152-165 generate stores the aggregate with generatedAt/generatedBy; lib/Db/ComplianceReportMapper.php has only findById/findAll; appinfo/routes.php:195-199 no update or delete route; the exported route only records an audit event (lib/Service/ComplianceReportService.php:211)", "owner": "ConductionNL/keepiq", "reachedOn": "Compliance section report list; snapshots reopen as stored", - "note": "Immutable at the application level: no edit or delete path. No signature or hash, so the snapshot is not tamper-evident against database access." + "note": "Immutable at the application level: no edit or delete path. No signature or hash, so the snapshot is not tamper-evident against database access.", + "change": "openspec/changes/archive/2026-07-18-compliance-reporting" }, "rowSource": "own", "provider": "keepiq", @@ -6585,7 +6695,8 @@ "evidence": "src/components/SecretDetailSidebar.vue:814 HoneyPanel -> src/components/HoneyPanel.vue:181 store.flag -> src/store/modules/honey.js:56 POST /api/v1/secrets/{id}/honey -> lib/Controller/HoneyController.php; lib/AppInfo/AuditStreamEventRegistrar.php:92 HoneyTripwireListener -> lib/Listener/HoneyTripwireListener.php:141,160,173 (UI read, machine retrieve, link access) -> lib/Service/HoneyTripwireService.php:91 raiseAlert, :173 notify owner and admins; admin alert list src/components/settings/HoneySection.vue:145", "owner": "ConductionNL/keepiq", "reachedOn": "Secret detail sidebar > Honey tripwire panel; alerts in the panel and in Nextcloud admin settings > Keepiq > Honey section", - "note": "You flag an existing secret as a decoy; any server-observed access (UI read, application retrieval, link share, share copy) raises an alert and a notification that bypasses notification preferences. Alerts can be acknowledged or snoozed." + "note": "You flag an existing secret as a decoy; any server-observed access (UI read, application retrieval, link share, share copy) raises an alert and a notification that bypasses notification preferences. Alerts can be acknowledged or snoozed.", + "change": "openspec/changes/archive/2026-07-20-honey-credentials" }, "rowSource": "own", "provider": "keepiq", @@ -6616,7 +6727,8 @@ "evidence": "src/views/SecretList.vue:170 menu action openGdpr -> src/dialogs/GdprExportDialog.vue:155 -> src/store/modules/export.js:250 exportGdprPackage, GET /api/v1/gdpr/metadata -> lib/Controller/GdprController.php:93 -> lib/Service/GdprService.php:102 collectMetadata (shares given/received, delegations, link shares, requests, suites, attachments)", "owner": "ConductionNL/keepiq", "reachedOn": "Secret list page > actions menu > GDPR export dialog", - "note": "Server metadata is combined client-side with the decrypted vault into one package; the export is audited as vault.gdpr_exported." + "note": "Server metadata is combined client-side with the decrypted vault into one package; the export is audited as vault.gdpr_exported.", + "spec": "openspec/specs/gdpr-compliance" }, "rowSource": "own", "provider": "keepiq", @@ -6648,7 +6760,8 @@ "evidence": "src/views/SecretList.vue:175 deletionOpen -> src/dialogs/AccountDeletionDialog.vue:208 -> src/store/modules/export.js:286 DELETE /api/v1/gdpr/account-data -> lib/Controller/GdprController.php:130 (confirmation phrase) -> lib/Service/AccountDeletionService.php:108 deleteAllFor; lib/Service/AccountShareCleanupService.php:84 transferDelegatedSecrets, :126 detachGrantedShares tombstones recipient copies, :162 removeReceivedShares; also on Nextcloud user deletion via lib/Listener/UserDeletedListener.php", "owner": "ConductionNL/keepiq", "reachedOn": "Secret list page > actions menu > Delete account data dialog; automatically when an admin deletes the Nextcloud user", - "note": "Delegated secrets transfer to the delegate, recipients keep a tombstoned copy with sync severed, received shares and the user's own data are removed. Separately, a normal single-secret delete still leaves recipient copies live (#83), which is outside the account-deletion path." + "note": "Delegated secrets transfer to the delegate, recipients keep a tombstoned copy with sync severed, received shares and the user's own data are removed. Separately, a normal single-secret delete still leaves recipient copies live (#83), which is outside the account-deletion path.", + "spec": "openspec/specs/gdpr-compliance" }, "rowSource": "own", "provider": "keepiq", @@ -6679,7 +6792,8 @@ "evidence": "lib/Service/AccountDeletionService.php:150 dispatches AccountDataDeletedEvent -> lib/AppInfo/AuditStreamEventRegistrar.php:96 AuditListener -> lib/Listener/AuditListener.php:120 anonymizeUser -> lib/Service/AuditService.php:270, lib/Db/AuditEntryMapper.php:285 anonymizeActor + metadata scrub of recipientId/delegatedTo", "owner": "ConductionNL/keepiq", "reachedOn": "automatic on in-app account data deletion and on Nextcloud user deletion", - "note": "Entries are kept with the actor replaced by a deleted-account marker. Object names (secret titles) on the entries are not scrubbed." + "note": "Entries are kept with the actor replaced by a deleted-account marker. Object names (secret titles) on the entries are not scrubbed.", + "spec": "openspec/specs/secret-audit-trail" }, "rowSource": "own", "provider": "keepiq", @@ -6741,7 +6855,8 @@ "evidence": "lib/Service/HoneyTripwireService.php:173 honey_access notification; emergency_access_requested/accessed and secret_compromised notification subjects in lib/Service; lib/Service/SiemService.php:363 siem_dead_letter; grep for failed-unlock, mass-export or admin-change alert rules in lib: none", "owner": "ConductionNL/keepiq", "reachedOn": "Nextcloud notifications for honey access, emergency access and breached secrets", - "note": "Real-time alerts exist for a fixed set of events (decoy touched, emergency access, compromised secret). There is no configurable risk rule engine for user or admin behaviour such as failed unlocks, bulk exports or policy changes; that would sit in a SIEM fed by the export." + "note": "Real-time alerts exist for a fixed set of events (decoy touched, emergency access, compromised secret). There is no configurable risk rule engine for user or admin behaviour such as failed unlocks, bulk exports or policy changes; that would sit in a SIEM fed by the export.", + "change": "openspec/changes/archive/2026-07-20-honey-credentials" }, "rowSource": "competitor", "provider": "keepiq", @@ -6801,7 +6916,8 @@ "evidence": "src/views/SecretList.vue:1002 (Import toolbar item) -> src/dialogs/ImportWizardDialog.vue:492 onFilePicked (file.text() in browser) -> src/store/modules/import.js:106 parseFile via src/import/parsers/index.js:23 (csv, bitwarden, keepass-xml, nc-passwords, cxf, backup) -> src/store/modules/import.js:251 encryptRow (rsaEncrypt client-side) -> import.js:480 POST /api/v1/secrets/import-batch -> appinfo/routes.php:95 -> lib/Controller/ImportController.php:98 -> lib/Service/ImportService.php:101", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> toolbar Import -> Import wizard", - "note": "Files are read and parsed in the browser and only ciphertext is posted in chunks of 50. Supported sources are generic CSV, Bitwarden JSON/CSV, KeePass XML, Nextcloud Passwords JSON, CXF and the own encrypted backup." + "note": "Files are read and parsed in the browser and only ciphertext is posted in chunks of 50. Supported sources are generic CSV, Bitwarden JSON/CSV, KeePass XML, Nextcloud Passwords JSON, CXF and the own encrypted backup.", + "change": "openspec/changes/archive/2026-06-14-secret-import" }, "rowSource": "own", "provider": "keepiq", @@ -6833,7 +6949,8 @@ "evidence": "src/import/parsers/ncPasswords.js:103 parseNcPasswords (label/url/username/password, folder tree resolved) registered at src/import/parsers/index.js:26 -> ImportWizardDialog.vue format select -> src/store/modules/import.js:480 POST /api/v1/secrets/import-batch", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> toolbar Import -> Import wizard (format 'Nextcloud Passwords (JSON backup)')", - "note": "File-based migration from the Passwords app's Predefined JSON backup, with folder hierarchy kept. There is no direct server-side migration by design (E2E); an encrypted Passwords backup is not handled." + "note": "File-based migration from the Passwords app's Predefined JSON backup, with folder hierarchy kept. There is no direct server-side migration by design (E2E); an encrypted Passwords backup is not handled.", + "change": "openspec/changes/archive/2026-06-14-secret-import" }, "rowSource": "own", "provider": "keepiq", @@ -6906,7 +7023,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-06-14-secret-import" }, "rowSource": "own", "provider": "keepiq", @@ -6946,7 +7064,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-06-14-secret-import" }, "rowSource": "own", "provider": "keepiq", @@ -6977,7 +7096,8 @@ "evidence": "src/store/modules/import.js:119 rejected rows at parse, import.js:427-447 chunk and per-index server rejections folded in -> summary step src/dialogs/ImportWizardDialog.vue:224-278 (Rejected rows table with row/name/reason) -> ImportWizardDialog.vue:545 downloadRejected (client-side CSV, import.js:501)", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> toolbar Import -> Import wizard -> summary step", - "note": "The summary lists counts plus a table of rejected rows with the reason and offers a client-side CSV download of them. The report is transient and not stored." + "note": "The summary lists counts plus a table of rejected rows with the reason and offers a client-side CSV download of them. The report is transient and not stored.", + "change": "openspec/changes/archive/2026-06-14-secret-import" }, "rowSource": "own", "provider": "keepiq", @@ -7022,7 +7142,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-07-18-cxf-import-export" }, "rowSource": "own", "provider": "keepiq", @@ -7133,7 +7254,8 @@ "evidence": "src/dialogs/ExportDialog.vue:44 Plaintext CSV mode -> warning acknowledgement checkbox -> master password field -> ExportDialog.vue:349 verifyMasterPassword (src/crypto/reauth.js:117, client-side decrypt of the private-key blob) -> :380 src/store/modules/export.js:122 exportCsv -> POST /api/v1/export/events before local download", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> actions menu 'Export data' -> Export dialog (Plaintext CSV)", - "note": "The CSV path requires acknowledging an unencrypted-file warning and re-entering the master password, verified in the browser, before the local download; the export is reported as an event first." + "note": "The CSV path requires acknowledging an unencrypted-file warning and re-entering the master password, verified in the browser, before the local download; the export is reported as an event first.", + "change": "openspec/changes/archive/2026-06-14-secret-export-gdpr" }, "rowSource": "own", "provider": "keepiq", @@ -7205,7 +7327,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "change": "openspec/changes/archive/2026-06-14-secret-import" }, "rowSource": "own", "provider": "keepiq", @@ -7298,7 +7421,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "spec": "openspec/specs/browser-extension-autofill" }, "rowSource": "own", "provider": "keepiq", @@ -7477,7 +7601,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-07-20-mobile-pwa" }, "rowSource": "own", "provider": "keepiq", @@ -7516,7 +7641,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-07-20-mobile-pwa" }, "rowSource": "own", "provider": "keepiq", @@ -7548,7 +7674,8 @@ "evidence": "src/App.vue:774 offlineStore.syncNow after online unlock -> src/store/modules/offline.js:106 GET /api/v1/offline/manifest (appinfo/routes.php:229) -> encryptSnapshot + IndexedDB writeSnapshot (src/offline/cache.js) ; offline: src/views/LockScreen.vue:715 unlockOffline -> offline.js:140 (no network) -> src/store/modules/secret.js:99 reads the offline vault ; read-only gates src/views/SecretList.vue:704 ; admin toggle src/views/settings/Settings.vue:27 OfflineCacheSection", "owner": "ConductionNL/keepiq", "reachedOn": "LockScreen while offline -> SecretList (read-only, stale-data banner src/App.vue:76)", - "note": "Each online unlock writes an encrypted snapshot to IndexedDB; offline, the lock screen unlocks from it without a network call and the vault is shown read-only with a stale-data banner. Admins can switch caching off, which purges snapshots." + "note": "Each online unlock writes an encrypted snapshot to IndexedDB; offline, the lock screen unlocks from it without a network call and the vault is shown read-only with a stale-data banner. Admins can switch caching off, which purges snapshots.", + "spec": "openspec/specs/offline-readonly-cache" }, "rowSource": "own", "provider": "keepiq", @@ -7755,7 +7882,8 @@ "issue": null, "needsLiveCheck": true } - ] + ], + "change": "openspec/changes/archive/2026-10-04-clients-extension-gaps" }, "rowSource": "own", "provider": "keepiq", @@ -7832,7 +7960,8 @@ "issue": null, "needsLiveCheck": false } - ] + ], + "spec": "none" }, "rowSource": "own", "provider": "thematiq", @@ -7862,7 +7991,8 @@ "evidence": "src/manifest.json:3 walkthrough (tour keepiq:getting-started, trigger first-visit, 7 steps) -> src/main.js:236 manifestForLockState (src/router/guards.js, withheld while locked) -> CnWalkthrough in @conduction/nextcloud-vue resolves nav-item refs via data-cn-route, emitted by src/components/KeepiqAppNav/KeepiqAppNav.vue:52 ; completion stored via GET/PUT /api/preferences/walkthrough_completed_version (src/router/guards.js:310, OpenRegister AppHost preferences controller)", "owner": "ConductionNL/nextcloud-vue", "reachedOn": "First unlocked visit to the app -> Getting started tour", - "note": "A seven-step tour starts on the first unlocked visit and points at Dashboard, All secrets, Features and roadmap, Reports and Flows; it is held back while the vault is locked. Completion is stored through the OpenRegister AppHost preferences endpoint." + "note": "A seven-step tour starts on the first unlocked visit and points at Dashboard, All secrets, Features and roadmap, Reports and Flows; it is held back while the vault is locked. Completion is stored through the OpenRegister AppHost preferences endpoint.", + "spec": "none" }, "rowSource": "own", "provider": "nextcloud-vue", @@ -7900,7 +8030,8 @@ "issue": "#208", "needsLiveCheck": false } - ] + ], + "spec": "openspec/specs/dashboard" }, "rowSource": "own", "provider": "keepiq", @@ -8060,7 +8191,8 @@ "evidence": "l10n/ holds 74 files (37 languages, l10n/nl.json 1173 strings) loaded through Nextcloud's translation layer; the language is each user's Nextcloud personal setting", "owner": "ConductionNL/keepiq", "reachedOn": "Every keepiq page, in the language chosen under Nextcloud personal settings", - "note": "Keepiq ships its own translations and follows the per-user Nextcloud language." + "note": "Keepiq ships its own translations and follows the per-user Nextcloud language.", + "spec": "hydra/openspec/specs/i18n-infrastructure" }, "rowSource": "competitor", "origin": "featureRequest", From 714930331cf8728bd50067422966ba31c7fb2026 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 7 Oct 2026 21:20:44 +0200 Subject: [PATCH 004/167] docs(parity): add rows for five specified user capabilities that had none Step 4: extension generator, extension sends, extension vault, extension offline snapshot and the metadata-only AI assistant tools were specified and built but had no capability row. Competitor cells are unknown. --- openspec/parity/capabilities.json | 115 ++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index dc1a0cec0..0090c8ae5 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -4320,6 +4320,29 @@ "keeper": "https://docs.keeper.io/keeperpam/privileged-access-manager/universal-secrets-sync : 'Synchronize Shared Secrets to Cloud Secret Management Services'; Universal Secrets Sync automatically pushes shared secrets to cloud secret stores (KeeperPAM)." } }, + { + "id": "apps-26", + "area": "apps", + "name": "Let an AI assistant report on expiring and unrotated secrets without ever seeing a secret.", + "keepiq": "yes", + "bitwarden": "unknown", + "onepassword": "unknown", + "passbolt": "unknown", + "keeper": "unknown", + "hashicorp-vault": "unknown", + "nextcloud-passwords": "unknown", + "built": { + "state": "built", + "evidence": "lib/Mcp/EntryMetadataTools.php, ExpiryReportTools.php:81 expiryReport, RotationStatusTools.php, MetadataAllowList.php; registered through lib/AppInfo/McpRegistrar.php behind the scannable-services opt-in; invocations audited as agent reads", + "owner": "ConductionNL/keepiq", + "reachedOn": "An MCP client (Hermiq) with the Keepiq tools enabled", + "change": "openspec/changes/archive/2026-10-04-hermiq-ai-tooling" + }, + "rowSource": "own", + "provider": "keepiq", + "providerHow": "read-from-code", + "source": "spec-round-2026-10-07" + }, { "id": "pki-01", "area": "pki", @@ -8205,6 +8228,98 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/package.json:121 depends on ember-intl but git ls-files holds no translations or locales files; the UI is English only. Note: rated by the lane from the source.", "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/locales/ holds 17 locales (cs-CZ to uk-UA); src/react-extension/components/Administration/DisplayInternationalizationAdministration sets the organisation default Note: proposed by the source reader as a capability no row covered." } + }, + { + "id": "clients-24", + "area": "clients", + "name": "Generate a password or passphrase in the browser extension, and get one suggested in a sign-up field.", + "keepiq": "yes", + "bitwarden": "unknown", + "onepassword": "unknown", + "passbolt": "unknown", + "keeper": "unknown", + "hashicorp-vault": "unknown", + "nextcloud-passwords": "unknown", + "built": { + "state": "built", + "evidence": "browser-extension/src/popup/generator-view.js (Generator tab, password and passphrase options, history); browser-extension/src/content/password-suggest.js suggests a strong password in sign-up and change-password fields; policy-locked options per openspec/specs/extension-generator-policy", + "owner": "ConductionNL/keepiq", + "reachedOn": "Extension popup -> Generator tab; a sign-up field on a website -> suggestion", + "spec": "openspec/specs/extension-generator" + }, + "rowSource": "own", + "provider": "keepiq", + "providerHow": "read-from-code", + "source": "spec-round-2026-10-07" + }, + { + "id": "clients-25", + "area": "clients", + "name": "Create a one-off send from the browser extension and end the ones you made.", + "keepiq": "yes", + "bitwarden": "unknown", + "onepassword": "unknown", + "passbolt": "unknown", + "keeper": "unknown", + "hashicorp-vault": "unknown", + "nextcloud-passwords": "unknown", + "built": { + "state": "built", + "evidence": "browser-extension/src/popup/send-view.js create form and My sends list with revoke (send('send-revoke')), offline refusal per openspec/specs/extension-send-details", + "owner": "ConductionNL/keepiq", + "reachedOn": "Extension popup -> Send tab", + "spec": "openspec/specs/extension-send" + }, + "rowSource": "own", + "provider": "keepiq", + "providerHow": "read-from-code", + "source": "spec-round-2026-10-07" + }, + { + "id": "clients-26", + "area": "clients", + "name": "Browse, search, add and edit vault items and folders from the browser extension.", + "keepiq": "yes", + "bitwarden": "unknown", + "onepassword": "unknown", + "passbolt": "unknown", + "keeper": "unknown", + "hashicorp-vault": "unknown", + "nextcloud-passwords": "unknown", + "built": { + "state": "built", + "evidence": "browser-extension/src/popup/vault-view.js (browse and search), item-detail.js (copy, reveal, edit), folder-view.js (manage folders)", + "owner": "ConductionNL/keepiq", + "reachedOn": "Extension popup -> Vault tab", + "spec": "openspec/specs/extension-vault" + }, + "rowSource": "own", + "provider": "keepiq", + "providerHow": "read-from-code", + "source": "spec-round-2026-10-07" + }, + { + "id": "clients-27", + "area": "clients", + "name": "Keep using the browser extension offline from an encrypted snapshot of the vault.", + "keepiq": "yes", + "bitwarden": "unknown", + "onepassword": "unknown", + "passbolt": "unknown", + "keeper": "unknown", + "hashicorp-vault": "unknown", + "nextcloud-passwords": "unknown", + "built": { + "state": "built", + "evidence": "browser-extension/src/lib/vault.js and src/background/vault-handlers.js keep and refresh the snapshot; a changed suite discards it", + "owner": "ConductionNL/keepiq", + "reachedOn": "Extension popup -> Vault tab while offline (Synced at, Sync now)", + "change": "openspec/changes/archive/2026-10-04-clients-extension-complete" + }, + "rowSource": "own", + "provider": "keepiq", + "providerHow": "read-from-code", + "source": "spec-round-2026-10-07" } ], "pending": [] From c29fd7f8cc876bbf2f812b902663976c9085aec8 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 7 Oct 2026 21:22:31 +0200 Subject: [PATCH 005/167] docs(parity): point every open and built capability at its canvas board Step 5: 152 rows name their board(s) on canvas 5NkFW28vZUUij43xzxHg5a; 48 rows have no board, with the reason (backend, a Nextcloud or integriq surface, or not designed yet). --- openspec/parity/capabilities.json | 897 +++++++++++++++++++++++++++++- 1 file changed, 892 insertions(+), 5 deletions(-) diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 0090c8ae5..363aa0821 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -459,6 +459,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:39 POST /api/1.0/password/create; src/lib/Controller/Api/PasswordApiController.php:134 create(); src/js/Manager/PasswordManager.js:53 createPasswordFromData() API.createPassword; src/vue/Dialog/CreatePassword.vue:25-28 password, username, name and website fields Note: The create dialog collects password, username, name and website and posts to the password create route. Confirms the docs rating.", "keeper": "https://docs.keeper.io/user-guides/record-types : 'Login: The set of data needed to successfully login to a website', created from Create New > Record in My Vault", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/kv-create-edit-form.hbs:42 KvObjectEditor free key/value rows; ui/lib/kv/addon/components/kv-create-edit-form.js:108 api.secrets.kvV2Write(path, backend, payload) | docs: https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2 Note: The KV create page stores any key/value set at a path, so a login can be kept as username/password/url keys, but there is no login item type and no personal vault (per-user paths need templated policies)." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqNieuwGeheim", + "KqGeheim" + ] } }, { @@ -492,6 +499,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:40 PATCH /api/1.0/password/update; src/lib/Controller/Api/PasswordApiController.php:195 update(); src/js/Manager/PasswordManager.js:118 API.updatePassword; src/lib/Encryption/Object/SseV1Encryption.php:31 server side encryption of revisions (SseV2/SseV3 alongside) Note: Edit goes through the update route, which stores a new revision encrypted with SSE (and CSE when the user enabled it).", "keeper": "https://docs.keeper.io/enterprise-guide/keeper-encryption-model : records are encrypted and decrypted on the device with a per-record AES-256 key (zero-knowledge); edit and save in the web vault", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:20 secret.details.edit route (create new version); ui/lib/kv/addon/components/kv-create-edit-form.js:108 kvV2Write; vault/barrier_aes_gcm.go:846 AESGCMBarrier.Put encrypts every storage entry (encrypt at :1026) Note: Editing writes a new KV version through the UI or CLI, and every storage write goes through the AES-GCM barrier." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -525,6 +536,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:41 DELETE /api/1.0/password/delete; src/lib/Controller/Api/AbstractObjectApiController.php:168 delete() moves to trash, deletes when already trashed; src/js/Manager/PasswordManager.js:263 deletePassword(); src/vue/Components/ContentList/Item/PasswordItem/PasswordItemActionMenu.vue:119 delete action Note: Delete moves the item to trash, a second delete from trash removes it for good.", "keeper": "https://docs.keeper.io/user-guides/web-vault#delete-a-record-or-folder : 'Right-click on a record or folder and select Delete'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/kv-delete-modal.js:23 modes delete, delete-metadata, destroy; ui/lib/kv/addon/components/page/secret/details.js:137 api.secrets.kvV2Delete Note: The KV details page offers delete, destroy and delete-all-metadata, gated by the caller's policy capabilities." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -556,6 +571,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:42 PATCH /api/1.0/password/restore; src/lib/Controller/Api/AbstractObjectApiController.php:204 restore() clones revision with trashed=false; src/vue/Section/Trash.vue:53-63 restorePasswordAction for passwords, :17 folders, :35 tags; src/lib/Helper/CleanUp/CleanDeletedEntitiesHelper.php:158 purge job Note: A Trash section lists trashed passwords, folders and tags with a restore action.", "keeper": "https://docs.keeper.io/user-guides/web-vault#deleted-items : 'view and restore previously deleted records from the Deleted Items section'; admins set retention via enforcement policy (https://docs.keeper.io/enterprise-guide/roles/enforcement-policies)", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/secret/details.hbs:75 Undelete button; ui/lib/kv/addon/components/page/secret/details.js:121 api.secrets.kvV2UndeleteVersions Note: Soft-deleted versions stay recoverable with Undelete until destroyed. There is no trash-bin list view; you undelete from the secret's own page." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqPrullenbak" } }, { @@ -589,6 +608,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:48 POST /api/1.0/folder/create; src/lib/Controller/Api/FolderApiController.php:96 create() with parent; src/js/Manager/FolderManager.js:19 createFolder(parent); src/js/Helper/router.js Folders route /folders/:folder? Note: Folders have a parent field and the Folders section browses the hierarchy.", "keeper": "https://docs.keeper.io/user-guides/web-vault#organization : 'organize your records into folders and subfolders'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:13 list-directory route /list/*path_to_secret; ui/lib/kv/addon/components/page/list.hbs:103 folder icon for directory keys, :120 link to list-directory Note: Paths with slashes render as nested folders in the KV list page." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqGeheimen", + "KqKluisBewerken" + ] } }, { @@ -622,6 +648,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/PasswordManager.js:193 movePassword(password, folder); src/vue/Components/ContentList/Item/PasswordItem/PasswordItemActionMenu.vue:67 Move action; src/js/Manager/DragManager.js:11 drag and drop; src/js/Actions/BatchActions/MoveItemsBatchAction.js batch move Note: Move is available from the item menu, by drag and drop, and as a batch action.", "keeper": "https://docs.keeper.io/user-guides/web-vault#move-shortcut-and-more : 'Drag and drop the record(s) ... into a folder or subfolder and click Move'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:8 KV routes are list, create, details, edit, patch, metadata, paths, configuration (no move); ui/lib/kv/addon/components/kv-create-edit-form.hbs:12 path is read-only when editing; searched 'rename', 'Move ' in ui/app ui/lib/kv ui/lib/core templates: no secret move Note: The path of an existing secret cannot be changed in the UI and the KV API has no move, so a move is copy, write and delete by hand." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqVerplaatsen" } }, { @@ -655,6 +685,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/SearchManager.js:103 _checkIfObjectMatchesQuery() uses entry[field].indexOf(search) at :116; :171 _processQuery() field:value syntax; :30 default fields website, username, url, notes, label Note: Search as you type is plain substring matching over several fields with a field:value syntax. No fuzzy or typo-tolerant matching (searched 'fuzz', 'levenshtein', 'similar' in src/js src/lib: no match).", "keeper": "https://docs.keeper.io/user-guides/web-vault#advanced-filters-and-quick-search : quick search and advanced filters; release notes describe case-insensitive partial-word matching, typo tolerance is not documented", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/list.hbs:51 KvListFilter in the list toolbar; ui/lib/kv/addon/routes/list-directory.js:33 paginate(keys, {filter: pageFilter}); ui/lib/core/addon/utils/paginate-list.ts:42 filterValue.toLowerCase().includes(filter.toLowerCase()); searched 'fuzzy', 'levenshtein', 'typo' in ui/app ui/lib: only a comment in ui/app/services/pagination.js:65 Note: The KV list filters by case-insensitive substring of the key name, only within the current directory level. No typo tolerance and no search across the whole vault, so partial." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimen" } }, { @@ -688,6 +722,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/SearchManager.js:38 subscribe('nextcloud:unified-search.search') runs the in-app search; :46 unified-search.reset; src/lib/AppInfo/Application.php:147 registers only a dashboard widget, no search provider Note: Typing in the Nextcloud header search bar while inside the Passwords app filters the vault, but there is no server search provider, so the bar finds nothing from other apps' pages (searched 'registerSearchProvider', 'ISearchProvider' in src/lib: no match). Moved up from no because the header bar does reach the vault in-app.", "keeper": "https://docs.keeper.io/enterprise-guide/integrations : Keeper is a standalone vault; its integration list has no Nextcloud search integration", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'nextcloud' in all .go .hbs .ts .js files: no match Note: Vault is a standalone server with its own UI; nothing integrates with a Nextcloud search bar." + }, + "screen": { + "board": null, + "reason": "Nextcloud's own unified search bar shows the results; no Keepiq board" } }, { @@ -719,6 +757,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Section/Favorites.vue:32 API.findPasswords({favorite: true}); src/vue/Components/ContentList/Item/PasswordItem/PasswordItemActionMenu.vue:35 favorite toggle; src/js/Actions/BatchActions/FavoriteItemsBatchAction.js batch favourite Note: Favourite flag on passwords and folders, with a Favorites section that filters on it.", "keeper": "https://docs.keeper.io/user-guides/web-vault#favorites : 'Record Favorites are used to easily identify your most frequently used records. Right-click on a record and select Add to Favorites'", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'favorite', 'favourite', 'bookmark', 'star' in ui/app/templates ui/app/components ui/lib/kv/addon ui/lib/core/addon/components: no match; ui/lib/kv/addon/routes.js:8 no favourites route Note: The source has no favourites or pinning of secrets, so the structural unknown settles to no." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimen" } }, { @@ -750,6 +792,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php tag_api create/update/delete; src/vue/Section/Tags.vue:51 find passwords by tag; src/vue/Dialog/CreatePassword/TagsField.vue; src/js/Actions/BatchActions/ManageTagsBatchAction.js Note: Tags are first class objects, set in the password dialog or by batch, and the Tags section lists passwords per tag.", "keeper": "not checked: no tag or label feature found in the web vault guide; the docs assistant could not answer", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/kv-metadata-fields.hbs:8 custom_metadata editor; ui/lib/kv/addon/components/page/secret/metadata/details.hbs:67 lists custom metadata key/values; ui/lib/kv/addon/routes/list-directory.js:33 list filter works on key names only Note: Custom metadata can serve as labels on a secret, but nothing filters the list by it." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqGeheimen", + "KqBulkLabels" + ] } }, { @@ -783,6 +832,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 types text, secret, email, url, file, data; src/vue/Dialog/CreatePassword.vue:132 up to 20 custom fields; src/vue/Dialog/CreatePassword/CustomField.vue:78 hidden (data) fields shown only with client.ui.custom.fields.show.hidden Note: Up to 20 custom fields per password, with a secret type and hidden data fields.", "keeper": "https://docs.keeper.io/user-guides/web-vault#custom-fields : custom fields incl. 'Hidden Field', 'Security Question & Answer', 'Multi-line Text'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/kv-object-editor.hbs:25 free key input per row, :36 MaskedInput for values when @isMasked; ui/lib/kv/addon/components/kv-create-edit-form.hbs:42 KvObjectEditor Note: A KV secret is any set of named fields chosen by the writer, and the values are masked in the form." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqNieuwGeheim", + "KqGeheim" + ] } }, { @@ -816,6 +872,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/CreatePassword/NotesField.vue:30 MaxNotesLength = 4096; src/lib/Services/ValidationService.php:54 'Field \"password\" can not be empty' Note: Notes live on a password entry (markdown, 4096 chars) and the server rejects an entry without a password, so there is no standalone secure note item.", "keeper": "https://docs.keeper.io/user-guides/record-types : 'Secure Note: Secure information that is masked when the record is viewed'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/kv-object-editor.hbs:43 multi-line Textarea value; ui/lib/kv/addon/components/kv-create-edit-form.js:108 kvV2Write of arbitrary payload (JSON mode also available) Note: Any key/value or JSON payload can be stored, so a note is just a secret with a text field." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqNieuwGeheim" } }, { @@ -849,6 +909,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 only text, secret, email, url, file, data field types; src/js/Helper/Import/BitwardenConversionHelper.js:84 object types Logins, Notes, Credit Cards, Identities flattened; :231 card fields mapped onto a password entry Note: The only item is a password entry. Imported Bitwarden cards and identities are flattened into password entries with custom fields, which is not a typed card or identity item.", "keeper": "https://docs.keeper.io/user-guides/record-types : Payment Card, Bank Account, Driver's License, Passport, Identity Card record types", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/kv-object-editor.hbs:25 untyped key/value rows only; searched 'card', 'identity document', 'passport' in ui/lib/kv/addon: no item templates Note: KV has no typed item templates, so cards and identity documents are only free key/value data." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqNieuwGeheimTypen", + "KqGeheimVarianten" + ] } }, { @@ -880,6 +947,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 field types; searched 'ssh', 'keypair', 'private key' in src/lib src/js src/vue: only share-related matches (e.g. 'shared') Note: No SSH key item type; a key can only be pasted into a secret or text custom field.", "keeper": "https://docs.keeper.io/user-guides/record-types : 'SSH Key: SSH information, such as public and private key strings'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/templates/vault/cluster/secrets/backend/sign.hbs:6 Sign SSH Key page (router.js ssh sign route); builtin/logical/ssh signs keys and issues OTPs; ui/lib/core/addon/components/kv-object-editor.hbs:43 key pair only as untyped KV text Note: The SSH engine signs keys rather than storing a key pair as an item; a key pair can be kept as untyped KV text." + }, + "screen": { + "board": null, + "reason": "not designed yet: no SSH key form on any board" } }, { @@ -921,6 +992,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'passkey', 'webauthn credential' storage in src/lib src/js src/vue: 'passkey' no match; WebAuthn code (src/js/Actions/WebAuthn/WebAuthnInitializeAction.js) is for unlocking the app, not stored items Note: No passkey item type in the vault.", "keeper": "https://docs.keeper.io/user-guides/passkeys : 'In the Keeper Vault, you will see a Passkey field attached to the record'; stored as a field on a record rather than its own type", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'passkey', 'webauthn', 'fido' in all .go .hbs .ts files: no match Note: No passkey item type or WebAuthn handling exists in the source, so the unknown settles to no." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimVarianten" } }, { @@ -954,6 +1029,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Helper/Import/BitwardenConversionHelper.js:218-219 imported totp stored as a plain 'otp' secret custom field; searched 'otpauth', 'totp' code generation in src/js src/vue: no generator Note: TOTP seeds from imports land as a secret custom field; nothing computes or shows the current code.", "keeper": "https://docs.keeper.io/user-guides/web-vault#two-factor-codes-for-totp : 'click Add Two-Factor Code to store ... TOTP ... in your Keeper records'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/totp-edit.hbs:24 Totp::KeyCreate (seed or generated key), :22 KeyQrCode; ui/app/templates/vault/cluster/secrets/backend/credentials.hbs:14 GenerateCredentialsTotp; ui/app/components/generate-credentials-totp.hbs:14 shows current code with validity progress; builtin/logical/totp Note: The TOTP engine stores a seed and shows the current code in the UI, but as a separate engine, not attached to a login item." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimVarianten" } }, { @@ -987,6 +1066,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/CreatePassword/CustomFields/FileCustomField.vue:48 MessageService.ncFilePicker(); src/vue/Components/Sidebar/PasswordSidebar/Details/DetailField.vue:58 file field resolves to server.baseUrl.webdav path Note: A file custom field links to a file in the user's Nextcloud Files by path; the file itself is not stored or encrypted in the vault.", "keeper": "https://docs.keeper.io/user-guides/secure-file-storage : upload files to records, encrypted; Secure File Storage add-on storage quota", "hashicorp-vault": "hashicorp/vault@v2.1.1 command/kv_put.go:49 'The data can also be consumed from a file on disk by prefixing with the \"@\" symbol'; command/base_helpers.go:136 kvbuilder parses key=@file; searched 'upload', 'attachment' in ui/lib/kv/addon ui/app/components: no KV file upload Note: The CLI can read a file's contents into a secret value, which the barrier encrypts, but there is no attachment concept and no upload in the web UI, so partial." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -1020,6 +1103,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Sidebar/PasswordSidebar/Tabs/Revisions.vue; src/vue/Components/Sidebar/PasswordSidebar/Revisions/Revision.vue:29-33 'Restore revision'; src/js/Manager/PasswordManager.js:337 API.restorePassword(id, revision.id); src/appinfo/routes.php:42 PATCH /api/1.0/password/restore Note: Every change creates a revision; the sidebar Revisions tab lists and restores them.", "keeper": "https://docs.keeper.io/user-guides/web-vault#record-history : 'view and restore previous versions of a record ... Click Restore'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:24 secret.metadata.versions and diff routes; ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs:52 Version diff, :132 'Create new version from' an older version Note: Version history, diff between versions and restore via a new version from an old one are all in the UI; CLI has kv rollback." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -1053,6 +1140,17 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/ContentList/Item/HeaderItem/BatchActionToolbar.vue:66-72 imports Delete, Move, Favorite, ManageTags, RestoreTrash and Share batch actions; src/js/Actions/BatchActions/ShareBatchAction.js:23 Note: Batch move, delete, favourite, tag, restore and share are in 2026.9.0; there is no batch export, export is a separate whole-vault flow.", "keeper": "https://docs.keeper.io/user-guides/web-vault#delete-multiple-records-or-folders-at-once : multi-select to move and delete; Share Admins can change owner of multiple records; bulk share or export of a selection not documented", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/list.hbs:94 each row is a LinkedBlock with a per-row Manage secret dropdown (:111), no checkbox; searched 'select all', 'bulk', 'Checkbox' in ui/lib/kv/addon and ui/app/components: no multi-select Note: The KV list acts on one secret at a time; there is no multi-select or bulk action." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqGeheimen", + "KqVerplaatsen", + "KqBulkLabels", + "KqBulkVerwijderen", + "KqBulkDelen", + "KqBulkRapport" + ] } }, { @@ -1086,6 +1184,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Section/Settings.vue:174-175 'Initial section' select bound to client.ui.section.default; src/js/Init/Application.js:186 applies it on load Note: The default view is configurable; there is only one item type so there is nothing to choose there.", "keeper": "https://docs.keeper.io/user-guides/web-vault#create-a-record : Record Type dropdown defaults to Login; only the order of record types can be set in vault settings (https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#record-types), no default type or view setting", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:8 no preferences route; ui/app/router.js settings routes are seal and auth only; KV has no item types to default (kv-object-editor.hbs:25 untyped rows) Note: There are no item types and no per-user view preferences in the UI." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqInstellingen" } }, { @@ -1119,6 +1221,10 @@ "keeper": "https://docs.keeper.io/user-guides/web-vault#advanced-filters-and-quick-search : 'Quick search shows recently viewed items'; no dashboard", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/dashboard/overview.hbs:23 SecretsEngines, :48 QuickActions, :50 ClientCount, :53 LearnMore widgets; searched 'recent' in ui/app/services ui/lib/core/addon/services and ui/app/components/dashboard: no recently used list Note: The dashboard lists mounted engines and quick actions, not recently used secrets.", "passbolt": "searched 'last_used', 'recently used', 'dashboard' in passbolt_api src/ plugins/ and passbolt_styleguide src/: no usage tracking; only a 'Recently modified' filter (passbolt/passbolt_styleguide@v5.16.0 src/react-quickaccess/components/FilterResourcesByRecentlyModifiedPage/FilterResourcesByRecentlyModifiedPage.js:193) sorted by modified date Note: Passbolt has no dashboard and no recently used list; the nearest is a recently modified filter in quickaccess and a modified sort in the workspace." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqDashboard" } }, { @@ -1149,6 +1255,10 @@ "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/secrets-manager-command-line-interface/secret-command : clone creates a new record from an existing one; web vault 'Create Duplicate' in the right-click menu (enforcement policy covers 'duplication of records', https://docs.keeper.io/enterprise-guide/roles/enforcement-policies)", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs:132 'Create new version from' copies an old version into the same path only; searched 'clone', 'Duplicate', 'Copy from' in ui templates: only a duplicate-keys warning (kv-object-editor.hbs:86) Note: You can seed a new version from an older one of the same secret, but not clone a secret to a new path, so no.", "passbolt": "searched 'clone', 'duplicate', 'copy as new' in passbolt_styleguide src/react-extension/components/Resource and src/react-quickaccess: only code comments about cloning entities (CreateResource.js:121), no user action; no API route Note: There is no duplicate or clone action on a resource." + }, + "screen": { + "board": null, + "reason": "not designed yet: clone is not on KqGeheim" } }, { @@ -1182,6 +1292,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/SearchManager.js:289-298 Ctrl+F opens search, typing starts live search; src/vue/Dialog/CreatePassword.vue:22 Ctrl+S saves; src/vue/Components/Navigation/AppNavigationSearch.vue:25-26 Esc/Enter Note: There are a few shortcuts (Ctrl+F, Ctrl+S, typing to search) but no keyboard navigation of the list itself (no arrow key or row focus handlers found in src/vue).", "keeper": "https://docs.keeper.io/keeperpam/commander-cli/commander-installation-setup/supershell-tui-vault-browser : SuperShell TUI vault browser with keyboard navigation; web vault only documents KeeperFill shortcuts (https://docs.keeper.io/user-guides/tips-and-tricks/keyboard-shortcuts)", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/linked-block.hbs:6 list row is a div role=link with a click handler only (no tabindex); ui/lib/kv/addon/components/page/list.hbs:111 HDS dropdown toggle per row (focusable) gives Details/Edit; ui/lib/kv/addon/components/kv-list-filter.js:55 Esc in filter goes up a directory; searched 'hotkey', 'ember-keyboard', 'shortcut' in ui: no match Note: The list can be driven by keyboard through each row's HDS dropdown and the filter input, but the row link itself is a click-only div and there are no shortcuts." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimen" } }, { @@ -1215,6 +1329,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/ContentList/Item/Header.vue:8-18 sort by label or edited, ascending or descending; src/js/Services/SettingsService.js:12-13 client.ui.sort.field default label Note: Lists sort by name or date changed; there is no sort by date added or date last used.", "onepassword": "https://support.1password.com/getting-started-windows/ : 'you can sort your items by their title, the date you last made changes to them, or how frequently you use them'. Sort by date added is not listed.", "keeper": "https://docs.keeper.io/user-guides/web-vault : records can be sorted by most recently used, name or date modified in the web vault and browser extension; sorting by date added is not described." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimen" } }, { @@ -1248,6 +1366,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/SecurityCheck/UserRulesSecurityCheck.php:94-96 hasDuplicates() flags items with the same password hash; searched 'merge' in src/js/Manager src/vue/Section: only import mode 'Merge with existing' (src/vue/Components/Import.vue:108) Note: Items that reuse a password are flagged as duplicates, but there is no find-duplicate-items-and-merge tool.", "onepassword": "https://support.1password.com/watchtower/ : Watchtower lists 'duplicate items' so they can be deleted; community answers (https://www.1password.community/discussions/1password/merging-duplicated-password/75287) confirm only exact duplicates in one vault are found and there is no merge.", "keeper": "https://docs.keeper.io/keeperpam/commander-cli/command-reference/record-commands : Commander 'find-duplicates' matches on titles, logins, passwords or URLs and '--merge' consolidates them; the vault UI has no duplicate finder, so this is CLI only." + }, + "screen": { + "board": null, + "reason": "not designed yet: no duplicates section on KqWachtwoordgezondheid" } }, { @@ -1281,6 +1403,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Sidebar/PasswordSidebar/Details/DetailField.vue:58 file fields link to the Nextcloud WebDAV path Note: A file field opens the linked file in Nextcloud, where Files can preview it; the vault itself stores no attachment to preview.", "onepassword": "https://www.1password.community/1password-at-home-31/preview-documents-in-1password-on-windows-17129 : Preview is offered in the Mac, iOS and Android apps; on Windows and Linux a document must be downloaded (images preview on 1Password.com). See also https://support.1password.com/files/", "keeper": "not checked: https://docs.keeper.io/enterprise-guide/secure-file-storage read; it describes storing and downloading files and does not state whether the vault previews them." + }, + "screen": { + "board": null, + "reason": "not designed yet: no attachment preview on KqGeheim" } }, { @@ -1314,6 +1440,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Controller/Api/PasswordApiController.php:146 create(hidden) flag; src/lib/Controller/Api/AbstractObjectApiController.php:101,122 lists and finds filter hidden=false; src/js/Actions/User/RecoverHiddenItemsAction.js recovers hidden items Note: The API has a hidden flag that removes an item from lists and search without deleting it, but the web UI offers no archive action; clients set it (e.g. private mode).", "onepassword": "https://support.1password.com/archive-delete-items/ : 'Learn how to archive and delete items to keep your information organized'; archived items are kept out of the everyday item list and suggestions.", "keeper": "not checked: no archive state for records found in https://docs.keeper.io/user-guides/web-vault or the record docs; only delete and the Deleted Items folder are described." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqArchief" } }, { @@ -1347,6 +1477,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/CreatePassword.vue:28 one url field; src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:17 extra 'Website' (url) custom fields Note: One primary URL per item; extra addresses can go in Website custom fields, but whether clients match on them is extension behaviour not in this repo.", "onepassword": "https://support.1password.com/autofill-behavior/ : 'If you only have one website field in an item, learn how to add more'; each website field has its own autofill behaviour.", "keeper": "https://docs.keeper.io/user-guides/tips-and-tricks/one-record-with-multiple-url-domains : store multiple website domains in one record through additional URL fields; KeeperFill matches every one." + }, + "screen": { + "board": null, + "reason": "not designed yet: KqGeheim shows one website address" } }, { @@ -1378,6 +1512,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/ViewRevision.vue shows one revision's fields; searched 'compare', 'diff' in src/vue/Dialog/ViewRevision.vue and src/vue/Components/Sidebar/PasswordSidebar: no match Note: You can view any one revision but not compare two side by side.", "onepassword": "https://support.1password.com/item-history/ : 'View previous versions' opens one earlier version of an item and 'Restore' brings it back; no side-by-side comparison is described.", "keeper": "https://docs.keeper.io/keeperpam/commander-cli/command-reference/record-commands : the Commander record-history command can 'see differences in between revision' (diff action); in the vault UI (https://docs.keeper.io/user-guides/web-vault) Record History opens one old version at a time and restores it, with no side-by-side view described." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -1409,6 +1547,10 @@ "hashicorp-vault": "searched 'qrcode', 'print(' in ui/app ui/lib: only the MFA enrolment QR code (ui/app/components/mfa/form/self-enroll.hbs). Rated by the lane from the source.", "passbolt": "searched 'qrcode', 'print(' in passbolt_styleguide src/react-extension: only the TOTP setup scanner (src/react-extension/components/MFA/TotpSetup/ScanTotpCode/ScanTotpCode.js). Rated by the lane from the source.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Actions/Password/PrintPasswordAction.js:27 print(); src/js/Actions/Password/PasswordActions.js:82 qrcode(); src/vue/Components/ContentList/Item/PasswordItem/PasswordItemActionMenu.vue:105,111 Note: proposed by the source reader as a capability no row covered." + }, + "screen": { + "board": null, + "reason": "not designed yet: no print or QR view" } }, { @@ -1440,6 +1582,10 @@ "hashicorp-vault": "searched 'favicon' in ui/app ui/lib: only the UI's own favicon (ui/app/index.html). Rated by the lane from the source.", "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/shared/components/Icons/ResourceIcon.js:29 icon comes from the item metadata (a KeePass icon set and colour) or the resource type default; searched 'favicon' in src/react-extension src/shared: no match. Rated by the lane from the source.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php service/favicon/{domain}/{size} and service/preview/{domain}/{view}; src/vue/Components/Sidebar/PasswordSidebar/Preview.vue:2; src/lib/Provider/Favicon, src/lib/Provider/Preview Note: proposed by the source reader as a capability no row covered. The 2026.7.0 changelog entry is a fix to favicon requests, so it shows the feature is live, not new." + }, + "screen": { + "board": null, + "reason": "not designed yet: no site icons or previews" } }, { @@ -1473,6 +1619,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Services/ClientService.js:12 passwords-client library does CSEv1 in the browser; src/js/Manager/EncryptionManager.js:13 install() enables client side encryption; src/lib/Controller/Api/PasswordApiController.php:138 cseType defaults to EncryptionService::DEFAULT_CSE_ENCRYPTION; src/lib/Encryption/Object/SseV1Encryption.php:31 server side encryption otherwise Note: Client side encryption exists but is opt-in; without it the server encrypts and can read values. The crypto itself lives in the external passwords-client npm package. Sharing an end-to-end encrypted item downgrades it to server-side encryption (src/js/Actions/Share/CreateShareAction.js:45,90; src/lib/Helper/Sharing/CreatePasswordShareHelper.php:187).", "keeper": "https://docs.keeper.io/enterprise-guide/keeper-encryption-model : zero-knowledge, end-to-end encryption; 'the login credentials never leave the device', records decrypted locally", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/barrier_aes_gcm.go:846 AESGCMBarrier.Put encrypts server-side before storage; ui/lib/kv/addon/components/kv-create-edit-form.js:108 UI sends plaintext payload to kvV2Write Note: Encryption happens in the server's barrier; the browser and CLI send plaintext over TLS, so the server can read values." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (encryption in the browser; the user meets it at KqOntgrendelen)" } }, { @@ -1506,6 +1656,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/SetupDialog.vue:61-62 inserts the 'encryption' slide on first run when !API.hasEncryption; :200 skippable; :167 EncryptionManager.install(); src/appinfo/routes.php:33 POST /api/1.0/account/challenge/set stores a challenge, not the passphrase Note: The first-run wizard offers to set an encryption passphrase that the server only knows as a challenge, but the step is skippable, so a vault can run without one.", "keeper": "https://docs.keeper.io/enterprise-guide/keeper-encryption-model : 'Master Password Key' is derived with PBKDF2 (1,000,000 iterations) and 'is NOT stored anywhere'", "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/credential/userpass/path_login.go:16 bcrypt compare of the password stored on the server; vault/barrier_aes_gcm.go:846 data keys live in the server barrier, not derived from a user secret Note: Userpass passwords are stored (bcrypt) and checked by the server; there is no client-held master key." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqKluisInstellen" } }, { @@ -1539,6 +1693,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/EncryptionManager.js:136-146 isPassphraseSecure(): length >= 12 and hash not in the breached hash list; src/vue/Dialog/SetupDialog/Encryption.vue:12 pattern .{12,} Note: The passphrase must be at least 12 characters and not appear in the breach database; there is no strength score or entropy check.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#login-settings : Master Password Complexity policy (length, digits, upper, lower, special); 'When creating the Keeper Vault, the user will be required to adhere to the complexity requirements'", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'password_policy', 'strength', 'min length' in builtin/credential/userpass/*.go: no match; builtin/credential/userpass/path_login.go:16 only bcrypt verification Note: Userpass accepts any password an admin or user sets; password policies only govern generated passwords, so no." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqKluisInstellen", + "KqBeheerBeleid" + ] } }, { @@ -1572,6 +1733,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/EncryptionManager.js:47-54 update(password, oldPassword) calls API.setAccountChallenge and updates the WebAuthn credential; :80-112 change passphrase form; src/appinfo/routes.php:33 POST /api/1.0/account/challenge/set Note: The passphrase can be changed with the old one; the keychain is re-wrapped so secrets stay readable.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#login-settings : Master Password Expiration forces users to choose a new Master Password on next login; the vault keys are re-wrapped, not lost", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/sidebar/user-menu.hbs:27 Reset password item; ui/app/components/page/userpass-reset-password.js:35 api.auth.userpassResetPassword Note: A userpass user can change their own login password from the user menu without data loss, because the password is a login credential and not a data key." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqInstellingen" } }, { @@ -1613,6 +1778,10 @@ "keeper": "not checked: no user-facing re-key-all-records operation found in the encryption model or vault guide", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:2120 sys/rotate; vault/logical_system.go:4761 handleRotate adds a new barrier key term (older terms stay for decrypting old data) Note: Rotation adds a new barrier key for new writes; existing data is not re-keyed in bulk, so partial.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/EncryptionManager.js:47-54 update() only re-wraps the keychain via setAccountChallenge; :28-48 install(encrypt) re-encrypts all objects only when first enabling CSE; searched 're-encrypt', 'reencrypt', 'rotate', 'rekey' in src/lib src/js src/vue: no match Note: There is no flow to re-key every secret to a new key after a compromise; a passphrase change keeps the same object keys. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqHerstelNaCompromittering" } }, { @@ -1646,6 +1815,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/KeepAliveManager.js:155-172 _checkLockdown() closes the session and routes to Authorize after user.session.lifetime; src/vue/Components/SessionTimeout.vue:1-5 countdown; src/appinfo/routes.php:23 GET /api/1.0/session/close Note: The vault locks itself after inactivity when a passphrase is set. No manual lock button found in src/vue (the close route is only called by the timeout).", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#account-settings : Logout Timer 'to automatically log out a user from Keeper when they are inactive' for Web, Mobile and Desktop", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/sidebar/user-menu.hbs:63 Log out; ui/app/services/auth.js:32 IDLE_TIMEOUT 3 min, :382 stops token renewal after idle so the session ends at token expiry; ui/app/components/token-expire-warning.js:12 expiry warning Note: The UI has a manual log out and stops renewing the token after three idle minutes, so an inactive session ends when the token TTL runs out. Moved to yes on the code, with the note that the idle window is the token TTL, not a separate setting." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqOntgrendelen", + "KqInstellingen" + ] } }, { @@ -1679,6 +1855,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Section/Settings.vue:92-106 'End session after' select (1 to 60 minutes) bound to user.session.lifetime; src/lib/Helper/Settings/UserSettingsHelper.php session/lifetime default 600 Note: Users choose the session lifetime in advanced settings when encryption is on.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#account-settings : the admin timer is the maximum; users choose their own timer up to it ('If a Keeper user's current timer is set greater than this value, it will be reduced')", "hashicorp-vault": "hashicorp/vault@v2.1.1 command/token_create.go:90 -ttl flag; vault/token_store.go:3172 requested ttl capped by role and mount max; ui/app/components/auth has no TTL field on the login form Note: A user can mint a token with a shorter TTL via CLI or API, within admin-set maxima, but the UI login offers no session length choice." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqInstellingen" } }, { @@ -1720,6 +1900,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Actions/WebAuthn/WebAuthnInitializeAction.js:44-51 stores the encryption passphrase as a browser PasswordCredential via navigator.credentials.store; src/vue/Section/Authorize.vue:81 WebAuthnAuthorizeAction on the login page Note: What the app calls WebAuthn login is the Credential Management API storing the passphrase in the browser, not a passkey that unlocks the vault cryptographically.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#biometric-login-with-a-passkey : biometric login with a passkey 'replaces all traditional login methods - including master password, SSO, and 2FA' (platform authenticators only)", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/login_mfa.go:50 MFA method types totp, duo, okta, pingid only; searched 'passkey', 'webauthn', 'fido' in all .go .hbs .ts: no match Note: No passkey or WebAuthn login exists in the source, so no." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqOntgrendelen", + "KqInstellingen" + ] } }, { @@ -1751,6 +1938,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Actions/WebAuthn/WebAuthnInitializeAction.js:20-21 requires window.PasswordCredential; :51 navigator.credentials.store Note: Biometric unlock only happens if the browser guards its stored credential with the device authenticator; the app itself does no biometric check.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#device-biometrics : 'Keeper natively supports Windows Hello, Touch ID, Face ID and Android biometrics'", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'webauthn', 'biometric', 'touchid', 'fingerprint' in .go .hbs .ts: no login path; vault/login_mfa.go:50 only totp, duo, okta, pingid Note: No biometric unlock; a push MFA on a phone (Duo, Okta) is the nearest thing and is a second factor, not an unlock." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" } }, { @@ -1811,6 +2002,14 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'recovery', 'recover' in src/lib src/js src/vue: only RecoverHiddenItemsAction (restores hidden items), no admin key escrow; src/js/Actions/User/UserAccountResetAction.js resets the account Note: No admin recovery of a forgotten passphrase; the only way out is an account reset that deletes the data.", "keeper": "https://docs.keeper.io/enterprise-guide/account-transfer-policy : zero-knowledge means an admin cannot reset the master password; Account Transfer moves a locked user's vault to another user so 'an Admin can assist a user who is unable to recover their vault'", "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/credential/userpass/path_user_password.go:39 users//password; ui/app/router.js access.method.item edit route for userpass users Note: An admin can set a new password for any userpass user; data is server-encrypted so nothing is lost." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqBeheerMensen", + "KqOntgrendelen", + "KqKluisInstellen" + ] } }, { @@ -1842,6 +2041,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'recovery', 'recovery code', 'kit' in src/lib src/js src/vue: no match; src/appinfo/routes.php:30-31 /api/1.0/account/reset wipes the account Note: No recovery code or kit; a lost passphrase means resetting the account.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#account-recovery : 'A 24-word auto-generated recovery phrase is a break-glass method of recovering a Keeper Vault if the user forgets their master password'", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'recovery code', 'backup code' in .go and .hbs: no match; recovery keys in vault/ are operator unseal keys, not per-user Note: There is no per-user recovery code or kit; a lost login is reset by an admin." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExporteren" } }, { @@ -1874,6 +2077,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Sidebar/PasswordSidebar/Tabs/Details.vue:32-39 shows 'Encryption applied on server' and 'on client' types (e.g. SSEv1r2, CSEv1r1) when advanced details are on Note: The details tab names the encryption scheme per item but not algorithms or key sizes.", "keeper": "https://docs.keeper.io/enterprise-guide/keeper-encryption-model : AES-256 record keys, PBKDF2 1,000,000 iterations, ECC secp256r1, RSA-2048 documented", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system.go:4674 handleKeyStatus returns term, install_time, encryptions only (no algorithm); vault/barrier_aes_gcm.go:69 AES-GCM is fixed in code | docs: https://developer.hashicorp.com/vault/docs/internals/security Note: The algorithm (AES-256-GCM) is fixed and documented, but key-status reports only the key term and counts, and no UI page shows the barrier algorithm. Moved to partial because the product does not show it, only the docs do." + }, + "screen": { + "board": null, + "reason": "not designed yet: no encryption details view in KqInstellingen" } }, { @@ -1907,6 +2114,10 @@ "keeper": "not checked: no option to wrap user vault keys with an organisation-issued certificate found", "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/credential/cert (TLS certificate auth method); builtin/logical/pki issues certificates from Vault's own CA; vault/barrier_aes_gcm.go:846 data is protected by the barrier key, not by a per-user key Note: Users can log in with certificates from Vault's own PKI via cert auth, but the certificate authenticates rather than wrapping a per-user vault key.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Encryption/Keychain/SseV2KeychainEncryption.php and src/lib/Encryption/Object/SseV1Encryption.php:31 derive keys from server secrets and user passphrase; searched 'certificate', 'x509', 'openssl_csr', 'CA' in src/lib: no certificate authority code Note: User keys are protected by server-held secrets or a passphrase-derived key, never by a certificate from an internal CA. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAlgemeen" } }, { @@ -1947,6 +2158,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 src/Model/Table/GpgkeysTable.php:161 isUnique fingerprint, one gpgkey per user (docs: keys inspector); plugins/PassboltCe/Metadata/src/Model/Table/MetadataKeysTable.php:198 MaxNoOfActiveMetadataKeysRule, src/Model/Rule/MaxNoOfActiveMetadataKeysRule.php:46 allows up to two active shared metadata keys Note: A user account holds one key, but shared metadata keys may deliberately have two active at once during rotation.", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/keyring.go:43 single activeTerm field, :129 adding a key moves activeTerm to the new term (old terms decrypt only); vault/logical_system.go:4674 key-status reports the one active term Note: The barrier keyring holds exactly one active encryption term; rotation replaces it rather than adding a parallel active key. Transit keys follow the same latest-version model.", "nextcloud-passwords": "source silent: src/lib/Encryption/Keychain/SseV2KeychainEncryption.php keeps one keychain per user and type, but there is no concept of multiple active keys to guard against Note: The app has one keychain per user and no key issuance lifecycle, so the row's guarantee does not map onto the code either way." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own" } }, { @@ -1979,6 +2194,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/EncryptionManager.js:220-226 _encryptPassword(): API.createPassword(new CSE copy) then _deleteObject(oldPasswordId) only if create succeeded; :385 _cleanDatabase() deletes old folders and tags afterwards Note: When enabling encryption the new copy is created before the old one is deleted, but the new copy is not decrypted and compared before the delete.", "hashicorp-vault": "not checked: hashicorp/vault@v2.1.1 vault/seal_rewrap_stubs_oss.go:8 and vault/seal_stubs_oss.go:20 seal rewrap is a no-op stub in CE; vault/logical_system_helpers.go:223 sealwrap/rewrap listed among enterprise paths; vault/logical_system.go:4761 barrier rotate does not re-encrypt existing data Note: source silent: the only bulk re-encryption (seal wrap rewrap) is Enterprise code not in the public tree, and CE never re-encrypts barrier data, so whether a rewrap is verified before the old copy goes cannot be read here.", "passbolt": "passbolt/passbolt_browser_extension@v5.16.0 src/all/background_page/service/metadata/rotateMetadata/rotateMetadataKeyService.js:58-74 create key, expire old, rotate all metadata, then delete old; passbolt/passbolt_api@v5.16.0 plugins/PassboltCe/Metadata/src/Service/MetadataKey/MetadataKeyDeleteService.php:71 key must be expired first, :76 refused while items still use the key; src/Service/RotateKey/AbstractMetadataRotateKeyUpdateService.php:114 modified-date conflict check Note: The old metadata key cannot be deleted while any item still uses it, but the re-encrypted payloads are not decrypted and checked before discard." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own" } }, { @@ -2012,6 +2231,10 @@ "keeper": "https://docs.keeper.io/enterprise-guide/account-transfer-policy : admins can 'Lock Account' from User Actions (and unlock it); this locks the user rather than revoking a key", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/token_store.go token revoke and revoke-orphan paths; ui/app/router.js access.leases routes for revoke; builtin/credential/userpass users can be disabled by removing or editing the user Note: Tokens and leases can be revoked; access comes back through a new login, not by reinstating the revoked credential.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'revoke', 'suspend', 'reinstate' in src/lib/Encryption src/lib/Services src/lib/Controller: only UserLoginAttemptHelper destroying login tokens Note: There is no per-user vault key revocation or reinstatement. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerMensen" } }, { @@ -2045,6 +2268,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/User/UserLoginAttemptHelper.php:26 MAX_FAILED_ATTEMPTS = 5; :92-112 registerFailedAttempt() destroys the client's login token and notifies the user; src/lib/Controller/Api/SessionApiController.php:203-221 Note: After five failed unlock attempts the client's app token is revoked automatically; there is no admin action to revoke a user's vault key.", "keeper": "not checked: no admin force-revoke of a compromised user key found", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/templates/vault/cluster/access/leases/list.hbs:65 force revoke prefix gated on forceRevokePrefix capability, :77 revoke prefix Note: An operator can force-revoke leases and tokens immediately in the UI, but there is no 'compromised' flag on a per-user key." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerMensen" } }, { @@ -2108,6 +2335,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Actions/Encryption/CheckPassphrase.js:47 passphrase recheck runs only on login at src/vue/Section/Authorize.vue:211; searched 'reprompt', 'protected' in src/vue/Dialog src/vue/Components/Sidebar: no per-item re-prompt Note: No master password re-prompt per item.", "onepassword": "https://support.1password.com/autofill-confirmation/ : 'Ask before filling' only asks for a confirmation click before filling cards or identities; a 1Password Community request to 'Enter master password again for revealing passwords' (https://1password.community/discussion/30438) was logged as a request, not shipped.", "keeper": "not checked: no per-record master password re-prompt found in https://docs.keeper.io/user-guides/web-vault or https://docs.keeper.io/enterprise-guide/roles/enforcement-policies." + }, + "screen": { + "board": null, + "reason": "not designed yet: KqBevestigMetHoofdwachtwoord confirms actions, not opening one item" } }, { @@ -2233,6 +2464,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Controller/Link/ConnectController.php:136 request() from the new client, :221 confirm() by the signed-in web session, :260 apply(codes); src/vue/Dialog/ConnectClient.vue with ConnectConfirm.vue; :242 new client notification Note: PassLink lets a new extension or app sign in by being approved from a browser where you are already signed in; no admin approval option.", "bitwarden": "bitwarden/server@v2026.9.1 src/Api/Auth/Controllers/AuthRequestsController.cs:78 POST auth-requests, :91 admin-request, :104 PUT {id} (approve); bitwarden/clients@web-v2026.9.0 libs/angular/src/auth/login-approval/login-approval-dialog.component.ts; bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts Note: proposed by the source reader as a capability no row covered.", "hashicorp-vault": "searched 'device approval', 'auth request' in vault/ ui/app: no device concept; control groups approve requests, not devices. Note: rated by the lane from the source." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqApparaatGoedkeuren", + "KqKluisInstellen" + ] } }, { @@ -2266,6 +2504,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:66 POST /api/1.0/share/create; src/lib/Controller/Api/ShareApiController.php:158 create(password, recipient, type, expires, editable, shareable); src/vue/Components/Sidebar/PasswordSidebar/Sharing/ShareEditForm.vue:12 share-user-select, :134 ShareBatchAction Note: Share with a Nextcloud user from the password sidebar, with edit, reshare and expiry options.", "keeper": "https://docs.keeper.io/user-guides/sharing : 'easily share a single record with another Keeper user'; users in the same tenant need no sharing approval (read as: another user of the organisation)", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/policy-form.ts:170 save ACL policy (policies route in ui/app/router.js); ui/app/models/identity/entity.js entity policies attribute; vault/policy.go path capabilities Note: Access to another user's secret is granted by an admin writing a policy on the path and attaching it to that user's entity; the owner has no share action of their own." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -2299,6 +2541,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:71 POST /api/1.0/share/recipients/resolve-group; src/lib/Helper/Sharing/RecipientSearchHelper.php:121-132 resolveGroup() returns current members if groups.enabled and sharer is in the group; src/js/Actions/Share/CreateGroupShareAction.js:33-45 creates one user share per member; src/vue/Components/Sidebar/PasswordSidebar/Sharing/ShareUserSelect.vue:64 groups offered as recipients; src/vue/Dialog/BatchActions/BatchShareDialog.vue:23 group share warning Note: Since 2026.9.0 you can pick a group in the share menu, but the client expands it into individual user shares for today's members; there is no standing group share. Moved from no, the docs FAQ predates this.", "keeper": "https://docs.keeper.io/enterprise-guide/teams : 'Teams can be added to Shared Folders in the vault', teams provisioned from the IdP via SCIM or AD Bridge", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/models/identity/group.js:15 fields name, type, policies, metadata (internal or external group); ui/app/router.js access.identity create/edit routes; vault/identity_store_util.go:3164 refreshExternalGroupMembershipsByEntityID Note: Identity groups, internal or mapped from LDAP/OIDC groups, carry policies that grant a path to every member." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -2340,6 +2586,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Actions/Share/CreateGroupShareAction.js:33 resolves group members once at share time; src/lib/Db/Share.php has a single receiver user; searched 'group' in src/lib/Cron/SynchronizeShares.php: no match Note: Group shares are a one-time fan-out, so later joiners get nothing and there is no approval flow.", "keeper": "https://docs.keeper.io/enterprise-guide/user-and-team-provisioning/approval-queue : SCIM/Bridge team members are queued until an Admin or another team member logs in (or Automator runs) and the Team Key is encrypted to them; approval is by admin or team member, not the record owner", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/identity_store_util.go:3164 external group membership refreshed at login, access immediate; ui/app/router.js control-groups routes (Enterprise feature, stubbed in CE) | docs: https://developer.hashicorp.com/vault/docs/enterprise/control-groups Note: New group members get access at once with no approval step; approval on access exists only as Enterprise control groups, which work per request rather than per new member." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqTeammap" } }, { @@ -2373,6 +2623,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Actions/Share/CreateGroupShareAction.js:33-45 one share per member; searched 'group' in src/lib/EventListener and src/lib/Cron: no group membership listener Note: Shares created through a group stay with each user after they leave the group; nothing removes them.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/nested-share-subfolders : 'If a user's access to a folder is removed, they will also lose access to the records within that folder'; team membership is driven by SCIM/Bridge (https://docs.keeper.io/enterprise-guide/teams)", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/identity_store_util.go:3164 refreshExternalGroupMembershipsByEntityID drops groups missing from the login's group aliases Note: External group membership is recomputed from the identity provider on login and renewal, so leavers lose that group's policies." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (group membership change)" } }, { @@ -2406,6 +2660,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Cron/SynchronizeShares.php:284-288 updatePasswords() runs updateTargetPasswords and updateSourcePasswords; :301 updateTargetPasswords() Note: A background job syncs owner edits to recipients' copies (and edits back when editable).", "keeper": "https://docs.keeper.io/user-guides/sharing : a share grants access to the same record with Can Edit / View Only permissions (not a copy); record shortcuts 'when edited, change together' (https://docs.keeper.io/user-guides/web-vault#move-shortcut-and-more)", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/kv-create-edit-form.js:108 one kvV2Write at one path; vault/policy.go readers are granted the path, not a copy Note: There are no copies: every reader with a policy on the path reads the same current version." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (re-encryption for recipients on save)" } }, { @@ -2438,6 +2696,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:68 DELETE /api/1.0/share/delete; src/vue/Components/Sidebar/PasswordSidebar/Sharing/ShareListItem.vue:185-186 deleteAction() API.deleteShare; src/vue/Components/Sidebar/PasswordSidebar/Sharing/Sharing.vue:21 revoke Note: Revoke from the share list in the sidebar.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/nested-share-subfolders : 'Remove Access: Immediately revoke a user's record/folder access entirely'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/policy-form.ts:170 edit and save a policy; ui/app/models/identity/group.js:42 policies attribute editable on group or entity Note: Removing the path from a policy, or the policy from a group or entity, takes the access away on the next request." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -2470,6 +2732,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Sidebar/PasswordSidebar/Sharing/Sharing.vue:28 share-list-item per share; src/appinfo/routes.php:62 GET /api/1.0/share/list; src/js/Helper/router.js Shares route /shared/:type? Note: The Share tab lists recipients and permissions, and the Shared section lists items shared by and with you.", "keeper": "https://docs.keeper.io/user-guides/sharing : 'The list of users the record is shared with will now appear in the record's sharing screen'; nested folders show all 'Users with Access'", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:3241 sys/capabilities-accessor and :3282 sys/capabilities answer per token and path; :3091 internal/ui/resultant-acl for the caller only; searched 'shared with', 'who has access' in ui templates: no match Note: You can check what a given token may do on a path, but there is no reverse view listing everyone who can read a secret." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -2502,6 +2768,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Controller/Api/ShareApiController.php:158 create() takes a single password id; searched 'FolderShare', 'folder_share', 'shareFolder' in src/lib src/js src/vue: no match Note: Only passwords can be shared; folders cannot.", "keeper": "https://docs.keeper.io/user-guides/sharing : 'Shared folders allow you to share multiple records at once and new records can be added to the folder as needed'", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:448 a path ending in '*' becomes a prefix rule (IsPrefix :454) covering every secret below it, including later ones; ui/app/components/policy-form.ts:170 policy editor Note: A prefix policy on a team path, attached to a group, behaves as a team folder." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqTeammap" } }, { @@ -2535,6 +2805,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'FolderShare', 'folder_share', 'shareFolder' in src/lib src/js src/vue: no match; src/vue/Components/Sharing/ShareOptionsForm.vue:67-70 editable/shareable apply to a single password share Note: No team folders; per-share edit and reshare flags exist only on individual password shares.", "keeper": "https://docs.keeper.io/user-guides/sharing : shared folder record permissions 'Can Edit', 'View Only', 'Can Edit & Share'", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:25 capabilities deny, read, update, list (plus create, delete, patch, sudo) Note: Each policy path grants a chosen set of capabilities, so read-only or edit per group is standard." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqTeammap" } }, { @@ -2567,6 +2841,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'FolderShare', 'folder_share', 'shareFolder', 'inherit' in src/lib src/js src/vue: no folder share inheritance Note: No folder sharing, so nothing to inherit.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/nested-share-subfolders : 'permissions flow down automatically from a parent folder through all nested folders' (up to five levels; invitation-only rollout); classic shared folders also hold subfolders", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:448 trailing '*' glob matches every deeper path; :444 '+' segment wildcard Note: Prefix globs make subfolders inherit the team path's access." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqTeammap" } }, { @@ -2641,6 +2919,10 @@ "keeper": "https://docs.keeper.io/enterprise-guide/account-transfer-policy : an admin locks a leaving user and transfers the whole vault to a recipient; manual admin action, not automatic on key revocation", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'owner' in vault/identity_store*.go and ui/lib/kv/addon: no ownership concept; secrets live at paths independent of any user key Note: No ownership exists to move; revoking a user's tokens leaves the secrets at their paths for everyone with policy access.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/EventListener/User/UserDeletedListener.php:44 on user deletion data is deleted; searched 'owner' transfer and 'revoke' in src/lib/Cron src/lib/Helper/User: only UserMoveCommand (manual) Note: Nothing transfers ownership automatically; when an owner goes, their shares go with them. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimVarianten" } }, { @@ -2713,6 +2995,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'public', 'share link', 'PublicPage' in src/lib/Controller: only src/lib/Controller/Link/ConnectController.php:256,300 (client pairing), no public share route Note: No public share links.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/one-time-share : One-Time Share link expires at a chosen time and is 'device-locked'; no link password", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/tools/wrap.ts:19 sys/wrapping/wrap with a wrap TTL (:31); vault/logical_system_paths.go:4450 wrapping/wrap, :4480 wrapping/unwrap Note: A single-use wrapping token with a TTL can carry a secret, and the UI has a Wrap tool, but there is no password and no public web link page." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqLinkDelen", + "KqGedeeldGeheim" + ] } }, { @@ -2745,6 +3034,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'send', 'one-time', 'PublicPage' in src/lib/Controller: only the PassLink connect flow; no anonymous secret send Note: No one-off send to people without an account.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/self-destructing-records : share with users outside Keeper and automatically delete the record and disable the link at the chosen time", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:4480 wrapping/unwrap accepts the wrapping token itself as auth; ui/app/components/tools/unwrap.ts:28 Unwrap tool (inside the logged-in UI) Note: Anyone holding the wrapping token can unwrap it once via API or CLI without an account; the UI unwrap tool needs a login, so there is no recipient page." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqNieuwBeveiligdBericht", + "KqBeveiligdBericht" + ] } }, { @@ -2778,6 +3074,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 no public link or send routes in src/appinfo/routes.php (share_api routes 62-75 are user shares only) Note: No links or sends exist to list or revoke.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/one-time-share : shares can be un-shared before expiry, and Commander offers 'the ability to remove previously created One-Time Shares'; no documented list of all your links in the vault", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/tools/lookup.ts wrapping lookup by token; vault/logical_system_paths.go:4513 wrapping/lookup; wrap response carries an accessor usable with token revoke-accessor; searched 'wrapping/list' in vault/: no list of issued wraps Note: You can look up or revoke a wrapping token you still hold (or its accessor), but there is no list of the wraps you created, so partial." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqNieuwBeveiligdBericht" } }, { @@ -2808,6 +3108,14 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'federat', 'cloudId', 'remote' in src/lib/Helper/Sharing src/lib/Controller/Api/ShareApiController.php: no match; ShareApiController.php:181 canShareWithUser() limits to local users Note: Sharing is limited to users of the same Nextcloud instance.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#creating-and-sharing : policies 'Share to users outside of the enterprise' and 'Receive items from users outside of the enterprise'", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'federat', 'remote user', 'external share' in vault/ and ui/app: no cross-instance sharing; access is only via this cluster's auth methods and policies Note: An outside person needs an identity in this Vault (an auth method); there is no sharing with accounts on another instance." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqLinkDelen", + "KqBinnengekomen", + "KqBeheerAlgemeen" + ] } }, { @@ -2837,6 +3145,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Sharing/ShareOptionsForm.vue:142 view/edit/custom presets on a single password share; no shared collection object (searched 'collection', 'FolderShare' in src/lib: no match) Note: View and edit presets exist per password share, but there are no shared collections to give roles on.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/nested-share-subfolders : roles Viewer, Share Manager, Content Manager, Content and Share Manager, Full Manager", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:25 capability set per path; ui/app/components/policy-form.ts:170 admin writes a policy per role Note: Roles such as manager or viewer are modelled as policies an admin writes; there are no built-in named collection roles." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqTeammap" } }, { @@ -2869,6 +3181,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Cron/SynchronizeShares.php:447-448 sendShareCreatedNotification and sendShareCreateMail; src/lib/Notification/ShareCreatedNotification.php; src/lib/Helper/Settings/UserSettingsHelper.php notification/shares and mail/shares settings Note: Recipients get a Nextcloud notification (and optionally a mail) when a share is created.", "keeper": "https://docs.keeper.io/user-guides/sharing : recipients receive an email to accept a share; Notification Center shows sharing requests (https://docs.keeper.io/user-guides/notification-center)", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'notification' in ui/app/templates ui/app/components ui/lib/kv/addon ui/lib/core/addon/components: only MFA push text (ui/app/components/mfa/form/mfa-field.hbs:62); no user notification centre or share mail Note: Access is granted by policy edits and nobody is told; there is no user notification of a new share." + }, + "screen": { + "board": null, + "reason": "a Nextcloud notification; no Keepiq board" } }, { @@ -2901,6 +3217,10 @@ "keeper": "https://docs.keeper.io/user-guides/web-vault#emergency-access : 'The Emergency Access feature is only available for consumer accounts' (business uses Account Transfer instead)", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'emergency', 'grantee' in ui/app ui/lib and vault/*.go: only the init page root-key text (ui/app/templates/vault/cluster/init.hbs:26) and lease force-revoke help (vault/logical_system_paths.go:3577) Note: No emergency contact feature exists; break-glass in Vault means root token generation by key holders.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'emergency', 'grantee', 'trusted contact' in src/lib src/js src/vue: only src/lib/Services/LoggingService.php (log level) and src/lib/Fetcher/NightlyAppFetcher.php Note: No emergency access feature. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqNoodtoegang" } }, { @@ -2933,6 +3253,10 @@ "keeper": "https://docs.keeper.io/user-guides/web-vault#emergency-access : Emergency Access (with its delay) is consumer-only, not in Keeper Enterprise", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'emergency', 'grantee' in ui/app ui/lib and vault/*.go: no emergency access flow Note: There is no emergency request to decline.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'emergency', 'grantee' in src/lib src/js src/vue: no emergency access code Note: No emergency access, so nothing to decline. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqNoodtoegang" } }, { @@ -2965,6 +3289,10 @@ "keeper": "https://docs.keeper.io/user-guides/web-vault#emergency-access : Emergency Access is consumer-only; not available on the Enterprise tier rated here", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'emergency', 'grantee', 'wait time' in ui/app ui/lib and vault/*.go: no emergency access flow Note: No time-based automatic emergency grant exists.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'emergency', 'grantee', 'waiting period' in src/lib src/js src/vue: no emergency access code Note: No emergency access. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqNoodtoegang" } }, { @@ -3025,6 +3353,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Sharing/ShareOptionsForm.vue:67-70 share flags editable and shareable only; no hide-password flag in src/lib/Controller/Api/ShareApiController.php:158-165 Note: Recipients always see the shared password.", "onepassword": "https://support.1password.com/create-share-vaults-teams/ : in 1Password Business a group can use items without revealing or copying passwords when the 'View and Copy Passwords' vault permission is removed; https://support.1password.com/permission-enforcement/ notes this permission is client-enforced.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/hiding-passwords : Privacy Screen stops a team or role from unmasking passwords on shared records, but the docs state masking is visual only and the password stays reachable through the API and browser inspection." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqDelen", + "KqTeammap" + ] } }, { @@ -3058,6 +3393,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Sharing/ShareOptionsForm.vue:55 expires date; src/lib/Controller/Api/ShareApiController.php:162 expires; src/lib/Cron/SynchronizeShares.php:133 deleteExpiredShares() Note: Shares take an expiry date and a background job removes them when it passes.", "onepassword": "https://support.1password.com/share-items/ : a shared item link expires after 1 hour to 30 days or after one view, and can go to specific email addresses, but it shares a copy, not the item itself; vault access for a team member has no expiry (https://support.1password.com/create-share-vaults-teams/).", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/time-limited-access : share credentials with other Keeper users 'on a temporary basis, automatically revoking access at a specified time'." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqDelen", + "KqGeheim" + ] } }, { @@ -3089,6 +3431,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'access request', 'ip', 'time window' in src/lib/Controller/Api/ShareApiController.php src/lib/Helper/Sharing: no match Note: No privileged access requests.", "bitwarden": "bitwarden/server@v2026.9.1 bitwarden_license/src/Services/Pam/Api/Endpoints/AccessRequestEndpoints.cs:17 inbox, :30 decision, :39 revoke; bitwarden/clients@web-v2026.9.0 bitwarden_license/bit-web/src/app/pam/access-rules/access-rule-edit/access-rule-edit.component.html:73 lease duration, ip-allowlist/ip-allowlist-editor.component.ts (admin rules only, behind FeatureFlag.Pam; no member request UI in the public clients) Note: proposed by the source reader as a capability no row covered.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js:127-129 control-groups, control-groups/configure, control-groups/:accessor (Enterprise control groups: a request waits for authorisers' approval); token_bound_cidrs (command/agent/approle_end_to_end_test.go:798) limits a token to IP ranges and max_lease_ttl bounds its time. Note: approval, time limit and IP limit all exist; control groups are Enterprise. Rated by the lane from the source." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqGeheimVarianten", + "KqDelen" + ] } }, { @@ -3152,6 +3501,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request links.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/one-time-share#bidirectional-sharing : bidirectional One-Time Share lets you 'collect signed documents, feedback or sensitive files and information from clients, contractors ... without requiring them to create a Keeper account'", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'secret request', 'request link' in ui templates: no match; ui/app/router.js has no request or fill-in route; unauthenticated paths are login, unwrap and health only Note: Vault has no inbound request link that an outsider fills in." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqGeheimVragen", + "KqGeheimInvullen" + ] } }, { @@ -3193,6 +3549,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request feature exists, so this part of it is absent too.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/one-time-share#bidirectional-sharing : the sender prepares the record's fields and writes instructions in Notes, then ticks 'Allow recipient to edit record fields and upload files'; required fields cannot be marked", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: There is no request form, so no field choice." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimVragen" } }, { @@ -3226,6 +3586,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request feature exists, so this part of it is absent too.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/one-time-share#bidirectional-sharing : what the recipient saves 'will automatically update within the sender's vault' under zero-knowledge encryption, but 'the sender and recipient can continue to edit the record until the share link expires', so not write-once", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists; write-once is not modelled. KV check-and-set (cas_required) makes writes conditional but is not a request." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimInvullen" } }, { @@ -3259,6 +3623,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request feature exists, so this part of it is absent too.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/one-time-share#bidirectional-sharing : 'Use the dropdown menu to select when you would like the share to expire'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists to expire." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimVragen" } }, { @@ -3291,6 +3659,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 src/Notification/Email/Redactor: no request-filled template; searched 'secret request' in src/ plugins/: no match Note: No secret requests, so no fill-in notification.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request feature exists, so this part of it is absent too.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists and no user notifications." + }, + "screen": { + "board": null, + "reason": "a Nextcloud notification; no Keepiq board" } }, { @@ -3324,6 +3696,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request feature exists, so this part of it is absent too.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/one-time-share#bidirectional-sharing : the recipient can keep editing the same record until the link expires, so a value can be updated without a new share; no explicit re-ask action", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimVragen" } }, { @@ -3357,6 +3733,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request feature exists, so this part of it is absent too.", "keeper": "https://docs.keeper.io/enterprise-guide/sharing/one-time-share#bidirectional-sharing : editing is possible 'until the share link expires or once access is revoked'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists to revoke." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -3389,6 +3769,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'secret request', 'pending request' in src/ plugins/: only account recovery requests (plugins/PassboltEe/AccountRecovery) Note: The only pending request list is for account recovery, not secret requests.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request feature exists, so this part of it is absent too.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists, so there is no open-requests list." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -3421,6 +3805,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'secret request' in src/ plugins/: no match; no API route lets a client ask a person for a value Note: An application can only read what is shared with its account; it cannot ask a person for a missing secret.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request feature exists, so this part of it is absent too.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match; ui/app/routes/vault/cluster/access/control-groups.js:21 control groups (Enterprise) approve access to an existing secret, not supply a new value Note: Control groups let a human approve an application's access request, but nothing lets an application ask a person to provide a secret value." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqApplicatie" } }, { @@ -3453,6 +3841,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'secret request' in src/ plugins/: no match Note: No secret request flow, so no reuse of values on file.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request feature exists, so this part of it is absent too.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own" } }, { @@ -3485,6 +3877,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'secret request', 'fill-in link' in src/ plugins/: no match Note: No fill-in links to recover.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"SecretRequest\", \"secret request\", \"fill in\", \"inbox\" in src/lib src/js src/vue: no match; src/lib/Db holds only Password, Folder, Tag, Share, Keychain, Challenge, Session, Registration entities Note: No secret request feature exists, so this part of it is absent too.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists, so there are no fill-in links to recover." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -3518,6 +3914,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php link/connect/request, await, confirm, apply; src/lib/Controller/Link/ConnectController.php:136 request(), :221 confirm(), :260 apply() creates a Registration and app token; src/vue/Dialog/ConnectClient.vue user-facing pairing dialog Note: A client (extension, mobile app) pairs through the PassLink connect flow and gets an app token under a user account; there is no separate machine identity with its own secrets.", "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/about/terminology : 'An Application is a permission boundary that defines which records and folders a workload can access'; created in the vault or with 'secrets-manager app create'", "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/credential/approle/path_role.go:126 role/ create, :280 secret-id management; ui/app/resources/auth/method.ts:60 approle among UI-managed auth methods Note: An admin registers an application as an AppRole role and issues role_id and secret_id." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqApplicatieRegistreren" } }, { @@ -3558,6 +3958,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'approve', 'pending' registration in src/ plugins/: only AccountRecovery request review and SelfRegistration domain allow list (plugins/PassboltCe/SelfRegistration); no application registration queue Note: No application registrations exist, so there is no approve or reject step. Moved from unknown to no on a documented search.", "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/credential/approle/path_role.go:126 roles are created directly by a caller with policy on the path; searched 'pending', 'approve' in builtin/credential/approle: no registration queue Note: There is no self-registration and no approval queue; an admin (or anyone with policy on auth/approle/role) creates roles directly.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Controller/Link/ConnectController.php:221 confirm() is called by the user who owns the account; searched 'approve' in src/lib/Controller src/lib/Settings: no admin approval queue Note: The user confirms their own client pairing; no administrator approves registrations. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerApplicaties" } }, { @@ -3590,6 +3994,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Controller/Link/ConnectController.php:242-243 sendNewClientNotification and sendNewClientMail to the registration's own user; src/lib/Notification/NewClientNotification.php:58 Note: The account owner is notified after a new client connects; no admin notification of pending registrations.", "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/credential/approle/path_role.go:126 roles created directly, no pending state; searched 'pending', 'approve' in builtin/credential/approle: no match Note: With no registration queue there is nothing to notify about.", "passbolt": "passbolt/passbolt_api@v5.16.0 src/Notification/Email/Redactor: admin emails cover account recovery requests and user setup, none for application registration; searched 'application registration' in src/ plugins/: no match Note: No application registrations, so no waiting notification. Moved from unknown to no on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqDashboard" } }, { @@ -3622,6 +4030,10 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/logical/pki/path_issue_sign.go:119 sign/ signs a CSR; builtin/credential/cert/path_login.go:44 cert auth login with that client certificate Note: A CSR can be signed by the PKI engine and the resulting certificate used to log in with cert auth, but these are two engines wired by the operator, not a single onboarding step.", "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'csr', 'certificate signing', 'certificate' in src/ plugins/: only the Azure SSO certificate parser in plugins/PassboltEe/Sso/src/Utility/Azure/Provider/AzureProvider.php:198 Note: Machines onboard with an OpenPGP key pair through the normal user setup, not a certificate signing request. Moved from unknown to no.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'csr', 'certificate', 'x509', 'openssl' in src/lib: no certificate onboarding; src/lib/Controller/Link/ConnectController.php:260 apply() uses codes, not a CSR Note: Clients onboard with a pairing code and an app token, not a certificate signing request. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqApplicatieRegistreren" } }, { @@ -3655,6 +4067,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:21-22 session request/open with challenge; src/lib/Controller/Api/SessionApiController.php:106-145 passphrase challenge and 2FA token; searched 'jwt', 'signed token', 'assertion' in src/lib: no match Note: Clients use a Nextcloud app password plus the passphrase challenge; no signed token login.", "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/about/security-encryption-model : the client registers an ECC secp256r1 public key and 'subsequent requests are signed with the Client Device Private Key'", "hashicorp-vault": "hashicorp/vault@v2.1.1 go.mod:158 vault-plugin-auth-jwt bundled (JWT login with a signed token); builtin/credential/cert/path_login.go:44 TLS client certificate login Note: Workloads log in with a signed JWT (e.g. CI OIDC tokens) or a client certificate instead of a stored password." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (machine API)" } }, { @@ -3688,6 +4104,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php POST /api/1.0/password/find; src/lib/Controller/Api/AbstractObjectApiController.php:119 find($criteria) filters on fields such as label Note: The public API finds passwords by field criteria, e.g. label. With client side encryption the label is encrypted, so the client has to search after decrypting.", "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/about/keeper-notation : 'keeper:///field/...', a secret is addressed by record title or UID", "hashicorp-vault": "hashicorp/vault@v2.1.1 command/kv_get.go vault kv get ; ui/lib/kv/addon/routes.js:15 secret/:name route Note: A secret is read by its path name through API, CLI or UI." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (machine API)" } }, { @@ -3720,6 +4140,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 src/Model/Entity/Permission.php:44 READ = 1, :45 UPDATE = 7, :46 OWNER = 15, update is a superset of read Note: Every permission level includes read, so there is no write-only access for a machine.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Sharing/ShareOptionsForm.vue:67-70 share flags editable and shareable only; src/lib/Controller/Api/ShareApiController.php:158-165 no write-only permission Note: Permissions are read, edit and reshare; edit always includes read, so there is no write-only access.", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:25 separate capabilities (create, update without read) Note: A policy with create/update and no read lets a client write a secret it cannot read back." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimSchrijven" } }, { @@ -3752,6 +4176,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/ApiObjects/PasswordObjectHelper.php:130-134 API objects expose updated, edited and revision; src/appinfo/routes.php:36 POST /api/1.0/password/show Note: A client can poll and compare the revision id to notice a change, but there is no versioned secret or rotation signal.", "hashicorp-vault": "hashicorp/vault@v2.1.1 command/agent/config/config.go:187 static_secret_render_interval for Vault Agent templates (re-renders KV v2 on an interval) Note: Vault Agent polls non-leased secrets such as KV v2 on a configurable interval and re-renders templates when they change.", "passbolt": "passbolt/passbolt_api@v5.16.0 src/Model/Table/ResourcesTable.php:593 resources expose a modified timestamp; plugins/PassboltCe/SecretRevisions/config/routes.php:39 GET /secret-revisions/resource/{id} lists secret revisions Note: A client can poll a resource's modified date or its secret revision list to spot a change, but there is no version number or change feed built for machines. Moved from unknown to partial." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (machine API)" } }, { @@ -3792,6 +4220,10 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:2899 sys/internal/specs/openapi serves an OpenAPI document of every mounted path; http/handler.go:613 /.well-known/ routing; vault/logical_system.go:6706 sys/well-known registry maps labels to mounts (used by ACME and OIDC) Note: Vault publishes a machine-readable OpenAPI description of its API and a /.well-known label registry, but no well-known document for discovering the secrets API itself, so partial.", "passbolt": "passbolt/passbolt_api@v5.16.0 plugins/PassboltCe/JwtAuthentication/config/routes.php:21 only well-known route is .well-known/jwks.json (the server JWT signing key); searched 'well-known' in config/ src/ plugins/: no API discovery document Note: The only well-known file publishes the JWT verification key; clients are configured with the server URL by hand. Moved from unknown to no.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'ICapability', 'registerCapability', 'well-known' in src/lib: only src/lib/Services/PasswordChangeUrlService.php:121 (probes websites' /.well-known/change-password); no discovery document for the API Note: No discovery document; clients hard-code /apps/passwords/api/1.0/. Moved from unknown on a documented search." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (machine API)" } }, { @@ -3824,6 +4256,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'lease', 'dynamic secret', 'ttl' in src/ plugins/: no secret lease code; resources are static stored items (src/Model/Table/ResourcesTable.php) Note: Secrets are stored values; nothing issues leased or expiring credentials.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'lease' in src/lib src/js: only logger/localisation 'release' substrings; src/appinfo/routes.php has no lease routes Note: No leases; secrets are static stored values.", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/expiration.go:1564 ExpirationManager.Register creates a lease with TTL for every dynamic secret and service token Note: Dynamic secrets and tokens get leases that the expiration manager revokes at TTL." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqApplicatie" } }, { @@ -3864,6 +4300,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'lease', 'renew', 'revoke' secret in src/ plugins/: no match Note: No leases to renew or revoke.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php: no lease renew or revoke routes; searched 'lease' in src/lib: no match Note: No leases to renew or revoke.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/controllers/vault/cluster/access/leases/show.js:41 renewLease; ui/app/templates/vault/cluster/access/leases/show.hbs:37 revokeLease Note: Leases can be renewed and revoked from the UI, CLI and API." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqApplicatie" } }, { @@ -3905,6 +4345,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/AppSettings/*.php admin settings list no lease time; searched 'lease' in src/lib: no match Note: No lease time limit setting.", "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/about/terminology : a client device can have an optional 'Access expiration'; this ends a device's access, it is not a per-fetch lease", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/forms/mount.ts:58 default_lease_ttl and :62 max_lease_ttl fields; vault/logical_system_paths.go:21 mounts//tune Note: Default and max lease TTL are set per mount in the UI and via tune, and per role in each engine." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerApplicaties" } }, { @@ -3977,6 +4421,10 @@ "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/about/one-time-token : applications are associated to shared folders or records ('secrets-manager share add'), and the application view lists them", "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/credential/approle/path_role.go:312 role//policies; vault/logical_system_paths.go:3241 sys/capabilities-accessor; ui/app/components/agents/registry/page/index.ts:116 aggregates an AI agent's policies (Enterprise agent registry, vault/agent_registry_ce.go:18 stub in CE) Note: An application's reach is read off its role policies, and the Enterprise agent registry aggregates policies per AI agent, but nothing lists the concrete secrets an application owns.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Db/Registration.php is a client pairing, not an owner of secrets; src/lib/Db/Password.php owned by a userId only Note: Secrets belong to users, not applications, so there is no per-application view. Moved from unknown." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqApplicatie" } }, { @@ -4010,6 +4458,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Command/*.php: occ commands are backup, pwned-list, system report, user delete/move only; no secret read command; no CLI client in this repo Note: No command-line binary for fetching secrets in CI.", "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/secrets-manager-command-line-interface/exec-command : 'ksm exec' replaces keeper:// notation in environment variables and runs the command", "hashicorp-vault": "hashicorp/vault@v2.1.1 command/agent/config/config.go:70 exec with env_template renders secrets into a child process's environment (command/agent/exec/exec.go); command/kv_get.go single vault binary | docs: https://developer.hashicorp.com/vault/docs/platform/github-actions Note: The single vault binary reads secrets in CI, and Agent exec mode passes them as environment variables without writing files." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (command-line tool)" } }, { @@ -4043,6 +4495,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Command/*.php setName list: passwords:backup:*, passwords:pwned-list:process, passwords:system:report, passwords:user:delete, passwords:user:move; none reads a vault Note: No CLI to read your own vault; occ commands are admin maintenance only.", "keeper": "https://docs.keeper.io/keeperpam/commander-cli/overview : Keeper Commander CLI gives command-line access to the user's vault", "hashicorp-vault": "hashicorp/vault@v2.1.1 command/kv_get.go vault kv get; command/kv_list.go vault kv list Note: The CLI reads any path the user's token allows." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (command-line tool)" } }, { @@ -4074,6 +4530,10 @@ "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/integrations/kubernetes-external-secrets-operator : External Secrets Operator provider syncs Keeper secrets into Kubernetes Secrets (also a Secrets Injector)", "hashicorp-vault": "hashicorp/vault@v2.1.1 go.mod:160 vault-plugin-auth-kubernetes and :173 vault-plugin-secrets-kubernetes bundled; ui/app/router.js mounts the kubernetes engine UI | docs: https://developer.hashicorp.com/vault/docs/platform/k8s/vso Note: Kubernetes auth and secrets engine ship in the binary; the Secrets Operator and Agent Injector are separate HashiCorp repos documented at the URL.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'kubernetes', 'k8s', 'operator' in src and scripts: no match Note: No Kubernetes integration in the app. Moved from unknown on a documented search." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (Kubernetes operator)" } }, { @@ -4105,6 +4565,10 @@ "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/integrations/github-actions : Keeper Secrets Manager GitHub Action; GitLab integration at https://docs.keeper.io/keeperpam/secrets-manager/integrations/gitlab-plugin", "hashicorp-vault": "hashicorp/vault@v2.1.1 go.mod:158 vault-plugin-auth-jwt bundled for GitHub and GitLab OIDC tokens | docs: https://developer.hashicorp.com/vault/docs/platform/github-actions (hashicorp/vault-action, separate repo) Note: JWT auth accepts CI OIDC tokens; the ready-made GitHub Action lives in its own repo per the docs.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'github action', 'gitlab', 'ci' integration in src and scripts: no match Note: No CI integration shipped with the app. Moved from unknown on a documented search." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (CI integration)" } }, { @@ -4165,6 +4629,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/SecurityCheck/UserRulesSecurityCheck.php:77-78 maximum age only marks a password as weak; searched 'rotate' in src/lib: no match Note: Old passwords are flagged, never rotated automatically.", "keeper": "https://docs.keeper.io/keeperpam/privileged-access-manager/password-rotation/rotation-overview : scheduled rotation of database, AD, cloud and machine credentials via the Keeper Gateway (KeeperPAM / rotation add-on)", "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/logical/database/path_creds_create.go:57 static-creds/; builtin/logical/database/path_rotate_credentials.go:21 rotate-root, :48 rotate-role Note: Static roles rotate a database user's password on a period or schedule, with manual rotate endpoints." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (rotation runner)" } }, { @@ -4196,6 +4664,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 package.json:70 passwords-client 1.0.0-beta.5974 (the official JS client library the web UI uses); src/js/Services/ClientService.js:12 import PasswordsClient Note: One official JavaScript library; other languages rely on third-party projects outside this repo.", "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/developer-sdk-library : Python, Java/Kotlin, JavaScript, .NET, Go, Ruby, Rust, PowerShell SDKs", "hashicorp-vault": "hashicorp/vault@v2.1.1 api/auth.go official Go client package api/ in-tree | docs: https://developer.hashicorp.com/vault/api-docs/libraries Note: The Go client is in the tree; other official and community libraries are listed in the docs." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (client libraries)" } }, { @@ -4226,6 +4698,10 @@ "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/integrations/terraform : Terraform Provider for Keeper Secrets Manager", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:2899 OpenAPI spec the provider tooling builds on | docs: https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2 (Terraform provider is the separate hashicorp/terraform-provider-vault repo) Note: The Terraform provider lives in its own repository; code here only shows the API it drives, so the docs rating stands.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'terraform' in src and scripts: no match Note: No Terraform provider in the app. Moved from unknown on a documented search; a third-party provider, if any, is outside this repo." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (Terraform provider)" } }, { @@ -4259,6 +4735,10 @@ "nextcloud-passwords": "not checked: a Conduction-specific integration the Passwords source cannot speak to; src/appinfo/routes.php exposes a general REST API such an integration could use Note: Nothing in the app targets Conduction apps; the rating depends on the Conduction side.", "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/integrations : long integration list with no Nextcloud or Conduction app integration", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'nextcloud', 'conduction' in all .go .hbs .ts .js files: no match Note: No Conduction or Nextcloud integration exists; such apps would have to call the HTTP API themselves." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (machine API)" } }, { @@ -4318,6 +4798,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'aws', 'azure', 'secrets manager', 'sync' targets in src/lib: no match Note: No push to cloud secret stores.", "onepassword": "https://developer.1password.com/docs/environments/aws-secrets-manager/ : 'Sync secrets from 1Password to AWS Secrets Manager (beta)' from a 1Password Environment; AWS is the only destination documented (Azure Key Vault, GitHub and the rest not found).", "keeper": "https://docs.keeper.io/keeperpam/privileged-access-manager/universal-secrets-sync : 'Synchronize Shared Secrets to Cloud Secret Management Services'; Universal Secrets Sync automatically pushes shared secrets to cloud secret stores (KeeperPAM)." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (sync runner)" } }, { @@ -4341,7 +4825,11 @@ "rowSource": "own", "provider": "keepiq", "providerHow": "read-from-code", - "source": "spec-round-2026-10-07" + "source": "spec-round-2026-10-07", + "screen": { + "board": null, + "reason": "backend: no screen of its own (assistant tools)" + } }, { "id": "pki-01", @@ -4374,6 +4862,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"certificate\", \"x509\", \"openssl_csr\", \"pki\" in src/lib src/js src/vue: no match; src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 only text, secret, email, url, file, data fields Note: No certificate or CA features in the app.", "keeper": "not checked: no private certificate authority feature found anywhere in the docs index; certificates appear only as secrets to store", "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/logical/pki/path_root.go:36 root/generate, builtin/logical/pki/path_intermediate.go:19 intermediate/generate; ui/lib/pki/addon/components/page/pki-issuer-generate-root.hbs and pki-issuer-generate-intermediate.hbs Note: Root and intermediate CAs are generated in the PKI engine with UI pages for both." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAlgemeen" } }, { @@ -4459,6 +4951,10 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Administration/DisplayHealthcheckAdministration/DisplayHealthcheckAdministration.js:1587 health check only reports the server's own SSL certificate; passbolt/passbolt_api@v5.16.0 searched 'certificate authority' in src/ plugins/: no match Note: The admin health check shows whether the web server's TLS certificate is self-signed, but there is no CA to report on.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"certificate\", \"x509\", \"openssl_csr\", \"pki\" in src/lib src/js src/vue: no match; src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 only text, secret, email, url, file, data fields Note: No certificate or CA features in the app.", "hashicorp-vault": "hashicorp/vault@v2.1.1 command/pki_health_check.go vault pki health-check; command/healthcheck/pki_ca_validity_period.go:27 CA validity check; ui/lib/pki/addon/components/page/pki-overview.hbs:8 overview shows issuer and role counts only Note: CA health is a CLI report; the UI PKI overview shows counts and quick actions, not health." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAlgemeen" } }, { @@ -4491,6 +4987,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 plugins/PassboltCe/ResourceTypes/src/Model/Entity/ResourceType.php:52-58 v5 types are password, default, TOTP, custom fields, note, pin code; no certificate type or inventory Note: No certificate type and no certificate inventory. The styleguide locale files carry an unused 'Certificate' string, but no component uses it.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"certificate\", \"x509\", \"openssl_csr\", \"pki\" in src/lib src/js src/vue: no match; src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 only text, secret, email, url, file, data fields Note: No certificate or CA features in the app.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/pki/addon/routes.js:42 certificates list per PKI mount; certificates stored as KV values are not parsed or listed Note: Each PKI mount lists the certificates it issued; certificates kept as secrets elsewhere are not included." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqCertificaten" } }, { @@ -4531,6 +5031,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 plugins/PassboltCe/ResourceTypes/src/Model/Entity/ResourceType.php:52 no certificate type; expiry reminders exist only for resource expiry dates (plugins/PassboltCe/PasswordExpiry) Note: There are no certificate expiry reminders; a user could only set a manual resource expiry date on a note holding a certificate.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"certificate\", \"x509\", \"openssl_csr\", \"pki\" in src/lib src/js src/vue: no match; src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 only text, secret, email, url, file, data fields Note: No certificate or CA features in the app.", "hashicorp-vault": "hashicorp/vault@v2.1.1 command/healthcheck/pki_ca_validity_period.go:27 ca_validity_period check warns about CA expiry on demand; searched 'notify', 'reminder' in builtin/logical/pki: no expiry notification Note: The CLI health check flags CAs nearing expiry when run, but nothing reminds anyone about leaf or CA expiry on its own, so partial." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqCertificaten" } }, { @@ -4563,6 +5067,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'certificate', 'renewal' in src/ plugins/: no match outside SSO Note: No certificate renewal guidance.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"certificate\", \"x509\", \"openssl_csr\", \"pki\" in src/lib src/js src/vue: no match; src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 only text, secret, email, url, file, data fields Note: No certificate or CA features in the app.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/pki/addon/routes.js no renewal route; searched 'renew' in ui/lib/pki/addon/components: no guided renewal Note: There is no renewal guide; certificates are simply re-issued, manually or by ACME clients." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqCertificaatVernieuwen" } }, { @@ -4624,6 +5132,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"certificate\", \"x509\", \"openssl_csr\", \"pki\" in src/lib src/js src/vue: no match; src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 only text, secret, email, url, file, data fields Note: No certificate or CA features in the app.", "keeper": "https://docs.keeper.io/keeperpam/secrets-manager/overview : KSM manages infrastructure secrets including 'certificates', stored as record fields or file attachments; expiry and subject are not parsed", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/utils/parse-pki-cert.js:60 notAfter parsed to not_valid_after for display; ui/lib/pki/addon/components/page/pki-issuer-import.hbs imports an external CA PEM; ui/lib/pki/addon/components/page/pki-certificate-details.hbs issued certs Note: Issued certificates and imported CA certificates show subject and expiry, but an external leaf certificate can only be stored as untyped KV text." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqCertificaten" } }, { @@ -4703,6 +5215,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Services/PasswordSecurityCheckService.php:28-36 statuses BREACHED, OUTDATED, DUPLICATE, GOOD with levels OK/WEAK/BAD; src/vue/Components/ContentList/Item/PasswordItem/PasswordItemSecurityIcon.vue:15 coloured shield per item, :35 securityColor Note: Each item shows a secure/weak/breached shield, but there is no strength score; weak means duplicate or old, not a low entropy estimate.", "keeper": "https://docs.keeper.io/user-guides/security-audit : 'information about each record's password strength', scored 0 to 100 (Very Weak to Very Strong)", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned', 'password age', 'health report' in ui/app ui/lib (.hbs .ts .js) and vault/*.go builtin sdk/helper: no password health code Note: Vault never scores stored passwords." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqWachtwoordgezondheid" } }, { @@ -4750,6 +5266,10 @@ "keeper": "https://docs.keeper.io/user-guides/security-audit : the Security Audit screen shows 'each record's password strength and reuse'", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned', 'password age', 'health report' in ui/app ui/lib (.hbs .ts .js) and vault/*.go builtin sdk/helper: no password health code Note: There is no reuse detection across secrets.", "passbolt": "passbolt/passbolt_styleguide@v5.16.0 searched 'reuse', 'reused', 'duplicate password' in src/react-extension src/shared: only an unrelated ShareDialog comment (src/react-extension/components/Share/ShareDialog.js:403); passbolt/passbolt_api@v5.16.0 same terms in src/ plugins/: no match Note: No reused password detection. Moved from unknown to no on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqWachtwoordgezondheid" } }, { @@ -4790,6 +5310,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 plugins/PassboltEe/PasswordExpiryPolicies default expiry period in days; passbolt/passbolt_styleguide@v5.16.0 src/react-extension/contexts/ResourceWorkspaceContext.js:443 'expired' filter, :1118 searchByExpired Note: An expiry period set by the admin marks passwords expired after that many days and an Expired filter lists them, but a user cannot ask ad hoc for passwords older than an age they choose.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/SecurityCheck/UserRulesSecurityCheck.php:77-78 password/security/age max age in days marks the revision OUTDATED Note: A user-set maximum age flags older passwords as weak.", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned', 'password age', 'health report' in ui/app ui/lib (.hbs .ts .js) and vault/*.go builtin sdk/helper: no password health code; KV metadata shows created_time per version (ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs) but no age filter Note: Version timestamps are visible per secret, but nothing lists secrets older than a chosen age." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqWachtwoordgezondheid" } }, { @@ -4831,6 +5355,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Provider/SecurityCheck/HaveIBeenPwnedProvider.php:28 range API https://api.pwnedpasswords.com/range/:range; src/lib/Provider/SecurityCheck/BigLocalDbSecurityCheckProvider.php local breach DB; src/lib/Command/ProcessPwnedPasswordsList.php:47 passwords:pwned-list:process Note: The client sends only a SHA-1 hash (length set by password/security/hash) to its own Nextcloud; the server checks a local breach list or HIBP by k-anonymity range, so the password itself never leaves.", "keeper": "https://docs.keeper.io/enterprise-guide/keeper-encryption-model#breachwatch : 'a HMAC_SHA512 hash is generated based on each stored password and sent to the server', compared as hashes-of-hashes in an HSM; the password itself is not sent but a peppered hash is (BreachWatch add-on)", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned', 'password age', 'health report' in ui/app ui/lib (.hbs .ts .js) and vault/*.go builtin sdk/helper: no password health code Note: No breach checking exists." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqWachtwoordgezondheid" } }, { @@ -4878,6 +5406,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Section/Security.vue:17 security-line per status, :51-52 'Secure', 'Weak', 'Breached', :96 API.findPasswords({status}) Note: The Security section groups the whole vault by health status.", "keeper": "https://docs.keeper.io/user-guides/security-audit : 'an overall security score' for the passwords in your vault", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned', 'password age', 'health report' in ui/app ui/lib (.hbs .ts .js) and vault/*.go builtin sdk/helper: no password health code Note: No vault health report exists; the PKI health check is about certificates." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqWachtwoordgezondheid" } }, { @@ -4911,6 +5443,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/SecurityCheck/UserRulesSecurityCheck.php:94-96 duplicate check runs on the server using the stored hash; src/lib/Services/PasswordSecurityCheckService.php breach check server side Note: Checks run on the server against a stored SHA-1 hash of each password, so the server learns the hash.", "keeper": "https://docs.keeper.io/enterprise-guide/security-audit : strength and reuse are calculated 'continuously from the user's Vault'; the summary sent to the admin is 'encrypted with the Enterprise Public Key' to preserve zero knowledge", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned' in ui/app ui/lib and vault/*.go builtin sdk/helper: no match Note: There are no password health checks at all, in the browser or on the server." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqWachtwoordgezondheid" } }, { @@ -4944,6 +5480,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:81 GET /api/1.0/service/password; src/lib/Controller/Api/ServiceApiController.php:101-106 generatePassword(strength, numbers, special); src/vue/Dialog/CreatePassword/PasswordControls.vue:17-21 numbers and special toggles, :25-35 strength Low to Ultra; src/lib/Provider/Words/RandomCharactersProvider.php:50 random characters only when the admin picks this provider Note: The generator offers strength levels and numbers/special toggles but no explicit length, and it produces random characters only if the admin selects that words provider; by default it builds word-based passwords. Moved down from yes on the code.", "keeper": "https://docs.keeper.io/user-guides/web-vault#password-generator : default 20 characters, length slider, lowercase/uppercase/digits/symbols sets", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:4352 sys/policies/password//generate; helper/random/parser.go:73 policy Length, helper/random/registry.go:16 charset rules; ui/app/components/tools/random.hbs:7 UI tool generates random bytes only Note: Password policies define length and charset rules and generate passwords via API or CLI; the options live in the policy rather than per request, and the UI tool gives random bytes, not a password generator." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqSleutelGenereren" } }, { @@ -4974,6 +5514,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Provider/Words/LocalWordsProvider.php, LeipzigCorporaProvider.php, SnakesWordsProvider.php, AutoWordsProvider.php word sources; src/lib/Controller/Api/ServiceApiController.php:106 wordsService->getPassword Note: The default generator builds passwords from dictionary words.", "keeper": "https://docs.keeper.io/user-guides/web-vault#passphrase-generator : 'generate a highly secure, random, yet easy-to-remember passphrase'", "hashicorp-vault": "hashicorp/vault@v2.1.1 helper/random/registry.go:16 only rule type is 'charset'; helper/random/string_generator.go:21 character sets, no word list Note: Generation is character based only; there is no word list for passphrases." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqSleutelGenereren" } }, { @@ -5060,6 +5604,10 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched '2fa directory', 'twofactorauth', 'inactive two-factor' in ui/app ui/lib: no match; stored secrets carry no website URL type Note: Vault has no website logins to inspect, so no two-factor advice.", "passbolt": "passbolt/passbolt_styleguide@v5.16.0 searched '2fa', 'two-factor', 'twofactorauth' site advisory in src/react-extension src/shared: only passbolt's own MFA settings; passbolt/passbolt_browser_extension@v5.16.0 same in src/all: no match Note: No advisory about sites that support two-factor login. Moved from unknown to no.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched '2fa', 'twofactor', '2factor' in src/js src/vue: only src/vue/Section/Authorize.vue:125 (vault login); no 2FA site directory Note: No check for websites that support 2FA. Moved from unknown on a documented search." + }, + "screen": { + "board": null, + "reason": "not designed yet" } }, { @@ -5090,6 +5638,10 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/kv-object-editor.hbs:25 untyped fields, no URL field to inspect; searched 'http://', 'insecure' warnings in ui/lib/kv/addon: none Note: Secrets have no website field, so nothing warns about http addresses.", "passbolt": "passbolt/passbolt_browser_extension@v5.16.0 searched 'insecure', 'http:' warnings in src/all: only an OpenPGP config flag (src/all/background_page/service/systemRequirementService/systemRequirementService.js:24); passbolt/passbolt_styleguide@v5.16.0 locales/en-UK/common.json: no http login warning string Note: Nothing warns about saved logins on plain http addresses. Moved from unknown to no on a documented search.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Services/PasswordSecurityCheckService.php:28-32 statuses BREACHED, OUTDATED, DUPLICATE, GOOD only; searched 'http:' warnings in src/js src/vue src/lib: only favicon/preview fallbacks Note: No warning for http logins. Moved from unknown on a documented search." + }, + "screen": { + "board": null, + "reason": "not designed yet" } }, { @@ -5184,6 +5736,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'passkey', 'webauthn' site directory in src/lib src/js: WebAuthn code is for vault login only (src/js/Actions/WebAuthn) Note: No passkey availability check.", "onepassword": "https://support.1password.com/watchtower/ : 'Passkeys available shows logins for websites that support passkeys, but don't yet have a passkey saved in the item.'", "keeper": "not checked: https://docs.keeper.io/user-guides/breachwatch and the passkey docs read; no report of saved logins whose sites accept passkeys." + }, + "screen": { + "board": null, + "reason": "not designed yet" } }, { @@ -5247,6 +5803,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Db/PasswordRevision.php fields (password, username, label, url, notes, customFields, status, edited) have no expiry; searched 'expir' in src/lib/Db: only share expiry Note: Only shares expire; secrets have no expiry date.", "keeper": "not checked: the docs describe no expiry date on a record's password; only shares can expire (https://docs.keeper.io/enterprise-guide/sharing/time-limited-access)", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes/secret/metadata.js:39 metadata edit form with delete_version_after; ui/lib/kv/addon/helpers/is-deleted.js:10 deletion_time set from it Note: A per-secret delete_version_after makes versions expire, but expiry deletes the version rather than flagging it for change." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -5293,6 +5853,10 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Administration/DisplayAdministrationPasswordExpiry/DisplayAdministrationPasswordExpiryAdvanced/DisplayAdministrationPasswordExpiryAdvanced.js:174 default_expiry_period, :206 policy_override; passbolt/passbolt_api@v5.16.0 plugins/PassboltCe/PasswordExpiry/src/Model/Dto/PasswordExpirySettingsDto.php:27 POLICY_OVERRIDE Note: Admins set a default expiry period and may allow users to override it per password (Pro policies plugin).", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/Settings/UserSettingsHelper.php password/security/age is a per-user setting; searched 'expir' in src/lib/Helper/AppSettings: no secret expiry setting Note: No admin default expiry.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes/configure.js:20 mount-level delete_version_after; ui/lib/kv/addon/routes/secret/metadata.js:39 per-secret override Note: A mount default with per-secret override exists, but it deletes versions and the per-secret value can be longer as well as shorter." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerBeleid" } }, { @@ -5334,6 +5898,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Cron/CheckPasswordsJob.php:95-107 notifies only when a revision turns LEVEL_BAD (breached); :122-123 sendBadPasswordNotification and mail; outdated passwords are LEVEL_WEAK and do not notify Note: There are notifications for breached passwords but no expiry or age reminders. Moved from unknown.", "keeper": "https://docs.keeper.io/user-guides/notification-center : 'Record reminders: Alerts for time-sensitive items such as expiring passwords or access keys'; how they are configured is not documented", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'reminder', 'notification' in ui/lib/kv/addon and ui/app/components: none for secrets; ui/lib/kv/addon/helpers/is-deleted.js:10 expiry only sets deletion_time Note: Nothing reminds anyone before or after a KV secret expires." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqDashboard" } }, { @@ -5367,6 +5935,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Controller/Api/PasswordApiController.php:195 update() creates a new revision with a new hash; src/lib/Helper/SecurityCheck/UserRulesSecurityCheck.php:77-78 age measured from edited Note: Changing the value creates a revision and resets the age status, which implicitly shows the value changed; there is no explicit mark-as-rotated action.", "keeper": "https://docs.keeper.io/keeperpam/privileged-access-manager/password-rotation/rotation-overview : KeeperPAM rotation changes the credential on the target and updates the record, with rotation events logged; no manual 'mark as rotated' for ordinary records", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs:52 Version diff between versions with created_time; builtin/logical/database/path_roles.go:331 last_vault_rotation on static roles Note: A new KV version with its timestamp and the version diff show that a value changed, and database static roles record their last rotation, but there is no 'mark as rotated' action, so partial." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqGeheim", + "KqGeheimVarianten" + ] } }, { @@ -5400,6 +5975,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Cron/CheckPasswordsJob.php:98-107 flags BREACHED revisions and notifies; src/lib/Services/PasswordSecurityCheckService.php:28 Note: Breached passwords are flagged; there is no key-compromise flag.", "keeper": "https://docs.keeper.io/enterprise-guide/breachwatch : BreachWatch prompts users to 'Resolve a breached password by either changing the password or ignoring it'; key-compromise flagging not documented", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'breach', 'compromis', 'needs rotation' in ui/lib/kv/addon ui/app/components and builtin/logical: no flagging of KV secrets; vault/logical_system.go:4761 barrier rotate does not mark secrets Note: No mechanism flags stored secrets for rotation after a breach or key compromise." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqGeheimVarianten", + "KqWachtwoordgezondheid" + ] } }, { @@ -5440,6 +6022,13 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/contexts/ResourceWorkspaceContext.js:443 'expired' filter, :1118 searchByExpired; src/react-extension/components/Resource/DisplayResourcesList/DisplayResourcesList.js:1272 empty state for expired list Note: Expired passwords are listed in an Expired filter with an expiry column; there is no dashboard and no health report.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/SecurityCheck/UserRulesSecurityCheck.php:77-78 over-age passwords become OUTDATED; src/vue/Section/Security.vue:51-52 listed under Weak; src/js/Helper/Dashboard/PasswordList.js:92 dashboard widget shows favourites/search only Note: Over-age passwords show in the Security section as weak, not on the dashboard, and not as a separate due-for-rotation list.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/dashboard/overview.hbs:23 SecretsEngines, :48 QuickActions, :50 ClientCount widgets only; no rotation-due widget Note: The dashboard shows no rotation status and there is no health report." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqDashboard", + "KqWachtwoordgezondheid" + ] } }, { @@ -5509,6 +6098,10 @@ "hashicorp-vault": "searched 'change-password' in ui/app ui/lib: no match; Vault stores no website logins. Rated by the lane from the source.", "passbolt": "searched 'change-password', 'well-known' in passbolt_styleguide src/react-extension src/shared and passbolt_api src/: no match. Rated by the lane from the source.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Services/PasswordChangeUrlService.php:61 getPasswordChangeUrl() probes /.well-known/change-password at :121; src/appinfo/routes.php:106 POST /api/1.0/service/password-change; src/vue/Components/ContentList/Item/PasswordItem/PasswordItemActionMenu.vue:99 openChangePasswordPage() Note: proposed by the source reader as a capability no row covered." + }, + "screen": { + "board": null, + "reason": "not designed yet: no change-password link on KqGeheim" } }, { @@ -5550,6 +6143,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/AppSettings/ServiceSettingsHelper.php:54-58 admin picks the words/security services only; src/lib/Helper/Settings/UserSettingsHelper.php password/generator/* are user settings Note: No organisation password policy; generator options are per-user.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#record-passwords : 'The password generator on end-user vaults will adhere to the minimum character length, minimum number of lowercase/uppercase/numbers/symbols', also per domain; manually typed passwords are not blocked on save", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:4390 sys/policies/password/ governs engine-generated passwords; ui/app/routes/vault/cluster/policies.js:8 UI manages only acl, egp, rgp policy types; KV writes are not checked against a password policy Note: Password policies govern passwords Vault generates, via API or CLI only; values typed into KV are never checked." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerBeleid" } }, { @@ -5583,6 +6180,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/EncryptionManager.js:139 hard-coded passphrase.length < 12; searched 'passphrase' in src/lib/Helper/AppSettings: no admin setting Note: The 12 character minimum is fixed in the client, not an admin setting.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#login-settings : Master Password Complexity policy per role (length, digits, upper, lower, special)", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'password_policy', 'strength', 'min length' in builtin/credential/userpass/*.go: no match Note: There is no master password, and userpass has no strength rule an admin could set." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerBeleid" } }, { @@ -5623,6 +6224,10 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Administration/: no application queue component; passbolt/passbolt_api@v5.16.0 no application registration route in config/routes.php or plugins Note: No application approval queue exists. Moved from unknown to no on a documented search.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Settings/AdminSettings.php:99-122 admin page parameters have no application queue; src/lib/Controller/Link/ConnectController.php:221 users confirm their own clients Note: No approval queue exists. Moved from unknown on a documented search.", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'pending', 'approve' in builtin/credential/approle: no match; ui/app/router.js has no application queue route Note: There is no application registration queue." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerApplicaties" } }, { @@ -5656,6 +6261,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'FolderShare', 'team folder' in src/lib: no match; src/lib/Command/UserDeleteCommand.php:51 deletes a user's whole vault instead Note: No team folders to remove a leaver from.", "keeper": "https://docs.keeper.io/enterprise-guide/user-management-and-lifecycle : Delete User removes the user 'from all Roles, Nodes and Teams'; Lock Account or SCIM/AD Bridge suspension blocks access while Account Transfer keeps the records", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/models/identity/entity.js:15 entity fields name, disabled, policies, metadata (disable or delete in Access > Entities); vault/identity_store_util.go:3164 external groups dropped on next login Note: Disabling or deleting the user's entity, or removing them in the IdP, removes every group-granted path in one step." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerMensen" } }, { @@ -5689,6 +6298,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'offline', 'serviceWorker', 'localStorage' in src/js src/vue: only src/js/Services/FaviconService.js:100 favicon cache in IndexedDB; no vault offline cache and no admin switch Note: The web app keeps no offline vault, so there is no switch; third-party clients may cache on their own.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#account-settings : 'Restrict offline access: ... prevent users from accessing their Keeper vault without internet access'", "hashicorp-vault": "hashicorp/vault@v2.1.1 command/agent/config/config.go Agent/Proxy cache is configured per agent process; no organisation-wide client caching switch in vault/ or ui/app/router.js Note: There are no offline-capable user clients; the only caches are per Vault Agent or Proxy configuration, so there is no org-wide switch." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAlgemeen" } }, { @@ -5722,6 +6335,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/AppSettings/ServiceSettingsHelper.php:54 service/security; src/lib/Settings/AdminSettings.php:103 securityServices, :142-151 HIBP and big local database options; src/lib/Command/ProcessPwnedPasswordsList.php:47 Note: The admin picks the breach check service (HIBP, local databases) for the whole instance.", "keeper": "https://docs.keeper.io/enterprise-guide/breachwatch : 'The BreachWatch capability can be deployed selectively to your organization via Role Enforcements' (BreachWatch add-on)", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'breach', 'pwned', 'haveibeenpwned' in ui/app ui/lib and vault/*.go: no match Note: No breach checking exists to switch on." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAlgemeen" } }, { @@ -5755,6 +6372,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/AppSettings/EntitySettingsHelper.php:31 entity/purge/timeout applies to deleted items in trash; searched 'revision' retention in src/lib/Helper/AppSettings: no match Note: The only retention knob is how long trashed items are kept; old revisions are kept forever.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#vault-features : 'Retention of Deleted Records' sets days before deleted records can be cleared or auto-purge; record-history retention is not configurable in the docs", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes/configure.js:20 max_versions and delete_version_after per mount; ui/lib/kv/addon/routes/secret/metadata.js:39 per secret Note: Version retention is set per mount and per secret in the UI." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerBeleid" } }, { @@ -5853,6 +6474,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Controller/Api/SessionApiController.php:115-116 requires Nextcloud 2FA at vault login when the user has 2FA; src/lib/Helper/AppSettings/*.php no export or policy settings Note: Nextcloud-enforced 2FA carries into the vault login, but the app has no export ban or other policy switches.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies : 2FA enforcement ('it cannot be disabled by the user') and Import and Export restriction ('RESTRICT_EXPORT')", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/identity_store.go:544 identity/mfa/login-enforcement/; ui/app/components/mfa/mfa-login-enforcement-form.hbs:72 enforcement targets (auth methods, groups, entities) Note: MFA can be enforced per auth method, group or entity from the UI; there is no personal vault export to ban, so partial." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerBeleid" } }, { @@ -5885,6 +6510,10 @@ "keeper": "https://docs.keeper.io/enterprise-guide/delegated-administration : administrative permissions granted per role and scoped to nodes", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:25 fine-grained capabilities per path; ui/app/components/policy-form.ts:170 policy editor; ui/app/router.js access.namespaces (Enterprise namespaces for delegated admins) Note: Admin rights are scoped per path with ACL policies; Enterprise namespaces add delegated tenant admins.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'AuthorizedAdminSetting', 'IDelegatedSettings' in src/lib: no match; src/lib/Controller/Admin/SettingsController.php:21 admin-only controller Note: The admin page is not delegable to a sub-admin role. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAlgemeen" } }, { @@ -5916,6 +6545,10 @@ "keeper": "https://docs.keeper.io/enterprise-guide/user-management-and-lifecycle : user status Invited ('has not completed their account setup yet'), Active, Locked", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js access.identity entities list; ui/app/routes/vault/cluster/clients/counts/client-list.ts:18 per-client list only on Enterprise (redirects when version.isCommunity) Note: There is no per-user vault to set up; admins see identity entities and, on Enterprise, a list of clients that used Vault in a period, so partial.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Command/SystemReportCommand.php:43 and src/lib/Helper/Survey/ServerReportHelper.php:296-297 report only aggregate SSE/CSE usage; no per-user vault list in src/lib/Settings/AdminSettings.php Note: Only aggregate encryption stats, no list of which users set up a vault. Moved from unknown." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerMensen" } }, { @@ -5947,6 +6580,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php resources admin_settings (/admin/settings) and admin_caches (/admin/caches); src/lib/Controller/Admin/SettingsController.php:51-92 index/show/update/destroy Note: There is an admin REST resource for app settings and caches, plus occ commands, but no API for managing users, shares or policies.", "keeper": "https://docs.keeper.io/enterprise-guide/developer-tools : Commander CLI and Python SDK manage the enterprise (users, roles, teams, reports); SCIM API for provisioning", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:2899 sys/internal/specs/openapi documents every admin path; api/ Go client Note: Every admin operation is an HTTP API call; the UI and CLI are clients of it." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (admin API)" } }, { @@ -5979,6 +6616,10 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Administration/DisplayHealthcheckAdministration/DisplayHealthcheckAdministration.js health check page covering database, SMTP, SSL, GPG and directory settings; passbolt/passbolt_api@v5.16.0 config/routes.php:169 GET /healthcheck Note: The health check page reports the state of core dependencies, but there is no list of connected integrations (SSO, SCIM, directory) with their status.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Settings/AdminSettings.php:99-116 image, words, favicon, preview and security services with 'enabled' => isAvailable(); src/lib/SetupChecks/BackgroundJobsBreachedPasswordsDatabaseUpdateSuccessSetupCheck.php setup check Note: The admin page lists external services with availability, and setup checks flag a failing breach DB update; no single integrations dashboard.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js secrets.backends and settings.auth lists; ui/app/components/secret-engines/catalog.hbs:27 enabled and disabled plugins; ui/app/components/dashboard/overview.hbs:23 SecretsEngines widget Note: The UI lists enabled engines and auth methods and a plugin catalog, but shows no connection health per integration." + }, + "screen": { + "board": null, + "reason": "the Integrations page belongs to integriq; no Keepiq board" } }, { @@ -6012,6 +6653,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/CreatePassword/CustomFields/FileCustomField.vue:48 file fields only link Nextcloud Files; no attachment storage or quota setting in src/lib/Helper/AppSettings Note: No attachments are stored, so there is no size limit or quota.", "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#creating-and-sharing : admins can switch off 'Upload files' per role; an admin-set size limit or per-user quota is not documented", "hashicorp-vault": "hashicorp/vault@v2.1.1 internalshared/configutil/listener.go:65 max_request_size is a listener-wide request cap; searched 'attachment', 'upload' in ui/lib/kv/addon: no attachments Note: There are no attachments to size or quota; only a server-wide request size limit in the config file." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAlgemeen" } }, { @@ -6044,6 +6689,10 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Administration/DisplayHealthcheckAdministration/DisplayHealthcheckAdministration.js:1017 'Using latest passbolt version', :1030 'Currently using {{currentVersion}} and it should be {{latestAvailableVersion}}', :466 PHP version; passbolt/passbolt_api@v5.16.0 config/routes.php:169 GET /healthcheck Note: The admin API status page shows the installed version against the latest release, plus PHP, database and environment details. Moved from partial to yes.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/templates/admin/settings.php:37-63 PHP and Nextcloud version support warnings; src/lib/Command/SystemReportCommand.php:43 occ passwords:system:report; src/lib/Helper/Survey/ServerReportHelper.php:149 installed_version Note: The admin page warns about unsupported PHP/Nextcloud versions; the app version and details are in the occ system report, not shown on the page.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/app-footer.hbs:9 installed version in the footer linking to its changelog (:7), :11 edition; vault/logical_system_paths.go:1763 sys/version-history Note: The UI shows the running version and edition, and the API gives version history." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAlgemeen" } }, { @@ -6116,6 +6765,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 fixed field types; src/lib/Helper/AppSettings/*.php no item type settings Note: No admin-defined item types.", "onepassword": "https://support.1password.com/custom-fields/ and https://support.1password.com/item-categories/ : categories are fixed; a 1Password Community answer states 'you cannot ... create new categories' (https://www.1password.community/discussions/1password/custom-item-templates/166721). Custom fields per item are the only extension.", "keeper": "https://docs.keeper.io/enterprise-guide/creating-new-record-types : an admin with 'Manage Record Types in Vault' creates a record template with labelled and required fields and publishes it to roles." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqItemtypeBewerken", + "KqBeheerAlgemeen" + ] } }, { @@ -6245,6 +6901,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Db/Password.php every item belongs to one user's vault; no organisation vault or policy in src/lib/Helper/AppSettings Note: There is no organisation vault, only personal vaults.", "onepassword": "https://support.1password.com/employee-vault/ and https://support.1password.com/offboarding/ : every Business member has an Employee vault owned by the organisation, and Business Watchtower reports on it, but no policy found that forbids keeping work logins in a personal or Employee vault (https://support.1password.com/team-policies/).", "keeper": "https://docs.keeper.io/enterprise-guide/account-transfer-policy : the enterprise can take over a leaver's vault, and https://docs.keeper.io/enterprise-guide/personal-vaults-for-enterprise-and-business-users separates a personal family vault; no enforcement found that forbids storing work logins in a personal vault." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerBeleid" } }, { @@ -6336,6 +6996,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Controller/Api/ShareApiController.php:181 canShareWithUser() accepts any Nextcloud user; src/js/Actions/Share/CreateShareAction.js:90 #disableCse() moves shared items to server-side encryption, so no key handover is needed Note: There is no confirmation step: any Nextcloud user can receive shares at once, because shared items are server-side encrypted rather than wrapped to a member key.", "bitwarden": "bitwarden/server@v2026.9.1 src/Core/AdminConsole/Enums/PolicyType.cs:27 AutomaticUserConfirmation ('Automatically confirm invited users'); bitwarden/clients@web-v2026.9.0 apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts:33 AutoConfirmPolicy Note: proposed by the source reader as a capability no row covered.", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/identity_store_group_aliases.go:21 group-alias maps an external group (OIDC, LDAP) to a Vault group and its policies. Note: Vault encrypts server-side, so there is no key handover to confirm; a new member gets the policies of their mapped groups at first login. Rated by the lane from the source." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerBeleid" } }, { @@ -6397,6 +7061,10 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 command/operator_raft_snapshot.go and command/operator_raft_snapshot_restore.go save and restore a snapshot from the CLI; CHANGELOG.md:286 names automated snapshots as an Enterprise feature whose code is not public. Note: manual snapshot and restore are in the public code; the schedule is Enterprise. Rated by the lane from the source.", "passbolt": "searched 'backup' in passbolt_api src/Command plugins/: only install-time options (src/Command/InstallCommand.php:98); no scheduled backup job. Rated by the lane from the source.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Cron/BackupJob.php:47 backup/interval; src/lib/Helper/AppSettings/BackupSettingsHelper.php:35-37 interval, max files, auto-restore after update; src/lib/Command/BackupRestoreCommand.php:45 passwords:backup:restore Note: proposed by the source reader as a capability no row covered." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAlgemeen" } }, { @@ -6430,6 +7098,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"activity\", \"audit\", \"syslog\", \"webhook\" in src/lib: no match; src/lib/AppInfo/Application.php registers no activity provider Note: No audit log; revisions record content changes but not reads, shares or deletions as log events.", "keeper": "https://docs.keeper.io/enterprise-guide/event-reporting : event tracking across '200+ event types'; events carry metadata, record contents stay zero-knowledge encrypted", "hashicorp-vault": "hashicorp/vault@v2.1.1 audit/hashstructure.go:25 salt.GetIdentifiedHMAC hashes string values; audit/entry_formatter.go formats every request and response; vault/logical_system_paths.go:1872 sys/audit/ enable device Note: Every request and response goes to the audit devices with secret values HMAC'd by default." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAudit" } }, { @@ -6463,6 +7135,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Controller/Api/AbstractObjectApiController.php:184 delete clones a revision with trashed=true instead of editing; src/lib/Services/Object/PasswordRevisionService.php revisions are append-only; searched \"activity\", \"audit\", \"syslog\", \"webhook\" in src/lib: no match; src/lib/AppInfo/Application.php registers no activity provider Note: Revisions are append-only history, which is the only thing resembling a tamper-resistant log; there is no audit log.", "keeper": "https://docs.keeper.io/enterprise-guide/event-reporting : ARAM 'is designed as an immutable, append-only audit system ... No modify or delete capability is exposed to administrators'", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:1845 sys/audit only enables, lists and disables devices, no entry edit API; internal/observability/event/sink_file.go:144 file sink appends and reopens Note: Vault offers no way to edit entries, but the log is an ordinary file, syslog or socket stream whose immutability depends on the sink." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own" } }, { @@ -6495,6 +7171,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 plugins/PassboltCe/Log/src/Command/ActionLogsPurgeCommand.php:58 --retention-in-days, :73 limit, :80 dry-run style counts Note: A purge command removes action log rows older than a chosen number of days.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"activity\", \"audit\", \"syslog\", \"webhook\" in src/lib: no match; src/lib/AppInfo/Application.php registers no activity provider Note: No audit log to purge.", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'retention', 'purge', 'max_age' in audit/ and internal/observability/event/sink_file.go: no match; sink_file.go:144 only Reopen for external log rotation Note: Vault never purges audit entries; retention is left to external log rotation." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own" } }, { @@ -6527,6 +7207,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 plugins/PassboltEe/AuditLog/config/routes.php:28 GET /actionlog/user/{id}; plugins/PassboltEe/AuditLog/src/Controller/UserLogsController.php:42 admin only; passbolt/passbolt_styleguide@v5.16.0 no component calls the user log, only resource and folder activity (src/react-extension/components/ResourceDetails/DisplayResourceDetails/ActivitiesServiceWorkerService.js:15) Note: Users see activity per resource and per folder, but a per-user trail exists only as an admin API route and no screen shows it.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Section/Recent.vue:35 recently edited list; src/vue/Components/Sidebar/PasswordSidebar/Tabs/Revisions.vue per-item history; searched \"activity\", \"audit\", \"syslog\", \"webhook\" in src/lib: no match; src/lib/AppInfo/Application.php registers no activity provider Note: You see recently edited items and each item's revisions, not an activity feed.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no activity or audit route for users; audit output goes only to external devices (vault/logical_system_paths.go:1872) Note: Users cannot see their own activity in Vault; the audit log lives outside it." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqMijnActiviteit" } }, { @@ -6560,6 +7244,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Sidebar/PasswordSidebar/Tabs/Revisions.vue; src/vue/Components/Sidebar/PasswordSidebar/Revisions/Revision.vue:69-72 revision label and created time; src/vue/Dialog/ViewRevision.vue Note: Each password's revisions are listed with dates and can be viewed.", "keeper": "https://docs.keeper.io/user-guides/web-vault#record-history : per-record version history with modified date and time; per-record event reports need ARAM", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs:52 version list with created and deleted times and diff; access history only in external audit devices (vault/logical_system_paths.go:1872) Note: A secret's change history is in the UI; who read it is only in the external audit log." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheim" } }, { @@ -6593,6 +7281,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"activity\", \"audit\", \"syslog\", \"webhook\" in src/lib: no match; src/lib/AppInfo/Application.php registers no activity provider Note: No admin audit log.", "keeper": "https://docs.keeper.io/enterprise-guide/event-reporting : Reporting & Alerts dashboard with 'Recent Activity' (last 1,000 events) and 'All Security Events' in all Business and Enterprise plans; custom reports need ARAM", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no audit route; vault/logical_system_paths.go:1845 sys/audit only manages devices Note: Admins read the audit log in their own tools (file, syslog, SIEM); Vault has no audit browser." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAudit" } }, { @@ -6634,6 +7326,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"activity\", \"audit\", \"syslog\", \"webhook\" in src/lib: no match; src/lib/AppInfo/Application.php registers no activity provider Note: No audit events to forward.", "keeper": "https://docs.keeper.io/enterprise-guide/event-reporting/syslog : Syslog SIEM streaming; webhooks for alerts (https://docs.keeper.io/enterprise-guide/webhooks)", "hashicorp-vault": "hashicorp/vault@v2.1.1 audit/backend_syslog.go syslog device; audit/backend_socket.go TCP/UDP socket device Note: Syslog and socket devices stream audit events to a SIEM." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAudit" } }, { @@ -6667,6 +7363,13 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Command/SystemReportCommand.php:43 technical system report only; searched \"activity\", \"audit\", \"syslog\", \"webhook\" in src/lib: no match; src/lib/AppInfo/Application.php registers no activity provider Note: No compliance report.", "keeper": "https://docs.keeper.io/enterprise-guide/compliance-reports : Compliance Reports add-on for SOX/PCI access audits; 'Individual User Reports can be exported in PDF, JSON, or CSV format'", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'compliance report', 'pdf' in ui/app/components templates: no match; CSV exports exist only for client counts and the agent registry (ui/app/utils/constants/agent-registry.ts CSV_COLUMNS) Note: No compliance report export exists." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqBeheerAudit", + "KqComplianceMomentopname" + ] } }, { @@ -6700,6 +7403,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched \"activity\", \"audit\", \"syslog\", \"webhook\" in src/lib: no match; src/lib/AppInfo/Application.php registers no activity provider Note: No compliance snapshots.", "keeper": "https://docs.keeper.io/enterprise-guide/compliance-reports : 'The system will generate and save a snapshot report'; re-running saves a new snapshot; immutability of a saved snapshot is not stated", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'compliance report', 'snapshot' in ui/app/components for compliance: only raft and recovery storage snapshots (ui/app/router.js recovery.snapshots) Note: No compliance snapshots exist; storage snapshots are backups, not signed reports." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqComplianceMomentopname" } }, { @@ -6732,6 +7439,13 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'honey', 'decoy', 'canary' in vault/*.go builtin ui/app ui/lib: only plugin reload and replication canaries (builtin/plugin/backend.go:113, vault/core_stubs_oss.go:123) Note: No decoy credential feature exists; an audit-log alert on a chosen path would have to be built in the SIEM.", "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'decoy', 'honeypot', 'canary' in src/ plugins/: no match Note: No decoy credentials or tripwire alerts. Moved from unknown to no on a documented search.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'decoy', 'honeypot', 'canary' in src/lib src/js src/vue: no match Note: No decoy credentials. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqGeheim", + "KqBeheerAudit" + ] } }, { @@ -6765,6 +7479,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/UserMigration/PasswordsMigrator.php:73-91 Nextcloud user_migration export of revisioned items, simple items and settings; :79 'export not possible due to E2EE'; src/vue/Section/Backup.vue in-app export Note: The vault plugs into Nextcloud's user data export and has its own backup export, but the Nextcloud export is refused when end-to-end encryption is on.", "keeper": "https://docs.keeper.io/user-guides/export-and-reports/vault-export : users export their vault records; not a full personal-data (account, events) export, and admins can restrict export", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no personal data export route; searched 'gdpr', 'personal data' in ui/app and vault/*.go: no export Note: There is no self-service personal data export; an admin can read the entity record by API." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqMijnGegevens" } }, { @@ -6797,6 +7515,10 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 config/routes.php:293 DELETE /users/{id}; src/Controller/Users/UsersDeleteController.php:157 admin only; src/Model/Table/UsersTable.php:483 sole-owned items deleted, shared ones transferred first (:128 _transferContentOwners) Note: Deletion handles shared secrets correctly through forced ownership transfer, but only an admin can do it; users cannot delete their own account.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php:30-31 /api/1.0/account/reset; src/js/Actions/User/UserAccountResetAction.js:34-63 reset with confirmation code; src/lib/Helper/User/DeleteUserDataHelper.php:136-145 deletes all objects including shares; src/lib/Cron/SynchronizeShares.php:111 deleteOrphanedTargetPasswords() Note: Account reset or user deletion wipes the vault and its shares, and the share job then removes recipients' copies; there is no option to hand shared items over first.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js access.identity entity delete by an admin; ui/app/models/identity/entity.js:15 entity fields; secrets live at paths with no owner so they survive the entity (vault/policy.go) Note: An admin can delete a user's entity and aliases and shared secrets stay put because they are not owned, but users cannot delete their own account, so partial." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqMijnGegevens" } }, { @@ -6829,6 +7551,10 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 audit/hashstructure.go:25 HMACs secret values at write time only; searched 'anonymi' in audit/ and vault/*.go: no rewrite of past entries on entity delete Note: Audit entries are HMAC'd for values at write time but identity fields are not anonymised later when an account is deleted.", "passbolt": "passbolt/passbolt_api@v5.16.0 src/Model/Table/UsersTable.php:559 user is only marked deleted = true, profile and username kept; plugins/PassboltCe/Log/src/Service/ActionLogs/ActionLogsDeleteService.php used only by migrations; searched 'anonym' in src/Model/Table plugins/PassboltCe/Log: no match Note: A deleted user is soft-deleted and log rows keep pointing at the intact user record, so entries are not anonymised. Moved from unknown to no.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Helper/User/DeleteUserDataHelper.php:136-145 deletes the user's data outright; searched 'activity', 'audit', 'anonymi' in src/lib: no match Note: There is no audit trail to anonymise; the user's data is simply deleted. Moved from unknown on a documented search." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own" } }, { @@ -6860,6 +7586,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'splunk', 'sentinel', 'syslog', 'webhook' in src/lib: no match Note: No SIEM connectors.", "keeper": "https://docs.keeper.io/enterprise-guide/event-reporting : built-in SIEM connectors for Splunk, Microsoft Sentinel, QRadar, Elastic, Datadog, Sumo Logic and more", "hashicorp-vault": "hashicorp/vault@v2.1.1 audit/backend_socket.go and audit/backend_syslog.go generic streams; no Splunk or Sentinel connector code in the tree | docs: https://developer.hashicorp.com/vault/tutorials/monitoring/monitor-telemetry-audit-splunk Note: code not public for the Splunk app; docs rating kept: generic syslog and socket streams, with a Splunk app for Enterprise documented separately." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAudit" } }, { @@ -6891,6 +7621,10 @@ "keeper": "https://docs.keeper.io/enterprise-guide/event-reporting#alerts : 'event-based triggers that will generate either email or SMS-based alerts'; see Recommended Alerts (https://docs.keeper.io/enterprise-guide/recommended-alerts)", "hashicorp-vault": "hashicorp/vault@v2.1.1 http/handler.go:546 sys/events/subscribe websocket stream of events such as KV data-write; audit/backend_socket.go real-time audit stream; searched 'alert', 'risk' in vault/eventbus: no risk rules Note: Real-time event and audit streams exist for an external system to alert on, but Vault itself classifies nothing as risky and sends no alerts, so partial.", "passbolt": "passbolt/passbolt_api@v5.16.0 src/Notification/Email/Redactor/User/UserAdminRoleRevokedEmailRedactor.php:43 AD/admin_role_revoked; src/Notification/Email/Redactor/User/UserDeleteAdminEmailRedactor.php:47 AD/user_deleted; src/Notification/Email/Redactor/Role/RoleUpdatedAdminEmailRedactor.php:43; plugins/PassboltEe/MfaPolicies/src/Notification/Email/MfaPoliciesSettingsUpdatedEmailRedactor.php:42 and other AD/settings_updated emails Note: Admins are emailed when sensitive admin events happen (role revoked, user deleted, security policies or SSO changed), but there is no risk scoring or alerting on user behaviour. Moved from unknown to partial." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqBeheerAudit" } }, { @@ -6954,6 +7688,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Import.vue:9-26 formats: backup JSON, Passwords/Folder/Tags CSV, ownCloud Passwords, Passman JSON/CSV, Enpass JSON, Bitwarden JSON/CSV, KeePassX, LastPass, Dashlane, RoboForm, SafeInCloud, Chrome, Firefox, custom CSV; src/js/Manager/ImportManager.js:69-79 client-side conversion helpers in src/js/Helper/Import/ Note: Sixteen source formats are parsed in the browser before objects are created through the API. No 1Password or KeePass .kdbx importer, but custom CSV covers the rest. Moved up from partial on the breadth of the client-side importers.", "keeper": "https://docs.keeper.io/user-guides/import-records-1/import-overview : imports from 1Password, Bitwarden, Dashlane, LastPass, KeePass, browsers and more in the Web Vault/Desktop App, where records are encrypted on the device before upload (zero-knowledge)", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'import', 'csv' in ui/lib/kv/addon and all .go .ts .hbs: no password manager importer Note: No importer for other password managers exists; bulk loading is scripting against the KV API." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqImporteren" } }, { @@ -6987,6 +7725,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Import.vue:9 'Database Backup' JSON and :13 'ownCloud Passwords' legacy; src/js/Manager/ImportManager.js:69 json Note: The app imports its own backup format (and the older ownCloud Passwords one), so moving between instances is covered.", "keeper": "https://docs.keeper.io/user-guides/import-records-1/import-a-.csv-file : no Nextcloud Passwords importer in the list, but a generic .csv import with field mapping would take its CSV export", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match; ui/lib/kv/addon/routes.js:8 no import route Note: No importer exists, for Nextcloud Passwords or anything else." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqImporteren" } }, { @@ -7020,6 +7762,10 @@ "keeper": "https://docs.keeper.io/user-guides/web-vault#field-mapping : field mapping screen ('Click any field to open a dropdown menu'), then 'a summary screen will display a preview of your vault' before import", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match; ui/lib/kv/addon/routes.js:8 no import route Note: No importer exists, so no mapping preview.", "passbolt": "passbolt/passbolt_browser_extension@v5.16.0 src/all/background_page/model/import/resources/resourcesCsvImportParser.js:112 row parser chosen automatically by header score; src/all/background_page/model/import/resources/csvRowParser/csvKdbxRowParser.js:24 fixed mapping; passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Resource/ImportResources/ImportResources.js no mapping step Note: Column mapping is fixed per detected format; there is no preview or manual mapping. Moved from unknown to no." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqImporteren" } }, { @@ -7061,6 +7807,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Helper/Import/BitwardenConversionHelper.js:17-20 _processFolders(data.items) and data.folders; src/js/Manager/ImportManager.js:114 _importFolders() then passwords with folderMapping at :137 Note: Folders from the source are recreated and passwords are placed in them.", "keeper": "https://docs.keeper.io/user-guides/import-records-1/import-a-.csv-file : 'supports advanced structure including Folders, Subfolders, Shared Folders'", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match; ui/lib/kv/addon/routes.js:8 no import route Note: No importer exists. KV paths are folders, so a scripted import can keep structure, but the product does nothing here." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqImporteren" } }, { @@ -7101,6 +7851,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Import.vue:103-108 mode: skip if same revision, skip always, overwrite, merge, create new; src/js/Manager/ImportManager.js:225,303 match by existing id Note: Existing objects are matched by id and handled per the chosen mode; content-level duplicate detection (same URL and username) is not done for foreign imports.", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match; ui/lib/kv/addon/routes.js:8 no import route Note: No importer exists, so no duplicate detection.", "passbolt": "passbolt/passbolt_browser_extension@v5.16.0 searched 'duplicate' in src/all/background_page/service/resource/import and src/all/background_page/model/import: only in parser tests about duplicate CSV columns Note: Imports never check for items that already exist. Moved from unknown to no." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqImporteren" } }, { @@ -7133,6 +7887,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Import.vue:327-336 'Import partially failed' with 'Some objects had errors:' joined into one message Note: Errors are collected and shown in a single message after the import; there is no structured report listing rejected rows.", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match; ui/lib/kv/addon/routes.js:8 no import route Note: No importer exists, so no import report.", "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Resource/ImportResources/ImportResourcesResult.js:249 resource errors, :257 folder errors, :376 Error details with a copyable stack; passbolt/passbolt_browser_extension@v5.16.0 src/all/background_page/service/resource/import/ImportResourcesService.js:271 importFoldersErrors Note: After import a result dialog lists counts and each rejected resource or folder with its error. Moved from unknown to yes." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqImporteren" } }, { @@ -7180,6 +7938,13 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match; ui/lib/kv/addon/routes.js:8 no import route Note: No Credential Exchange Format support.", "passbolt": "passbolt/passbolt_browser_extension@v5.16.0 searched 'cxf', 'credential exchange' in src/: only binary image matches; passbolt/passbolt_styleguide@v5.16.0 same in src/: no code match Note: No Credential Exchange Format import or export. Moved from unknown to no.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'cxf', 'credential exchange' in src: no match; src/vue/Components/Import.vue:9-26 and src/vue/Components/Export.vue:7-11 list no CXF format Note: No Credential Exchange Format support. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqImporteren", + "KqExporteren" + ] } }, { @@ -7212,6 +7977,10 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match Note: No Credential Exchange Protocol support.", "passbolt": "passbolt/passbolt_browser_extension@v5.16.0 searched 'cxp', 'credential exchange' in src/: no code match; passbolt/passbolt_styleguide@v5.16.0 same: no match Note: No Credential Exchange Protocol transfer. Moved from unknown to no.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'cxp', 'credential exchange' in src: no match Note: No Credential Exchange Protocol support. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqCxpOverdracht" } }, { @@ -7259,6 +8028,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Export.vue:7 'Database Backup' with optional backup password (:20-30, minlength 10); src/js/Manager/ExportManager.js:47-51 encrypts each section with options.password; src/lib/Command/BackupCreateCommand.php:45 and src/lib/Cron/BackupJob.php server-side backups; src/lib/Command/BackupRestoreCommand.php:45 Note: Users export an optionally password-encrypted JSON backup that imports back; admins also get scheduled encrypted server backups with occ restore.", "keeper": "https://docs.keeper.io/user-guides/export-and-reports/vault-export : 'Encrypted Keepass (.kdbx)' export protected by a chosen master password or key file, re-importable (https://docs.keeper.io/user-guides/import-records-1/import-from-keepass-kdbx)", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_raft.go:142 sys/storage/raft/snapshot (save and restore), :158 snapshot-force; ui/app/components/raft-storage-overview.hbs:12 Download snapshot; ui/app/components/raft-storage-restore.hbs restore upload; ui/app/components/recovery/page/snapshots/snapshot-manage.hbs:6 Secrets recovery (Enterprise) Note: Integrated storage snapshots stay barrier-encrypted and can be downloaded and restored from the UI, even in CE; automated snapshots and single-secret recovery are Enterprise." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExporteren" } }, { @@ -7292,6 +8065,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Export.vue:8 'Predefined CSV', :11 'Custom CSV', :9-10 xlsx/ods; src/js/Actions/Encryption/CheckPassphrase.js used only at src/vue/Section/Authorize.vue:211, not in export Note: Plaintext CSV and spreadsheet exports exist, but the export does not ask for the passphrase again.", "keeper": "https://docs.keeper.io/user-guides/export-and-reports/vault-export : plaintext CSV export exists and can be restricted by role policy; a master password re-prompt for export is not documented", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:8 no export route; searched 'csv' in ui/lib/kv/addon: no match; CSV exports only for client counts and agent registry Note: There is no plaintext export of secrets; a user can only script reads with the CLI or API." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExporteren" } }, { @@ -7324,6 +8101,10 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Resource/ExportResources/ExportResources.js:82 exports the selected folders (with subfolders) and :177 selected resources Note: Users export a selection of passwords or folders, or everything.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Export.vue:42-60 choose passwords, folders, tags; :70 excludeShared; :81-84 CSV database choice; :99 header toggle Note: You choose which object types go in, whether shared items are included, and CSV columns for custom CSV.", "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_raft.go:142 snapshot is the whole store; ui/lib/kv/addon/routes.js:8 no export route Note: The only export is a whole-cluster snapshot; nothing lets you choose what goes in." + }, + "screen": { + "board": null, + "reason": "not designed yet: KqExporteren has no selection step" } }, { @@ -7365,6 +8146,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Components/Import.vue:19 'KeePassX CSV'; src/js/Helper/Import/CsvConversionHelper.js:423 keepass mapping; searched 'kdbx' in src/js: no match Note: KeePass works through its CSV export only; .kdbx files are not read.", "keeper": "https://docs.keeper.io/user-guides/import-records-1/import-from-keepass : 'Import from KeePass (.XML)' and (.kdbx) importers", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match Note: No KeePass import exists." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqImporteren" } }, { @@ -7412,6 +8197,10 @@ "nextcloud-passwords": "not in the cloned repos, docs rating kept: marius-wieschollek/passwords@2026.9.0 src/js/Services/AppStoreService.js:19 Apps section lists clients from the handbook app store; src/lib/Controller/Link/ConnectController.php:136 PassLink pairing used by the extension; code not public for this (passwords-webextension repo not read); docs rating kept: https://git.mdns.eu/nextcloud/passwords/-/wikis/Administrators/Feature-Comparison Note: Autofill lives in the separate extension repo; the server side it pairs with is here. Docs rating kept.", "keeper": "https://docs.keeper.io/user-guides/browser-extensions : KeeperFill 'Autofill Your Passwords' on websites", "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/: no match Note: There is no browser extension." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" } }, { @@ -7458,6 +8247,10 @@ "passbolt": "passbolt/passbolt_browser_extension@v5.16.0 src/all/background_page/pagemod/webIntegrationPagemod.js:78 attach only if assertTopFrameAttachConstraint, :87 frameId === TOP_FRAME_ID Note: The web integration attaches to the top frame only, so logins inside iframes cannot be filled.", "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/app ui/lib: no match Note: No browser extension, so no frame autofill.", "nextcloud-passwords": "source silent: extension/mobile repo not read (passwords-webextension) Note: Frame filling is extension behaviour that this repo cannot show." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" } }, { @@ -7491,6 +8284,10 @@ "nextcloud-passwords": "not in the cloned repos, docs rating kept: marius-wieschollek/passwords@2026.9.0 code not public for this (passwords-webextension repo not read); docs rating kept: https://git.mdns.eu/nextcloud/passwords/-/wikis/Administrators/Feature-Comparison (extension 'Detect new passwords', 'Detect changed passwords') Note: Save and update prompts are extension features. Docs rating kept.", "keeper": "https://docs.keeper.io/user-guides/browser-extensions#save-prompt : 'Keeper will offer to save a password to the vault, if you login manually on a site'; 'Prompt to Change' policy for updates", "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/app ui/lib: no match Note: No browser extension, so no save-login prompt." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" } }, { @@ -7532,6 +8329,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/Dialog/CreatePassword/CustomFields/CustomFieldType.vue:14-19 no OTP field type; server stores no OTP item for a client to compute; docs rating kept: https://git.mdns.eu/nextcloud/passwords/-/wikis/Administrators/Feature-Comparison ('One Time Passwords' not supported) Note: Without an OTP item type there is nothing for a client to fill; extension code itself not read.", "keeper": "https://docs.keeper.io/user-guides/browser-extensions#autofilling-2fa-codes : 'Upon autofilling your username and password via KeeperFill, when prompted, a stored two-factor code will also be autofilled'", "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/app ui/lib: no match Note: No browser extension, so no one-time code fill." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" } }, { @@ -7565,6 +8366,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'passkey' in src: no match, so the server stores no passkeys a client could serve; extension repo not read Note: No passkey storage server side, so the extension cannot act as a passkey provider against this vault.", "keeper": "https://docs.keeper.io/user-guides/browser-extensions#passkeys : create a passkey and log in with a passkey through KeeperFill", "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/app ui/lib: no match Note: No browser extension and no passkey support (searched 'webauthn', 'passkey': no match)." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" } }, { @@ -7598,6 +8403,10 @@ "keeper": "https://docs.keeper.io/user-guides/browser-extensions#stay-logged-in : 'Inactivity Logout Timer which automatically logs you out of Keeper after a period of inactivity'; admin Logout Timer policy", "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/app ui/lib: no match Note: No browser extension exists to lock.", "nextcloud-passwords": "source silent: extension/mobile repo not read; server side src/lib/Controller/Api/SessionApiController.php session lifetime also applies to extension sessions Note: Server sessions of any client expire, but whether the extension locks its own UI is not in this repo." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" } }, { @@ -7638,6 +8447,10 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/public contains favicon, fonts, images, robots.txt, no web app manifest; ui/app/index.html:14 viewport meta but no manifest link; the only service worker is for authenticated downloads (ui/lib/service-worker-authenticated-download) Note: The UI has no web app manifest, so it cannot be installed as a phone app.", "passbolt": "passbolt/passbolt_api@v5.16.0 git ls-files: no web app manifest or service worker in webroot/ or templates/ (only .ddev addon manifests); passbolt/passbolt_browser_extension@v5.16.0 the web UI runs inside the browser extension Note: The web vault cannot be installed as a phone app from the browser; phones use the native apps. Moved from unknown to no.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'manifest', 'webmanifest', 'serviceWorker' in src: no match Note: The web app ships no web manifest or service worker, so it cannot be installed as a PWA. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqTelefoon" } }, { @@ -7679,6 +8492,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/vue/App.vue:68-79 isMobile at innerWidth <= 768 switches navigation and session timer; src/vue/App.vue:24 mobile session-timeout Note: The web UI has a mobile layout mode.", "keeper": "https://docs.keeper.io/user-guides/ios : full native iPhone/iPad app (and Android, https://docs.keeper.io/user-guides/android)", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/index.html:14 viewport meta; ui/lib/core/addon/components/sidebar/frame.hbs:53 Hds::AppSideNav @isResponsive=true @isCollapsible=true; ui/lib/kv/addon/components/page/list.hbs:100 'level is-mobile' rows Note: The UI shell is responsive (collapsible side nav, viewport meta, mobile-aware list rows), but it is a desktop admin console with wide forms and tables, not designed for phone flows, so partial." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqTelefoon" } }, { @@ -7712,6 +8529,10 @@ "nextcloud-passwords": "not in the cloned repos, docs rating kept: marius-wieschollek/passwords@2026.9.0 searched 'offline', 'serviceWorker' in src/js src/vue: no vault offline storage in the web app; docs rating kept: https://git.mdns.eu/nextcloud/passwords/-/wikis/Users/F.A.Q (some third-party apps cache offline) Note: The web app has no offline mode; offline reading exists only in some third-party clients. Docs rating kept.", "keeper": "https://docs.keeper.io/enterprise-guide/vault-offline-access : a local encrypted copy of the vault opens offline; 'Editing records' is among the actions not available offline; admin policy 1 to 30 days or off", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/public has no service worker for offline data (the only one is ui/lib/service-worker-authenticated-download); command/agentproxyshared/cache is a machine-side proxy cache, not a user vault Note: The web UI needs a live server; there is no offline read-only mode for users." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqGeheimVarianten" } }, { @@ -7773,6 +8594,10 @@ "nextcloud-passwords": "not in the cloned repos, docs rating kept: marius-wieschollek/passwords@2026.9.0 src/js/Services/AppStoreService.js:19 Apps section lists third-party apps from the handbook app store; source silent: extension/mobile repo not read; docs rating kept: https://github.com/marius-wieschollek/passwords#apps Note: Native mobile apps are third party and outside this repo. Docs rating kept.", "keeper": "https://docs.keeper.io/user-guides/autofill-setup-for-ios : system autofill and passkeys on iOS; Android autofill at https://docs.keeper.io/user-guides/autofill-setup-for-android", "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/app ui/lib: no match Note: No native mobile apps." + }, + "screen": { + "board": null, + "reason": "not designed yet: KqTelefoon is the installed web app, not a native app" } }, { @@ -7842,6 +8667,10 @@ "nextcloud-passwords": "not in the cloned repos, docs rating kept: marius-wieschollek/passwords@2026.9.0 src/js/Services/AppStoreService.js:19 app catalogue fetched from the handbook; source silent: extension repo not read; docs rating kept: https://git.mdns.eu/nextcloud/passwords/-/wikis/Users/Browser-Extension Note: Firefox, Chrome and Edge per docs, no Safari. Docs rating kept.", "keeper": "https://docs.keeper.io/user-guides/browser-extensions : KeeperFill for Chrome, Firefox, Safari, Microsoft Edge, Opera and other Chromium browsers", "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/app ui/lib: no match Note: No browser extensions." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (browser store packages)" } }, { @@ -7873,6 +8702,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'ssh', 'agent' in src/lib src/js: no SSH agent or key item Note: No SSH agent.", "keeper": "https://docs.keeper.io/keeperpam/privileged-access-manager/ssh-agent : 'Keeper's built-in SSH agent' serves SSH keys stored in the vault (documented under KeeperPAM)", "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/logical/ssh/path_sign.go:17 sign/ signs a user's public key; command/ssh.go:70 vault ssh -mode=ca signs the local key and connects; searched 'SSH_AUTH_SOCK', 'ssh-agent' in command builtin: no agent Note: Vault signs users' own keys as an SSH CA and the CLI can connect with them, but no SSH agent serves private keys kept in Vault." + }, + "screen": { + "board": null, + "reason": "backend: no screen of its own (SSH agent in the command-line tool)" } }, { @@ -7920,6 +8753,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/appinfo/routes.php link/connect/request..apply; src/lib/Controller/Link/ConnectController.php:136-260 PassLink request, confirm, apply; src/vue/Dialog/ConnectClient.vue with ConnectLink.vue QR/link and ConnectConfirm.vue; src/appinfo/routes.php user/createAppToken Note: The PassLink flow pairs an extension or app with the logged-in Nextcloud session via QR code or link.", "keeper": "https://docs.keeper.io/enterprise-guide/integrations : standalone product; the extension links to a Keeper account, with no Nextcloud integration", "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'nextcloud' in all .go .hbs .ts .js files: no match Note: No Nextcloud integration and no browser extension." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" } }, { @@ -7951,6 +8788,13 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 http/handler.go:546 sys/events/subscribe; http/events_stubs_oss.go entHandleEventsSubscribe stub in CE | docs: https://developer.hashicorp.com/vault/docs/concepts/events Note: Event notifications for KV writes and deletes exist for external subscribers (Enterprise handler not in CE); there is no flow builder.", "passbolt": "passbolt/passbolt_api@v5.16.0 searched 'webhook', 'workflow' in src/ plugins/: no match; events only feed email notifications and the action log Note: No flow builder or webhooks to automate follow-up on vault events. Moved from unknown to no.", "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'workflowengine', 'IOperation', 'flow' in src/lib: no match; events in src/lib/Events are internal only Note: No Nextcloud Flow integration or webhooks. Moved from unknown on a documented search." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "PqFlows", + "PqFlow" + ] } }, { @@ -7996,6 +8840,10 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/ThemeManager.js:13 uses OCA.Theming color; src/lib/Helper/Settings/ThemeSettingsHelper.php exposes theming to clients; src/lib/Controller/Link/ConnectController.php:304 theme() for paired clients Note: The app follows the Nextcloud theme, and passes it to paired clients.", "keeper": "https://docs.keeper.io/enterprise-guide/user-and-team-provisioning/custom-invite-and-logo : admins upload the company logo to the vault and customise the invite; users pick a colour theme (https://docs.keeper.io/user-guides/web-vault#settings); no full house-style theming", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/config-ui/addon/routes.js:8 custom messages and login-settings only; searched 'custom_logo', 'brand_color', 'custom-theme' in ui/app ui/lib vault/*.go: no match Note: Admins can post custom banner messages, but the UI cannot be themed with logo or colours." + }, + "screen": { + "board": null, + "reason": "Nextcloud theming or thematiq; no Keepiq board" } }, { @@ -8027,6 +8875,10 @@ "keeper": "https://docs.keeper.io/enterprise-guide/roles/enforcement-policies#vault-features : 'Disable in-app onboarding' hides the 'Quick Start' module that otherwise appears 'when they login in for the first time'", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/utils/constants/wizard.ts:8 wizards for acl-policy, auth-methods, secret-engines, namespace, agent-registry; ui/app/components/wizard/index.ts:58 intro shown until dismissed; ui/app/components/page/policies.hbs:41 AclWizard mounted on the policies page; ui/app/components/wizard/guided-start.hbs:6 Guided Start Note: First-visit intro and guided-start wizards appear on the policies, auth methods, secrets engines and namespaces pages for admins, but there is no end-user tour of the app on first open, so partial.", "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/AuthenticationSetup/SetupAuthentication/SetupAuthentication.js step-by-step first-run setup (key, passphrase, recovery kit, security token); src/react-extension/components/Administration/HomePage/AdministrationHomePage.js:101 admin Getting Started card for metadata settings Note: First use is a guided setup wizard for the account, but there is no product walkthrough of the vault itself. Moved from unknown to partial." + }, + "screen": { + "board": null, + "reason": "not designed yet: no walkthrough board" } }, { @@ -8068,6 +8920,10 @@ "keeper": "https://docs.keeper.io/enterprise-guide/risk-management-dashboard : admin Risk Management Dashboard and Admin Console dashboard with organisation figures; end users get the Security Audit screen, not a dashboard", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js dashboard route; ui/app/components/dashboard/overview.hbs:23 SecretsEngines, :36 ClusterConfiguration, :50 ClientCount, :27 Replication widgets Note: The Vault dashboard shows its own figures: mounted engines, client counts, replication and cluster configuration.", "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Administration/HomePage/AdministrationHomePage.js:90-130 cards linking to settings pages, no figures; searched 'dashboard' in src/react-extension/components: no vault statistics component Note: There is no dashboard with vault figures; the admin home is a list of settings. Moved from unknown to no." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqDashboard" } }, { @@ -8101,6 +8957,10 @@ "nextcloud-passwords": "source silent: extension/mobile repo not read; the web app has no offline mode (searched 'offline', 'serviceWorker' in src/js src/vue: no match) Note: The web app cannot edit offline; whether third-party mobile apps queue edits is outside this repo.", "onepassword": "https://support.1password.com/sync/ : in the apps data is cached locally so you can view and edit it without an internet connection, and changes reach other devices when you next go online.", "keeper": "not checked: https://docs.keeper.io/user-guides/vault-offline-access read; current docs list creating and editing records offline while older release notes (https://docs.keeper.io/release-notes/desktop/web-vault-+-desktop-app/older/vault-version-14.4.0) describe read-only offline access, so it is not settled." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqOfflineConflict" } }, { @@ -8166,6 +9026,10 @@ "nextcloud-passwords": "source silent: extension/mobile repo not read; server side each client pairs per account via src/lib/Controller/Link/ConnectController.php:136 Note: Multi-account switching is client behaviour outside this repo.", "onepassword": "https://support.1password.com/multiple-accounts/ : add multiple accounts to the apps and browser extension, see all items at once or 'Switch to a specific account'.", "keeper": "https://docs.keeper.io/user-guides/tips-and-tricks/personal-and-business-vaults : switch between business and personal accounts in the web vault, browser extension and mobile apps ('Switch Account', 'Add Account')." + }, + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" } }, { @@ -8227,6 +9091,10 @@ "bitwarden": "bitwarden/clients@web-v2026.9.0 apps/web/src/locales holds 66 locales, chosen in the account preferences. Note: rated by the lane from the source.", "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/package.json:121 depends on ember-intl but git ls-files holds no translations or locales files; the UI is English only. Note: rated by the lane from the source.", "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/locales/ holds 17 locales (cs-CZ to uk-UA); src/react-extension/components/Administration/DisplayInternationalizationAdministration sets the organisation default Note: proposed by the source reader as a capability no row covered." + }, + "screen": { + "board": null, + "reason": "Nextcloud personal language setting; no Keepiq board" } }, { @@ -8250,7 +9118,11 @@ "rowSource": "own", "provider": "keepiq", "providerHow": "read-from-code", - "source": "spec-round-2026-10-07" + "source": "spec-round-2026-10-07", + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" + } }, { "id": "clients-25", @@ -8273,7 +9145,11 @@ "rowSource": "own", "provider": "keepiq", "providerHow": "read-from-code", - "source": "spec-round-2026-10-07" + "source": "spec-round-2026-10-07", + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" + } }, { "id": "clients-26", @@ -8296,7 +9172,14 @@ "rowSource": "own", "provider": "keepiq", "providerHow": "read-from-code", - "source": "spec-round-2026-10-07" + "source": "spec-round-2026-10-07", + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": [ + "KqExtensie", + "KqExtensieBewerken" + ] + } }, { "id": "clients-27", @@ -8319,7 +9202,11 @@ "rowSource": "own", "provider": "keepiq", "providerHow": "read-from-code", - "source": "spec-round-2026-10-07" + "source": "spec-round-2026-10-07", + "screen": { + "canvas": "5NkFW28vZUUij43xzxHg5a", + "board": "KqExtensie" + } } ], "pending": [] From d139fb3e63276d20b3e5de2d12f1bd692813d32c Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 7 Oct 2026 21:58:12 +0200 Subject: [PATCH 006/167] docs(parity): re-rate the twelve rows whose changes were archived on 4 October Ten rows are built on development and read built; apps-22 (registry publication owed) and clients-01 (store listings owed) read building. audit-15 stays partial with sharper evidence: no alert for failed unlocks or admin changes. --- openspec/parity/capabilities.json | 156 +++++++++++++++++------------- 1 file changed, 87 insertions(+), 69 deletions(-) diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 363aa0821..8a75916cb 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -1978,7 +1978,7 @@ "id": "crypto-11", "area": "crypto", "name": "Let an administrator restore a user's access after a forgotten master password.", - "keepiq": "no", + "keepiq": "yes", "bitwarden": "yes", "onepassword": "yes", "passbolt": "yes", @@ -1986,11 +1986,13 @@ "hashicorp-vault": "yes", "nextcloud-passwords": "no", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/crypto-organisation-account-recovery on 2026-09-27. Before: src/components/settings/AdminSuiteSection.vue:21,180 admin can only force-revoke a suite so the user starts a new empty vault; grep -rni 'escrow\\|recovery key\\|account recovery' lib: only emergency-access envelopes escrowed to a contact's certificate, never to an admin", + "state": "built", + "evidence": "src/views/LockScreen.vue:306 ForgotPasswordRecovery on /lock files a request (appinfo/routes.php:302 recoveryUser#createRequest); src/App.vue:200 RecoveryOfficerPanel lets officers compare the phrase and approve (lib/Service/RecoveryRequestService.php:222 approve, threshold of distinct officers, self-approval refused); src/App.vue:197 AccountRecoveryEnrolment enrols the user; policy, officers and threshold in AccountRecoverySection (src/views/settings/adminAreas.js:56); all 16 tasks of the archived change ticked, Playwright flow in the change's task 6.1", "owner": "ConductionNL/keepiq", - "note": "By zero-knowledge design an administrator cannot restore access to a user's secrets. The admin can force-revoke the locked suite so the user can set up a fresh vault, but the old secrets stay unreadable unless the user has an emergency contact or a backup file.", - "change": "openspec/changes/archive/2026-10-04-crypto-organisation-account-recovery" + "note": "Recovery officers, not one administrator, restore access: a user enrolled under the account recovery policy files a request from the lock screen, a threshold of officers approves after comparing a verification phrase, and the user sets a new master password and keeps their secrets. Users who never enrolled still cannot be recovered, by zero-knowledge design.", + "change": "openspec/changes/archive/2026-10-04-crypto-organisation-account-recovery", + "reachedOn": "Lock screen (/lock) -> Forgot master password; officers approve from the recovery panel in the user menu; policy under Administration -> Keepiq - People", + "readOn": "2026-10-07" }, "rowSource": "competitor", "provider": "keepiq", @@ -2439,7 +2441,7 @@ "id": "crypto-24", "area": "crypto", "name": "Approve a sign-in on a new device from a device where you are already signed in, or have an administrator approve it.", - "keepiq": "no", + "keepiq": "yes", "bitwarden": "yes", "onepassword": "unknown", "passbolt": "yes", @@ -2447,12 +2449,13 @@ "hashicorp-vault": "no", "nextcloud-passwords": "yes", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/crypto-new-device-approval on 2026-09-27. Before: searched 'device approval', 'auth request', 'approveLogin', 'LoginRequest' in lib/ src/ appinfo/routes.php: no match; the vault unlocks with the user's own passphrase on each device", + "state": "built", + "evidence": "src/views/LockScreen.vue:303 DeviceApprovalRequest ('Approve from another device', and 'Ask your organisation instead' for recovery-enrolled users, src/components/DeviceApprovalRequest.vue:24); src/App.vue:422 DeviceApprovalDialog on an unlocked device seals the unlock key (lib/Controller/DeviceApprovalController.php:173 approve, vault-key proof required); lib/BackgroundJob/ExpireDeviceApprovalsJob.php expires requests; admin switch DeviceApprovalSection (src/views/settings/adminAreas.js:28); extension locked popup uses browser-extension/src/lib/api.js:841", "owner": "ConductionNL/keepiq", - "reachedOn": "nothing", - "note": "Sign-in is Nextcloud's; a new device unlocks the vault with the user's passphrase, and there is no approve-from-another-device or admin approval flow.", - "change": "openspec/changes/archive/2026-10-04-crypto-new-device-approval" + "reachedOn": "Lock screen (/lock) -> Approve from another device; the approving device gets the approval dialog while unlocked", + "note": "A new device asks for approval on the lock screen; an unlocked device of the same user compares the phrase and approves, and the new device unlocks without the master password. Administrators approve only through organisation account recovery (crypto-11), for users enrolled in it; there is no plain admin approval.", + "change": "openspec/changes/archive/2026-10-04-crypto-new-device-approval", + "readOn": "2026-10-07" }, "rowSource": "competitor", "origin": "changelog", @@ -3084,7 +3087,7 @@ "id": "sharing-17", "area": "sharing", "name": "Share with someone outside the organisation who has their own account elsewhere.", - "keepiq": "no", + "keepiq": "yes", "bitwarden": "partial", "onepassword": "yes", "passbolt": "no", @@ -3092,11 +3095,13 @@ "hashicorp-vault": "no", "nextcloud-passwords": "no", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/sharing-federated-recipients on 2026-09-27. Before: grep -rn 'federat\\|remote share\\|outside.*organi[sz]ation' lib/Controller lib/Service: no sharing-related hits (the one federated-cloud-ID hit in EncryptionSuiteProvisioningService.php:331 is only used to name a certificate's common name, not to share with an external account). All sharing paths (user-to-user, team folder, link, send) target a Nextcloud user ID, group ID, or an anonymous public link on this instance.", + "state": "built", + "evidence": "src/dialogs/SecretShareDialog.vue:137 FederatedShareForm verifies the partner's certificate chain in the browser and encrypts; appinfo/routes.php:392 federatedShare#create; lib/Federation/KeepiqSecretFederationProvider.php:81 shareReceived; src/views/IncomingSharesView.vue ('Incoming from other organisations', src/manifest.json:137) accepts into a read-only copy; partners pinned in FederationPartnersSection (src/views/settings/adminAreas.js:31); .github/workflows/federation.yml two-instance test", "owner": "ConductionNL/keepiq", - "note": "Sharing with an account on a different Nextcloud instance is not supported; a password-protected public link (sharing-14) is the closest substitute.", - "change": "openspec/changes/archive/2026-10-04-sharing-federated-recipients" + "note": "Shares go to a user on another Nextcloud 33 or newer instance running Keepiq, over OCM, once both administrators have pinned each other as partners. The recipient gets a read-only copy that follows the owner's updates and ends when the owner revokes.", + "change": "openspec/changes/archive/2026-10-04-sharing-federated-recipients", + "reachedOn": "Secret share dialog -> Other organisation; recipient: Incoming from other organisations; partners under Administration -> Keepiq", + "readOn": "2026-10-07" }, "rowSource": "competitor", "provider": "keepiq", @@ -3122,7 +3127,7 @@ "id": "sharing-18", "area": "sharing", "name": "Give people roles such as manager or viewer on a shared collection.", - "keepiq": "no", + "keepiq": "yes", "bitwarden": "yes", "onepassword": "yes", "passbolt": "yes", @@ -3130,10 +3135,13 @@ "hashicorp-vault": "partial", "nextcloud-passwords": "no", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/sharing-team-folder-manager-role on 2026-09-27. Before: The only role-like concept in the codebase is a team folder membership grade of read or write (folder-permission-grades spec, sharing-09); there is no manager/viewer role vocabulary and no 'collection' concept beyond team folders. grep -rln 'manager\\|viewer' src/store/modules lib/Controller for role-purposed hits: none.", + "state": "built", + "evidence": "src/modals/TeamFolderDialog.vue:113 Viewer, Editor and Manager per member; lib/Service/TeamFolderQueryService.php:271 resolveGrade ranks read < write < manage, :359 loadManageableTeamFolder lets a manager add, remove and grade members; tests/e2e/workflows/team-folder-manager.spec.ts", "owner": "ConductionNL/keepiq", - "change": "openspec/changes/archive/2026-10-04-sharing-team-folder-manager-role" + "change": "openspec/changes/archive/2026-10-04-sharing-team-folder-manager-role", + "reachedOn": "Team folder dialog (folder actions -> Team folder) -> member grade", + "note": "Team folder members are viewers, editors or managers. A manager adds and removes members and sets viewer or editor; only the owner names managers and deletes or unshares the folder.", + "readOn": "2026-10-07" }, "rowSource": "competitor", "provider": "keepiq", @@ -3326,7 +3334,7 @@ "id": "sharing-24", "area": "sharing", "name": "Let a colleague sign in with a shared login without being able to see or copy the password.", - "keepiq": "no", + "keepiq": "yes", "bitwarden": "yes", "onepassword": "yes", "passbolt": "partial", @@ -3334,12 +3342,13 @@ "hashicorp-vault": "no", "nextcloud-passwords": "no", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/sharing-use-only-and-expiring-shares on 2026-09-27. Before: git grep -i 'hidePassword|useOnly|can_view' src lib: no match; team folder roles are read-only or edit (sharing-09), both reveal the value", + "state": "built", + "evidence": "src/components/share/ShareRestrictionFields.vue use-only checkbox in BulkShareDialog (src/dialogs/BulkShareDialog.vue:36), GroupShareForm and TeamFolderDialog; web app hides reveal, copy, edit and versions for use-only copies; browser-extension/src/lib/useOnly.js:22 fills the password field only, no display or copy; lib/Controller/UseOnlyController.php records each use (appinfo/routes.php:149); lib/Db/Secret.php:101 refuses onward shares of the copy; CLI refuses show and copy", "owner": "ConductionNL/keepiq", - "reachedOn": "nothing", - "note": "Every recipient who can use a secret can also reveal it.", - "change": "openspec/changes/archive/2026-10-04-sharing-use-only-and-expiring-shares" + "reachedOn": "Share dialog -> Use only (cannot see or copy); the recipient fills through the browser extension", + "note": "A use-only recipient can fill the login through the extension but cannot reveal, copy, edit, export or share it onward in the web app, extension or CLI. The dialog says honestly that the browser still holds the value while filling.", + "change": "openspec/changes/archive/2026-10-04-sharing-use-only-and-expiring-shares", + "readOn": "2026-10-07" }, "rowSource": "competitor", "origin": "tender", @@ -3366,7 +3375,7 @@ "id": "sharing-25", "area": "sharing", "name": "Share an item with a colleague for a set period, after which their access ends by itself.", - "keepiq": "no", + "keepiq": "yes", "bitwarden": "partial", "onepassword": "partial", "passbolt": "no", @@ -3374,12 +3383,13 @@ "hashicorp-vault": "partial", "nextcloud-passwords": "yes", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/sharing-use-only-and-expiring-shares on 2026-09-27. Before: lib/Controller/ShareController.php: no expiry field on a user share (git grep -i expir lib/Controller/ShareController.php: no match); time-bound access exists only for public links (sharing-14) and ownership handover (sharing-11)", + "state": "built", + "evidence": "src/components/share/ShareRestrictionFields.vue end date on direct, group and team-folder shares; lib/Controller/ShareController.php:132 expiresAt, PATCH /api/v1/shares/{id}; lib/Db/SecretMapper.php:149 excludeAccessExpired on list, search, extension match and offline manifest; lib/BackgroundJob/ExpireSharesJob.php:83 revokes every 15 minutes; share_access_ending/ended notifications in lib/Notification/KeepiqNotifier.php:297", "owner": "ConductionNL/keepiq", - "reachedOn": "nothing", - "note": "A share to a colleague lasts until it is revoked by hand.", - "change": "openspec/changes/archive/2026-10-04-sharing-use-only-and-expiring-shares" + "reachedOn": "Share dialog -> Access ends on (date)", + "note": "A share, group share or team folder membership can carry an end date. After it the copy disappears from every read path, a job revokes it, and owner and recipient are notified, with a rotation hint for the owner.", + "change": "openspec/changes/archive/2026-10-04-sharing-use-only-and-expiring-shares", + "readOn": "2026-10-07" }, "rowSource": "competitor", "origin": "changelog", @@ -4674,7 +4684,7 @@ "id": "apps-22", "area": "apps", "name": "Manage secrets as code with a Terraform provider.", - "keepiq": "no", + "keepiq": "partial", "bitwarden": "yes", "onepassword": "yes", "passbolt": "partial", @@ -4682,11 +4692,13 @@ "hashicorp-vault": "yes", "nextcloud-passwords": "no", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/apps-terraform-provider on 2026-09-27. Before: grep -rli 'terraform' . --include=*.md --include=*.php --include=*.go: no hits", - "note": "No Terraform provider exists for managing keepiq secrets/applications as code.", + "state": "building", + "evidence": "integrations/terraform-provider-keepiq/internal/provider/resource_secret.go:20 resource keepiq_secret with write-only value, ephemeral_secret.go, data_secret_metadata.go, resource_application.go, resource_lease_policy.go; docs/ generated by tfplugindocs; .github/workflows/integrations-terraform.yml tests and mirrors on tf-v* tags; task 3.3 owed: the mirror repository and the Terraform and OpenTofu registry entries need an organisation admin", + "note": "The provider is written and tested (secret, ephemeral secret, metadata data source, application and lease policy), but it is not published: terraform init cannot resolve conductionnl/keepiq until an organisation admin creates the mirror repository and registers it.", "owner": "ConductionNL/keepiq", - "change": "openspec/changes/archive/2026-10-04-apps-terraform-provider" + "change": "openspec/changes/archive/2026-10-04-apps-terraform-provider", + "reachedOn": "Terraform: build the provider from integrations/terraform-provider-keepiq and use it with dev_overrides; not yet in a registry", + "readOn": "2026-10-07" }, "rowSource": "competitor", "provider": "keepiq", @@ -6492,13 +6504,14 @@ "hashicorp-vault": "yes", "nextcloud-passwords": "no", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/admin-scoped-roles on 2026-09-27 for the missing half: named admin roles with a chosen set of permissions; the whole-section delegation and the vault_admin group are built. Before: lib/Service/TeamFolderOffboardingService.php:45 and lib/Service/DelegationAuthorizer.php:49 vault_admin group; admin settings via #[AuthorizedAdminSetting(AdminSettings::class)] (lib/Controller/SettingsController.php:159) so Nextcloud admin delegation can hand the whole Keepiq section to a group; admin handover reachable since f13ad8e6: appinfo/routes.php:150 delegation#handover -> lib/Controller/DelegationController.php:168 createAdminHandover, called from src/components/share/AdminHandoverPanel.vue (mounted in SecretDetailSidebar.vue:700, shown when isVaultAdmin) via src/store/modules/delegation.js:169", + "state": "built", + "evidence": "lib/Settings/PolicyAdminSettings.php, ApplicationAdminSettings, PeopleAdminSettings, AuditAdminSettings plus AdminSettings (General), registered in appinfo/info.xml:235; lib/Service/AdminAreaAuthorizer.php:105 holds() on Nextcloud's delegated settings guards every admin route per area; src/views/settings/adminAreas.js:23 renders only the delegated area's sections; tests/e2e/workflows/admin-area-delegation.spec.ts", "owner": "ConductionNL/keepiq", - "reachedOn": "Nextcloud admin delegation for the Keepiq settings section; vault_admin group for offboarding and for the admin handover panel in the secret sidebar", - "note": "There are two coarse levers: Nextcloud's delegation of the whole Keepiq admin section, and a hard-coded vault_admin group that unlocks offboarding and admin handover. The handover now has a route, a controller call and a UI panel (f13ad8e6, closing #184), so both levers work end to end. There is still no role editor and no per-permission role, so a person cannot be given only the permissions they need: partial.", + "reachedOn": "Nextcloud Administration -> Administration privileges: delegate Keepiq - General, Policies, Applications, People or Audit to a group", + "note": "An administrator delegates any of five Keepiq admin areas (General, Policies, Applications, People, Audit) to a group, and each area's routes and screens check that delegation. The areas are fixed: there is no custom role with a hand-picked set of permissions. The vault_admin alias for People goes in keepiq#1043.", "defects": [], - "change": "openspec/changes/archive/2026-10-04-admin-scoped-roles" + "change": "openspec/changes/archive/2026-10-04-admin-scoped-roles", + "readOn": "2026-10-07" }, "rowSource": "competitor", "provider": "keepiq", @@ -7036,7 +7049,7 @@ "id": "admin-27", "area": "admin", "name": "Administrators schedule automatic encrypted backups of every vault on the server and restore them from the command line.", - "keepiq": "no", + "keepiq": "yes", "bitwarden": "partial", "onepassword": "unknown", "passbolt": "no", @@ -7044,12 +7057,13 @@ "hashicorp-vault": "partial", "nextcloud-passwords": "yes", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/admin-scheduled-vault-backups on 2026-09-27. Before: searched 'backup' in lib/Command lib/BackgroundJob: no match; the encrypted-backup export (portability-09) is per user and started by hand", + "state": "built", + "evidence": "lib/BackgroundJob/ScheduledVaultBackupJob.php:68 runs when due with retention; lib/Backup/ArchiveWriter.php and ArchiveCipher.php write an AES-256-GCM archive wrapped for the configured RSA public key; lib/Command/BackupCreate, BackupList, BackupVerify and BackupRestore.php:69 keepiq:backup:restore; VaultBackupSection (src/views/settings/adminAreas.js:30) sets schedule, retention and key; restore verified live 4 Oct per the archived change's task 4.2", "owner": "ConductionNL/keepiq", - "reachedOn": "nothing", - "note": "There is no scheduled server-side backup of all vaults and no restore command; an instance relies on the Nextcloud database backup.", - "change": "openspec/changes/archive/2026-10-04-admin-scheduled-vault-backups" + "reachedOn": "Administration -> Keepiq - General -> Vault backups; occ keepiq:backup:*", + "note": "Scheduled encrypted backups of every vault table, encrypted for an administrator's public key, with retention, a Back up now action and occ commands to list, verify and restore. Restored vaults unlock with the master passwords that were valid at backup time.", + "change": "openspec/changes/archive/2026-10-04-admin-scheduled-vault-backups", + "readOn": "2026-10-07" }, "rowSource": "competitor", "origin": "featureRequest", @@ -7605,11 +7619,12 @@ "nextcloud-passwords": "partial", "built": { "state": "built", - "evidence": "lib/Service/HoneyTripwireService.php:173 honey_access notification; emergency_access_requested/accessed and secret_compromised notification subjects in lib/Service; lib/Service/SiemService.php:363 siem_dead_letter; grep for failed-unlock, mass-export or admin-change alert rules in lib: none", + "evidence": "lib/Service/HoneyTripwireService.php:174 honey_access; lib/Service/EmergencyAccessService.php:295 emergency_access_requested; lib/Service/CompromiseContainmentService.php:333 secret_compromised; lib/Service/DeviceApprovalService.php:163 device_approval_requested; lib/Service/RecoveryRequestService.php:504 recovery_requested; lib/Service/SiemService.php:384 siem_dead_letter; git grep -iE 'failed.?unlock|unlock_failed|policy_changed' lib: no hits", "owner": "ConductionNL/keepiq", - "reachedOn": "Nextcloud notifications for honey access, emergency access and breached secrets", - "note": "Real-time alerts exist for a fixed set of events (decoy touched, emergency access, compromised secret). There is no configurable risk rule engine for user or admin behaviour such as failed unlocks, bulk exports or policy changes; that would sit in a SIEM fed by the export.", - "change": "openspec/changes/archive/2026-07-20-honey-credentials" + "reachedOn": "Nextcloud notifications for decoy access, emergency access, compromised secrets, new-device and recovery requests", + "note": "Real-time alerts cover a fixed set of risky events: a decoy touched, emergency access, a compromised secret, a new-device approval request and an account recovery request. Nothing alerts on failed unlocks (the vault unlocks in the browser, so the server does not see a wrong master password) or on admin changes such as policy edits; those reach only the audit trail and the SIEM export. Partial is right, not generous.", + "change": "openspec/changes/archive/2026-07-20-honey-credentials", + "readOn": "2026-10-07" }, "rowSource": "competitor", "provider": "keepiq", @@ -8164,11 +8179,11 @@ "hashicorp-vault": "no", "nextcloud-passwords": "yes", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/clients-extension-store-release on 2026-09-27 for the missing half: a packaged, signed extension in the browser stores. Matrix corrected: matching already covers shared and team folder secrets, because every recipient copy is a row owned by the recipient (lib/Service/RecipientSecretCopyService.php:112-113, lib/Service/TeamFolderShareService.php:295), so the owner-scoped match (lib/Controller/ExtensionController.php:195) returns them. Before: browser-extension/src/popup/popup.js:45 send('match') -> browser-extension/src/background/service-worker.js:91 doMatch -> browser-extension/src/lib/api.js:97 GET /api/v1/extension/match -> appinfo/routes.php:407 -> lib/Controller/ExtensionController.php:180 (owner's secrets by registrable domain) ; click -> service-worker.js:113 doFill (decrypt in worker) -> browser-extension/src/content/content-script.js:95 fillCredential", + "state": "building", + "evidence": "browser-extension/build.mjs Chrome and Firefox targets; .github/workflows/extension-release.yml builds and packs on extension-v* tags; browser-extension/src/lib/version.js minimum server version and update state; docs/browser-extension/release.md, privacy.md, permissions.md, rollout.md; autofill itself built (browser-extension/src/background/service-worker.js); owed per the archived change's tasks 2.1-3.2: the green release run, the store accounts and credentials, and the store listings", "owner": "ConductionNL/keepiq", - "reachedOn": "browser-extension popup (browser-extension/src/popup/popup.js) -> matched candidate list -> click to fill", - "note": "Autofill works in code: the popup lists owned secrets matching the site, decrypts in the worker and fills on click. The extension is not packaged or published, and matching only covers secrets the user owns, not shared or team-folder secrets. Corrected 2026-09-27: matching does cover shared and team folder secrets, because every recipient copy is a row owned by the recipient (lib/Service/RecipientSecretCopyService.php:112-113, lib/Service/TeamFolderShareService.php:295) and the match is owner-scoped (lib/Controller/ExtensionController.php:195). The missing half is the store release.", + "reachedOn": "Browser extension popup (loaded unpacked or from the self-hosted build); not yet in the Chrome, Edge or Firefox stores", + "note": "Autofill works, including shared and team-folder secrets, and the release pipeline for Chrome and Firefox is built. The extension is still not in any browser store: the store accounts, credentials and listings are owed, so users load it by hand.", "defects": [ { "at": "browser-extension/build.mjs:1", @@ -8183,7 +8198,8 @@ "needsLiveCheck": false } ], - "change": "openspec/changes/archive/2026-10-04-clients-extension-store-release" + "change": "openspec/changes/archive/2026-10-04-clients-extension-store-release", + "readOn": "2026-10-07" }, "rowSource": "own", "provider": "keepiq", @@ -8294,7 +8310,7 @@ "id": "clients-04", "area": "clients", "name": "Fill in the current one-time code together with the login.", - "keepiq": "partial", + "keepiq": "yes", "bitwarden": "yes", "onepassword": "yes", "passbolt": "yes", @@ -8302,11 +8318,11 @@ "hashicorp-vault": "no", "nextcloud-passwords": "no", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/clients-extension-store-release on 2026-09-27 for the missing half: filling a one-time code field that appears after the login step; filling a code field present at fill time is built. Before: browser-extension/src/background/service-worker.js:137 totpCodeForHost on fill -> :155 doTotpForHost (totp-typed secret matched by host, seed decrypted in worker, computeTotp browser-extension/src/lib/totp-service.js) -> :141 'fill-otp' -> content-script.js:115 fillOtp ; popup.js:66 copyWithAutoClear and popup.js:106 live code", + "state": "built", + "evidence": "browser-extension/src/content/otp-watch.js watches for a code field that appears after the login step; browser-extension/src/background/router.js:1275 doOtpFieldDetected fills only for the same tab, site, unexpired intent and unlocked vault; one-shot intent in chrome.storage.session holds no seed or code; tests/e2e/workflows/extension-otp-next-step.spec.ts", "owner": "ConductionNL/keepiq", - "reachedOn": "browser-extension popup (browser-extension/src/popup/popup.js) -> fill a login; the matching authenticator code is filled into an OTP field and copied", - "note": "After filling a login the worker looks for a totp-typed secret on the same host, fills a detected one-time-code field and copies the code with a 30 second clipboard clear. The OTP field has to be on the page at fill time, and the extension is not distributed.", + "reachedOn": "Browser extension: fill a login on a site with a matching totp secret; the code field is filled now or on the next step", + "note": "After a login fill the extension fills the one-time code, whether the code field is on the same page or appears on the next step. Distribution of the extension is tracked on clients-01.", "defects": [ { "at": "browser-extension/build.mjs:1", @@ -8315,7 +8331,8 @@ "needsLiveCheck": false } ], - "change": "openspec/changes/archive/2026-10-04-clients-extension-store-release" + "change": "openspec/changes/archive/2026-10-04-clients-extension-store-release", + "readOn": "2026-10-07" }, "rowSource": "own", "provider": "keepiq", @@ -8677,7 +8694,7 @@ "id": "clients-13", "area": "clients", "name": "Sign in over SSH with keys kept in the vault through an SSH agent.", - "keepiq": "no", + "keepiq": "yes", "bitwarden": "yes", "onepassword": "yes", "passbolt": "no", @@ -8685,12 +8702,13 @@ "hashicorp-vault": "partial", "nextcloud-passwords": "no", "built": { - "state": "specified", - "evidence": "Specified in openspec/changes/clients-ssh-agent on 2026-09-27. Before: grep -ril 'ssh-agent|ssh agent' lib src cli browser-extension: no hits; ssh_key exists only as a stored secret type (src/cxf/cxf.js:355)", + "state": "built", + "evidence": "cli/main.go:60 keepiq ssh-agent (--socket, --confirm, --idle, --folder, --locked); cli/sshagent/agent.go:130 Sign for Ed25519, ECDSA and RSA with SHA-2; cli/sshagent/socket.go:19 0700 directory, 0600 socket and peer uid check; idle lock drops keys; cli/sshagent/integration_test.go runs ssh against a real sshd", "owner": "ConductionNL/keepiq", - "reachedOn": "none", - "note": "SSH keys can be stored as secrets, but nothing exposes them to an SSH agent.", - "change": "openspec/changes/archive/2026-10-04-clients-ssh-agent" + "reachedOn": "CLI: eval \"$(keepiq ssh-agent)\" then ssh or git over SSH", + "note": "The CLI runs an SSH agent that serves the user's ssh_key secrets, with optional confirmation per use, an idle lock and a folder filter. Linux and macOS only; Windows prints 'not supported yet'.", + "change": "openspec/changes/archive/2026-10-04-clients-ssh-agent", + "readOn": "2026-10-07" }, "rowSource": "competitor", "provider": "keepiq", From aac6c6508d0e807f1bff924cd1b7272aa9e1685e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 7 Oct 2026 21:59:04 +0200 Subject: [PATCH 007/167] docs(openspec): specify the getting started walkthrough after the fact clients-17 was built without a spec. The new main spec describes the manifest tour, the lock-state gate and the per-user completion preference as they are on development. clients-16 stays spec none: theming comes from Nextcloud theming or thematiq. --- openspec/parity/capabilities.json | 9 ++- .../specs/getting-started-walkthrough/spec.md | 78 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 openspec/specs/getting-started-walkthrough/spec.md diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 8a75916cb..8789149ea 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -8877,11 +8877,12 @@ "nextcloud-passwords": "yes", "built": { "state": "built", - "evidence": "src/manifest.json:3 walkthrough (tour keepiq:getting-started, trigger first-visit, 7 steps) -> src/main.js:236 manifestForLockState (src/router/guards.js, withheld while locked) -> CnWalkthrough in @conduction/nextcloud-vue resolves nav-item refs via data-cn-route, emitted by src/components/KeepiqAppNav/KeepiqAppNav.vue:52 ; completion stored via GET/PUT /api/preferences/walkthrough_completed_version (src/router/guards.js:310, OpenRegister AppHost preferences controller)", - "owner": "ConductionNL/nextcloud-vue", + "evidence": "src/manifest.json:3 walkthrough (tour keepiq:getting-started, trigger first-visit, 6 steps) -> src/main.js:236 manifestForLockState (src/router/guards.js, withheld while locked) -> CnWalkthrough in @conduction/nextcloud-vue resolves nav-item refs via data-cn-route, emitted by src/components/KeepiqAppNav/KeepiqAppNav.vue:52 ; completion stored via GET/PUT /api/preferences/walkthrough_completed_version (appinfo/routes.php:544 preferences#getPreference, lib/Controller/PreferencesController.php:94)", + "owner": "ConductionNL/keepiq", "reachedOn": "First unlocked visit to the app -> Getting started tour", - "note": "A seven-step tour starts on the first unlocked visit and points at Dashboard, All secrets, Features and roadmap, Reports and Flows; it is held back while the vault is locked. Completion is stored through the OpenRegister AppHost preferences endpoint.", - "spec": "none" + "note": "A six-step tour starts on the first unlocked visit and points at Dashboard, All secrets, Features and roadmap, Reports and Flows; it is held back while the vault is locked. Completion is stored per user through Keepiq's own preferences endpoint (lib/Controller/PreferencesController.php), under the key the OpenRegister AppHost used. The tour renderer, CnWalkthrough, comes from @conduction/nextcloud-vue; the tour content, the lock gate and the preference store are Keepiq's.", + "spec": "openspec/specs/getting-started-walkthrough", + "readOn": "2026-10-07" }, "rowSource": "own", "provider": "nextcloud-vue", diff --git a/openspec/specs/getting-started-walkthrough/spec.md b/openspec/specs/getting-started-walkthrough/spec.md new file mode 100644 index 000000000..dd0aac37e --- /dev/null +++ b/openspec/specs/getting-started-walkthrough/spec.md @@ -0,0 +1,78 @@ +# Getting started walkthrough Specification + +**Status**: done + +**OpenSpec changes:** none. Written after the fact on 7 October 2026 to describe the code on development (parity row clients-17). + +## Purpose +A user who opens Keepiq for the first time gets a short guided tour of the main screens. Keepiq declares the tour in its manifest and decides when it may show; the tour itself is drawn by `CnWalkthrough` from `@conduction/nextcloud-vue`, which Keepiq consumes. Code: `src/manifest.json` (the `walkthrough` block), `src/router/guards.js` (`manifestForLockState`), `src/main.js` (the root render), `src/components/KeepiqAppNav/KeepiqAppNav.vue` (`data-cn-route` on each menu entry) and `lib/Controller/PreferencesController.php` (the completion preference). + +## Requirements + +### Requirement: The manifest declares a first-visit tour + +Keepiq SHALL declare one tour, `keepiq:getting-started`, in the `walkthrough` block of `src/manifest.json`, with `trigger: "first-visit"` and `completionConfigKey: "walkthrough_completed_version"`. The tour MUST open with a welcome step and then ask the user to open Dashboard, All secrets, Features and roadmap, Reports and Flows from the menu, each step advancing when the user reaches that route. The Flows step MUST let the user move on without opening Flows. + +#### Scenario: A first visit starts the tour + +@e2e exclude The e2e global setup (tests/e2e/global-setup.ts) suppresses the tour for every automated run, so no browser test sees it; the tour logic is CnWalkthrough's, tested in @conduction/nextcloud-vue. + +- **WHEN** a user who has never finished the tour opens Keepiq with the vault unlocked +- **THEN** the welcome step of `keepiq:getting-started` SHALL appear +- **AND** each later step SHALL point at its menu entry and advance when the user opens that page + +#### Scenario: A user skips the last step + +@e2e exclude Suppressed in e2e runs by tests/e2e/global-setup.ts; `allowManualNext` on the go-flows step in src/manifest.json is declarative. + +- **WHEN** the tour reaches the Flows step +- **THEN** the user MAY continue without opening Flows + +### Requirement: Tour steps find their menu entries + +Every menu entry that Keepiq renders in its own navigation SHALL carry `data-cn-route` set to its route name, so the tour can resolve a step whose target is `{kind: "nav-item", ref: }`. + +#### Scenario: A nav-item step highlights its entry + +@e2e exclude Suppressed in e2e runs by tests/e2e/global-setup.ts; the attribute is rendered at src/components/KeepiqAppNav/KeepiqAppNav.vue:52, :179 and :219. + +- **WHEN** the tour shows the step that targets `SecretList` +- **THEN** the step SHALL anchor to the menu entry rendered with `data-cn-route="SecretList"` + +### Requirement: The tour waits for an unlocked vault + +Keepiq SHALL withhold the `walkthrough` block from the manifest it passes to the app shell while the vault is locked, and SHALL pass it again as soon as the vault is unlocked. A missing session store or a lock flag that is not a boolean MUST count as locked. While locked, the shell MUST NOT request the completion preference. + +#### Scenario: The lock screen shows no tour + +@e2e exclude Covered by vitest tests/router/guards.spec.js 'withholds the walkthrough while the vault is locked' and by tests/e2e/workflows/vault-unlock.spec.ts, which fails on any preference request behind the lock screen. + +- **WHEN** a user opens Keepiq and the vault is locked +- **THEN** the manifest passed to the shell SHALL have no `walkthrough` block +- **AND** no request to `/api/preferences/walkthrough_completed_version` SHALL be sent + +#### Scenario: Unlocking brings the tour + +@e2e exclude Covered by vitest tests/router/guards.spec.js 'offers the walkthrough once the vault is unlocked'; src/main.js:236 reads the lock state in the root render, so the manifest updates on unlock. + +- **WHEN** the user unlocks the vault on a first visit +- **THEN** the shell SHALL receive the manifest with its `walkthrough` block and the tour SHALL start + +### Requirement: Completion is stored per user + +Keepiq SHALL serve `GET` and `PUT /api/preferences/{key}` for the signed-in user only. The key MUST be lower-cased and reduced to `[a-z0-9-]`, at most 64 characters, so `walkthrough_completed_version` is stored as the user value `pref_walkthroughcompletedversion`, the key OpenRegister's AppHost used before. A value over 4 KB MUST be refused with 400, and an empty value MUST delete the preference. A finished tour MUST NOT start again for that user. + +#### Scenario: Completion survives a reload + +@e2e exclude Covered by PHPUnit tests/Unit/Controller/AppShellControllersTest.php testThePreferenceRoundTripsUnderTheLegacyKey; the PUT itself comes from CnWalkthrough in @conduction/nextcloud-vue. + +- **WHEN** the user finishes the tour and the shell stores the version under `walkthrough_completed_version` +- **THEN** a later `GET /api/preferences/walkthrough_completed_version` SHALL return that version +- **AND** the tour SHALL NOT start on the next visit + +#### Scenario: Another user still gets the tour + +@e2e exclude Covered by PHPUnit tests/Unit/Controller/AppShellControllersTest.php testPreferencesArePerUser. + +- **WHEN** alice has finished the tour and bob opens Keepiq for the first time +- **THEN** bob's `GET /api/preferences/walkthrough_completed_version` SHALL return `{"value": null}` From 84346beedfb928a142e4abcd97b2616b8f01d46e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 7 Oct 2026 23:30:47 +0200 Subject: [PATCH 008/167] docs(openspec): archive pass, changes the code has delivered Archives client-side-key-generator and standalone-app-shell; the latter removes every apphost-adoption requirement, so that capability is retired. --- .../design.md | 0 .../proposal.md | 0 .../specs/key-generator/spec.md | 0 .../specs/org-password-policies/spec.md | 0 .../specs/passphrase-generator/spec.md | 0 .../tasks.md | 0 .../.openspec.yaml | 1 + .../design.md | 0 .../proposal.md | 0 .../specs/app-shell/spec.md | 0 .../specs/apphost-adoption/spec.md | 0 .../specs/mcp-metadata-surface/spec.md | 0 .../2026-10-07-standalone-app-shell}/tasks.md | 0 openspec/parity/capabilities.json | 2 +- openspec/specs/app-shell/spec.md | 265 ++++++++++++++++++ openspec/specs/apphost-adoption/spec.md | 107 ------- openspec/specs/key-generator/spec.md | 13 +- openspec/specs/mcp-metadata-surface/spec.md | 17 +- openspec/specs/org-password-policies/spec.md | 7 +- openspec/specs/passphrase-generator/spec.md | 36 +++ 20 files changed, 332 insertions(+), 116 deletions(-) rename openspec/changes/{client-side-key-generator => archive/2026-10-07-client-side-key-generator}/design.md (100%) rename openspec/changes/{client-side-key-generator => archive/2026-10-07-client-side-key-generator}/proposal.md (100%) rename openspec/changes/{client-side-key-generator => archive/2026-10-07-client-side-key-generator}/specs/key-generator/spec.md (100%) rename openspec/changes/{client-side-key-generator => archive/2026-10-07-client-side-key-generator}/specs/org-password-policies/spec.md (100%) rename openspec/changes/{client-side-key-generator => archive/2026-10-07-client-side-key-generator}/specs/passphrase-generator/spec.md (100%) rename openspec/changes/{client-side-key-generator => archive/2026-10-07-client-side-key-generator}/tasks.md (100%) rename openspec/changes/{standalone-app-shell => archive/2026-10-07-standalone-app-shell}/.openspec.yaml (60%) rename openspec/changes/{standalone-app-shell => archive/2026-10-07-standalone-app-shell}/design.md (100%) rename openspec/changes/{standalone-app-shell => archive/2026-10-07-standalone-app-shell}/proposal.md (100%) rename openspec/changes/{standalone-app-shell => archive/2026-10-07-standalone-app-shell}/specs/app-shell/spec.md (100%) rename openspec/changes/{standalone-app-shell => archive/2026-10-07-standalone-app-shell}/specs/apphost-adoption/spec.md (100%) rename openspec/changes/{standalone-app-shell => archive/2026-10-07-standalone-app-shell}/specs/mcp-metadata-surface/spec.md (100%) rename openspec/changes/{standalone-app-shell => archive/2026-10-07-standalone-app-shell}/tasks.md (100%) create mode 100644 openspec/specs/app-shell/spec.md delete mode 100644 openspec/specs/apphost-adoption/spec.md create mode 100644 openspec/specs/passphrase-generator/spec.md diff --git a/openspec/changes/client-side-key-generator/design.md b/openspec/changes/archive/2026-10-07-client-side-key-generator/design.md similarity index 100% rename from openspec/changes/client-side-key-generator/design.md rename to openspec/changes/archive/2026-10-07-client-side-key-generator/design.md diff --git a/openspec/changes/client-side-key-generator/proposal.md b/openspec/changes/archive/2026-10-07-client-side-key-generator/proposal.md similarity index 100% rename from openspec/changes/client-side-key-generator/proposal.md rename to openspec/changes/archive/2026-10-07-client-side-key-generator/proposal.md diff --git a/openspec/changes/client-side-key-generator/specs/key-generator/spec.md b/openspec/changes/archive/2026-10-07-client-side-key-generator/specs/key-generator/spec.md similarity index 100% rename from openspec/changes/client-side-key-generator/specs/key-generator/spec.md rename to openspec/changes/archive/2026-10-07-client-side-key-generator/specs/key-generator/spec.md diff --git a/openspec/changes/client-side-key-generator/specs/org-password-policies/spec.md b/openspec/changes/archive/2026-10-07-client-side-key-generator/specs/org-password-policies/spec.md similarity index 100% rename from openspec/changes/client-side-key-generator/specs/org-password-policies/spec.md rename to openspec/changes/archive/2026-10-07-client-side-key-generator/specs/org-password-policies/spec.md diff --git a/openspec/changes/client-side-key-generator/specs/passphrase-generator/spec.md b/openspec/changes/archive/2026-10-07-client-side-key-generator/specs/passphrase-generator/spec.md similarity index 100% rename from openspec/changes/client-side-key-generator/specs/passphrase-generator/spec.md rename to openspec/changes/archive/2026-10-07-client-side-key-generator/specs/passphrase-generator/spec.md diff --git a/openspec/changes/client-side-key-generator/tasks.md b/openspec/changes/archive/2026-10-07-client-side-key-generator/tasks.md similarity index 100% rename from openspec/changes/client-side-key-generator/tasks.md rename to openspec/changes/archive/2026-10-07-client-side-key-generator/tasks.md diff --git a/openspec/changes/standalone-app-shell/.openspec.yaml b/openspec/changes/archive/2026-10-07-standalone-app-shell/.openspec.yaml similarity index 60% rename from openspec/changes/standalone-app-shell/.openspec.yaml rename to openspec/changes/archive/2026-10-07-standalone-app-shell/.openspec.yaml index e3966d7a1..d88d16ab5 100644 --- a/openspec/changes/standalone-app-shell/.openspec.yaml +++ b/openspec/changes/archive/2026-10-07-standalone-app-shell/.openspec.yaml @@ -1,2 +1,3 @@ schema: spec-driven created: 2026-10-05 +retire_capabilities: true diff --git a/openspec/changes/standalone-app-shell/design.md b/openspec/changes/archive/2026-10-07-standalone-app-shell/design.md similarity index 100% rename from openspec/changes/standalone-app-shell/design.md rename to openspec/changes/archive/2026-10-07-standalone-app-shell/design.md diff --git a/openspec/changes/standalone-app-shell/proposal.md b/openspec/changes/archive/2026-10-07-standalone-app-shell/proposal.md similarity index 100% rename from openspec/changes/standalone-app-shell/proposal.md rename to openspec/changes/archive/2026-10-07-standalone-app-shell/proposal.md diff --git a/openspec/changes/standalone-app-shell/specs/app-shell/spec.md b/openspec/changes/archive/2026-10-07-standalone-app-shell/specs/app-shell/spec.md similarity index 100% rename from openspec/changes/standalone-app-shell/specs/app-shell/spec.md rename to openspec/changes/archive/2026-10-07-standalone-app-shell/specs/app-shell/spec.md diff --git a/openspec/changes/standalone-app-shell/specs/apphost-adoption/spec.md b/openspec/changes/archive/2026-10-07-standalone-app-shell/specs/apphost-adoption/spec.md similarity index 100% rename from openspec/changes/standalone-app-shell/specs/apphost-adoption/spec.md rename to openspec/changes/archive/2026-10-07-standalone-app-shell/specs/apphost-adoption/spec.md diff --git a/openspec/changes/standalone-app-shell/specs/mcp-metadata-surface/spec.md b/openspec/changes/archive/2026-10-07-standalone-app-shell/specs/mcp-metadata-surface/spec.md similarity index 100% rename from openspec/changes/standalone-app-shell/specs/mcp-metadata-surface/spec.md rename to openspec/changes/archive/2026-10-07-standalone-app-shell/specs/mcp-metadata-surface/spec.md diff --git a/openspec/changes/standalone-app-shell/tasks.md b/openspec/changes/archive/2026-10-07-standalone-app-shell/tasks.md similarity index 100% rename from openspec/changes/standalone-app-shell/tasks.md rename to openspec/changes/archive/2026-10-07-standalone-app-shell/tasks.md diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 8789149ea..a5108fc90 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -5514,7 +5514,7 @@ "evidence": "src/generator/generator.js generatePassphrase (EFF large word list src/generator/eff-large-wordlist.js, crypto.getRandomValues) <- src/dialogs/KeyGeneratorModal.vue Password/Passphrase switch; policy fit + generator_allow_passphrase (lib/Service/PasswordPolicyService.php). Tests: tests/vitest/generator.spec.js, tests/dialogs/KeyGeneratorModal.spec.js, tests/e2e/workflows/key-generator.spec.ts. Change: openspec/changes/client-side-key-generator (supersedes health-passphrase-generator).", "owner": "ConductionNL/keepiq", "reachedOn": "SecretList page (/secrets) -> New secret dialog -> Generate a strong key -> Passphrase", - "change": "openspec/changes/client-side-key-generator" + "change": "openspec/changes/archive/2026-10-07-client-side-key-generator" }, "rowSource": "competitor", "provider": "keepiq", diff --git a/openspec/specs/app-shell/spec.md b/openspec/specs/app-shell/spec.md new file mode 100644 index 000000000..87fd8a957 --- /dev/null +++ b/openspec/specs/app-shell/spec.md @@ -0,0 +1,265 @@ +# app-shell Specification + +## Purpose +Keepiq is usable on its own: a Nextcloud with no other Conduction app installed runs the vault in full. Keepiq owns its route table, its public health endpoint, its admin-only metrics endpoint, its per-user preferences and its admin section, instead of borrowing them from OpenRegister's AppHost. Integrations that need another app, such as flows through OpenRegister or the connection registry in integriq, appear only when that app is installed, and the vault's own surfaces work the same with or without them (ADR-006 in `openspec/architecture/`). + +## Requirements + +### Requirement: Keepiq Operates Without Any Other Conduction App + +Keepiq SHALL be installable, enableable, upgradable and fully usable on a Nextcloud instance where neither OpenRegister nor integriq is installed, or where either is installed but disabled. `Application::register()` and `Application::boot()` SHALL NOT reference, autoload, or resolve any `OCA\OpenRegister\…` or `OCA\Integriq\…` class, and SHALL NOT call `OCA\OpenRegister\AppHost\Bootstrap::register()`. The frontend SHALL mount `CnAppRoot` with an empty `requiresApps` list, so the app shell never replaces Keepiq's UI with a missing-app screen. The behaviour of the app SHALL be identical whether OpenRegister is enabled, disabled or absent, except for the optional integrations named in "Optional Integrations Appear Only When Their App Is Present". + +#### Scenario: Keepiq is usable on an instance without OpenRegister or integriq + +- **GIVEN** a Nextcloud instance on which neither OpenRegister nor integriq is installed +- **WHEN** an admin runs `occ app:enable keepiq` and a user opens `/apps/keepiq/` +- **THEN** the enable MUST succeed with every repair step completing without a warning that names OpenRegister +- **AND** the Keepiq shell MUST render the lock screen, and after unlock the vault list, with no "OpenRegister missing" or missing-dependency screen +- **AND** creating, reading, sharing and deleting a secret MUST work as on an instance with OpenRegister enabled + +#### Scenario: Disabling OpenRegister changes nothing in Keepiq + +- **GIVEN** an instance where Keepiq and OpenRegister are both enabled and a user has an unlocked vault +- **WHEN** an admin runs `occ app:disable openregister` and the user reloads Keepiq +- **THEN** every Nextcloud page MUST still answer HTTP 200 (no instance-wide 500) +- **AND** the user's vault MUST still load and unlock + +#### Scenario: Bootstrap loads no foreign class + +- **GIVEN** OpenRegister and integriq are both absent from the autoload path +- **WHEN** Keepiq's `Application::register()` and `Application::boot()` run +- **THEN** neither MUST throw, no autoload of an `OCA\OpenRegister\` or `OCA\Integriq\` name MUST be attempted, and every Keepiq registrar (domain overrides, admin areas, MCP, event registrars, platform integration) MUST have run +- @e2e exclude bootstrap wiring — no UI surface; covered by a unit test that runs register() with a recording autoloader and asserts no foreign prefix is requested + +### Requirement: Keepiq Owns Its Route Table + +`appinfo/routes.php` SHALL return one static route table defined by Keepiq on every instance, with no `class_exists()` branch on another app. Every route SHALL resolve to a public method on a `OCA\Keepiq\Controller\…` class. The table SHALL contain the dashboard page, every domain route, the settings routes, `health#index`, `metrics#index`, `preferences#getPreference` and `preferences#setPreference`, and the SPA catch-all as the last route. It SHALL NOT contain `settings#load` or any `store#…` route. + +#### Scenario: Every route resolves to a Keepiq controller + +- **GIVEN** the route table returned by `appinfo/routes.php` with no OpenRegister class on the autoload path +- **WHEN** each entry's `name` is resolved to a controller class and method +- **THEN** every entry MUST resolve to an existing public method on a class in `OCA\Keepiq\Controller`, every route name MUST appear once, and `dashboard#catchAll` MUST be the last route +- @e2e exclude route-table contract — covered by the routes unit test and hydra gate-14 (route-reachability) + +#### Scenario: The table is the same with or without OpenRegister + +- **GIVEN** the route table read once with OpenRegister absent and once with a stub `OCA\OpenRegister\AppHost\Routes` class defined +- **WHEN** the two tables are compared +- **THEN** they MUST be identical +- @e2e exclude route-table contract — covered by the routes unit test + +#### Scenario: Removed OpenRegister-only routes answer 404 + +- **GIVEN** an admin session +- **WHEN** the admin calls `POST /apps/keepiq/api/settings/load` or `GET /apps/keepiq/api/store/items` +- **THEN** the response MUST be HTTP 404 and no configuration import MUST be attempted +- @e2e exclude API-only — covered by the Newman collection + +### Requirement: Public Health Endpoint + +Keepiq SHALL serve `GET /apps/keepiq/api/health` from its own `HealthController`, publicly accessible (`#[PublicPage]`, `#[NoCSRFRequired]`) per hydra ADR-006. The controller SHALL run two checks, `database` (critical: a trivial query against a Keepiq table) and `filesystem` (degraded: the temp directory exists and is writable, checked without writing a file). The response SHALL contain ONLY the keys `status`, `app`, `version` and `checks`. `status` SHALL be `ok` when all checks pass, `degraded` when only a degraded check fails, and `error` with HTTP 503 when a critical check fails. Each `checks` value SHALL be `"ok"` or `"failed: "`, and no value SHALL carry secret material. + +#### Scenario: Anonymous health check succeeds + +- **GIVEN** a healthy instance with Keepiq enabled and no OpenRegister installed +- **WHEN** `GET /apps/keepiq/api/health` is called with no authentication +- **THEN** the response MUST be HTTP 200 with `status = "ok"`, `app = "keepiq"`, the installed app version, `checks.database = "ok"` and `checks.filesystem = "ok"` +- @e2e exclude API-only endpoint — covered by the Keepiq Newman collection and a contract test + +#### Scenario: Health response carries no secret material + +- **GIVEN** an instance containing secrets, encryption suites, shares and link-share tokens +- **WHEN** `GET /apps/keepiq/api/health` is called with no authentication +- **THEN** the response body MUST contain ONLY the keys `status`, `app`, `version` and `checks`, and the `checks` values MUST be limited to `"ok"` or `"failed: "` strings, with no secret values, suite identifiers, key material, share tokens, user identifiers or row counts +- @e2e exclude API-only endpoint — covered by the Keepiq Newman collection and a contract test + +#### Scenario: Degraded filesystem does not mask database health + +- **GIVEN** an instance where the temp filesystem is not writable but the database is reachable +- **WHEN** `GET /apps/keepiq/api/health` is called +- **THEN** the response MUST be HTTP 200 with `status = "degraded"`, `checks.filesystem` beginning `failed:` and `checks.database = "ok"` +- @e2e exclude API-only endpoint — covered by a unit test with a non-writable temp path + +#### Scenario: Unreachable database reports an error + +- **GIVEN** a database query that throws +- **WHEN** `GET /apps/keepiq/api/health` is called +- **THEN** the response MUST be HTTP 503 with `status = "error"`, and `checks.database` MUST begin `failed:` followed by the exception's class name only, never its message +- @e2e exclude API-only endpoint — covered by a unit test with a throwing query builder + +### Requirement: Admin-Only Metrics Endpoint + +Keepiq SHALL serve `GET /apps/keepiq/api/metrics` from its own `MetricsController` in Prometheus text exposition format 0.0.4 (`Content-Type: text/plain; version=0.0.4`). Access SHALL be restricted to admins and to users delegated Keepiq's General admin area: the method SHALL declare `#[AuthorizedAdminSetting(settings: AdminSettings::class)]`, the explicit admin posture that hydra gate-30 (public-monitoring) accepts for an admin-only scrape endpoint, and SHALL NOT carry `#[NoAdminRequired]` or `#[PublicPage]`. The output SHALL contain `keepiq_info{version,php_version,nextcloud_version} 1`, `keepiq_up 1` and `keepiq_suites_total`, computed as one SQL `COUNT(*)` over `keepiq_enc_suites` with `status = 'active'`. It SHALL contain no identifier, key material or owner. + +#### Scenario: Metrics report the active suite count + +- **GIVEN** a seeded instance with N active and M revoked encryption suites +- **WHEN** `GET /apps/keepiq/api/metrics` is called by an admin +- **THEN** the output MUST contain `keepiq_info{…} 1`, `keepiq_up 1` and `keepiq_suites_total N` (active suites only, matching a direct filtered table count), with `Content-Type: text/plain; version=0.0.4` +- @e2e exclude API-only endpoint — covered by the Keepiq Newman collection and a contract test + +#### Scenario: Non-admin user cannot scrape metrics + +- **GIVEN** an authenticated non-admin user +- **WHEN** that user calls `GET /apps/keepiq/api/metrics` +- **THEN** the request MUST be rejected with HTTP 403 +- @e2e exclude API-only endpoint — covered by the Keepiq Newman collection + +### Requirement: Per-User Preferences Endpoint + +Keepiq SHALL serve `GET /apps/keepiq/api/preferences/{key}` and `PUT /apps/keepiq/api/preferences/{key}` from its own `PreferencesController`, for any logged-in user (`#[NoAdminRequired]`). The key SHALL be normalised by lower-casing and stripping every character outside `[a-z0-9-]`, then cut to 64 characters. A key that is empty after this SHALL be refused with HTTP 400. The value SHALL be stored through `OCP\IConfig` user values under app `keepiq` and key `pref_`, which is the same storage key the AppHost controller used, so existing values survive. `GET` SHALL answer `{"value": }`. `PUT` SHALL answer `{"value": }`, and with an empty value SHALL delete the preference and answer `{"value": null}`. A value longer than 4096 bytes SHALL be refused with HTTP 400 and nothing stored. A user SHALL only ever read or write their own preferences. Values are UI state (for example the walkthrough's completed version), are stored in plain text, and SHALL NOT be used for secret material. + +#### Scenario: The walkthrough preference round-trips + +- **GIVEN** a logged-in user with no stored preference +- **WHEN** the user calls `PUT /apps/keepiq/api/preferences/walkthrough_completed_version` with value `0.3.7`, then `GET` on the same URL +- **THEN** the `PUT` MUST answer `{"value": "0.3.7"}`, the `GET` MUST answer `{"value": "0.3.7"}`, and the value MUST be stored as user value `pref_walkthroughcompletedversion` of app `keepiq` +- @e2e exclude API-only endpoint — the walkthrough's use of it is covered by the existing walkthrough e2e flow; the contract is covered by a contract test + +#### Scenario: Preferences are per user + +- **GIVEN** user A has stored a preference under key `walkthrough_completed_version` +- **WHEN** user B calls `GET /apps/keepiq/api/preferences/walkthrough_completed_version` +- **THEN** the response MUST be `{"value": null}` +- @e2e exclude API-only endpoint — covered by a contract test + +#### Scenario: An invalid key is refused + +- **GIVEN** a logged-in user +- **WHEN** the user calls `GET /apps/keepiq/api/preferences/%20%21` (a key with no `[a-z0-9-]` character) +- **THEN** the response MUST be HTTP 400 and no user value MUST be read or written +- @e2e exclude API-only endpoint — covered by a unit test + +### Requirement: Native Admin Section And Version Card + +Keepiq's admin section (`OCA\Keepiq\Sections\SettingsSection`) SHALL implement `OCP\Settings\IIconSection` directly. The General admin area SHALL render the version card through `CnAdminSettingsShell` with the re-import action hidden (`showReimport: false`), because Keepiq imports no configuration. The `config_version` app value that the card compares against SHALL be written by Keepiq's own `InitializeSettings` repair step, set to the installed app version once its domain default-config seeding has completed. A version read with no matching write SHALL therefore not exist (hydra gate-59). + +#### Scenario: The admin section renders without OpenRegister + +- **GIVEN** an instance without OpenRegister +- **WHEN** an admin opens Administration settings → Keepiq +- **THEN** the Keepiq section MUST be listed with its icon, the General area MUST render, and the domain settings (password policy, session timeout, CA auto-renew) MUST be readable and writable + +#### Scenario: The version card is up to date after install + +- **GIVEN** a fresh install where `occ app:enable keepiq` has run its repair steps +- **WHEN** an admin opens the General admin area +- **THEN** the version card MUST show the installed version as up to date, and no "Re-import configuration" button MUST be shown + +### Requirement: Legacy OpenRegister Rows Are Removed When Empty + +A Keepiq repair step (`OCA\Keepiq\Repair\RemoveLegacyRegisterRows`, replacing `MigrateSchemaApplicationId`) SHALL remove what earlier Keepiq versions created in OpenRegister, and nothing else: + +- every `openregister_schemas` row whose `application` is `keepiq` or `doriath`, with its per-schema data tables; +- every `openregister_registers` row whose `application` is `keepiq` or `doriath` and whose `schemas` list no longer names a schema that still exists; +- every `openregister_configurations` row whose `app` is `keepiq` or `doriath` once none of the registers and schemas it lists still exists. + +Rows SHALL be selected by `application` (or `app`), never by slug alone, because OpenRegister's uniqueness key is (organisation, application, slug); a register without an application SHALL never be deleted. A schema or register SHALL count as empty only when every per-schema data table belonging to it holds zero rows, and so does the legacy `openregister_objects` table when it still exists. Data tables SHALL be found by their physical name (`openregister_table__`) in the database catalogue, not from a register's `schemas` list; when the catalogue cannot be read, nothing SHALL be removed. + +Another app's register that lists a removed schema SHALL lose only that entry; every other entry SHALL stay exactly as stored. All row changes SHALL run in one transaction that is rolled back on failure. Empty data tables SHALL be dropped after the commit, each only after a final row count of zero. Every removal SHALL be logged before it happens, naming the table, id, slug, application and the row counts that justified it. The step SHALL reach OpenRegister's tables through `IDBConnection` only, reference no OpenRegister class, be idempotent, and never fail the install or upgrade: an error is logged and the step ends. + +#### Scenario: Empty leftovers are removed when OpenRegister is installed + +- **GIVEN** an instance with a `doriath` register listing schema `example` (`application = keepiq`), that schema's empty data table, and an empty `keepiq` register +- **WHEN** `occ upgrade` runs Keepiq's repair steps +- **THEN** both registers, both Keepiq configuration entries, the schema row and the empty data table MUST be gone, and every other OpenRegister register, schema, configuration and table MUST be untouched, including another app's schema with the slug `example` +- @e2e exclude repair step with no UI surface — covered by RemoveLegacyRegisterRowsTest (unit, in-memory OpenRegister tables) + +#### Scenario: Rows that still hold objects are kept + +- **GIVEN** a Keepiq-keyed schema with at least one row in `openregister_objects` or in its per-schema data table +- **WHEN** the repair step runs +- **THEN** that schema, its data table and its register MUST be kept, and a warning naming the schema slug and row count MUST be logged + +#### Scenario: Tables are found by name, not from the register's list + +- **GIVEN** a Keepiq-keyed schema whose register no longer lists it, while `openregister_table__` still holds rows +- **WHEN** the repair step runs +- **THEN** that schema and its data table MUST be kept +- @e2e exclude repair step with no UI surface — covered by RemoveLegacyRegisterRowsTest (unit, in-memory OpenRegister tables) + +#### Scenario: Another application's register with a Keepiq slug is left alone + +- **GIVEN** an empty register with slug `keepiq` whose `application` is another app, or empty +- **WHEN** the repair step runs +- **THEN** that register MUST be kept +- @e2e exclude repair step with no UI surface — covered by RemoveLegacyRegisterRowsTest (unit, in-memory OpenRegister tables) + +#### Scenario: A failure rolls back and the next run converges + +- **GIVEN** a run that fails after its first row change +- **WHEN** the run ends and the step runs again later +- **THEN** the first run MUST have changed nothing, and the second MUST remove the leftovers +- @e2e exclude repair step with no UI surface — covered by RemoveLegacyRegisterRowsTest (unit, in-memory OpenRegister tables) +- @e2e exclude repair step with no UI surface — covered by RemoveLegacyRegisterRowsTest (unit, in-memory OpenRegister tables) + +#### Scenario: Nothing happens without OpenRegister's tables + +- **GIVEN** an instance where OpenRegister was never installed +- **WHEN** the repair step runs +- **THEN** it MUST end without error and without issuing any query against an `openregister_*` table +- @e2e exclude repair step with no UI surface — covered by RemoveLegacyRegisterRowsTest (unit, in-memory OpenRegister tables) + +#### Scenario: A second run is a no-op + +- **GIVEN** an instance where the repair step already removed the leftovers +- **WHEN** the repair step runs again +- **THEN** it MUST end without error, delete nothing and drop nothing +- @e2e exclude repair step with no UI surface — covered by RemoveLegacyRegisterRowsTest (unit, in-memory OpenRegister tables) + +### Requirement: Optional Integrations Appear Only When Their App Is Present + +Each Keepiq integration with another app SHALL be reachable only when that app is installed and enabled, and its absence SHALL NOT affect any other part of Keepiq: + +- **MCP tools** (`mcp-metadata-surface`) require OpenRegister. +- **The Flows and FlowDetail pages** require OpenRegister, whose flow engine they render (hydra ADR-110). The `FlowsMenu` entry SHALL carry `visibleIf.appInstalled: "openregister"`, and both pages SHALL carry `requiresApp: {"id": "openregister", "name": "OpenRegister"}`. +- **The Integrations page and connection reporting** (`admin-integrations`) require integriq, as already declared in `src/manifest.d/80-connection-registry.json` and by `ConnectionReporter`'s by-name event lookup. +- **The AI companion** (`CnAiCompanion`, mounted by `CnAppRoot`'s `aiCompanion` prop) requires Hermiq, its chat backend. `aiCompanion` SHALL be `true` only when `hermiq` is enabled for the user (`OC.appswebroots`), so that without Hermiq the shell issues no request to `/apps/hermiq/`. + +#### Scenario: Flows are hidden without OpenRegister + +- **GIVEN** an instance without OpenRegister and a logged-in user with an unlocked vault +- **WHEN** the user looks at Keepiq's navigation +- **THEN** no Flows entry MUST be shown + +#### Scenario: A deep link to Flows without OpenRegister explains the missing app + +- **GIVEN** an instance without OpenRegister +- **WHEN** a user opens `/apps/keepiq/flows` or `/apps/keepiq/flows/1` directly (the router uses HTML5 history mode) +- **THEN** the page MUST render the missing-dependency screen naming OpenRegister instead of the flow list or canvas, and the rest of the shell (navigation, vault) MUST stay usable + +#### Scenario: Flows work when OpenRegister is present + +- **GIVEN** an instance with OpenRegister enabled +- **WHEN** a user opens the Flows entry and then one flow +- **THEN** the flow list and the flow canvas MUST render as before this change + +#### Scenario: The AI companion is absent without Hermiq + +- **GIVEN** an instance without Hermiq +- **WHEN** a user loads Keepiq +- **THEN** no AI companion MUST be rendered, and no request to `/apps/hermiq/` MUST be issued + +#### Scenario: Integrations are hidden without integriq + +- **GIVEN** an instance without integriq and an admin user +- **WHEN** the admin looks at Keepiq's settings navigation +- **THEN** no Integrations entry MUST be shown, and saving a connection-bearing setting MUST NOT throw + +### Requirement: Domain Surfaces Unchanged + +This change SHALL NOT alter any controller, service, middleware, listener, migration or repair step that touches secrets, encryption suites, shares, certificates, application secrets or JWT authentication, other than its registration path. Their routes SHALL keep their names, URLs, verbs and auth postures. `DashboardController` SHALL keep its argon2-WASM CSP opt-in. + +#### Scenario: Dashboard WASM CSP survives the change + +- **GIVEN** the app with its own shell and no OpenRegister +- **WHEN** a user opens the Keepiq SPA and accesses a link share whose AES key is derived by the argon2-browser WASM module +- **THEN** the page response MUST carry the `wasm-unsafe-eval` CSP opt-in and the client-side key derivation MUST succeed + +#### Scenario: Domain routes keep their contract + +- **GIVEN** the route table before and after this change +- **WHEN** the domain routes (secrets, suites, CA, shares, delegations, public link-share access, public secret-request fill, JWT token exchange, applications, dashboard settings) are compared +- **THEN** each MUST resolve to the same controller method at the same URL and verb, with the same auth attributes +- @e2e exclude route-table contract — covered by the routes unit test and `PublicRouteSurfaceContractTest` diff --git a/openspec/specs/apphost-adoption/spec.md b/openspec/specs/apphost-adoption/spec.md deleted file mode 100644 index 369a17f3b..000000000 --- a/openspec/specs/apphost-adoption/spec.md +++ /dev/null @@ -1,107 +0,0 @@ -# apphost-adoption Specification - -## Purpose -TBD - created by archiving change adopt-apphost. Update Purpose after archive. -## Requirements -### Requirement: Public Declarative Health Endpoint - -Keepiq SHALL serve `GET /apps/keepiq/api/health` through the AppHost `GenericHealthController` from manifest descriptors (`database` critical, `filesystem` degraded), publicly accessible per ADR-006, and the response SHALL contain no secret material. - -#### Scenario: Anonymous health check succeeds - -- **GIVEN** a healthy instance with keepiq enabled -- **WHEN** `GET /apps/keepiq/api/health` is called with no authentication -- **THEN** the response MUST be HTTP 200 with `status = "ok"`, `checks.database = "ok"`, and `checks.filesystem = "ok"` -- @e2e exclude API-only endpoint — covered by the OR AppHost Newman contract collection - -#### Scenario: Health response carries no secret material - -- **GIVEN** an instance containing secrets, encryption suites, shares, and link-share tokens -- **WHEN** `GET /apps/keepiq/api/health` is called with no authentication -- **THEN** the response body MUST contain ONLY the keys `status`, `app`, `version`, and `checks`, and the `checks` values MUST be limited to `"ok"` or `"failed: "` strings — no secret values, suite identifiers, key material, share tokens, user identifiers, or row counts -- @e2e exclude API-only endpoint — covered by the OR AppHost Newman contract collection - -#### Scenario: Degraded filesystem does not mask database health - -- **GIVEN** an instance where the temp filesystem is not writable but the database is reachable -- **WHEN** `GET /apps/keepiq/api/health` is called -- **THEN** the response MUST report `status = "degraded"` with `checks.filesystem` beginning `failed:` and `checks.database = "ok"`, per the `severity: degraded` descriptor -- @e2e exclude API-only endpoint — covered by the OR AppHost Newman contract collection - -### Requirement: Admin-Only Declarative Metrics Endpoint - -Keepiq SHALL serve `GET /apps/keepiq/api/metrics` through the AppHost `GenericMetricsController` in Prometheus text exposition format 0.0.4, restricted to admin users per ADR-006, with `keepiq_suites_total` computed as a SQL `COUNT(*)` over `keepiq_enc_suites` filtered to `status = 'active'`. - -#### Scenario: Metrics parity after adoption - -- **GIVEN** a seeded instance with N active and M revoked encryption suites -- **WHEN** `GET /apps/keepiq/api/metrics` is called by an admin -- **THEN** the output MUST contain `keepiq_info{version,php_version,nextcloud_version}`, `keepiq_up 1`, and `keepiq_suites_total N` (active suites only, matching a direct filtered table count), as `Content-Type: text/plain; version=0.0.4` -- @e2e exclude API-only endpoint — covered by the OR AppHost Newman contract collection - -#### Scenario: Non-admin user cannot scrape metrics - -- **GIVEN** an authenticated non-admin user -- **WHEN** that user calls `GET /apps/keepiq/api/metrics` -- **THEN** the request MUST be rejected (HTTP 403), tightening the pre-adoption any-authenticated-user posture to ADR-006 admin-only -- @e2e exclude API-only endpoint — covered by the OR AppHost Newman contract collection - -### Requirement: Boilerplate Plumbing Served by AppHost Generics - -Keepiq SHALL delete its local `HealthController`, `MetricsController`, and `DeepLinkRegistrationListener`, and SHALL shrink `AdminSettings` and `SettingsSection` to one-line engine-backed subclasses (they must physically exist for NC class-name instantiation via info.xml and as the `#[AuthorizedAdminSetting(AdminSettings::class)]` target — the AppHost stub floor), wiring their responsibilities through `AppHost\Bootstrap::register()` and `AppHost\Routes::standard()`, with all route names, URLs, and public-route postures unchanged for domain endpoints. `InitializeSettings` SHALL be retained bespoke (it seeds domain default-config keys the generic repair step does not) and re-registered after `Bootstrap::register()` so the concrete wins over the generic alias. - -#### Scenario: Domain routes unchanged after Routes::standard adoption - -- **GIVEN** the adopted `appinfo/routes.php` built from `Routes::standard($extra)` -- **WHEN** the route table is compared with the pre-adoption baseline -- **THEN** every domain route (secrets, suites, CA, shares, delegations, public link-share access, public secret-request fill, JWT token exchange, applications, dashboard settings) MUST resolve to the same controller method at the same URL and verb, and the SPA catch-all MUST remain the last route -- @e2e exclude API-only endpoint — covered by the OR AppHost Newman contract collection - -#### Scenario: Register configuration imported by the retained repair step - -- **GIVEN** a fresh install with the bespoke `lib/Repair/InitializeSettings.php` retained (it seeds domain default-config keys) and re-registered after `Bootstrap::register()` -- **WHEN** `occ app:enable keepiq` runs and repair steps execute -- **THEN** the register configuration from `keepiq_register.json` (plus `register.d/` fragments) MUST be imported and the app's register/schema config keys resolved, identical to the pre-adoption repair step, and the domain default-config keys MUST be seeded -- @e2e exclude install-time repair step — verified via occ in CI, no UI surface - -#### Scenario: Admin settings page still renders through the generic section - -- **GIVEN** the local `AdminSettings` and `SettingsSection` are deleted and Bootstrap registers the AppHost generics -- **WHEN** an admin opens the Keepiq section in Nextcloud admin settings -- **THEN** the settings form MUST render and the domain admin settings (password policy, session timeout, CA auto-renew) MUST remain readable and writable via the retained `SettingsController` subclass methods - -### Requirement: AppHost Prelude Registers OpenRegister With Public API Only - -Because Nextcloud registers app autoloaders in sorted order, `OCA\OpenRegister\` is not autoloadable inside Keepiq's `Application::register()`. Keepiq SHALL put OpenRegister's PSR-4 prefix on the autoloader itself (`OpenRegisterAutoloader::register()`) before any `OCA\OpenRegister\…` reference, using only public API (`IAppManager`) and without booting OpenRegister. The prelude MUST NOT throw. When OpenRegister is absent or disabled, or enabled but missing from disk (which Nextcloud's Coordinator already logs), it SHALL register nothing and return false, so the caller falls through to its degraded path, and this SHALL be re-checked on every call, including after an earlier successful registration, with the prelude's loader taken off the autoload chain again after a disable. Any other failure, including an enabled OpenRegister without a `lib/` directory, one without a loadable `AppHost\Bootstrap` (older than AppHost, or an incomplete deploy), and a throwing or unparseable `AppHost\Bootstrap`, SHALL be recorded and logged once per request at boot, and SHALL NOT abort the caller's registration. The whole of this wiring SHALL live in `OpenRegisterAutoloader::bootstrapAppHost()`, so that each branch is reachable from a unit test. - -#### Scenario: Enabled OpenRegister is autoloadable during register() - -- **GIVEN** OpenRegister is installed and enabled -- **WHEN** `OpenRegisterAutoloader::register()` runs from `Application::register()` -- **THEN** exactly one autoloader MUST be added that resolves `OCA\OpenRegister\…` names to files under OpenRegister's `lib/`, and it MUST NOT claim names outside that prefix -- @e2e exclude bootstrap autoloading — no UI surface; covered by unit tests and the AppHost Newman collection - -#### Scenario: Disabled or absent OpenRegister is not loaded - -- **GIVEN** OpenRegister is installed but disabled, or not installed -- **WHEN** the prelude runs -- **THEN** it MUST return false without adding an autoloader and without throwing, and nothing SHALL be logged -- @e2e exclude bootstrap autoloading — no UI surface; covered by unit tests - -#### Scenario: Unexpected failure leaves one log line - -- **GIVEN** resolving OpenRegister fails for any reason other than the app being absent, disabled, or enabled but missing from disk — for example an enabled OpenRegister without `lib/`, an OpenRegister without a loadable `AppHost\Bootstrap`, or a throwing or unparseable `AppHost\Bootstrap` -- **WHEN** the prelude runs and the app then boots -- **THEN** `bootstrapAppHost()` MUST NOT throw, and `Application::boot()` MUST log exactly one warning for that request, carrying the reason -- @e2e exclude bootstrap logging — no UI surface; every branch of `bootstrapAppHost()` and `reportFailure()` is covered by unit tests. `Application` itself makes one call to `bootstrapAppHost()`, which the AppHost Newman collection covers on the enabled path. - -### Requirement: Domain Surfaces Excluded From Adoption - -Keepiq SHALL retain its `DashboardController` (argon2-WASM CSP and `summary()` aggregation), `DashboardService`, and every controller, service, middleware, listener, and repair step that touches secrets, encryption suites, shares, certificates, or JWT authentication, unchanged by this adoption. - -#### Scenario: Dashboard WASM CSP survives adoption - -- **GIVEN** the adopted app with AppHost wiring in place -- **WHEN** a user opens the Keepiq SPA and accesses a link share whose AES key derives via the argon2-browser WASM module -- **THEN** the page response MUST still carry the `wasm-unsafe-eval` CSP opt-in and the client-side key derivation MUST succeed - diff --git a/openspec/specs/key-generator/spec.md b/openspec/specs/key-generator/spec.md index d625b5d57..19aadc6b1 100644 --- a/openspec/specs/key-generator/spec.md +++ b/openspec/specs/key-generator/spec.md @@ -58,13 +58,20 @@ Invalid example: `^[^\s<>]$` — no length quantifier, cannot determine output l - THEN the system MUST return a validation error explaining the regex is invalid ### Requirement: Default Generation -When no regex is provided, the system MUST generate a key from the resolved character set at the specified length. +When no regex is provided, the system MUST generate a key from the resolved character set at the specified length. Generation MUST run in the browser with `crypto.getRandomValues`, drawing each character uniformly, so the plaintext value is never sent to the server: only its ciphertext is, once the secret is saved. #### Scenario: Default generation +@e2e tests/e2e/workflows/key-generator.spec.ts - GIVEN a request with `length = 16`, `include_special_characters = true`, `excluded_characters = ""` - WHEN the generator processes the request - THEN the system MUST return a 16-character random string using alphanumeric and special characters +#### Scenario: Generate in the browser +@e2e tests/e2e/workflows/key-generator.spec.ts +- GIVEN a vault user creating a secret in the new secret dialog on /secrets +- WHEN the user opens the generator and generates a password +- THEN the value MUST be generated without any request to `/api/v1/generate-key` + ### Requirement: Special Characters Definition The special characters set MUST be the OWASP recommended symbol set: @@ -105,10 +112,10 @@ The key generator API endpoint MUST require a valid Nextcloud session. Unauthent - THEN the system MUST reject it with HTTP 401 ### Requirement: Frontend Integration -The key generator MUST be callable from the secret creation UI with a configuration modal, and the generated value MUST be inserted directly into the key field. +The key generator MUST be callable from the secret creation UI with a configuration modal, and the generated value MUST be inserted directly into the key field. The modal and the browser extension MUST use the same generator module (`src/generator/generator.js`). `POST /api/v1/generate-key` is deprecated for the app's own use and kept only for API clients. #### Scenario: Generate from the secret creation UI -@e2e exclude Key-generator modal is a sub-interaction of secret creation; the generated-value insertion is exercised within the secret-crud-encryption e2e flow and the generator's own vitest unit tests. +@e2e tests/e2e/workflows/key-generator.spec.ts - GIVEN a user is creating a secret - WHEN they open the key generator modal and generate a value - THEN the generated value MUST be inserted directly into the key field diff --git a/openspec/specs/mcp-metadata-surface/spec.md b/openspec/specs/mcp-metadata-surface/spec.md index d101e89cd..338a5dc13 100644 --- a/openspec/specs/mcp-metadata-surface/spec.md +++ b/openspec/specs/mcp-metadata-surface/spec.md @@ -66,7 +66,9 @@ The system MUST expose `keepiq.rotationStatus` (`#[McpTool(name: 'rotationStatus - **THEN** no tool marks a flag rotated, dismisses it, or flags a secret ### Requirement: Surface Is Exposed Only Through The Scannable-Services Opt-In -`lib/Mcp/KeepiqScannableServices.php` MUST implement `OCA\OpenRegister\Mcp\IMcpScannableServices`, MUST be registered under the `IMcpScannableServices::keepiq` DI alias, and MUST list exactly the classes carrying the three read tools. Keepiq MUST NOT register an `IMcpToolProvider`, MUST NOT declare `x-openregister-mcp` on the placeholder register (nothing to derive; vault rows never enter OR per the `integration-boundary` capability), and MUST NOT expose any write tool. Tool visibility is governed by OpenRegister's default-deny grant whitelist; Keepiq MUST NOT implement a parallel grant system. +`lib/Mcp/KeepiqScannableServices.php` MUST implement `OCA\OpenRegister\Mcp\IMcpScannableServices`, MUST be registered under the `IMcpScannableServices::keepiq` DI alias, and MUST list exactly the classes carrying the three read tools. Keepiq MUST NOT register an `IMcpToolProvider`, MUST NOT declare `x-openregister-mcp` on any register (nothing to derive; vault rows never enter OR per the `integration-boundary` capability), and MUST NOT expose any write tool. Tool visibility is governed by OpenRegister's default-deny grant whitelist; Keepiq MUST NOT implement a parallel grant system. + +The surface MUST exist only when OpenRegister is installed and enabled, because OpenRegister's MCP endpoint is what offers it. Keepiq MUST register the alias as a string-to-string service alias that autoloads nothing at registration time, so that registering it neither requires nor detects OpenRegister. `KeepiqScannableServices` and the `#[McpTool]`-carrying classes MUST be loaded only when OpenRegister's scanner resolves the alias, which can only happen while OpenRegister is running. Keepiq MUST NOT put OpenRegister's autoload prefix on the autoloader to make this work. Without OpenRegister, no Keepiq code path MUST resolve the alias or instantiate a tool class. #### Scenario: The surface is exactly three read tools - **WHEN** Hermiq lists the `keepiq.*` tool catalog @@ -77,6 +79,19 @@ The system MUST expose `keepiq.rotationStatus` (`#[McpTool(name: 'rotationStatus #### Scenario: Ungranted agent sees nothing - **WHEN** an agent without a Keepiq grant lists tools - **THEN** no `keepiq.*` tool is offered +@e2e exclude registry-shape contract; covered by the MCP surface probe (tests/integration/Mcp/McpSurfaceTest.php). + +#### Scenario: Without OpenRegister nothing MCP-related loads +- **GIVEN** an instance where OpenRegister is not installed +- **WHEN** Keepiq registers and boots, and a user works through the vault +- **THEN** `Application::register()` MUST complete with the alias registered, and no `OCA\OpenRegister\Mcp\…` name and no `OCA\Keepiq\Mcp\…` class MUST be autoloaded during the request +@e2e exclude bootstrap wiring — no UI surface; covered by a unit test that registers McpRegistrar against a recording context and autoloader. + +#### Scenario: With OpenRegister the tools appear without extra setup +- **GIVEN** an instance where OpenRegister is installed and enabled, and an agent with a Keepiq read grant +- **WHEN** the agent lists tools through OpenRegister's MCP endpoint +- **THEN** the three `keepiq.*` read tools MUST be offered, exactly as before this change +@e2e exclude agent-facing MCP surface — verified live through OpenRegister's MCP endpoint as part of the change's live check, as hermiq-ai-tooling was (#1048). ### Requirement: Invocations Are Audited As Agent Reads Every tool invocation MUST be recorded through the existing `AuditService` with an `mcp` actor attribution and the invoking principal, distinguishing agent reads from UI reads, and MUST carry no result payload in the audit entry (counts only), consistent with the audit trail's own no-secret-content rule. diff --git a/openspec/specs/org-password-policies/spec.md b/openspec/specs/org-password-policies/spec.md index c152339a3..dfbb77d63 100644 --- a/openspec/specs/org-password-policies/spec.md +++ b/openspec/specs/org-password-policies/spec.md @@ -34,14 +34,13 @@ MUST validate it server-side before persisting. - THEN the system MUST reject it, and MUST persist a valid policy otherwise ### Requirement: Generator Locked to Policy -The system MUST clamp the server-side key generator to the policy so it can never emit a -value below the length floor or missing a required character class, rejecting a -non-compliant `regex` override. This enforcement is server-authoritative. +The system MUST clamp the key generator to the policy so it can never emit a value below the length floor or missing a required character class, rejecting a non-compliant `regex` override. The generator runs in the browser, so the clamp is applied there, under the same honest-client model as the save checks: the server never sees a generated value. The policy MUST also carry `generator_allow_passphrase` (default true), with which an administrator switches passphrase generation off. #### Scenario: Generated value is always compliant +@e2e exclude Needs an org-policy write that would leak into every other spec on the shared e2e instance. Covered by tests/vitest/generator.spec.js ("raises the length to the floor and forces the required classes") and tests/dialogs/KeyGeneratorModal.spec.js ("the org policy floor clamps the length"). - GIVEN a policy floor of length 20 with a required symbol - WHEN a user generates a password -- THEN the value MUST be at least 20 characters and able to contain a symbol +- THEN the value MUST be at least 20 characters and contain a symbol ### Requirement: Client-Side Save Enforcement The system MUST, in the secret create/edit dialog, block a manually entered login value diff --git a/openspec/specs/passphrase-generator/spec.md b/openspec/specs/passphrase-generator/spec.md new file mode 100644 index 000000000..2c442a0ee --- /dev/null +++ b/openspec/specs/passphrase-generator/spec.md @@ -0,0 +1,36 @@ +# passphrase-generator Specification + +## Purpose +Beside a random password, the key generator can make a passphrase: several words from the EFF large word list, with a separator, optional capitals and an optional digit. Generation runs in the browser, so the passphrase never travels to the server, and it follows the organisation's password policy, including whether passphrases are allowed at all. + +## Requirements + +### Requirement: Generate a passphrase +The key generator MUST offer a passphrase mode with a word count (4 to 12, default 5), a separator (default `-`), whether to capitalise each word (default false) and whether to add a digit (default false). It MUST draw each word uniformly from the EFF large word list of 7,776 words with `crypto.getRandomValues`, in the browser. A word count below 4 or above 12 MUST be refused with a message that names the allowed range. + +#### Scenario: A vault user generates a passphrase for a new secret +@e2e tests/e2e/workflows/key-generator.spec.ts +- **GIVEN** a vault user creating a secret in the new secret dialog on /secrets +- **WHEN** the user opens the generator, switches to Passphrase, picks 6 words with a space as separator and generates +- **THEN** the value field holds six words from the list separated by spaces + +#### Scenario: Too few words +@e2e exclude The dialog's number field bounds the count to 4 to 12; the refusal of an out-of-range count is a module contract covered by tests/vitest/generator.spec.js ("refuses fewer than 4 or more than 12 words"). +- **GIVEN** a signed-in user +- **WHEN** a passphrase of 3 words is requested +- **THEN** the generator refuses and names the allowed range of 4 to 12 words + +### Requirement: Passphrases follow the organisation password policy +When the organisation password policy is on, a generated passphrase MUST meet the policy's length floor and required character classes, by adding words, capitalising, adding a digit or using a symbol separator. An administrator MUST be able to switch passphrases off in the organisation policy section; the generator dialog then offers Password only and the generator refuses the mode. + +#### Scenario: The policy requires a digit and 30 characters +@e2e exclude Needs an org-policy write that would leak into every other spec on the shared e2e instance. Covered by tests/vitest/generator.spec.js ("meets a policy of 30 characters and a digit from 4 words"). +- **GIVEN** an organisation policy with a length floor of 30 and a required digit +- **WHEN** a user generates a passphrase of 4 words +- **THEN** the passphrase is at least 30 characters long and contains a digit + +#### Scenario: An administrator switches passphrases off +@e2e exclude Needs an org-policy write that would leak into every other spec on the shared e2e instance. Covered by tests/components/OrgPasswordPolicySection.spec.js, tests/dialogs/KeyGeneratorModal.spec.js ("offers Password only when the organisation switched passphrases off") and tests/vitest/generator.spec.js. +- **GIVEN** an administrator who switched off passphrases in the organisation policy section of the Keepiq admin settings +- **WHEN** a user opens the generator +- **THEN** the dialog offers Password only From 3755e7c990da0c08828d7bd639dd965b24848c7f Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 8 Oct 2026 07:27:43 +0200 Subject: [PATCH 009/167] docs(openspec): link capabilities to the split design canvas and its new boards Every row with a Keepiq board now points at canvas part 2 (QAAxpcsFKBCvDUGbQbwtCa); clients-15 keeps part 1 for pipelinq's flow boards. Five of the six new boards are mapped (KqRondleiding gives clients-17 its first board); KqMeldingen is a catalogue of empty states with no capability of its own. --- openspec/parity/capabilities.json | 371 ++++++++++++++++-------------- 1 file changed, 196 insertions(+), 175 deletions(-) diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index a5108fc90..c0d492f1a 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -461,7 +461,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/kv-create-edit-form.hbs:42 KvObjectEditor free key/value rows; ui/lib/kv/addon/components/kv-create-edit-form.js:108 api.secrets.kvV2Write(path, backend, payload) | docs: https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2 Note: The KV create page stores any key/value set at a path, so a login can be kept as username/password/url keys, but there is no login item type and no personal vault (per-user paths need templated policies)." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqNieuwGeheim", "KqGeheim" @@ -501,7 +501,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:20 secret.details.edit route (create new version); ui/lib/kv/addon/components/kv-create-edit-form.js:108 kvV2Write; vault/barrier_aes_gcm.go:846 AESGCMBarrier.Put encrypts every storage entry (encrypt at :1026) Note: Editing writes a new KV version through the UI or CLI, and every storage write goes through the AES-GCM barrier." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -538,7 +538,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/kv-delete-modal.js:23 modes delete, delete-metadata, destroy; ui/lib/kv/addon/components/page/secret/details.js:137 api.secrets.kvV2Delete Note: The KV details page offers delete, destroy and delete-all-metadata, gated by the caller's policy capabilities." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -573,7 +573,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/secret/details.hbs:75 Undelete button; ui/lib/kv/addon/components/page/secret/details.js:121 api.secrets.kvV2UndeleteVersions Note: Soft-deleted versions stay recoverable with Undelete until destroyed. There is no trash-bin list view; you undelete from the secret's own page." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqPrullenbak" } }, @@ -610,7 +610,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:13 list-directory route /list/*path_to_secret; ui/lib/kv/addon/components/page/list.hbs:103 folder icon for directory keys, :120 link to list-directory Note: Paths with slashes render as nested folders in the KV list page." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqGeheimen", "KqKluisBewerken" @@ -650,7 +650,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:8 KV routes are list, create, details, edit, patch, metadata, paths, configuration (no move); ui/lib/kv/addon/components/kv-create-edit-form.hbs:12 path is read-only when editing; searched 'rename', 'Move ' in ui/app ui/lib/kv ui/lib/core templates: no secret move Note: The path of an existing secret cannot be changed in the UI and the KV API has no move, so a move is copy, write and delete by hand." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqVerplaatsen" } }, @@ -687,7 +687,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/list.hbs:51 KvListFilter in the list toolbar; ui/lib/kv/addon/routes/list-directory.js:33 paginate(keys, {filter: pageFilter}); ui/lib/core/addon/utils/paginate-list.ts:42 filterValue.toLowerCase().includes(filter.toLowerCase()); searched 'fuzzy', 'levenshtein', 'typo' in ui/app ui/lib: only a comment in ui/app/services/pagination.js:65 Note: The KV list filters by case-insensitive substring of the key name, only within the current directory level. No typo tolerance and no search across the whole vault, so partial." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimen" } }, @@ -759,7 +759,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'favorite', 'favourite', 'bookmark', 'star' in ui/app/templates ui/app/components ui/lib/kv/addon ui/lib/core/addon/components: no match; ui/lib/kv/addon/routes.js:8 no favourites route Note: The source has no favourites or pinning of secrets, so the structural unknown settles to no." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimen" } }, @@ -794,7 +794,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/kv-metadata-fields.hbs:8 custom_metadata editor; ui/lib/kv/addon/components/page/secret/metadata/details.hbs:67 lists custom metadata key/values; ui/lib/kv/addon/routes/list-directory.js:33 list filter works on key names only Note: Custom metadata can serve as labels on a secret, but nothing filters the list by it." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqGeheimen", "KqBulkLabels" @@ -834,7 +834,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/kv-object-editor.hbs:25 free key input per row, :36 MaskedInput for values when @isMasked; ui/lib/kv/addon/components/kv-create-edit-form.hbs:42 KvObjectEditor Note: A KV secret is any set of named fields chosen by the writer, and the values are masked in the form." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqNieuwGeheim", "KqGeheim" @@ -874,7 +874,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/kv-object-editor.hbs:43 multi-line Textarea value; ui/lib/kv/addon/components/kv-create-edit-form.js:108 kvV2Write of arbitrary payload (JSON mode also available) Note: Any key/value or JSON payload can be stored, so a note is just a secret with a text field." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqNieuwGeheim" } }, @@ -911,7 +911,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/kv-object-editor.hbs:25 untyped key/value rows only; searched 'card', 'identity document', 'passport' in ui/lib/kv/addon: no item templates Note: KV has no typed item templates, so cards and identity documents are only free key/value data." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqNieuwGeheimTypen", "KqGeheimVarianten" @@ -950,7 +950,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: no SSH key form on any board" + "reason": "not designed yet: no SSH key form on any board; on the missing-boards list for the design session (decision 96)" } }, { @@ -994,7 +994,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'passkey', 'webauthn', 'fido' in all .go .hbs .ts files: no match Note: No passkey item type or WebAuthn handling exists in the source, so the unknown settles to no." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimVarianten" } }, @@ -1031,7 +1031,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/totp-edit.hbs:24 Totp::KeyCreate (seed or generated key), :22 KeyQrCode; ui/app/templates/vault/cluster/secrets/backend/credentials.hbs:14 GenerateCredentialsTotp; ui/app/components/generate-credentials-totp.hbs:14 shows current code with validity progress; builtin/logical/totp Note: The TOTP engine stores a seed and shows the current code in the UI, but as a separate engine, not attached to a login item." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimVarianten" } }, @@ -1068,7 +1068,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 command/kv_put.go:49 'The data can also be consumed from a file on disk by prefixing with the \"@\" symbol'; command/base_helpers.go:136 kvbuilder parses key=@file; searched 'upload', 'attachment' in ui/lib/kv/addon ui/app/components: no KV file upload Note: The CLI can read a file's contents into a secret value, which the barrier encrypts, but there is no attachment concept and no upload in the web UI, so partial." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -1105,7 +1105,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:24 secret.metadata.versions and diff routes; ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs:52 Version diff, :132 'Create new version from' an older version Note: Version history, diff between versions and restore via a new version from an old one are all in the UI; CLI has kv rollback." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -1142,7 +1142,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/list.hbs:94 each row is a LinkedBlock with a per-row Manage secret dropdown (:111), no checkbox; searched 'select all', 'bulk', 'Checkbox' in ui/lib/kv/addon and ui/app/components: no multi-select Note: The KV list acts on one secret at a time; there is no multi-select or bulk action." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqGeheimen", "KqVerplaatsen", @@ -1186,7 +1186,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:8 no preferences route; ui/app/router.js settings routes are seal and auth only; KV has no item types to default (kv-object-editor.hbs:25 untyped rows) Note: There are no item types and no per-user view preferences in the UI." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqInstellingen" } }, @@ -1223,7 +1223,7 @@ "passbolt": "searched 'last_used', 'recently used', 'dashboard' in passbolt_api src/ plugins/ and passbolt_styleguide src/: no usage tracking; only a 'Recently modified' filter (passbolt/passbolt_styleguide@v5.16.0 src/react-quickaccess/components/FilterResourcesByRecentlyModifiedPage/FilterResourcesByRecentlyModifiedPage.js:193) sorted by modified date Note: Passbolt has no dashboard and no recently used list; the nearest is a recently modified filter in quickaccess and a modified sort in the workspace." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqDashboard" } }, @@ -1258,7 +1258,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: clone is not on KqGeheim" + "reason": "not designed yet: clone is not on KqGeheim; on the missing-boards list for the design session (decision 96)" } }, { @@ -1294,7 +1294,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/linked-block.hbs:6 list row is a div role=link with a click handler only (no tabindex); ui/lib/kv/addon/components/page/list.hbs:111 HDS dropdown toggle per row (focusable) gives Details/Edit; ui/lib/kv/addon/components/kv-list-filter.js:55 Esc in filter goes up a directory; searched 'hotkey', 'ember-keyboard', 'shortcut' in ui: no match Note: The list can be driven by keyboard through each row's HDS dropdown and the filter input, but the row link itself is a click-only div and there are no shortcuts." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimen" } }, @@ -1331,7 +1331,7 @@ "keeper": "https://docs.keeper.io/user-guides/web-vault : records can be sorted by most recently used, name or date modified in the web vault and browser extension; sorting by date added is not described." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimen" } }, @@ -1369,7 +1369,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: no duplicates section on KqWachtwoordgezondheid" + "reason": "not designed yet: no duplicates section on KqWachtwoordgezondheid; on the missing-boards list for the design session (decision 96)" } }, { @@ -1406,7 +1406,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: no attachment preview on KqGeheim" + "reason": "not designed yet: no attachment preview on KqGeheim; on the missing-boards list for the design session (decision 96)" } }, { @@ -1442,7 +1442,7 @@ "keeper": "not checked: no archive state for records found in https://docs.keeper.io/user-guides/web-vault or the record docs; only delete and the Deleted Items folder are described." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqArchief" } }, @@ -1480,7 +1480,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: KqGeheim shows one website address" + "reason": "not designed yet: KqGeheim shows one website address; on the missing-boards list for the design session (decision 96)" } }, { @@ -1514,7 +1514,7 @@ "keeper": "https://docs.keeper.io/keeperpam/commander-cli/command-reference/record-commands : the Commander record-history command can 'see differences in between revision' (diff action); in the vault UI (https://docs.keeper.io/user-guides/web-vault) Record History opens one old version at a time and restores it, with no side-by-side view described." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -1550,7 +1550,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: no print or QR view" + "reason": "not designed yet: no print or QR view; on the missing-boards list for the design session (decision 96)" } }, { @@ -1585,7 +1585,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: no site icons or previews" + "reason": "not designed yet: no site icons or previews; on the missing-boards list for the design session (decision 96)" } }, { @@ -1658,7 +1658,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/credential/userpass/path_login.go:16 bcrypt compare of the password stored on the server; vault/barrier_aes_gcm.go:846 data keys live in the server barrier, not derived from a user secret Note: Userpass passwords are stored (bcrypt) and checked by the server; there is no client-held master key." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqKluisInstellen" } }, @@ -1695,7 +1695,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'password_policy', 'strength', 'min length' in builtin/credential/userpass/*.go: no match; builtin/credential/userpass/path_login.go:16 only bcrypt verification Note: Userpass accepts any password an admin or user sets; password policies only govern generated passwords, so no." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqKluisInstellen", "KqBeheerBeleid" @@ -1735,7 +1735,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/sidebar/user-menu.hbs:27 Reset password item; ui/app/components/page/userpass-reset-password.js:35 api.auth.userpassResetPassword Note: A userpass user can change their own login password from the user menu without data loss, because the password is a login credential and not a data key." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqInstellingen" } }, @@ -1780,7 +1780,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/js/Manager/EncryptionManager.js:47-54 update() only re-wraps the keychain via setAccountChallenge; :28-48 install(encrypt) re-encrypts all objects only when first enabling CSE; searched 're-encrypt', 'reencrypt', 'rotate', 'rekey' in src/lib src/js src/vue: no match Note: There is no flow to re-key every secret to a new key after a compromise; a passphrase change keeps the same object keys. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqHerstelNaCompromittering" } }, @@ -1817,7 +1817,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/core/addon/components/sidebar/user-menu.hbs:63 Log out; ui/app/services/auth.js:32 IDLE_TIMEOUT 3 min, :382 stops token renewal after idle so the session ends at token expiry; ui/app/components/token-expire-warning.js:12 expiry warning Note: The UI has a manual log out and stops renewing the token after three idle minutes, so an inactive session ends when the token TTL runs out. Moved to yes on the code, with the note that the idle window is the token TTL, not a separate setting." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqOntgrendelen", "KqInstellingen" @@ -1857,7 +1857,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 command/token_create.go:90 -ttl flag; vault/token_store.go:3172 requested ttl capped by role and mount max; ui/app/components/auth has no TTL field on the login form Note: A user can mint a token with a shorter TTL via CLI or API, within admin-set maxima, but the UI login offers no session length choice." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqInstellingen" } }, @@ -1902,7 +1902,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/login_mfa.go:50 MFA method types totp, duo, okta, pingid only; searched 'passkey', 'webauthn', 'fido' in all .go .hbs .ts: no match Note: No passkey or WebAuthn login exists in the source, so no." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqOntgrendelen", "KqInstellingen" @@ -1940,7 +1940,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'webauthn', 'biometric', 'touchid', 'fingerprint' in .go .hbs .ts: no login path; vault/login_mfa.go:50 only totp, duo, okta, pingid Note: No biometric unlock; a push MFA on a phone (Duo, Okta) is the nearest thing and is a second factor, not an unlock." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -2006,7 +2006,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/credential/userpass/path_user_password.go:39 users//password; ui/app/router.js access.method.item edit route for userpass users Note: An admin can set a new password for any userpass user; data is server-encrypted so nothing is lost." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqBeheerMensen", "KqOntgrendelen", @@ -2045,7 +2045,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'recovery code', 'backup code' in .go and .hbs: no match; recovery keys in vault/ are operator unseal keys, not per-user Note: There is no per-user recovery code or kit; a lost login is reset by an admin." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExporteren" } }, @@ -2082,7 +2082,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: no encryption details view in KqInstellingen" + "reason": "not designed yet: no encryption details view in KqInstellingen; on the missing-boards list for the design session (decision 96)" } }, { @@ -2118,7 +2118,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Encryption/Keychain/SseV2KeychainEncryption.php and src/lib/Encryption/Object/SseV1Encryption.php:31 derive keys from server secrets and user passphrase; searched 'certificate', 'x509', 'openssl_csr', 'CA' in src/lib: no certificate authority code Note: User keys are protected by server-held secrets or a passphrase-derived key, never by a certificate from an internal CA. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAlgemeen" } }, @@ -2235,7 +2235,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'revoke', 'suspend', 'reinstate' in src/lib/Encryption src/lib/Services src/lib/Controller: only UserLoginAttemptHelper destroying login tokens Note: There is no per-user vault key revocation or reinstatement. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerMensen" } }, @@ -2272,7 +2272,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/templates/vault/cluster/access/leases/list.hbs:65 force revoke prefix gated on forceRevokePrefix capability, :77 revoke prefix Note: An operator can force-revoke leases and tokens immediately in the UI, but there is no 'compromised' flag on a per-user key." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerMensen" } }, @@ -2340,7 +2340,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: KqBevestigMetHoofdwachtwoord confirms actions, not opening one item" + "reason": "not designed yet: KqBevestigMetHoofdwachtwoord confirms actions, not opening one item; on the missing-boards list for the design session (decision 96)" } }, { @@ -2469,7 +2469,7 @@ "hashicorp-vault": "searched 'device approval', 'auth request' in vault/ ui/app: no device concept; control groups approve requests, not devices. Note: rated by the lane from the source." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqApparaatGoedkeuren", "KqKluisInstellen" @@ -2509,7 +2509,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/policy-form.ts:170 save ACL policy (policies route in ui/app/router.js); ui/app/models/identity/entity.js entity policies attribute; vault/policy.go path capabilities Note: Access to another user's secret is granted by an admin writing a policy on the path and attaching it to that user's entity; the owner has no share action of their own." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -2546,7 +2546,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/models/identity/group.js:15 fields name, type, policies, metadata (internal or external group); ui/app/router.js access.identity create/edit routes; vault/identity_store_util.go:3164 refreshExternalGroupMembershipsByEntityID Note: Identity groups, internal or mapped from LDAP/OIDC groups, carry policies that grant a path to every member." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -2591,7 +2591,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/identity_store_util.go:3164 external group membership refreshed at login, access immediate; ui/app/router.js control-groups routes (Enterprise feature, stubbed in CE) | docs: https://developer.hashicorp.com/vault/docs/enterprise/control-groups Note: New group members get access at once with no approval step; approval on access exists only as Enterprise control groups, which work per request rather than per new member." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqTeammap" } }, @@ -2701,7 +2701,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/policy-form.ts:170 edit and save a policy; ui/app/models/identity/group.js:42 policies attribute editable on group or entity Note: Removing the path from a policy, or the policy from a group or entity, takes the access away on the next request." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -2737,7 +2737,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:3241 sys/capabilities-accessor and :3282 sys/capabilities answer per token and path; :3091 internal/ui/resultant-acl for the caller only; searched 'shared with', 'who has access' in ui templates: no match Note: You can check what a given token may do on a path, but there is no reverse view listing everyone who can read a secret." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -2773,7 +2773,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:448 a path ending in '*' becomes a prefix rule (IsPrefix :454) covering every secret below it, including later ones; ui/app/components/policy-form.ts:170 policy editor Note: A prefix policy on a team path, attached to a group, behaves as a team folder." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqTeammap" } }, @@ -2810,7 +2810,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:25 capabilities deny, read, update, list (plus create, delete, patch, sudo) Note: Each policy path grants a chosen set of capabilities, so read-only or edit per group is standard." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqTeammap" } }, @@ -2846,7 +2846,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:448 trailing '*' glob matches every deeper path; :444 '+' segment wildcard Note: Prefix globs make subfolders inherit the team path's access." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqTeammap" } }, @@ -2924,7 +2924,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/EventListener/User/UserDeletedListener.php:44 on user deletion data is deleted; searched 'owner' transfer and 'revoke' in src/lib/Cron src/lib/Helper/User: only UserMoveCommand (manual) Note: Nothing transfers ownership automatically; when an owner goes, their shares go with them. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimVarianten" } }, @@ -3000,7 +3000,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/tools/wrap.ts:19 sys/wrapping/wrap with a wrap TTL (:31); vault/logical_system_paths.go:4450 wrapping/wrap, :4480 wrapping/unwrap Note: A single-use wrapping token with a TTL can carry a secret, and the UI has a Wrap tool, but there is no password and no public web link page." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqLinkDelen", "KqGedeeldGeheim" @@ -3039,7 +3039,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:4480 wrapping/unwrap accepts the wrapping token itself as auth; ui/app/components/tools/unwrap.ts:28 Unwrap tool (inside the logged-in UI) Note: Anyone holding the wrapping token can unwrap it once via API or CLI without an account; the UI unwrap tool needs a login, so there is no recipient page." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqNieuwBeveiligdBericht", "KqBeveiligdBericht" @@ -3079,7 +3079,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/tools/lookup.ts wrapping lookup by token; vault/logical_system_paths.go:4513 wrapping/lookup; wrap response carries an accessor usable with token revoke-accessor; searched 'wrapping/list' in vault/: no list of issued wraps Note: You can look up or revoke a wrapping token you still hold (or its accessor), but there is no list of the wraps you created, so partial." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqNieuwBeveiligdBericht" } }, @@ -3115,7 +3115,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'federat', 'remote user', 'external share' in vault/ and ui/app: no cross-instance sharing; access is only via this cluster's auth methods and policies Note: An outside person needs an identity in this Vault (an auth method); there is no sharing with accounts on another instance." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqLinkDelen", "KqBinnengekomen", @@ -3155,7 +3155,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:25 capability set per path; ui/app/components/policy-form.ts:170 admin writes a policy per role Note: Roles such as manager or viewer are modelled as policies an admin writes; there are no built-in named collection roles." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqTeammap" } }, @@ -3227,8 +3227,11 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'emergency', 'grantee', 'trusted contact' in src/lib src/js src/vue: only src/lib/Services/LoggingService.php (log level) and src/lib/Fetcher/NightlyAppFetcher.php Note: No emergency access feature. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", - "board": "KqNoodtoegang" + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", + "board": [ + "KqNoodtoegang", + "KqBevestigMetHoofdwachtwoord" + ] } }, { @@ -3263,7 +3266,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'emergency', 'grantee' in src/lib src/js src/vue: no emergency access code Note: No emergency access, so nothing to decline. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqNoodtoegang" } }, @@ -3299,7 +3302,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'emergency', 'grantee', 'waiting period' in src/lib src/js src/vue: no emergency access code Note: No emergency access. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqNoodtoegang" } }, @@ -3364,7 +3367,7 @@ "keeper": "https://docs.keeper.io/enterprise-guide/sharing/hiding-passwords : Privacy Screen stops a team or role from unmasking passwords on shared records, but the docs state masking is visual only and the password stays reachable through the API and browser inspection." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqDelen", "KqTeammap" @@ -3405,7 +3408,7 @@ "keeper": "https://docs.keeper.io/enterprise-guide/sharing/time-limited-access : share credentials with other Keeper users 'on a temporary basis, automatically revoking access at a specified time'." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqDelen", "KqGeheim" @@ -3443,7 +3446,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js:127-129 control-groups, control-groups/configure, control-groups/:accessor (Enterprise control groups: a request waits for authorisers' approval); token_bound_cidrs (command/agent/approle_end_to_end_test.go:798) limits a token to IP ranges and max_lease_ttl bounds its time. Note: approval, time limit and IP limit all exist; control groups are Enterprise. Rated by the lane from the source." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqGeheimVarianten", "KqDelen" @@ -3513,7 +3516,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'secret request', 'request link' in ui templates: no match; ui/app/router.js has no request or fill-in route; unauthenticated paths are login, unwrap and health only Note: Vault has no inbound request link that an outsider fills in." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqGeheimVragen", "KqGeheimInvullen" @@ -3561,7 +3564,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: There is no request form, so no field choice." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimVragen" } }, @@ -3598,7 +3601,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists; write-once is not modelled. KV check-and-set (cas_required) makes writes conditional but is not a request." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimInvullen" } }, @@ -3635,7 +3638,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists to expire." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimVragen" } }, @@ -3708,7 +3711,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimVragen" } }, @@ -3745,7 +3748,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists to revoke." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -3781,7 +3784,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists, so there is no open-requests list." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -3817,7 +3820,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match; ui/app/routes/vault/cluster/access/control-groups.js:21 control groups (Enterprise) approve access to an existing secret, not supply a new value Note: Control groups let a human approve an application's access request, but nothing lets an application ask a person to provide a secret value." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqApplicatie" } }, @@ -3889,7 +3892,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no secret-request route; searched 'secret request', 'request link' in ui templates: no match Note: No request flow exists, so there are no fill-in links to recover." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -3926,8 +3929,11 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/credential/approle/path_role.go:126 role/ create, :280 secret-id management; ui/app/resources/auth/method.ts:60 approle among UI-managed auth methods Note: An admin registers an application as an AppRole role and issues role_id and secret_id." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", - "board": "KqApplicatieRegistreren" + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", + "board": [ + "KqApplicatieRegistreren", + "KqApplicaties" + ] } }, { @@ -3970,7 +3976,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Controller/Link/ConnectController.php:221 confirm() is called by the user who owns the account; searched 'approve' in src/lib/Controller src/lib/Settings: no admin approval queue Note: The user confirms their own client pairing; no administrator approves registrations. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerApplicaties" } }, @@ -4006,7 +4012,7 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 src/Notification/Email/Redactor: admin emails cover account recovery requests and user setup, none for application registration; searched 'application registration' in src/ plugins/: no match Note: No application registrations, so no waiting notification. Moved from unknown to no on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqDashboard" } }, @@ -4042,7 +4048,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'csr', 'certificate', 'x509', 'openssl' in src/lib: no certificate onboarding; src/lib/Controller/Link/ConnectController.php:260 apply() uses codes, not a CSR Note: Clients onboard with a pairing code and an app token, not a certificate signing request. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqApplicatieRegistreren" } }, @@ -4152,7 +4158,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/policy.go:25 separate capabilities (create, update without read) Note: A policy with create/update and no read lets a client write a secret it cannot read back." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimSchrijven" } }, @@ -4268,7 +4274,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/expiration.go:1564 ExpirationManager.Register creates a lease with TTL for every dynamic secret and service token Note: Dynamic secrets and tokens get leases that the expiration manager revokes at TTL." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqApplicatie" } }, @@ -4312,7 +4318,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/controllers/vault/cluster/access/leases/show.js:41 renewLease; ui/app/templates/vault/cluster/access/leases/show.hbs:37 revokeLease Note: Leases can be renewed and revoked from the UI, CLI and API." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqApplicatie" } }, @@ -4357,7 +4363,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/forms/mount.ts:58 default_lease_ttl and :62 max_lease_ttl fields; vault/logical_system_paths.go:21 mounts//tune Note: Default and max lease TTL are set per mount in the UI and via tune, and per role in each engine." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerApplicaties" } }, @@ -4433,8 +4439,11 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Db/Registration.php is a client pairing, not an owner of secrets; src/lib/Db/Password.php owned by a userId only Note: Secrets belong to users, not applications, so there is no per-application view. Moved from unknown." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", - "board": "KqApplicatie" + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", + "board": [ + "KqApplicatie", + "KqApplicaties" + ] } }, { @@ -4876,8 +4885,11 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 builtin/logical/pki/path_root.go:36 root/generate, builtin/logical/pki/path_intermediate.go:19 intermediate/generate; ui/lib/pki/addon/components/page/pki-issuer-generate-root.hbs and pki-issuer-generate-intermediate.hbs Note: Root and intermediate CAs are generated in the PKI engine with UI pages for both." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", - "board": "KqBeheerAlgemeen" + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", + "board": [ + "KqBeheerAlgemeen", + "KqRootVernieuwen" + ] } }, { @@ -4965,7 +4977,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 command/pki_health_check.go vault pki health-check; command/healthcheck/pki_ca_validity_period.go:27 CA validity check; ui/lib/pki/addon/components/page/pki-overview.hbs:8 overview shows issuer and role counts only Note: CA health is a CLI report; the UI PKI overview shows counts and quick actions, not health." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAlgemeen" } }, @@ -5001,7 +5013,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/pki/addon/routes.js:42 certificates list per PKI mount; certificates stored as KV values are not parsed or listed Note: Each PKI mount lists the certificates it issued; certificates kept as secrets elsewhere are not included." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqCertificaten" } }, @@ -5045,7 +5057,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 command/healthcheck/pki_ca_validity_period.go:27 ca_validity_period check warns about CA expiry on demand; searched 'notify', 'reminder' in builtin/logical/pki: no expiry notification Note: The CLI health check flags CAs nearing expiry when run, but nothing reminds anyone about leaf or CA expiry on its own, so partial." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqCertificaten" } }, @@ -5081,7 +5093,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/pki/addon/routes.js no renewal route; searched 'renew' in ui/lib/pki/addon/components: no guided renewal Note: There is no renewal guide; certificates are simply re-issued, manually or by ACME clients." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqCertificaatVernieuwen" } }, @@ -5146,7 +5158,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/utils/parse-pki-cert.js:60 notAfter parsed to not_valid_after for display; ui/lib/pki/addon/components/page/pki-issuer-import.hbs imports an external CA PEM; ui/lib/pki/addon/components/page/pki-certificate-details.hbs issued certs Note: Issued certificates and imported CA certificates show subject and expiry, but an external leaf certificate can only be stored as untyped KV text." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqCertificaten" } }, @@ -5229,7 +5241,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned', 'password age', 'health report' in ui/app ui/lib (.hbs .ts .js) and vault/*.go builtin sdk/helper: no password health code Note: Vault never scores stored passwords." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqWachtwoordgezondheid" } }, @@ -5280,7 +5292,7 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 searched 'reuse', 'reused', 'duplicate password' in src/react-extension src/shared: only an unrelated ShareDialog comment (src/react-extension/components/Share/ShareDialog.js:403); passbolt/passbolt_api@v5.16.0 same terms in src/ plugins/: no match Note: No reused password detection. Moved from unknown to no on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqWachtwoordgezondheid" } }, @@ -5324,7 +5336,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned', 'password age', 'health report' in ui/app ui/lib (.hbs .ts .js) and vault/*.go builtin sdk/helper: no password health code; KV metadata shows created_time per version (ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs) but no age filter Note: Version timestamps are visible per secret, but nothing lists secrets older than a chosen age." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqWachtwoordgezondheid" } }, @@ -5369,7 +5381,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned', 'password age', 'health report' in ui/app ui/lib (.hbs .ts .js) and vault/*.go builtin sdk/helper: no password health code Note: No breach checking exists." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqWachtwoordgezondheid" } }, @@ -5420,8 +5432,11 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned', 'password age', 'health report' in ui/app ui/lib (.hbs .ts .js) and vault/*.go builtin sdk/helper: no password health code Note: No vault health report exists; the PKI health check is about certificates." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", - "board": "KqWachtwoordgezondheid" + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", + "board": [ + "KqWachtwoordgezondheid", + "KqRapporten" + ] } }, { @@ -5457,7 +5472,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'zxcvbn', 'strength', 'reuse', 'pwned', 'breach', 'haveibeenpwned' in ui/app ui/lib and vault/*.go builtin sdk/helper: no match Note: There are no password health checks at all, in the browser or on the server." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqWachtwoordgezondheid" } }, @@ -5494,7 +5509,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:4352 sys/policies/password//generate; helper/random/parser.go:73 policy Length, helper/random/registry.go:16 charset rules; ui/app/components/tools/random.hbs:7 UI tool generates random bytes only Note: Password policies define length and charset rules and generate passwords via API or CLI; the options live in the policy rather than per request, and the UI tool gives random bytes, not a password generator." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqSleutelGenereren" } }, @@ -5528,7 +5543,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 helper/random/registry.go:16 only rule type is 'charset'; helper/random/string_generator.go:21 character sets, no word list Note: Generation is character based only; there is no word list for passphrases." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqSleutelGenereren" } }, @@ -5619,7 +5634,7 @@ }, "screen": { "board": null, - "reason": "not designed yet" + "reason": "not designed yet: on the missing-boards list for the design session (decision 96)" } }, { @@ -5653,7 +5668,7 @@ }, "screen": { "board": null, - "reason": "not designed yet" + "reason": "not designed yet: on the missing-boards list for the design session (decision 96)" } }, { @@ -5751,7 +5766,7 @@ }, "screen": { "board": null, - "reason": "not designed yet" + "reason": "not designed yet: on the missing-boards list for the design session (decision 96)" } }, { @@ -5817,7 +5832,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes/secret/metadata.js:39 metadata edit form with delete_version_after; ui/lib/kv/addon/helpers/is-deleted.js:10 deletion_time set from it Note: A per-secret delete_version_after makes versions expire, but expiry deletes the version rather than flagging it for change." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -5867,7 +5882,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes/configure.js:20 mount-level delete_version_after; ui/lib/kv/addon/routes/secret/metadata.js:39 per-secret override Note: A mount default with per-secret override exists, but it deletes versions and the per-secret value can be longer as well as shorter." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerBeleid" } }, @@ -5912,7 +5927,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'reminder', 'notification' in ui/lib/kv/addon and ui/app/components: none for secrets; ui/lib/kv/addon/helpers/is-deleted.js:10 expiry only sets deletion_time Note: Nothing reminds anyone before or after a KV secret expires." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqDashboard" } }, @@ -5949,7 +5964,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs:52 Version diff between versions with created_time; builtin/logical/database/path_roles.go:331 last_vault_rotation on static roles Note: A new KV version with its timestamp and the version diff show that a value changed, and database static roles record their last rotation, but there is no 'mark as rotated' action, so partial." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqGeheim", "KqGeheimVarianten" @@ -5989,7 +6004,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'breach', 'compromis', 'needs rotation' in ui/lib/kv/addon ui/app/components and builtin/logical: no flagging of KV secrets; vault/logical_system.go:4761 barrier rotate does not mark secrets Note: No mechanism flags stored secrets for rotation after a breach or key compromise." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqGeheimVarianten", "KqWachtwoordgezondheid" @@ -6036,7 +6051,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/dashboard/overview.hbs:23 SecretsEngines, :48 QuickActions, :50 ClientCount widgets only; no rotation-due widget Note: The dashboard shows no rotation status and there is no health report." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqDashboard", "KqWachtwoordgezondheid" @@ -6113,7 +6128,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: no change-password link on KqGeheim" + "reason": "not designed yet: no change-password link on KqGeheim; on the missing-boards list for the design session (decision 96)" } }, { @@ -6157,7 +6172,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_paths.go:4390 sys/policies/password/ governs engine-generated passwords; ui/app/routes/vault/cluster/policies.js:8 UI manages only acl, egp, rgp policy types; KV writes are not checked against a password policy Note: Password policies govern passwords Vault generates, via API or CLI only; values typed into KV are never checked." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerBeleid" } }, @@ -6194,7 +6209,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'password_policy', 'strength', 'min length' in builtin/credential/userpass/*.go: no match Note: There is no master password, and userpass has no strength rule an admin could set." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerBeleid" } }, @@ -6238,7 +6253,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'pending', 'approve' in builtin/credential/approle: no match; ui/app/router.js has no application queue route Note: There is no application registration queue." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerApplicaties" } }, @@ -6275,7 +6290,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/models/identity/entity.js:15 entity fields name, disabled, policies, metadata (disable or delete in Access > Entities); vault/identity_store_util.go:3164 external groups dropped on next login Note: Disabling or deleting the user's entity, or removing them in the IdP, removes every group-granted path in one step." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerMensen" } }, @@ -6312,7 +6327,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 command/agent/config/config.go Agent/Proxy cache is configured per agent process; no organisation-wide client caching switch in vault/ or ui/app/router.js Note: There are no offline-capable user clients; the only caches are per Vault Agent or Proxy configuration, so there is no org-wide switch." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAlgemeen" } }, @@ -6349,7 +6364,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'breach', 'pwned', 'haveibeenpwned' in ui/app ui/lib and vault/*.go: no match Note: No breach checking exists to switch on." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAlgemeen" } }, @@ -6386,7 +6401,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes/configure.js:20 max_versions and delete_version_after per mount; ui/lib/kv/addon/routes/secret/metadata.js:39 per secret Note: Version retention is set per mount and per secret in the UI." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerBeleid" } }, @@ -6488,7 +6503,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/identity_store.go:544 identity/mfa/login-enforcement/; ui/app/components/mfa/mfa-login-enforcement-form.hbs:72 enforcement targets (auth methods, groups, entities) Note: MFA can be enforced per auth method, group or entity from the UI; there is no personal vault export to ban, so partial." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerBeleid" } }, @@ -6525,7 +6540,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'AuthorizedAdminSetting', 'IDelegatedSettings' in src/lib: no match; src/lib/Controller/Admin/SettingsController.php:21 admin-only controller Note: The admin page is not delegable to a sub-admin role. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAlgemeen" } }, @@ -6560,7 +6575,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Command/SystemReportCommand.php:43 and src/lib/Helper/Survey/ServerReportHelper.php:296-297 report only aggregate SSE/CSE usage; no per-user vault list in src/lib/Settings/AdminSettings.php Note: Only aggregate encryption stats, no list of which users set up a vault. Moved from unknown." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerMensen" } }, @@ -6668,7 +6683,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 internalshared/configutil/listener.go:65 max_request_size is a listener-wide request cap; searched 'attachment', 'upload' in ui/lib/kv/addon: no attachments Note: There are no attachments to size or quota; only a server-wide request size limit in the config file." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAlgemeen" } }, @@ -6704,7 +6719,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/components/app-footer.hbs:9 installed version in the footer linking to its changelog (:7), :11 edition; vault/logical_system_paths.go:1763 sys/version-history Note: The UI shows the running version and edition, and the API gives version history." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAlgemeen" } }, @@ -6780,7 +6795,7 @@ "keeper": "https://docs.keeper.io/enterprise-guide/creating-new-record-types : an admin with 'Manage Record Types in Vault' creates a record template with labelled and required fields and publishes it to roles." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqItemtypeBewerken", "KqBeheerAlgemeen" @@ -6916,7 +6931,7 @@ "keeper": "https://docs.keeper.io/enterprise-guide/account-transfer-policy : the enterprise can take over a leaver's vault, and https://docs.keeper.io/enterprise-guide/personal-vaults-for-enterprise-and-business-users separates a personal family vault; no enforcement found that forbids storing work logins in a personal vault." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerBeleid" } }, @@ -7011,7 +7026,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/identity_store_group_aliases.go:21 group-alias maps an external group (OIDC, LDAP) to a Vault group and its policies. Note: Vault encrypts server-side, so there is no key handover to confirm; a new member gets the policies of their mapped groups at first login. Rated by the lane from the source." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerBeleid" } }, @@ -7077,7 +7092,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 src/lib/Cron/BackupJob.php:47 backup/interval; src/lib/Helper/AppSettings/BackupSettingsHelper.php:35-37 interval, max files, auto-restore after update; src/lib/Command/BackupRestoreCommand.php:45 passwords:backup:restore Note: proposed by the source reader as a capability no row covered." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAlgemeen" } }, @@ -7114,7 +7129,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 audit/hashstructure.go:25 salt.GetIdentifiedHMAC hashes string values; audit/entry_formatter.go formats every request and response; vault/logical_system_paths.go:1872 sys/audit/ enable device Note: Every request and response goes to the audit devices with secret values HMAC'd by default." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAudit" } }, @@ -7223,8 +7238,11 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no activity or audit route for users; audit output goes only to external devices (vault/logical_system_paths.go:1872) Note: Users cannot see their own activity in Vault; the audit log lives outside it." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", - "board": "KqMijnActiviteit" + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", + "board": [ + "KqMijnActiviteit", + "KqRapporten" + ] } }, { @@ -7260,7 +7278,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/components/page/secret/metadata/version-history.hbs:52 version list with created and deleted times and diff; access history only in external audit devices (vault/logical_system_paths.go:1872) Note: A secret's change history is in the UI; who read it is only in the external audit log." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheim" } }, @@ -7297,7 +7315,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no audit route; vault/logical_system_paths.go:1845 sys/audit only manages devices Note: Admins read the audit log in their own tools (file, syslog, SIEM); Vault has no audit browser." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAudit" } }, @@ -7342,7 +7360,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 audit/backend_syslog.go syslog device; audit/backend_socket.go TCP/UDP socket device Note: Syslog and socket devices stream audit events to a SIEM." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAudit" } }, @@ -7379,7 +7397,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'compliance report', 'pdf' in ui/app/components templates: no match; CSV exports exist only for client counts and the agent registry (ui/app/utils/constants/agent-registry.ts CSV_COLUMNS) Note: No compliance report export exists." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqBeheerAudit", "KqComplianceMomentopname" @@ -7419,7 +7437,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'compliance report', 'snapshot' in ui/app/components for compliance: only raft and recovery storage snapshots (ui/app/router.js recovery.snapshots) Note: No compliance snapshots exist; storage snapshots are backups, not signed reports." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqComplianceMomentopname" } }, @@ -7455,7 +7473,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'decoy', 'honeypot', 'canary' in src/lib src/js src/vue: no match Note: No decoy credentials. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqGeheim", "KqBeheerAudit" @@ -7495,7 +7513,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js has no personal data export route; searched 'gdpr', 'personal data' in ui/app and vault/*.go: no export Note: There is no self-service personal data export; an admin can read the entity record by API." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqMijnGegevens" } }, @@ -7531,7 +7549,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/router.js access.identity entity delete by an admin; ui/app/models/identity/entity.js:15 entity fields; secrets live at paths with no owner so they survive the entity (vault/policy.go) Note: An admin can delete a user's entity and aliases and shared secrets stay put because they are not owned, but users cannot delete their own account, so partial." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqMijnGegevens" } }, @@ -7602,7 +7620,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 audit/backend_socket.go and audit/backend_syslog.go generic streams; no Splunk or Sentinel connector code in the tree | docs: https://developer.hashicorp.com/vault/tutorials/monitoring/monitor-telemetry-audit-splunk Note: code not public for the Splunk app; docs rating kept: generic syslog and socket streams, with a Splunk app for Enterprise documented separately." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAudit" } }, @@ -7638,7 +7656,7 @@ "passbolt": "passbolt/passbolt_api@v5.16.0 src/Notification/Email/Redactor/User/UserAdminRoleRevokedEmailRedactor.php:43 AD/admin_role_revoked; src/Notification/Email/Redactor/User/UserDeleteAdminEmailRedactor.php:47 AD/user_deleted; src/Notification/Email/Redactor/Role/RoleUpdatedAdminEmailRedactor.php:43; plugins/PassboltEe/MfaPolicies/src/Notification/Email/MfaPoliciesSettingsUpdatedEmailRedactor.php:42 and other AD/settings_updated emails Note: Admins are emailed when sensitive admin events happen (role revoked, user deleted, security policies or SSO changed), but there is no risk scoring or alerting on user behaviour. Moved from unknown to partial." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqBeheerAudit" } }, @@ -7705,7 +7723,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'import', 'csv' in ui/lib/kv/addon and all .go .ts .hbs: no password manager importer Note: No importer for other password managers exists; bulk loading is scripting against the KV API." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqImporteren" } }, @@ -7742,7 +7760,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match; ui/lib/kv/addon/routes.js:8 no import route Note: No importer exists, for Nextcloud Passwords or anything else." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqImporteren" } }, @@ -7779,7 +7797,7 @@ "passbolt": "passbolt/passbolt_browser_extension@v5.16.0 src/all/background_page/model/import/resources/resourcesCsvImportParser.js:112 row parser chosen automatically by header score; src/all/background_page/model/import/resources/csvRowParser/csvKdbxRowParser.js:24 fixed mapping; passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Resource/ImportResources/ImportResources.js no mapping step Note: Column mapping is fixed per detected format; there is no preview or manual mapping. Moved from unknown to no." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqImporteren" } }, @@ -7824,7 +7842,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match; ui/lib/kv/addon/routes.js:8 no import route Note: No importer exists. KV paths are folders, so a scripted import can keep structure, but the product does nothing here." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqImporteren" } }, @@ -7868,7 +7886,7 @@ "passbolt": "passbolt/passbolt_browser_extension@v5.16.0 searched 'duplicate' in src/all/background_page/service/resource/import and src/all/background_page/model/import: only in parser tests about duplicate CSV columns Note: Imports never check for items that already exist. Moved from unknown to no." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqImporteren" } }, @@ -7904,7 +7922,7 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Resource/ImportResources/ImportResourcesResult.js:249 resource errors, :257 folder errors, :376 Error details with a copyable stack; passbolt/passbolt_browser_extension@v5.16.0 src/all/background_page/service/resource/import/ImportResourcesService.js:271 importFoldersErrors Note: After import a result dialog lists counts and each rejected resource or folder with its error. Moved from unknown to yes." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqImporteren" } }, @@ -7955,7 +7973,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'cxf', 'credential exchange' in src: no match; src/vue/Components/Import.vue:9-26 and src/vue/Components/Export.vue:7-11 list no CXF format Note: No Credential Exchange Format support. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqImporteren", "KqExporteren" @@ -7994,7 +8012,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'cxp', 'credential exchange' in src: no match Note: No Credential Exchange Protocol support. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqCxpOverdracht" } }, @@ -8045,7 +8063,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 vault/logical_system_raft.go:142 sys/storage/raft/snapshot (save and restore), :158 snapshot-force; ui/app/components/raft-storage-overview.hbs:12 Download snapshot; ui/app/components/raft-storage-restore.hbs restore upload; ui/app/components/recovery/page/snapshots/snapshot-manage.hbs:6 Secrets recovery (Enterprise) Note: Integrated storage snapshots stay barrier-encrypted and can be downloaded and restored from the UI, even in CE; automated snapshots and single-secret recovery are Enterprise." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExporteren" } }, @@ -8082,8 +8100,11 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/lib/kv/addon/routes.js:8 no export route; searched 'csv' in ui/lib/kv/addon: no match; CSV exports only for client counts and agent registry Note: There is no plaintext export of secrets; a user can only script reads with the CLI or API." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", - "board": "KqExporteren" + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", + "board": [ + "KqExporteren", + "KqBevestigMetHoofdwachtwoord" + ] } }, { @@ -8119,7 +8140,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: KqExporteren has no selection step" + "reason": "not designed yet: KqExporteren has no selection step; on the missing-boards list for the design session (decision 96)" } }, { @@ -8163,7 +8184,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'keepass', 'lastpass', 'bitwarden', '1password', 'credential exchange', 'cxf' in all .go .ts .hbs (non-test): no match Note: No KeePass import exists." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqImporteren" } }, @@ -8215,7 +8236,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/: no match Note: There is no browser extension." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -8265,7 +8286,7 @@ "nextcloud-passwords": "source silent: extension/mobile repo not read (passwords-webextension) Note: Frame filling is extension behaviour that this repo cannot show." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -8302,7 +8323,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/app ui/lib: no match Note: No browser extension, so no save-login prompt." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -8348,7 +8369,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/app ui/lib: no match Note: No browser extension, so no one-time code fill." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -8385,7 +8406,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 top-level tree has ui/ (web app), command/ (CLI) and server code only; searched 'browser extension', 'autofill' in ui/app ui/lib: no match Note: No browser extension and no passkey support (searched 'webauthn', 'passkey': no match)." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -8422,7 +8443,7 @@ "nextcloud-passwords": "source silent: extension/mobile repo not read; server side src/lib/Controller/Api/SessionApiController.php session lifetime also applies to extension sessions Note: Server sessions of any client expire, but whether the extension locks its own UI is not in this repo." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -8466,7 +8487,7 @@ "nextcloud-passwords": "marius-wieschollek/passwords@2026.9.0 searched 'manifest', 'webmanifest', 'serviceWorker' in src: no match Note: The web app ships no web manifest or service worker, so it cannot be installed as a PWA. Moved from unknown on a documented search." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqTelefoon" } }, @@ -8511,7 +8532,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/app/index.html:14 viewport meta; ui/lib/core/addon/components/sidebar/frame.hbs:53 Hds::AppSideNav @isResponsive=true @isCollapsible=true; ui/lib/kv/addon/components/page/list.hbs:100 'level is-mobile' rows Note: The UI shell is responsive (collapsible side nav, viewport meta, mobile-aware list rows), but it is a desktop admin console with wide forms and tables, not designed for phone flows, so partial." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqTelefoon" } }, @@ -8548,7 +8569,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 ui/public has no service worker for offline data (the only one is ui/lib/service-worker-authenticated-download); command/agentproxyshared/cache is a machine-side proxy cache, not a user vault Note: The web UI needs a live server; there is no offline read-only mode for users." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqGeheimVarianten" } }, @@ -8614,7 +8635,7 @@ }, "screen": { "board": null, - "reason": "not designed yet: KqTelefoon is the installed web app, not a native app" + "reason": "not designed yet: KqTelefoon is the installed web app, not a native app; on the missing-boards list for the design session (decision 96)" } }, { @@ -8773,7 +8794,7 @@ "hashicorp-vault": "hashicorp/vault@v2.1.1 searched 'nextcloud' in all .go .hbs .ts .js files: no match Note: No Nextcloud integration and no browser extension." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -8896,8 +8917,8 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/AuthenticationSetup/SetupAuthentication/SetupAuthentication.js step-by-step first-run setup (key, passphrase, recovery kit, security token); src/react-extension/components/Administration/HomePage/AdministrationHomePage.js:101 admin Getting Started card for metadata settings Note: First use is a guided setup wizard for the account, but there is no product walkthrough of the vault itself. Moved from unknown to partial." }, "screen": { - "board": null, - "reason": "not designed yet: no walkthrough board" + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", + "board": "KqRondleiding" } }, { @@ -8941,7 +8962,7 @@ "passbolt": "passbolt/passbolt_styleguide@v5.16.0 src/react-extension/components/Administration/HomePage/AdministrationHomePage.js:90-130 cards linking to settings pages, no figures; searched 'dashboard' in src/react-extension/components: no vault statistics component Note: There is no dashboard with vault figures; the admin home is a list of settings. Moved from unknown to no." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqDashboard" } }, @@ -8978,7 +8999,7 @@ "keeper": "not checked: https://docs.keeper.io/user-guides/vault-offline-access read; current docs list creating and editing records offline while older release notes (https://docs.keeper.io/release-notes/desktop/web-vault-+-desktop-app/older/vault-version-14.4.0) describe read-only offline access, so it is not settled." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqOfflineConflict" } }, @@ -9047,7 +9068,7 @@ "keeper": "https://docs.keeper.io/user-guides/tips-and-tricks/personal-and-business-vaults : switch between business and personal accounts in the web vault, browser extension and mobile apps ('Switch Account', 'Add Account')." }, "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -9139,7 +9160,7 @@ "providerHow": "read-from-code", "source": "spec-round-2026-10-07", "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -9166,7 +9187,7 @@ "providerHow": "read-from-code", "source": "spec-round-2026-10-07", "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } }, @@ -9193,7 +9214,7 @@ "providerHow": "read-from-code", "source": "spec-round-2026-10-07", "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": [ "KqExtensie", "KqExtensieBewerken" @@ -9223,7 +9244,7 @@ "providerHow": "read-from-code", "source": "spec-round-2026-10-07", "screen": { - "canvas": "5NkFW28vZUUij43xzxHg5a", + "canvas": "QAAxpcsFKBCvDUGbQbwtCa", "board": "KqExtensie" } } From 4bdd54dec3017e79f4256c28d45b19f682005ed2 Mon Sep 17 00:00:00 2001 From: SudoThijn Date: Thu, 8 Oct 2026 11:07:00 +0200 Subject: [PATCH 010/167] feat(extension): replace the old browser extension with a WXT setup --- .../.claude/commands/opsx/apply.md | 155 + .../.claude/commands/opsx/archive.md | 160 + .../.claude/commands/opsx/explore.md | 174 + .../.claude/commands/opsx/propose.md | 109 + .../.claude/commands/opsx/sync.md | 143 + .../skills/openspec-apply-change/SKILL.md | 159 + .../skills/openspec-archive-change/SKILL.md | 117 + .../.claude/skills/openspec-explore/SKILL.md | 289 ++ .../.claude/skills/openspec-propose/SKILL.md | 113 + .../skills/openspec-sync-specs/SKILL.md | 147 + browser-extension/.gitattributes | 2 + browser-extension/.gitignore | 10 +- browser-extension/.npmrc | 10 + browser-extension/CLAUDE.md | 76 + browser-extension/README.md | 174 +- browser-extension/THIRD-PARTY-NOTICES.txt | 21 - browser-extension/WXT-AND-BROWSERS.md | 139 + browser-extension/assets/icon.svg | 22 + browser-extension/build.mjs | 157 - browser-extension/capture/.gitignore | 1 - browser-extension/capture/chromium.mjs | 360 -- browser-extension/capture/compose.yaml | 42 - browser-extension/capture/firefox.mjs | 109 - browser-extension/capture/fixtures.mjs | 221 -- browser-extension/capture/setup.sh | 125 - browser-extension/entrypoints/background.ts | 83 + browser-extension/entrypoints/content.ts | 51 + .../entrypoints/popup/index.html | 23 + browser-extension/entrypoints/popup/main.ts | 37 + browser-extension/entrypoints/popup/popup.css | 122 + browser-extension/eslint.config.mjs | 24 + browser-extension/icons/icon-128.png | Bin 2317 -> 0 bytes browser-extension/icons/icon-16.png | Bin 345 -> 0 bytes browser-extension/icons/icon-32.png | Bin 546 -> 0 bytes browser-extension/icons/icon-48.png | Bin 870 -> 0 bytes browser-extension/load-check/chromium.mjs | 71 - browser-extension/load-check/firefox.mjs | 61 - browser-extension/manifest.json | 63 - browser-extension/manifests/browsers.mjs | 70 - browser-extension/openspec/README.md | 28 + .../architecture/adr-001-bitwarden-parity.md | 27 + .../adr-002-key-lifetime-and-vault-cache.md | 99 + .../adr-003-keepiq-api-contract.md | 112 + .../architecture/adr-004-popup-ui-react.md | 30 + .../ext-accounts-and-unlock/.openspec.yaml | 2 + .../changes/ext-accounts-and-unlock/design.md | 145 + .../ext-accounts-and-unlock/proposal.md | 66 + .../specs/account-management/spec.md | 155 + .../specs/api-client/spec.md | 73 + .../specs/vault-unlock/spec.md | 136 + .../changes/ext-accounts-and-unlock/tasks.md | 52 + .../changes/ext-autofill/.openspec.yaml | 2 + .../openspec/changes/ext-autofill/design.md | 117 + .../openspec/changes/ext-autofill/proposal.md | 60 + .../ext-autofill/specs/autofill/spec.md | 187 + .../ext-autofill/specs/login-capture/spec.md | 121 + .../openspec/changes/ext-autofill/tasks.md | 44 + .../changes/ext-generator/.openspec.yaml | 2 + .../openspec/changes/ext-generator/design.md | 129 + .../changes/ext-generator/proposal.md | 58 + .../specs/credential-generator/spec.md | 223 ++ .../openspec/changes/ext-generator/tasks.md | 49 + .../openspec/changes/ext-send/.openspec.yaml | 2 + .../openspec/changes/ext-send/design.md | 120 + .../openspec/changes/ext-send/proposal.md | 63 + .../changes/ext-send/specs/send/spec.md | 184 + .../openspec/changes/ext-send/tasks.md | 38 + .../changes/ext-settings/.openspec.yaml | 2 + .../openspec/changes/ext-settings/design.md | 126 + .../openspec/changes/ext-settings/proposal.md | 66 + .../ext-settings/specs/settings/spec.md | 245 ++ .../openspec/changes/ext-settings/tasks.md | 46 + .../changes/ext-vault-browse/.openspec.yaml | 2 + .../changes/ext-vault-browse/design.md | 149 + .../changes/ext-vault-browse/proposal.md | 74 + .../specs/item-detail/spec.md | 122 + .../specs/popup-shell/spec.md | 106 + .../ext-vault-browse/specs/vault-list/spec.md | 150 + .../ext-vault-browse/specs/vault-sync/spec.md | 158 + .../changes/ext-vault-browse/tasks.md | 46 + .../changes/ext-vault-edit/.openspec.yaml | 2 + .../openspec/changes/ext-vault-edit/design.md | 130 + .../changes/ext-vault-edit/proposal.md | 77 + .../specs/folder-management/spec.md | 121 + .../ext-vault-edit/specs/item-editing/spec.md | 208 + .../openspec/changes/ext-vault-edit/tasks.md | 34 + browser-extension/openspec/config.yaml | 51 + browser-extension/package-lock.json | 3526 +++++++++++++++++ browser-extension/package.json | 27 + browser-extension/public/icon/128.png | Bin 0 -> 3163 bytes browser-extension/public/icon/16.png | Bin 0 -> 481 bytes browser-extension/public/icon/32.png | Bin 0 -> 812 bytes browser-extension/public/icon/48.png | Bin 0 -> 1211 bytes browser-extension/public/icon/96.png | Bin 0 -> 2449 bytes browser-extension/safari/README.md | 25 - browser-extension/safari/convert.sh | 18 - browser-extension/scripts/render-icons.mjs | 41 - .../src/background/clipboard-clear.js | 150 - .../src/background/generator-handlers.js | 192 - browser-extension/src/background/router.js | 1785 --------- .../src/background/service-worker.js | 36 - .../src/background/vault-handlers.js | 622 --- .../src/background/vault-sync.js | 266 -- browser-extension/src/browser-action.ts | 9 + .../src/content/content-script.js | 302 -- browser-extension/src/content/inpage-shim.js | 114 - browser-extension/src/content/otp-watch.js | 54 - .../src/content/password-suggest.js | 201 - browser-extension/src/content/save-prompt.js | 168 - browser-extension/src/crypto/index.js | 56 - browser-extension/src/lib/api.js | 881 ---- browser-extension/src/lib/argon2-wasm.js | 20 - browser-extension/src/lib/capture.js | 51 - browser-extension/src/lib/deviceApproval.js | 208 - .../src/lib/extension-settings.js | 74 - browser-extension/src/lib/field-detect.js | 128 - browser-extension/src/lib/fillScope.js | 24 - browser-extension/src/lib/folder-rules.js | 106 - browser-extension/src/lib/generator-state.js | 197 - browser-extension/src/lib/item-form.js | 307 -- browser-extension/src/lib/match.js | 129 - browser-extension/src/lib/pin-unlock.js | 140 - browser-extension/src/lib/policy.js | 48 - browser-extension/src/lib/send-form.js | 115 - browser-extension/src/lib/server-url.js | 83 - browser-extension/src/lib/totp-service.js | 50 - browser-extension/src/lib/usage.js | 26 - browser-extension/src/lib/useOnly.js | 78 - browser-extension/src/lib/vault-index.js | 138 - browser-extension/src/lib/vault.js | 255 -- browser-extension/src/lib/version.js | 39 - browser-extension/src/messages.ts | 28 + .../src/offscreen/offscreen.html | 10 - browser-extension/src/offscreen/offscreen.js | 15 - browser-extension/src/passkey/cbor.js | 90 - browser-extension/src/passkey/consent.html | 19 - browser-extension/src/passkey/consent.js | 26 - browser-extension/src/passkey/orchestrator.js | 172 - browser-extension/src/passkey/rp.js | 60 - .../src/passkey/vault-passkey.js | 14 - browser-extension/src/passkey/webauthn.js | 260 -- browser-extension/src/popup/clipboard.js | 32 - browser-extension/src/popup/folder-view.js | 237 -- browser-extension/src/popup/generator-view.js | 365 -- browser-extension/src/popup/item-detail.js | 340 -- browser-extension/src/popup/popup.css | 560 --- browser-extension/src/popup/popup.html | 396 -- browser-extension/src/popup/popup.js | 944 ----- browser-extension/src/popup/send-view.js | 178 - browser-extension/src/popup/vault-view.js | 676 ---- browser-extension/src/popup/views.js | 115 - browser-extension/src/totp/index.js | 13 - browser-extension/src/unlock/ceremony.js | 229 -- browser-extension/src/unlock/unlock.html | 27 - browser-extension/src/unlock/unlock.js | 67 - browser-extension/tsconfig.json | 3 + browser-extension/web-ext.config.example.ts | 12 + browser-extension/wxt.config.ts | 45 + 158 files changed, 10097 insertions(+), 13404 deletions(-) create mode 100644 browser-extension/.claude/commands/opsx/apply.md create mode 100644 browser-extension/.claude/commands/opsx/archive.md create mode 100644 browser-extension/.claude/commands/opsx/explore.md create mode 100644 browser-extension/.claude/commands/opsx/propose.md create mode 100644 browser-extension/.claude/commands/opsx/sync.md create mode 100644 browser-extension/.claude/skills/openspec-apply-change/SKILL.md create mode 100644 browser-extension/.claude/skills/openspec-archive-change/SKILL.md create mode 100644 browser-extension/.claude/skills/openspec-explore/SKILL.md create mode 100644 browser-extension/.claude/skills/openspec-propose/SKILL.md create mode 100644 browser-extension/.claude/skills/openspec-sync-specs/SKILL.md create mode 100644 browser-extension/.gitattributes create mode 100644 browser-extension/.npmrc create mode 100644 browser-extension/CLAUDE.md delete mode 100644 browser-extension/THIRD-PARTY-NOTICES.txt create mode 100644 browser-extension/WXT-AND-BROWSERS.md create mode 100644 browser-extension/assets/icon.svg delete mode 100644 browser-extension/build.mjs delete mode 100644 browser-extension/capture/.gitignore delete mode 100644 browser-extension/capture/chromium.mjs delete mode 100644 browser-extension/capture/compose.yaml delete mode 100644 browser-extension/capture/firefox.mjs delete mode 100644 browser-extension/capture/fixtures.mjs delete mode 100644 browser-extension/capture/setup.sh create mode 100644 browser-extension/entrypoints/background.ts create mode 100644 browser-extension/entrypoints/content.ts create mode 100644 browser-extension/entrypoints/popup/index.html create mode 100644 browser-extension/entrypoints/popup/main.ts create mode 100644 browser-extension/entrypoints/popup/popup.css create mode 100644 browser-extension/eslint.config.mjs delete mode 100644 browser-extension/icons/icon-128.png delete mode 100644 browser-extension/icons/icon-16.png delete mode 100644 browser-extension/icons/icon-32.png delete mode 100644 browser-extension/icons/icon-48.png delete mode 100644 browser-extension/load-check/chromium.mjs delete mode 100644 browser-extension/load-check/firefox.mjs delete mode 100644 browser-extension/manifest.json delete mode 100644 browser-extension/manifests/browsers.mjs create mode 100644 browser-extension/openspec/README.md create mode 100644 browser-extension/openspec/architecture/adr-001-bitwarden-parity.md create mode 100644 browser-extension/openspec/architecture/adr-002-key-lifetime-and-vault-cache.md create mode 100644 browser-extension/openspec/architecture/adr-003-keepiq-api-contract.md create mode 100644 browser-extension/openspec/architecture/adr-004-popup-ui-react.md create mode 100644 browser-extension/openspec/changes/ext-accounts-and-unlock/.openspec.yaml create mode 100644 browser-extension/openspec/changes/ext-accounts-and-unlock/design.md create mode 100644 browser-extension/openspec/changes/ext-accounts-and-unlock/proposal.md create mode 100644 browser-extension/openspec/changes/ext-accounts-and-unlock/specs/account-management/spec.md create mode 100644 browser-extension/openspec/changes/ext-accounts-and-unlock/specs/api-client/spec.md create mode 100644 browser-extension/openspec/changes/ext-accounts-and-unlock/specs/vault-unlock/spec.md create mode 100644 browser-extension/openspec/changes/ext-accounts-and-unlock/tasks.md create mode 100644 browser-extension/openspec/changes/ext-autofill/.openspec.yaml create mode 100644 browser-extension/openspec/changes/ext-autofill/design.md create mode 100644 browser-extension/openspec/changes/ext-autofill/proposal.md create mode 100644 browser-extension/openspec/changes/ext-autofill/specs/autofill/spec.md create mode 100644 browser-extension/openspec/changes/ext-autofill/specs/login-capture/spec.md create mode 100644 browser-extension/openspec/changes/ext-autofill/tasks.md create mode 100644 browser-extension/openspec/changes/ext-generator/.openspec.yaml create mode 100644 browser-extension/openspec/changes/ext-generator/design.md create mode 100644 browser-extension/openspec/changes/ext-generator/proposal.md create mode 100644 browser-extension/openspec/changes/ext-generator/specs/credential-generator/spec.md create mode 100644 browser-extension/openspec/changes/ext-generator/tasks.md create mode 100644 browser-extension/openspec/changes/ext-send/.openspec.yaml create mode 100644 browser-extension/openspec/changes/ext-send/design.md create mode 100644 browser-extension/openspec/changes/ext-send/proposal.md create mode 100644 browser-extension/openspec/changes/ext-send/specs/send/spec.md create mode 100644 browser-extension/openspec/changes/ext-send/tasks.md create mode 100644 browser-extension/openspec/changes/ext-settings/.openspec.yaml create mode 100644 browser-extension/openspec/changes/ext-settings/design.md create mode 100644 browser-extension/openspec/changes/ext-settings/proposal.md create mode 100644 browser-extension/openspec/changes/ext-settings/specs/settings/spec.md create mode 100644 browser-extension/openspec/changes/ext-settings/tasks.md create mode 100644 browser-extension/openspec/changes/ext-vault-browse/.openspec.yaml create mode 100644 browser-extension/openspec/changes/ext-vault-browse/design.md create mode 100644 browser-extension/openspec/changes/ext-vault-browse/proposal.md create mode 100644 browser-extension/openspec/changes/ext-vault-browse/specs/item-detail/spec.md create mode 100644 browser-extension/openspec/changes/ext-vault-browse/specs/popup-shell/spec.md create mode 100644 browser-extension/openspec/changes/ext-vault-browse/specs/vault-list/spec.md create mode 100644 browser-extension/openspec/changes/ext-vault-browse/specs/vault-sync/spec.md create mode 100644 browser-extension/openspec/changes/ext-vault-browse/tasks.md create mode 100644 browser-extension/openspec/changes/ext-vault-edit/.openspec.yaml create mode 100644 browser-extension/openspec/changes/ext-vault-edit/design.md create mode 100644 browser-extension/openspec/changes/ext-vault-edit/proposal.md create mode 100644 browser-extension/openspec/changes/ext-vault-edit/specs/folder-management/spec.md create mode 100644 browser-extension/openspec/changes/ext-vault-edit/specs/item-editing/spec.md create mode 100644 browser-extension/openspec/changes/ext-vault-edit/tasks.md create mode 100644 browser-extension/openspec/config.yaml create mode 100644 browser-extension/package-lock.json create mode 100644 browser-extension/package.json create mode 100644 browser-extension/public/icon/128.png create mode 100644 browser-extension/public/icon/16.png create mode 100644 browser-extension/public/icon/32.png create mode 100644 browser-extension/public/icon/48.png create mode 100644 browser-extension/public/icon/96.png delete mode 100644 browser-extension/safari/README.md delete mode 100755 browser-extension/safari/convert.sh delete mode 100644 browser-extension/scripts/render-icons.mjs delete mode 100644 browser-extension/src/background/clipboard-clear.js delete mode 100644 browser-extension/src/background/generator-handlers.js delete mode 100644 browser-extension/src/background/router.js delete mode 100644 browser-extension/src/background/service-worker.js delete mode 100644 browser-extension/src/background/vault-handlers.js delete mode 100644 browser-extension/src/background/vault-sync.js create mode 100644 browser-extension/src/browser-action.ts delete mode 100644 browser-extension/src/content/content-script.js delete mode 100644 browser-extension/src/content/inpage-shim.js delete mode 100644 browser-extension/src/content/otp-watch.js delete mode 100644 browser-extension/src/content/password-suggest.js delete mode 100644 browser-extension/src/content/save-prompt.js delete mode 100644 browser-extension/src/crypto/index.js delete mode 100644 browser-extension/src/lib/api.js delete mode 100644 browser-extension/src/lib/argon2-wasm.js delete mode 100644 browser-extension/src/lib/capture.js delete mode 100644 browser-extension/src/lib/deviceApproval.js delete mode 100644 browser-extension/src/lib/extension-settings.js delete mode 100644 browser-extension/src/lib/field-detect.js delete mode 100644 browser-extension/src/lib/fillScope.js delete mode 100644 browser-extension/src/lib/folder-rules.js delete mode 100644 browser-extension/src/lib/generator-state.js delete mode 100644 browser-extension/src/lib/item-form.js delete mode 100644 browser-extension/src/lib/match.js delete mode 100644 browser-extension/src/lib/pin-unlock.js delete mode 100644 browser-extension/src/lib/policy.js delete mode 100644 browser-extension/src/lib/send-form.js delete mode 100644 browser-extension/src/lib/server-url.js delete mode 100644 browser-extension/src/lib/totp-service.js delete mode 100644 browser-extension/src/lib/usage.js delete mode 100644 browser-extension/src/lib/useOnly.js delete mode 100644 browser-extension/src/lib/vault-index.js delete mode 100644 browser-extension/src/lib/vault.js delete mode 100644 browser-extension/src/lib/version.js create mode 100644 browser-extension/src/messages.ts delete mode 100644 browser-extension/src/offscreen/offscreen.html delete mode 100644 browser-extension/src/offscreen/offscreen.js delete mode 100644 browser-extension/src/passkey/cbor.js delete mode 100644 browser-extension/src/passkey/consent.html delete mode 100644 browser-extension/src/passkey/consent.js delete mode 100644 browser-extension/src/passkey/orchestrator.js delete mode 100644 browser-extension/src/passkey/rp.js delete mode 100644 browser-extension/src/passkey/vault-passkey.js delete mode 100644 browser-extension/src/passkey/webauthn.js delete mode 100644 browser-extension/src/popup/clipboard.js delete mode 100644 browser-extension/src/popup/folder-view.js delete mode 100644 browser-extension/src/popup/generator-view.js delete mode 100644 browser-extension/src/popup/item-detail.js delete mode 100644 browser-extension/src/popup/popup.css delete mode 100644 browser-extension/src/popup/popup.html delete mode 100644 browser-extension/src/popup/popup.js delete mode 100644 browser-extension/src/popup/send-view.js delete mode 100644 browser-extension/src/popup/vault-view.js delete mode 100644 browser-extension/src/popup/views.js delete mode 100644 browser-extension/src/totp/index.js delete mode 100644 browser-extension/src/unlock/ceremony.js delete mode 100644 browser-extension/src/unlock/unlock.html delete mode 100644 browser-extension/src/unlock/unlock.js create mode 100644 browser-extension/tsconfig.json create mode 100644 browser-extension/web-ext.config.example.ts create mode 100644 browser-extension/wxt.config.ts diff --git a/browser-extension/.claude/commands/opsx/apply.md b/browser-extension/.claude/commands/opsx/apply.md new file mode 100644 index 000000000..f540757f3 --- /dev/null +++ b/browser-extension/.claude/commands/opsx/apply.md @@ -0,0 +1,155 @@ +--- +name: "OPSX: Apply" +description: Implement tasks from an OpenSpec change (Experimental) +category: Workflow +tags: [workflow, artifacts, experimental] +--- + +Implement tasks from an OpenSpec change. + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. + +**Steps** + +1. **Select the change** + + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select + + Always announce: "Using change: " and how to override (e.g., `/opsx:apply `). + +2. **Check status to understand the schema** + ```bash + openspec status --change "" --json + ``` + Parse the JSON to understand: + - `schemaName`: The workflow being used (e.g., "spec-driven") + - `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints + - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others) + +3. **Get apply instructions** + + ```bash + openspec instructions apply --change "" --json + ``` + + This returns: + - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema) + - Progress (total, complete, remaining) + - Task list with status + - Dynamic instruction based on current state + + **Handle states:** + - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue` + - If `state: "all_done"`: congratulate, suggest archive + - Otherwise: proceed to implementation + +4. **Read context files** + + Read every file path listed under `contextFiles` from the apply instructions output. + The files depend on the schema being used: + - **spec-driven**: proposal, specs, design, tasks + - Other schemas: follow the contextFiles from CLI output + +5. **Show current progress** + + Display: + - Schema being used + - Progress: "N/M tasks complete" + - Remaining tasks overview + - Dynamic instruction from CLI + +6. **Implement tasks (loop until done or blocked)** + + For each pending task: + - Show which task is being worked on + - Make the code changes required + - Keep changes minimal and focused + - Mark task complete in the tasks file: `- [ ]` → `- [x]` + - Continue to next task + + **Pause if:** + - Task is unclear → ask for clarification + - Implementation reveals a design issue → suggest updating artifacts + - Error or blocker encountered → report and wait for guidance + - User interrupts + +7. **On completion or pause, show status** + + Display: + - Tasks completed this session + - Overall progress: "N/M tasks complete" + - If all done: suggest archive + - If paused: explain why and wait for guidance + +**Output During Implementation** + +``` +## Implementing: (schema: ) + +Working on task 3/7: +[...implementation happening...] +✓ Task complete + +Working on task 4/7: +[...implementation happening...] +✓ Task complete +``` + +**Output On Completion** + +``` +## Implementation Complete + +**Change:** +**Schema:** +**Progress:** 7/7 tasks complete ✓ + +### Completed This Session +- [x] Task 1 +- [x] Task 2 +... + +All tasks complete! You can archive this change with `/opsx:archive`. +``` + +**Output On Pause (Issue Encountered)** + +``` +## Implementation Paused + +**Change:** +**Schema:** +**Progress:** 4/7 tasks complete + +### Issue Encountered + + +**Options:** +1.