Skip to content

[PM-38443] feat: Add FillAssistRepository and sync integration#2812

Merged
andrebispo5 merged 27 commits into
mainfrom
pm-38443/consume-fill-assist-data-and-cache
Jul 21, 2026
Merged

[PM-38443] feat: Add FillAssistRepository and sync integration#2812
andrebispo5 merged 27 commits into
mainfrom
pm-38443/consume-fill-assist-data-and-cache

Conversation

@andrebispo5

@andrebispo5 andrebispo5 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🗒️ Note

To avoid further conflicts, this PR contains the following stacked approved PRs:
#2876
#2882

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-38443

📔 Objective

Adds FillAssistRepository (protocol + DefaultFillAssistRepository) which orchestrates the full fill-assist sync pipeline: feature flag check, 6-hour interval guard, manifest fetch, cid/sourceUrl cache comparison, forms download, schema version validation, CSS selector parsing, and per-account storage. Wires the repository into SyncService (non-blocking) and ServiceContainer.

Also fixes APIService.fillAssistService to use a plain HTTPService(client: URLSession.shared) — the standard CertificateHTTPClient deliberately blocks 302 redirects for SSO, which broke the GitHub Releases CDN redirect chain.

@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context t:feature labels Jun 19, 2026
@andrebispo5
andrebispo5 marked this pull request as ready for review June 19, 2026 17:29
@andrebispo5
andrebispo5 requested review from a team and matt-livefront as code owners June 19, 2026 17:29
Copilot AI review requested due to automatic review settings June 19, 2026 17:29
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

Reviewed the FillAssistRepository addition, its SyncService/ServiceContainer wiring, the logoutAccount cache-clearing relocation into AuthRepository.logout, the APIService.fillAssistService redirect fix, the new per-user fillAssistEnabled state, and the DataFingerprintService integrity check. The architecture follows established repository/DI conventions and test coverage is broad. One correctness regression was found on the account-deletion path.

Code Review Details
  • ⚠️ : Fill-assist cache clearing was moved out of logoutAccount into AuthRepository.logout, but AuthRepository.deleteAccount does not call clearRules, so the delete-account flow now leaves the cached data, timestamp, and Keychain fingerprint behind.
    • BitwardenShared/Core/Platform/Services/StateService.swift:1951

Notes on existing reviewer threads (no new action from me):

  • The syncRules() await ahead of the main vault sync in fetchSync (already raised by another reviewer) remains a blocking call on an external CDN; the author appears to have retained it deliberately, and it swallows its own errors as non-fatal.
  • The per-user fillAssistEnabled preference intentionally survives logout, consistent with the biometric-preference pattern (AuthRepositoryTests regression test for PM-17704), so it is not flagged.

Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new Fill-Assist sync orchestration layer (FillAssistRepository + DefaultFillAssistRepository) and wires it into the app’s dependency graph and vault sync flow so Fill-Assist rules can be fetched, validated, cached per-account, and consulted by hostname.

Changes:

  • Added FillAssistRepository / DefaultFillAssistRepository implementing the fill-assist sync + caching pipeline.
  • Integrated the repository into ServiceContainer/Services and injected it into DefaultSyncService.
  • Updated mocks/tests wiring to accommodate the new dependency.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
BitwardenShared/Core/Vault/Services/SyncServiceTests.swift Injects a MockFillAssistRepository into DefaultSyncService test setup.
BitwardenShared/Core/Vault/Services/SyncService.swift Calls fill-assist sync during vault sync completion path.
BitwardenShared/Core/Platform/Services/TestHelpers/ServiceContainer+Mocks.swift Adds fillAssistRepository to mock container factory.
BitwardenShared/Core/Platform/Services/Services.swift Adds HasFillAssistRepository to the Services composition.
BitwardenShared/Core/Platform/Services/ServiceContainer.swift Instantiates DefaultFillAssistRepository and injects it into DefaultSyncService and container.
BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift New repository implementing manifest fetch, schema check, integrity check, parsing, and per-account caching.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +482 to +484
// Sync fill-assist rules alongside vault data. Failures must not block vault sync.
await fillAssistRepository.syncFillAssistRules()

Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
Comment on lines +33 to +37
typealias Services = HasConfigService
& HasEnvironmentService
& HasFillAssistAPIService
& HasStateService

Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.53377% with 191 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.88%. Comparing base (7ecd504) to head (49ad11a).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...Menu/AddFillAssistRule/AddFillAssistRuleView.swift 0.00% 47 Missing ⚠️
...enShared/Core/Platform/Services/StateService.swift 24.13% 44 Missing ⚠️
...AddFillAssistRule/AddFillAssistRuleProcessor.swift 0.00% 42 Missing ⚠️
...form/Settings/Settings/AutoFill/AutoFillView.swift 4.16% 23 Missing ⚠️
...Kit/UI/Platform/DebugMenu/DebugMenuProcessor.swift 15.38% 11 Missing ⚠️
...t/UI/Platform/DebugMenu/DebugMenuCoordinator.swift 16.66% 10 Missing ⚠️
...e/Autofill/Repositories/FillAssistRepository.swift 95.74% 6 Missing ⚠️
...enu/AddFillAssistRule/AddFillAssistRuleState.swift 0.00% 3 Missing ⚠️
...orShared/Core/Platform/Services/StateService.swift 0.00% 2 Missing ⚠️
...Shared/Core/Auth/Services/KeychainRepository.swift 50.00% 2 Missing ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2812      +/-   ##
==========================================
- Coverage   81.26%   78.88%   -2.38%     
==========================================
  Files        1028     1158     +130     
  Lines       66273    74157    +7884     
==========================================
+ Hits        53858    58502    +4644     
- Misses      12415    15655    +3240     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@andrebispo5
andrebispo5 marked this pull request as draft June 23, 2026 15:12
@andrebispo5
andrebispo5 marked this pull request as ready for review June 25, 2026 17:28
Comment on lines +9 to +22
/// Fetches and caches fill-assist rules for the active account.
///
func syncFillAssistRules() async

/// Returns the cached fill-assist rules for a given hostname, or `nil` if unavailable.
///
/// - Parameter hostname: The hostname to look up.
/// - Returns: The cached `FillAssistHostRules`, or `nil`.
///
func fillAssistRules(for hostname: String) async -> FillAssistHostRules?

/// Clears all cached fill-assist data for the active account.
///
func clearFillAssistRules() async throws

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⛏️ Given that this is the "Fill Assist" repository, I think we can remove the use of "FillAssist" in the function names as it's a bit repetitive IMO.

Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
let sourceUrl = environmentService.fillAssistRulesURL
let userId = try await stateService.getActiveAccountId()
let lastFetch = appSettingsStore.fillAssistLastFetchTimestamp(userId: userId)
if let lastFetch, Date().timeIntervalSince(lastFetch) < Constants.fillAssistUpdateInterval {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🎨 Use TimeProvider instead of Date() so you can better mock the date on the tests.
This applies to all Date() in this file.

Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
Comment thread BitwardenShared/Core/Autofill/Repositories/FillAssistRepository.swift Outdated
)
}

// Sync fill-assist rules alongside vault data. Failures must not block vault sync.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⛏️ I'd remove "Failures must not block vault sync." as the function doesn't throw any errors so I think it's a direct assumption.

}

// Sync fill-assist rules alongside vault data. Failures must not block vault sync.
await fillAssistRepository.syncFillAssistRules()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⚠️ Add tests for this.

Comment on lines +227 to +283
private func makeManifest(cid: String) throws -> FillAssistManifestResponseModel {
let json = """
{
"buildId": "v1",
"gitSha": "abc",
"maps": { "forms": { "v1": {
"cid": "\(cid)",
"filename": "forms.v1.json",
"schema": "forms.v1.schema.json"
}}},
"timestamp": "2026-06-11T14:29:32.184Z"
}
"""
return try JSONDecoder.pascalOrSnakeCaseDecoder.decode(
FillAssistManifestResponseModel.self,
from: Data(json.utf8),
)
}

private func makeFormsMapWithPathnames() throws -> FormsMapResponseModel {
let json = """
{
"schemaVersion": "1.0.0",
"hosts": {
"example.com": {
"forms": [{ "category": "account-login", "fields": { "username": ["input#user1"] } }],
"pathnames": {
"/login": {
"forms": [{ "category": "account-login", "fields": { "username": ["input#user2"] } }]
}
}
}
}
}
"""
return try JSONDecoder.pascalOrSnakeCaseDecoder.decode(
FormsMapResponseModel.self,
from: Data(json.utf8),
)
}

private func makeFormsMap(schemaVersion: String = "1.0.0") throws -> FormsMapResponseModel {
let json = """
{
"schemaVersion": "\(schemaVersion)",
"hosts": {
"example.com": {
"forms": [{ "category": "account-login", "fields": { "username": ["input#user"] } }]
}
}
}
"""
return try JSONDecoder.pascalOrSnakeCaseDecoder.decode(
FormsMapResponseModel.self,
from: Data(json.utf8),
)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤔 Can't we directly initialize the structs instead of using JSON decoding?

Base automatically changed from pm-38443/fill-assist-storage to main June 29, 2026 17:13
…epositoryTests

- Add @mainactor so featureFlagsBool mutations compile in Swift Testing
- Move helper methods to a private extension to satisfy type_body_length
…t fillAssistClient for tests

- Filter pooled entries with empty attribute arrays before writing host rules,
  so hosts with only unsupported selectors (e.g. shadow DOM) are correctly excluded
- Add fillAssistClient parameter to APIService.init (defaults to URLSession.shared)
  so FillAssistAPIServiceTests can inject MockHTTPClient for download calls
@andrebispo5
andrebispo5 force-pushed the pm-38443/consume-fill-assist-data-and-cache branch from 6b5d552 to 4d9842c Compare June 29, 2026 17:15
@andrebispo5
andrebispo5 requested a review from fedemkr June 29, 2026 20:21
…uct initialization

Add memberwise inits to FormsMapResponseModel and FormsMapHostEntry so
test helpers can construct fixtures without JSON string construction.
Comment thread BitwardenShared/Core/Autofill/Models/FormsMapResponseModel.swift
Comment thread BitwardenShared/Core/Autofill/Models/FormsMapResponseModel.swift
Comment on lines +294 to +352
func makeManifest(cid: String) -> FillAssistManifestResponseModel {
let entry = FillAssistManifestEntryModel(
cid: cid,
deprecated: false,
filename: "forms.v1.json",
schema: "forms.v1.schema.json",
)
return FillAssistManifestResponseModel(
buildId: "v1",
gitSha: "abc",
maps: ["forms": ["v1": entry]],
timestamp: Date(timeIntervalSinceReferenceDate: 0),
)
}

private func makeFormsMap(
schemaVersion: String = "1.0.0",
usernameSelector: String = "input#user",
hosts: [String: String]? = nil,
) -> FormsMapResponseModel {
let content = { (selector: String) in
FormsMapContent(
category: "account-login",
container: nil,
fields: ["username": [.single(selector)]],
actions: nil,
)
}
let resolvedHosts: [String: FormsMapHostEntry] = if let hosts {
hosts.mapValues { FormsMapHostEntry(forms: [content($0)]) }
} else {
["example.com": FormsMapHostEntry(forms: [content(usernameSelector)])]
}
return FormsMapResponseModel(hosts: resolvedHosts, schemaVersion: schemaVersion)
}

private func makeFormsMapWithPathnames() -> FormsMapResponseModel {
let loginContent = FormsMapContent(
category: "account-login",
container: nil,
fields: ["username": [.single("input#user1")]],
actions: nil,
)
let pathnameContent = FormsMapContent(
category: "account-login",
container: nil,
fields: ["username": [.single("input#user2")]],
actions: nil,
)
return FormsMapResponseModel(
hosts: [
"example.com": FormsMapHostEntry(
forms: [loginContent],
pathnames: ["/login": FormsMapPathnameEntry(forms: [pathnameContent])],
),
],
schemaVersion: "1.0.0",
)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⛏️ Add function docs.

@github-actions github-actions Bot added the app:authenticator Bitwarden Authenticator app context label Jul 2, 2026
@andrebispo5
andrebispo5 requested a review from fedemkr July 2, 2026 10:45
andrebispo5 and others added 12 commits July 10, 2026 10:06
…c-skip behavior

syncRules_withinUpdateInterval now also stubs cached data to match the
updated skip condition, and a new test covers a fresh timestamp with
no readable cache. Fixture builders moved to a separate file to stay
under the file length limit.
Adds setFillAssistEnabledError to MockStateService and a test verifying
AutoFillProcessor reverts the toggle and shows an alert when persisting
the preference fails.
…uests

The tests still mocked the old download(filename:)-based HTTPClient
API, but the production code was migrated to fillAssistService.send(_:)
in an earlier commit. Update the mock setup and assertions to match.
The reprompt handler only proceeded when the app extension's canAutofill
was true, blocking Fill Assist-driven autofill from the main app context.
fedemkr
fedemkr previously approved these changes Jul 16, 2026

@fedemkr fedemkr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good, approving as forceSync should be handled in a different PR.

cached != nil,
timeProvider.presentTime.timeIntervalSince(lastFetch) < Constants.FillAssist.updateInterval {
return
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤔 I believe we should consider checking forceSync here to bypass this check. Otherwise users will need to wait up to 6 hours when a change is done in map-the-web.
I think it can be done in a different PR so not to keep adding stuff to this one.

fedemkr
fedemkr previously approved these changes Jul 20, 2026
…e flag

The autofill-capability bypass only checked the persisted user toggle,
not the live fillAssistTargetingRules flag, so disabling the flag after
a user had opted in wouldn't revert the bypass. Also strengthens the
flagOff test to actually exercise canAutofill == false.
Copilot AI review requested due to automatic review settings July 21, 2026 14:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 54 out of 54 changed files in this pull request and generated 2 comments.

Comment on lines +26 to +30
public func fingerprint(for data: Encodable) throws -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
return try encoder.encode(data).generatedHash(using: SHA256.self)
}
Comment on lines 1948 to 1952
appSettingsStore.setDefaultUriMatchType(nil, userId: knownUserId)
appSettingsStore.setDisableAutoTotpCopy(nil, userId: knownUserId)
appSettingsStore.setEncryptedUserKey(key: nil, userId: knownUserId)
appSettingsStore.setFillAssistCachedData(nil, userId: knownUserId)
appSettingsStore.setFillAssistLastFetchTimestamp(nil, userId: knownUserId)
appSettingsStore.setHasPerformedSyncAfterLogin(nil, userId: knownUserId)
appSettingsStore.setLastSyncTime(nil, userId: knownUserId)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@fedemkr fedemkr added the ai-review Request a Claude code review label Jul 21, 2026
appSettingsStore.setEncryptedUserKey(key: nil, userId: knownUserId)
appSettingsStore.setFillAssistCachedData(nil, userId: knownUserId)
appSettingsStore.setFillAssistLastFetchTimestamp(nil, userId: knownUserId)
appSettingsStore.setHasPerformedSyncAfterLogin(nil, userId: knownUserId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: Removing fill-assist clearing from logoutAccount leaves cache behind on account deletion.

Details and fix

This PR removed setFillAssistCachedData(nil) and setFillAssistLastFetchTimestamp(nil) from logoutAccount and relocated that clearing into AuthRepository.logout (via fillAssistRepository.clearRules). However, AuthRepository.deleteAccount does not call clearRules — it goes deleteAccountstateService.deleteAccount()logoutAccount(userInitiated: true).

Before this change, logoutAccount cleared the fill-assist cache for both the logout and the delete-account flows. Now the delete-account flow leaves fillAssistCachedData_<userId>, fillAssistLastFetchTimestamp_<userId>, and the Keychain fingerprint (fillAssistRulesFingerprint_<userId>) behind after the account is deleted.

Fix: call fillAssistRepository.clearRules(userId:) in AuthRepository.deleteAccount as well (before stateService.deleteAccount()), mirroring logout.

AuthRepositoryTests.test_deleteAccount has no clearRulesCalled assertion, so this gap is currently untested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

appSettingsStore.setEncryptedUserKey(key: nil, userId: knownUserId)
appSettingsStore.setFillAssistCachedData(nil, userId: knownUserId)
appSettingsStore.setFillAssistLastFetchTimestamp(nil, userId: knownUserId)
appSettingsStore.setHasPerformedSyncAfterLogin(nil, userId: knownUserId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: Removing fill-assist clearing from logoutAccount leaves cache behind on account deletion.

Details and fix

This PR removed setFillAssistCachedData(nil) and setFillAssistLastFetchTimestamp(nil) from logoutAccount and relocated that clearing into AuthRepository.logout (via fillAssistRepository.clearRules). However, AuthRepository.deleteAccount does not call clearRules — it goes deleteAccountstateService.deleteAccount()logoutAccount(userInitiated: true).

Before this change, logoutAccount cleared the fill-assist cache for both the logout and the delete-account flows. Now the delete-account flow leaves fillAssistCachedData_<userId>, fillAssistLastFetchTimestamp_<userId>, and the Keychain fingerprint (fillAssistRulesFingerprint_<userId>) behind after the account is deleted.

Fix: call fillAssistRepository.clearRules(userId:) in AuthRepository.deleteAccount as well (before stateService.deleteAccount()), mirroring logout.

AuthRepositoryTests.test_deleteAccount has no clearRulesCalled assertion, so this gap is currently untested.

@andrebispo5
andrebispo5 merged commit 87e2fd9 into main Jul 21, 2026
50 checks passed
@andrebispo5
andrebispo5 deleted the pm-38443/consume-fill-assist-data-and-cache branch July 21, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review app:authenticator Bitwarden Authenticator app context app:password-manager Bitwarden Password Manager app context t:feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants