Skip to content

Keeper: Nested Shared Folder support and add-account toggles#682

Open
pkamble-ks wants to merge 11 commits into
gnachman:masterfrom
pkamble-ks:master
Open

Keeper: Nested Shared Folder support and add-account toggles#682
pkamble-ks wants to merge 11 commits into
gnachman:masterfrom
pkamble-ks:master

Conversation

@pkamble-ks

@pkamble-ks pkamble-ks commented Jun 11, 2026

Copy link
Copy Markdown

Summary

Extends the Keeper Security password manager integration to support Nested Shared Folders (NSF) alongside the classic vault.

  • Listing: Merges classic vault records (list --format=json) with NSF records (nsf-list --records --format=json). Entries are labeled (Classic) or (Nested).
  • Mutations: Add, edit password, and delete route to the correct Commander verbs based on record type (record-* / rm vs nsf-record-* / nsf-rm). Account IDs use internal classic: / nested: prefixes so routing survives the host protocol; Commander still receives plain UIDs.
  • Add account: New records default to Nested Shared Folders. A Use classic permission model checkbox (declared in the adapter handshake) adds to the classic vault when checked.
  • Protocol: Adapters can declare optional addAccountToggles in the handshake; the Password Manager Add Account panel renders them as checkboxes and passes flags on add.
  • Reliability: Login validates with a lightweight list call instead of blocking on sync-down. Connectivity and HTML proxy errors surface actionable messages.

Commander Service Mode must allow: list, get, sync-down, record-add, record-update, rm, nsf-list, nsf-record-add, nsf-record-update, nsf-rm.

Test plan

  • cd pwmplugin && swift test --filter KeeperIntegrationTests — 21/21 passed
  • Manual: list classic and nested records; confirm (Classic) / (Nested) labels
  • Manual: fill password and enter username for both record types
  • Manual: edit password on classic and nested records
  • Manual: add record with toggle off (nested, default) and on (classic)
  • Manual: delete classic and nested records; confirm list refreshes after Sync Down
  • Manual: login with unreachable URL or bad API key — confirm actionable error message

Keeper: Nested Share Subfolder support and add-account toggles
List classic vault and Nested Shared Folder records together, route
add/edit/delete to the correct Commander verbs, and expose optional
checkboxes on the Add Account panel. Also improve login validation
and connectivity error messages.
Keeper: add Nested Shared Folder support and add-account toggles.
@pkamble-ks pkamble-ks changed the title Keeper: Nested Share Subfolder support and add-account toggles Keeper: Nested Shared Folder support and add-account toggles Jun 22, 2026

@gnachman gnachman left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — nested-folder support is a nice addition. A few things to look at, mostly on the iTerm2 host-side integration (the password-manager UI and credential plumbing shared with the other adapters). Inline comments below.

<windowStyleMask key="styleMask" titled="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="344" y="373" width="330" height="136"/>
<rect key="contentRect" x="344" y="373" width="330" height="196"/>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The New Account panel height is bumped from 136 to 196 for all password managers, but configureFirstAddAccountToggle only toggles hidden on the checkbox/label — nothing shrinks the panel or repositions the OK/Cancel buttons when there's no toggle. Every adapter that doesn't declare an add-account toggle (Keychain, 1Password, Bitwarden, LastPass, KeePassXC) will now show ~60px of empty space between the Password field and the buttons. Could the panel resize (or the controls collapse) when firstAddAccountToggleDescription is nil so the non-toggle case looks the same as before?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Reverted the xib default to 136px and only expand the panel to 196px in configureFirstAddAccountToggle when the adapter declares a toggle. Adapters without a toggle (Bitwarden, KeePassXC, etc.) should look the same as before.

guard let field = fields.first(where: { $0.key == key }) else { return nil }
if field.persistInKeychain {
return try? SSKeychain.password(forService: keychainCredentialServiceName, account: key)
if keychainSettingsCacheCheckedKeys.contains(key) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two concerns with this keychain cache:

  1. It's never invalidated across instances. PasswordManagerDataSourceProvider.forTerminal and .forBrowser each hold their own process-lifetime _keeper, but they share one keychain entry. invalidateKeychainSettingsCache only runs on the instance that did the write (via storeSettingsValue), so if the API key is updated on one side, the other keeps serving the stale value for the rest of the session.
  2. Misses are cached too (the key goes into keychainSettingsCacheCheckedKeys even when the value is nil), so a key read before it's populated is pinned to nil on that instance.

Net effect is a stale/empty credential that survives until the app restarts. Was there a measured cost to the uncached SSKeychain.password reads that motivated the cache? If so it may be worth keying invalidation off the actual keychain rather than only local writes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the keychain settings cache entirely. storedSettingsValue reads from the keychain directly again, so terminal/browser instances and keychain updates no longer go stale for the rest of the session.

masterPassword = loadPersistedCredentials()
guard masterPassword == nil else { return }

if let secretFromSettings = persistedSecretSettingsValue(),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes credential precedence: persistedSecretSettingsValue() now wins over loadPersistedCredentials(). They read different keychain entries — the secret settings field is stored under account: <fieldKey> (e.g. apiKey), whereas a successful login persists the working credential under account: identifier. So if the value typed into the settings field ever diverges from the credential that actually logged in, the (possibly stale) settings value now shadows the known-good one and auto-login can fail where it previously succeeded. Is the precedence intentional? Falling back to loadPersistedCredentials() first (or only using the settings value when there's no persisted credential) would preserve the old behavior.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unintentional regression — fixed.
loadPersistedCredentials() (login credential under identifier) is tried first; the settings keychain value is only used as a fallback when there is no persisted login credential.

KeeperAdapterLog.write("setPassword: success uid=\(uid) via \(verb)")
return
}
lastResponse = result.raw

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the .none source case (any accountID without a classic:/nested: prefix — i.e. records already stored by existing users before this change), setPassword/deleteRecord try the first verb then the second, and lastResponse is overwritten unconditionally. So when the first verb fails for a real reason (e.g. a genuine permission error on a classic record), the code then runs the nsf verb on the same UID and the user is shown the nsf error ("not found in any nested shared folder") instead of the real one. Consider preserving the first meaningful error, or only falling through to the second verb when the first failure looks like a wrong-record-type/not-found signal.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. For the .none source case, the second verb is only tried when the first failure looks like not-found / wrong record type (keeperCommandFailureLooksLikeWrongRecordType). Otherwise the first error is surfaced to the user.

let response = try JSONDecoder().decode(RecordAddResponse.self, from: data)
guard response.status == "success", let uid = response.data?.record_uid, !uid.isEmpty else {
let extractedUid: String? = response.data?.effectiveUid
?? response.message?.trimmingCharacters(in: .whitespacesAndNewlines)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addRecord falls back to using the response message field as the record UID, and unlike the get/set/delete paths it doesn't run the result through validatedRecordUID. If a Commander response ever puts something other than a bare UID in message, the returned identifier becomes nested:<that text> and later get/set/delete on it will fail validation (the add appears to succeed but the record can't be operated on). Could this parse the UID only from a structured field and validate it before returning?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The returned UID must pass validatedRecordUID. Structured data.record_uid / data.uid is preferred; message is only used as a fallback when it validates as a bare UID.

let uid = try validatedRecordUID(parsed.uid)

func attempt(_ verb: String) throws -> (success: Bool, raw: Data) {
let cmd = "\(verb) --force -r \(uid) \(passwordCommandFragment(password: newPassword))"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming --force is intentional/accepted on record-update/nsf-record-update (and record-add/nsf-record-add at line 791)? The test mock matches commands by prefix, so the flag isn't actually exercised — worth a note in the PR that it's been confirmed against a live Commander.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, intentional. Confirmed against live Commander Service Mode: classic record-add failed without --force under password policy, and updates needed it as well. The integration mock matches commands by prefix so it does not assert the flag explicitly.

}()

static let isEnabled: Bool = {
ProcessInfo.processInfo.environment["ITERM2_KEEPER_ADAPTER_LOG_DISABLED"] == nil

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logger is enabled by default and writes to a persistent file at ~/Library/Logs/iterm2-keeper-adapter.log. Passwords are redacted, but account titles and usernames are written in plaintext. For a password-manager integration it'd be safer to default this off (opt-in via the env var) rather than opt-out.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to opt-in: logging is off by default and enabled with ITERM2_KEEPER_ADAPTER_LOG_ENABLED=1.

Comment thread docs/notes-3.7.txt Outdated
optional toggles in their handshake that the
Add Account panel renders as checkboxes. The
Keeper adapter uses this to offer a “Use
classic permission model” choice — leaving

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this line uses an em dash, which the project style avoids in user-facing text. Could swap it for " - " or reword.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — replaced the em dash with a period per project style.

Address PR review: panel sizing, credentials, fallback errors, opt-in logging
@pkamble-ks

Copy link
Copy Markdown
Author

@gnachman Thanks for the review, pushed fixes addressing all inline comments (commit ae7e41d, now on this branch via fork merge).

Summary:

  • New Account panel: default 136px, expands to 196px only when adapter declares a toggle
  • Removed keychain settings cache; restored login-credential precedence
  • Unprefixed account IDs: second verb only on not-found/wrong-type errors
  • addRecord validates UID before returning
  • Debug logging opt-in via ITERM2_KEEPER_ADAPTER_LOG_ENABLED=1
  • Release notes punctuation fix
  • --force on add/update confirmed against live Commander Service Mode

Tests still 21/21. Happy to adjust anything else.

@gnachman

gnachman commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Review notes

Reviewed the diff plus the PR head blob (pkamble-ks/iTerm2@63cda37) to check the credential path. Findings below, most severe first. Several correctness items hinge on real Keeper Commander output shapes that the mock server encodes as assumptions, so they are worth confirming against a live Commander before merge.

Correctness

  1. Editing the API key in Settings is silently ignored; the stale key is used forever. AdapterPasswordDataSource.setSettingsValue nils masterPassword/authToken when a settings field changes but never calls deletePersistedCredentials(). The old key was persisted under keychain account = identifier at login, while the new key is stored under account = apiKey. On the next auth, hydratePersistedCredentialsIfNeeded sees masterPassword == nil, loadPersistedCredentials() returns the old key and returns early, so persistedSecretSettingsValue() (the new key) is never consulted. After a user updates a rejected or rotated key, the adapter keeps authenticating with the old one.

  2. An NSF record that also appears in list can become permanently uneditable. In listAccountsRecords, the merge inserts the list copy first and skips the nsf-list copy for the same UID, so the record is labeled classic: (the mock has list returning record_category:"Classic" even for shared:true rows). For a known source, setPassword/deleteRecord use a single verb with no cross-type fallback (.classic -> ["record-update"]). If that record is only mutable via nsf-record-update/nsf-rm, every edit and delete fails with no recovery.

  3. NSF folders can leak into the account list as bogus credentials. taggedAsNested rebuilds each record through an init that hardcodes itemType = nil, discarding the "Item Type" signal. isFolder uses itemType == "folder" as one of its two folder tests, so an nsf-list --records folder row identified only by "Item Type":"Folder" (no lowercase "Type", so effectiveType is nil) flips to isFolder == false after the rebuild and is emitted as an account with an empty username.

  4. A successful nested add can report "Add failed". The nested path reads the new UID from response.message and runs the entire string through validatedRecordUID (^[A-Za-z0-9_-]{15,}$). The mock returns a bare UID, but if real nsf-record-add returns a sentence (e.g. "Record NSFUID... added"), the whole-string regex rejects it and the else branch throws "Add failed" even though the record was created, orphaning it and producing a duplicate on retry.

  5. Legacy (unprefixed) records rely on a fragile English-substring heuristic to route. For source == .none (pre-existing bare UIDs), the classic -> nested fallback only fires when the failure text matches a fixed list ("not found", "nested shared folder", ...). A nested record whose record-update rejection is phrased differently (e.g. "permission denied", or a localized message) fails keeperCommandFailureLooksLikeWrongRecordType, breaks the loop, and never tries nsf-record-update, so the operation permanently fails.

Cleanup / efficiency

  1. setPassword and deleteRecord duplicate ~55 lines of verb-fallback machinery (the attempt closure, the per-source verb array built by an identical switch parsed.source, the index == 0 && count > 1 wrong-type continue, and the lastResponse/lastNetworkError unwind). They already differ subtly (one surfaces keeperUserFacingPasswordUpdateError, the other the raw string). Extracting one shared fallback runner would also let items 2 and 5 be fixed in a single place.

  2. Three sequential blocking round-trips per list refresh. Every refresh runs sync-down, then list --format=json, then nsf-list --records --format=json as separate POST-then-poll cycles (they cannot overlap), where the old code used a single ls -R -l. This triples worst-case latency and the HTTP-429 exposure that the deleted comment ("Do not call get per record ... Commander Service Mode often returns HTTP 429") specifically warned about.

Conventions

  1. Two CLAUDE.md violations:
    • IntegrationTests.swift: var env = ProcessInfo.processInfo.environment; process.environment = env is never mutated, which triggers the Swift "never mutated; consider let" warning ("treat warnings as errors").
    • Five new log strings use em dashes (setPassword: FAILED uid=... — ..., deleteRecord: FAILED ..., and three no stdin input — aborting lines), against the "no em dashes in strings ... shared with third parties" rule (debug logs are shareable).

Cleared

Release-notes line widths (all under 50), no fatalError/assert added, the HTML-detection helper is string matching (not the inline-HTML rule), and KeeperAdapterLog.swift is in the SwiftPM target dir so no xcodeproj step is needed.

Prefer nsf-list on duplicate UIDs, fix folder filtering and NSF add UID
parsing, dedupe mutation paths, drop auto sync-down on list, invalidate
login credentials on API key change, and fix Add Account panel layout.
Live Commander validation completed.
Address Keeper adapter and Add Account panel review feedback
@pkamble-ks

Copy link
Copy Markdown
Author

@gnachman Thanks for the detailed second pass — pushed fixes in b149796 (merged to master via PR #4). Summary by item:

Correctness

  • API key in Settings: setSettingsValue now calls deletePersistedCredentials() when a persistInKeychain settings field changes, so the old login keychain entry cannot override the updated API key.
  • Duplicate UID merge: nsf-list now wins when the same UID appears in both list and nsf-list, so records route as (Nested) / nested: instead of (Classic).
  • NSF folders in list: taggedAsNested preserves itemType via withRecordCategory(), so "Item Type":"Folder" rows stay filtered out.
  • NSF add “Add failed”: added extractRecordUID(from:) to pull the UID from sentence-style Commander message strings before validation.
  • Unprefixed ID fallback: replaced the English-substring heuristic with a shared runKeeperMutationAttempts helper; for source == .none, both verbs are tried and the first failure is surfaced if both fail.

Cleanup / efficiency

  • Deduped mutation paths: setPassword and deleteRecord share the same fallback runner.
  • List latency: removed automatic sync-down from the default list path (syncFirst defaults to false); Sync Down remains on the explicit custom command.

Conventions

  • var envlet env in IntegrationTests.swift.
  • Em dashes removed from new log strings in main.swift and KeeperCommanderClient.swift.

Add Account panel (from live testing)

  • Fixed layout after round 1 sizing: fields were clipped and the checkbox overlapped User Name when the sheet opened. Panel now uses explicit compact (136) vs expanded (196) field positions with fixed frames re-applied after beginSheet.

Tests & live validation

  • Integration tests now 23/23 (added duplicate-UID merge, NSF folder exclusion, and sentence-style NSF add message cases).
  • Validated against live Commander: list, add (classic + nested), set password, delete, Sync Down, and API key change.

Happy to adjust anything else.

@gnachman

Copy link
Copy Markdown
Owner

Thanks for the substantial work here. I went through it in depth. Most of my feedback is architectural, with a batch of concrete bugs at the end. The recurring theme is Keeper-specific concepts leaking into shared password-manager infrastructure, and I would like to see those pulled back before this is ready for line-by-line review.

1. The API key is modeled as both a master password and a settings field

The handshake sets requiresMasterPassword: true with masterPasswordLabel: "API key" AND declares an apiKey settings field (isSecret: true, persistInKeychain: true). So the same secret has two entry points and two keychain items (account = adapter identifier vs account = "apiKey"). Almost all of the credential complexity in this PR is downstream cleanup for that one decision:

  • persistedSecretSettingsValue() exists only to let the Settings copy seed masterPassword when the prompt copy is absent.
  • hydratePersistedCredentialsIfNeeded() prefers loadPersistedCredentials() (account = identifier) before the settings copy, so once you authenticate once, editing the API key in Settings silently has no effect (the two items diverge and the identifier-keyed copy wins).
  • The standardHeader change (persistInKeychain && credentialAlreadyAvailable -> omit) is another patch on the same overload.

masterPassword alone is sufficient: the prompt already collects the key, persistsCredentials: true stores it, and it re-hydrates on launch. Please pick one source of truth: keep the master-password prompt (labeled "API key") and drop the apiKey settings field, keeping only serviceURL. That deletes persistedSecretSettingsValue(), removes the divergence/staleness, and removes the need for the header omission. The only thing the settings field uniquely offered was editing the key later without an auth failure, which is better handled by a "clear credentials / re-enter" affordance than a duplicate secret.

If a future adapter genuinely needs a login-only settings secret, add an explicit omitIfAuthenticated: Bool (default false) to SettingsField and gate the header omission on that, rather than overloading persistInKeychain. Not needed for Keeper if the field is dropped.

2. Do not bake the (Classic)/(Nested) label into accountName

AdapterPasswordDataSource returns each list entry's accountName as displayTitleWithSource ("Example (Nested)"), but the add path returns the raw typed name. The host matches accounts by exact [accountName isEqualToString:], so this regresses generic host behavior:

  • After add, didAddAccount never finds the new row, so it is not selected.
  • The pre-add duplicate guard in reallyAdd never matches, so duplicate accounts can be created.

Please model record source as a first-class field on the account and let the host format the label. Keep accountName a stable identity.

3. Keep classic:/nested: routing internal to the adapter

The classic:/nested: account-id prefixes are threaded through the host's shared AccountIdentifier, and the classic-vs-nested split is re-parsed from a colon-prefixed string in several places rather than modeled. Even if it is meant to be opaque, this is Keeper routing living in shared types. Prefer keeping the routing fully internal to the adapter.

4. Add-account toggles and flags-add belong on the base data source, not AdapterCapabilities

Right now the flags-carrying add(...) and addAccountToggleDescriptions live on AdapterCapabilities, so iTermPasswordManagerWindowController has to conformsToProtocol:@protocol(iTermAdapterCapabilities) and downcast to render the toggles and again to call the flags-add (via respondsToSelector:, which silently falls back to the flags-less add and drops the user's toggle choice). Adding an account is a generic operation, so the generic Add Account UI should not know what an adapter is.

Please move addAccountToggleDescriptions and the flags-carrying add(...) onto the base PasswordManagerDataSource, with trivial defaults for sources that have no toggles (toggle descriptions -> nil, flags-add -> forward to the plain add ignoring flags). The controller then only ever talks to PasswordManagerDataSource, all the iTermAdapterCapabilities downcasting and the respondsToSelector: fallback in the add path go away, and the silent-drop bug goes with them. The adapter Settings-sheet capabilities (settingsValue, settingsFieldDescriptions, showAdapterSettingsSheet) are genuinely adapter-specific and can stay on AdapterCapabilities.

5. Correctness bugs

  1. Classic records from plain list have no category/source, so sourceLabel == nil, so they get a bare identifier and no "(Classic)" label. The same record added via the classic toggle gets classic:UID. The identifier churns across refreshes and the promised label never shows for classic records.
  2. Login validation switched to list under a roughly 20s budget: a slow/cold Commander or a large vault will report a login failure with valid credentials.
  3. The tryAll fallback does catch { networkError = error; break }, so it aborts on the first thrown error and never tries the alternate verb, defeating the classic/nested fallback for unprefixed ids.
  4. addRecord decodes data as a non-optional object; if nsf-record-add returns data as a JSON string (as the get paths show Commander sometimes does) it throws typeMismatch and reports a created record as a failure.
  5. The merge routes any UID present in both list and nsf-list to the nested verbs (nsf overwrites list). A genuinely classic record that also surfaces in nsf-list then gets nsf-record-update/nsf-rm, which Commander rejects.
  6. KeeperAdapterLog writes raw, unredacted Commander response bodies to a file in ~/Library/Logs (env-gated off by default, and command passwords are redacted). For a password manager, response bodies can contain secrets. Please reconsider logging response bodies at all.

6. Question: cache invalidation after set-password

invalidateListAccountsCache() is called after add, delete, and set-password. Add and delete clearly change the account set, but the cached list holds only Account(identifier, userName, accountName, hasOTP) (no password), and SetPasswordRequest carries only accountIdentifier and newPassword, so a password edit cannot change any cached field. Can you confirm the set-password invalidation is necessary and explain why? If not, dropping it avoids a full Commander round-trip (sync + list + nsf-list) after every password edit.

7. Cleanup

  • sourceLabel duplicates two near-identical record_category-vs-source switch blocks, and setPassword/deleteRecord duplicate the same three-case routing switch. Parameterize into one helper each.
  • configureNewAccountPanelFieldLayoutShowingToggle: is two parallel branches of about 18 absolute magic Y values kept in sync by hand. They are all derivable: each label = field + 3, the generate button = field - 3, each row steps by 27, and the two branches differ only by the starting Y (panelHeight - 34). It collapses to a single cascade (behavior-identical, verified against the current constants):
- (void)configureNewAccountPanelFieldLayoutShowingToggle:(BOOL)showingToggle {
    const CGFloat fieldHeight = 27;
    const CGFloat labelOffset = 3;
    const CGFloat topMargin = 34;
    const CGFloat toggleGap = 5;
    const CGFloat panelHeight = showingToggle ? kNewAccountPanelHeightWithToggle
                                              : kNewAccountPanelHeightWithoutToggle;

    CGFloat y = panelHeight - topMargin;
    iTermFixViewY(_newAccount, y);
    iTermFixViewY(_newAccountLabel, y + labelOffset);
    y -= fieldHeight;

    iTermFixViewY(_newUserName, y);
    iTermFixViewY(_newUserNameLabel, y + labelOffset);
    y -= fieldHeight;

    iTermFixViewY(_newAccountPassword, y);
    iTermFixViewY(_newPasswordLabel, y + labelOffset);
    iTermFixViewY(_generatePasswordButton, y - labelOffset);

    if (!showingToggle) {
        return;
    }
    y -= fieldHeight + toggleGap;
    iTermFixViewY(_addAccountToggleCheckbox, y);
    y -= fieldHeight;
    iTermFixViewY(_addAccountToggleLabel, y);
}

Drop Settings apiKey, expose sourceLabel for host formatting, move
add-account toggles onto PasswordManagerDataSource, and fix list
merge/timeout/fallback/logging issues from review.
Keeper: NSF support with adapter-local routing and host sourceLabel
@pkamble-ks

Copy link
Copy Markdown
Author

@gnachman Thanks for the thorough architectural review — addressed in 40885a1 (on this PR via cec0504).

1. API key as master password only

Dropped the Settings apiKey field. Handshake keeps requiresMasterPassword with masterPasswordLabel: "API key" and Settings declares only serviceURL. Removed persistedSecretSettingsValue() and the related header-omission path. To rotate a key: Reset Keeper Security Configuration, then authenticate again via the prompt.

2. Stable accountName + first-class source

Adapter returns bare titles; added optional sourceLabel on the protocol/PasswordManagerAccount. The host formats name (Classic) / name (Nested) for display. Add selection and the duplicate-name guard match on the stable accountName again.

3. Routing stays adapter-internal

Wire account IDs are bare UIDs (legacy classic:/nested: prefixes are still stripped if present). Classic vs nested verb choice is internal; set/delete always try classic then nested.

4. Toggles / flags-add on PasswordManagerDataSource

Moved addAccountToggleDescriptions and the flags-carrying add(...) onto PasswordManagerDataSource. The Add Account UI no longer downcasts to iTermAdapterCapabilities for those. Settings-sheet APIs remain on AdapterCapabilities.

5. Correctness

  1. Classic list rows get a Classic label via taggedAsClassic when category/source is missing.
  2. Login validation timeout raised to 120s.
  3. tryAll continues after thrown errors so the alternate verb still runs.
  4. addRecord accepts data as an object or a JSON string.
  5. Duplicate UIDs prefer the classic list row over nsf-list.
  6. Adapter logs response byte counts only — no raw bodies.

6. Set-password cache invalidation

Confirmed unnecessary and dropped. The list cache has no password fields, and set-password does not change identity/username/title, so invalidating forced a needless Commander round-trip.

7. Cleanup

Shared normalizedKeeperSourceLabel and a single runKeeperMutationAttempts helper for set/delete. Panel layout uses the Y-cascade you suggested.

Happy to adjust anything else before line-by-line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants