1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-25 00:53:22 +00:00

[WIP] Fix qualification pipeline, add debug capture, human-readable conditions

This commit is contained in:
Miles Blackwood
2026-02-19 16:48:36 -05:00
parent b11f111fd8
commit e92c9ba300
11 changed files with 545 additions and 129 deletions

View File

@@ -5,11 +5,13 @@ This document describes how to use the autofill debug mode for troubleshooting f
## Enabling Debug Mode
1. Set the dev flag in your `.env.development` file:
```
DEV_FLAGS={"autofillDebugMode": true}
```
2. Rebuild the browser extension:
```bash
npm run build:watch
```
@@ -19,35 +21,67 @@ This document describes how to use the autofill debug mode for troubleshooting f
## Using Debug Mode
When debug mode is enabled, the browser console will display:
```
[Bitwarden Debug] Autofill debug mode enabled. Use window.__BITWARDEN_AUTOFILL_DEBUG__
```
### Available Methods
### Right-Click Context Menu (Easiest Path)
With debug mode enabled, right-clicking any element on the page reveals a **[Debug] Copy autofill debug info** item in the Bitwarden context menu. Clicking it copies a plain-text summary of all field qualification decisions for the current page to your clipboard.
This summary includes:
- All fields that were evaluated (both qualified and rejected)
- Why each field passed or failed each condition
- Human-readable explanations and fix suggestions for failures
Paste the copied text directly into a support ticket or bug report. Field values are never captured.
### Console API
#### `exportSession(format?: 'json' | 'summary' | 'console')`
Exports the current debug session in the specified format.
```javascript
// Export as JSON (default)
const data = __BITWARDEN_AUTOFILL_DEBUG__.exportSession('json');
const data = __BITWARDEN_AUTOFILL_DEBUG__.exportSession("json");
console.log(JSON.parse(data));
// Export as human-readable summary
console.log(__BITWARDEN_AUTOFILL_DEBUG__.exportSession('summary'));
console.log(__BITWARDEN_AUTOFILL_DEBUG__.exportSession("summary"));
// Output to console with pretty formatting
__BITWARDEN_AUTOFILL_DEBUG__.exportSession('console');
__BITWARDEN_AUTOFILL_DEBUG__.exportSession("console");
```
#### `exportSummary()`
Returns a human-readable summary of the most recent session.
```javascript
console.log(__BITWARDEN_AUTOFILL_DEBUG__.exportSummary());
```
#### `startSession(name?: string)`
Starts a new debug session, optionally with a stable name for diffing.
Named sessions produce deterministic, timestamp-prefixed IDs — useful for comparing the same page across different deploys or configurations:
```javascript
// Named session: session_2026-02-19T12-00-00-000Z_login-form-test
__BITWARDEN_AUTOFILL_DEBUG__.startSession("login-form-test");
// Anonymous session: session_2026-02-19T12-00-00-000Z_ab3f2
__BITWARDEN_AUTOFILL_DEBUG__.startSession();
```
The ISO timestamp prefix means sessions sort naturally by date, enabling meaningful diffs between weekly deploys.
#### `setTracingDepth(depth: number)`
Configures how deep to trace precondition qualifiers.
```javascript
@@ -62,6 +96,7 @@ __BITWARDEN_AUTOFILL_DEBUG__.setTracingDepth(2);
```
#### `getTracingDepth()`
Returns the current tracing depth.
```javascript
@@ -70,11 +105,12 @@ console.log(`Current tracing depth: ${depth}`);
```
#### `getSessions()`
Returns an array of all session IDs in memory.
```javascript
const sessions = __BITWARDEN_AUTOFILL_DEBUG__.getSessions();
console.log('Active sessions:', sessions);
console.log("Active sessions:", sessions);
```
## Enhanced Console Logging
@@ -82,6 +118,7 @@ console.log('Active sessions:', sessions);
When debug mode is enabled, the console automatically displays enhanced qualification messages:
### Field Qualified
```
✅ Field Qualified: opid_12345
Field: <input type="text" ...>
@@ -90,6 +127,7 @@ When debug mode is enabled, the console automatically displays enhanced qualific
```
### Field Rejected
```
❌ Field Rejected: opid_12345
Field: <input type="text" ...>
@@ -101,19 +139,20 @@ When debug mode is enabled, the console automatically displays enhanced qualific
## Understanding Debug Output
### JSON Export Structure
```json
{
"sessionId": "session_1234567890_abc123",
"sessionId": "session_2026-02-19T12-00-00-000Z_abc12",
"startTime": 1234567890000,
"endTime": 1234567891000,
"url": "https://example.com/login",
"qualifications": [
{
"fieldId": "opid_12345",
"elementSelector": "input[type='text']#username",
"elementSelector": "#username",
"attempts": [
{
"attemptId": "attempt_1234567890_xyz789",
"attemptId": "attempt_2026-02-19T12-00-00-500Z",
"timestamp": 1234567890500,
"vector": "inline-menu",
"result": {
@@ -122,6 +161,7 @@ When debug mode is enabled, the console automatically displays enhanced qualific
"pass": [
{
"name": "isUsernameField",
"description": "Field is recognized as a username or email input",
"functionSource": "function isUsernameField(field) { ... }"
}
],
@@ -138,7 +178,7 @@ When debug mode is enabled, the console automatically displays enhanced qualific
"tracingDepth": 0
}
},
"triggeredBy": "page-load"
"triggeredBy": "setupOverlayListeners"
}
],
"finalDecision": { ... }
@@ -148,30 +188,40 @@ When debug mode is enabled, the console automatically displays enhanced qualific
```
### Summary Export Structure
```
================================================================================
Bitwarden Autofill Debug Summary
================================================================================
Session ID: session_1234567890_abc123
Session ID: session_2026-02-19T12-00-00-000Z_abc12
URL: https://example.com/login
Start Time: 2026-02-06T12:00:00.000Z
End Time: 2026-02-06T12:00:01.000Z
Start Time: 2026-02-19T12:00:00.000Z
End Time: 2026-02-19T12:00:01.000Z
Duration: 1.00s
Total Fields Qualified: 2
--------------------------------------------------------------------------------
Field ID: opid_12345
Selector: input[type='text']#username
Selector: #username
Attempts: 1
Final Decision: ✅ QUALIFIED
Passed Conditions:
isUsernameField
notCurrentlyInSandboxedIframe
notCurrentlyInSandboxedIframe — Field is not in a sandboxed iframe
isUsernameField — Field is recognized as a username or email input
Vector: inline-menu
Timestamp: 2026-02-06T12:00:00.500Z
--------------------------------------------------------------------------------
Field ID: opid_67890
Selector: [name="search"]
Attempts: 1
Final Decision: ❌ REJECTED
Passed Conditions:
✓ notCurrentlyInSandboxedIframe
Failed Conditions:
✗ fieldIsForLoginForm — Field is part of a login form
→ Fix: Add autocomplete="username" or autocomplete="email" to the field
================================================================================
⚠️ WARNING: This debug data may contain sensitive information.
@@ -189,32 +239,60 @@ Debug data tracks which autofill vector triggered the qualification:
- **keyboard-shortcut**: Autofill triggered by keyboard shortcut (Ctrl+Shift+L)
- **page-load**: Autofill triggered automatically on page load
### Architectural Reality
All 4 entry points (inline menu, popup, keyboard shortcut, context menu) ultimately call the same fill path in the background service worker: `autofillService.doAutoFill()` → `generateFillScript()`. They use the same core logic and do not bypass each other.
However, there are **two distinct qualification systems**:
| System | Used by | Where it runs |
| ------------------------------------- | ------------------------ | ------------------------- |
| `InlineMenuFieldQualificationService` | Inline menu overlay only | Content script |
| `generateFillScript()` field matching | All 4 entry points | Background service worker |
The `InlineMenuFieldQualificationService` is used exclusively to decide **whether to show the overlay icon on a given field**. The debug session records only this decision. The actual fill logic uses simpler, independent field detection in the background.
This means:
- A field _rejected_ by inline menu qualification (no overlay shown) may still be _filled_ by keyboard shortcut or context menu, because the two qualification systems use different criteria.
- A field _accepted_ by inline menu qualification may fail to fill if `generateFillScript()` doesn't match it.
### Why Other Vectors Are Not Tracked
The debug service lives in the content script inside `AutofillOverlayContentService`. The popup, keyboard shortcut, and context menu flows run entirely in the background service worker and only send the rendered fill script to the content script — they never invoke the content script's qualification service. There is no point in those paths where the current vector can be set.
To track those vectors would require:
1. A new message from the background to the content script announcing "autofill was triggered via [vector]"
2. The content script debug service recording it against the current session
This is tracked as future work.
## Precondition Tracing
Some qualification functions depend on other qualifiers (preconditions). For example:
- `isNewPasswordField` depends on `isPasswordField`
- `isCurrentPasswordField` depends on `isPasswordField`
The tracing depth controls how deep to capture these dependencies:
### Depth = 0 (No Tracing)
Only captures the top-level condition result. Fastest, minimal data.
### Depth = 1 (Immediate Preconditions) - Default
Captures the direct preconditions of the qualification.
Example: For `isNewPasswordField`, captures:
- `isNewPasswordField` (top level)
- `isPasswordField` (immediate precondition)
### Depth = 2+ (Full Chain)
Captures the entire chain of preconditions recursively.
Example: For a complex qualification, captures:
- `isFieldForLoginForm` (top level)
- `isUsernameFieldForLoginForm` (precondition)
- `isUsernameField` (precondition of precondition)
- ... (and so on)
Captures the entire chain of preconditions recursively.
## Performance Impact
@@ -235,48 +313,85 @@ Example: For a complex qualification, captures:
⚠️ **Never share debug output publicly or with untrusted parties**
While field values are redacted, debug output still contains:
- Page URLs
- Field IDs, names, and attributes
- Form structure
- Qualification logic (function source code)
This information could be used to identify:
- Internal applications
- Custom form fields
- Business logic patterns
## QA / Regression Workflow
Use named sessions for deterministic, diffable output across deployments:
```javascript
// Before a deploy
__BITWARDEN_AUTOFILL_DEBUG__.startSession("checkout-form-baseline");
// Focus fields on the page to trigger qualification
const before = __BITWARDEN_AUTOFILL_DEBUG__.exportSession("json");
// After a deploy
__BITWARDEN_AUTOFILL_DEBUG__.startSession("checkout-form-after-deploy");
// Focus same fields
const after = __BITWARDEN_AUTOFILL_DEBUG__.exportSession("json");
// Diff — attempt IDs and timestamps in conditions are stable; only real changes show up
```
Session IDs use ISO timestamps (`session_2026-02-19T12-00-00-000Z_name`) so they sort naturally and identify when each capture was taken.
## Troubleshooting Common Issues
### Debug API Not Available
```javascript
typeof window.__BITWARDEN_AUTOFILL_DEBUG__ === 'undefined'
typeof window.__BITWARDEN_AUTOFILL_DEBUG__ === "undefined";
```
**Solution**: Verify that:
1. Dev flag is set correctly in `.env.development`
2. Extension was rebuilt after setting the flag
3. You're on a page where autofill is active (not a chrome:// or browser settings page)
### No Sessions Found
```javascript
__BITWARDEN_AUTOFILL_DEBUG__.getSessions() // returns []
__BITWARDEN_AUTOFILL_DEBUG__.getSessions(); // returns []
```
**Solution**:
1. Focus a form field to trigger qualification
2. Sessions expire after 5 minutes - check timing
3. Verify debug mode is enabled (check console for initialization message)
### Missing Precondition Data
```javascript
// result.meta.preconditions is undefined
```
**Solution**: Increase tracing depth:
```javascript
__BITWARDEN_AUTOFILL_DEBUG__.setTracingDepth(2);
```
### Empty qualifications Array in Export
If `exportSession('json')` shows `"qualifications": []`, the session captured no field evaluations.
**Solution**: The session may have started after fields were already evaluated. Either:
1. Start a named session before navigating: `startSession('my-test')` — then navigate to the page
2. Or refresh the page after enabling debug mode so all fields are re-evaluated
## Example Workflow
1. Enable debug mode and rebuild extension
@@ -296,16 +411,6 @@ __BITWARDEN_AUTOFILL_DEBUG__.setTracingDepth(2);
8. Focus the field again to capture with higher depth
9. Export and analyze:
```javascript
const json = __BITWARDEN_AUTOFILL_DEBUG__.exportSession('json');
const json = __BITWARDEN_AUTOFILL_DEBUG__.exportSession("json");
// Save to file or analyze
```
## Future Enhancements
Planned features (not yet implemented):
- Visual debug panel UI in popup/options
- Download JSON button
- Copy to clipboard functionality
- JSON-DSL representation of conditions
- Filter by field type or qualification result
- Session replay/comparison

View File

@@ -38,6 +38,7 @@ import {
openAddEditVaultItemPopout,
openVaultItemPasswordRepromptPopout,
} from "../../vault/popup/utils/vault-popout-window";
import { COPY_AUTOFILL_DEBUG_ID } from "../enums/autofill-message.enums";
import { LockedVaultPendingNotificationsData } from "../background/abstractions/notification.background";
import { AutofillCipherTypeId } from "../types";
@@ -76,6 +77,13 @@ export class ContextMenuClickedHandler {
this.copyToClipboard({ text: await this.getIdentifier(tab, info), tab: tab });
break;
case COPY_AUTOFILL_DEBUG_ID:
if (!tab.id) {
return;
}
this.copyToClipboard({ text: await this.getAutofillDebugExport(tab), tab: tab });
break;
default:
await this.cipherAction(info, tab);
}
@@ -279,4 +287,21 @@ export class ContextMenuClickedHandler {
);
});
}
private async getAutofillDebugExport(tab: chrome.tabs.Tab): Promise<string> {
const tabId = tab.id!;
return new Promise<string>((resolve) => {
BrowserApi.sendTabsMessage(
tabId,
{ command: "getAutofillDebugExport" },
undefined,
(debugText: string) => {
resolve(
debugText ||
"No autofill debug data available. Enable debug mode and focus a form field first.",
);
},
);
});
}
}

View File

@@ -27,6 +27,8 @@ import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { COPY_AUTOFILL_DEBUG_ID } from "../enums/autofill-message.enums";
import { devFlagEnabled } from "../../platform/flags";
import { InitContextMenuItems } from "./abstractions/main-context-menu-handler";
export class MainContextMenuHandler {
@@ -204,6 +206,14 @@ export class MainContextMenuHandler {
await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] });
}
if (devFlagEnabled("autofillDebugMode")) {
await MainContextMenuHandler.create({
id: COPY_AUTOFILL_DEBUG_ID,
parentId: ROOT_ID,
title: "[Debug] Copy autofill debug info",
contexts: ["all"],
});
}
} catch (error) {
if (error instanceof Error) {
this.logService.warning(error.message);

View File

@@ -1,3 +1,4 @@
import { devFlagEnabled } from "../../platform/flags";
import { DebugExportFormat } from "../models/autofill-debug-data";
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service";
@@ -7,7 +8,6 @@ import DomElementVisibilityService from "../services/dom-element-visibility.serv
import { DomQueryService } from "../services/dom-query.service";
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
import { setupAutofillInitDisconnectAction } from "../utils";
import { devFlagEnabled } from "../../platform/flags";
import AutofillInit from "./autofill-init";
@@ -60,8 +60,15 @@ import AutofillInit from "./autofill-init";
Array.from(debugService.sessionStore.keys())[0] || "",
);
},
startSession: (name?: string) => {
const sessionId = debugService.startSession(globalThis.location.href, name);
// eslint-disable-next-line no-console
console.log(`[Bitwarden Debug] Session started: ${sessionId}`);
return sessionId;
},
setTracingDepth: (depth: number) => {
debugService.setTracingDepth(depth);
// eslint-disable-next-line no-console
console.log(`[Bitwarden Debug] Precondition tracing depth set to ${depth}`);
},
getTracingDepth: () => {
@@ -72,6 +79,7 @@ import AutofillInit from "./autofill-init";
},
};
/* eslint-disable no-console */
console.log(
"%c[Bitwarden Debug] Autofill debug mode enabled. Use window.__BITWARDEN_AUTOFILL_DEBUG__",
"color: #175DDC; font-weight: bold; font-size: 12px",
@@ -79,9 +87,11 @@ import AutofillInit from "./autofill-init";
console.log("Available methods:");
console.log(" - exportSession(format?: 'json' | 'summary' | 'console')");
console.log(" - exportSummary()");
console.log(" - startSession(name?: string) — named sessions are diffable across deploys");
console.log(" - setTracingDepth(depth: number)");
console.log(" - getTracingDepth()");
console.log(" - getSessions()");
/* eslint-enable no-console */
}
}
})(window);

View File

@@ -3,6 +3,8 @@ export const AutofillMessageCommand = {
collectPageDetailsResponse: "collectPageDetailsResponse",
} as const;
export const COPY_AUTOFILL_DEBUG_ID = "copy-autofill-debug";
export type AutofillMessageCommandType =
(typeof AutofillMessageCommand)[keyof typeof AutofillMessageCommand];

View File

@@ -27,6 +27,8 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
getInlineMenuFormFieldData: ({
message,
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData | void>;
generatedPasswordModifyLogin: () => Promise<void>;
getAutofillDebugExport: () => string;
};
export interface AutofillOverlayContentService {

View File

@@ -20,6 +20,8 @@ export type AutofillVector =
export type QualificationCondition = {
name: string;
description?: string;
suggestion?: string;
functionSource?: string;
};
@@ -53,11 +55,17 @@ export interface InlineMenuFieldQualificationService {
isFieldForAccountCreationForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
isFieldForIdentityForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean;
isFieldForCardholderName(field: AutofillField): boolean;
isFieldForCardholderNameWithResult(field: AutofillField): QualificationResult;
isFieldForCardNumber(field: AutofillField): boolean;
isFieldForCardNumberWithResult(field: AutofillField): QualificationResult;
isFieldForCardExpirationDate(field: AutofillField): boolean;
isFieldForCardExpirationDateWithResult(field: AutofillField): QualificationResult;
isFieldForCardExpirationMonth(field: AutofillField): boolean;
isFieldForCardExpirationMonthWithResult(field: AutofillField): QualificationResult;
isFieldForCardExpirationYear(field: AutofillField): boolean;
isFieldForCardExpirationYearWithResult(field: AutofillField): QualificationResult;
isFieldForCardCvv(field: AutofillField): boolean;
isFieldForCardCvvWithResult(field: AutofillField): QualificationResult;
isFieldForIdentityTitle(field: AutofillField): boolean;
isFieldForIdentityFirstName(field: AutofillField): boolean;
isFieldForIdentityMiddleName(field: AutofillField): boolean;

View File

@@ -1,12 +1,13 @@
import AutofillField from "../models/autofill-field";
import AutofillPageDetails from "../models/autofill-page-details";
/* eslint-disable no-console */
import { devFlagEnabled } from "../../platform/flags";
import {
AutofillDebugSession,
DebugExportFormat,
FieldQualificationRecord,
QualificationAttempt,
} from "../models/autofill-debug-data";
import { devFlagEnabled } from "../../platform/flags";
import AutofillField from "../models/autofill-field";
import AutofillPageDetails from "../models/autofill-page-details";
import {
AutofillVector,
QualificationResult,
@@ -40,8 +41,11 @@ export class AutofillDebugService {
this.tracingDepth = depth;
}
startSession(url: string): string {
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
startSession(url: string, sessionName?: string): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const sessionId = sessionName
? `session_${timestamp}_${sessionName}`
: `session_${timestamp}_${Math.random().toString(36).substring(2, 7)}`;
this.currentSession = {
sessionId,
startTime: Date.now(),
@@ -92,7 +96,7 @@ export class AutofillDebugService {
}
const attempt: QualificationAttempt = {
attemptId: `attempt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
attemptId: `attempt_${new Date().toISOString().replace(/[:.]/g, "-")}`,
timestamp: Date.now(),
vector,
result,
@@ -105,7 +109,9 @@ export class AutofillDebugService {
exportCurrentSession(format: DebugExportFormat = "json"): string {
if (!this.currentSession) {
return format === "json" ? JSON.stringify({ error: "No active session" }) : "No active session";
return format === "json"
? JSON.stringify({ error: "No active session" })
: "No active session";
}
return this.exportSession(this.currentSession.sessionId, format);
@@ -115,7 +121,9 @@ export class AutofillDebugService {
const session = this.sessionStore.get(sessionId);
if (!session) {
return format === "json" ? JSON.stringify({ error: "Session not found" }) : "Session not found";
return format === "json"
? JSON.stringify({ error: "Session not found" })
: "Session not found";
}
switch (format) {
@@ -168,7 +176,8 @@ export class AutofillDebugService {
lines.push("");
lines.push("Passed Conditions:");
for (const condition of fieldRecord.finalDecision.conditions.pass) {
lines.push(` ${condition.name}`);
const desc = condition.description ? ` ${condition.description}` : "";
lines.push(`${condition.name}${desc}`);
}
}
@@ -176,14 +185,20 @@ export class AutofillDebugService {
lines.push("");
lines.push("Failed Conditions:");
for (const condition of fieldRecord.finalDecision.conditions.fail) {
lines.push(` ${condition.name}`);
const desc = condition.description ? ` ${condition.description}` : "";
lines.push(`${condition.name}${desc}`);
if (condition.suggestion) {
lines.push(` → Fix: ${condition.suggestion}`);
}
}
}
if (fieldRecord.finalDecision.meta) {
lines.push("");
lines.push(`Vector: ${fieldRecord.finalDecision.meta.vector}`);
lines.push(`Timestamp: ${new Date(fieldRecord.finalDecision.meta.timestamp).toISOString()}`);
lines.push(
`Timestamp: ${new Date(fieldRecord.finalDecision.meta.timestamp).toISOString()}`,
);
}
}
@@ -230,7 +245,9 @@ export class AutofillDebugService {
if (fieldRecord.finalDecision.conditions.pass.length > 0) {
console.group("✓ Passed Conditions");
for (const condition of fieldRecord.finalDecision.conditions.pass) {
console.log(`${condition.name}`);
console.log(
`${condition.name}${condition.description ? `${condition.description}` : ""}`,
);
if (condition.functionSource) {
console.log(`Function:`, condition.functionSource);
}
@@ -241,7 +258,12 @@ export class AutofillDebugService {
if (fieldRecord.finalDecision.conditions.fail.length > 0) {
console.group("✗ Failed Conditions");
for (const condition of fieldRecord.finalDecision.conditions.fail) {
console.log(`${condition.name}`);
console.log(
`${condition.name}${condition.description ? `${condition.description}` : ""}`,
);
if (condition.suggestion) {
console.log(` → Fix: ${condition.suggestion}`);
}
if (condition.functionSource) {
console.log(`Function:`, condition.functionSource);
}
@@ -252,7 +274,9 @@ export class AutofillDebugService {
if (fieldRecord.finalDecision.meta) {
console.group("Metadata");
console.log(`Vector: ${fieldRecord.finalDecision.meta.vector}`);
console.log(`Timestamp: ${new Date(fieldRecord.finalDecision.meta.timestamp).toISOString()}`);
console.log(
`Timestamp: ${new Date(fieldRecord.finalDecision.meta.timestamp).toISOString()}`,
);
console.log(`Field Snapshot:`, fieldRecord.finalDecision.meta.fieldSnapshot);
if (fieldRecord.finalDecision.meta.pageSnapshot) {
console.log(`Page Snapshot:`, fieldRecord.finalDecision.meta.pageSnapshot);

View File

@@ -54,7 +54,11 @@ import {
} from "./abstractions/autofill-overlay-content.service";
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
import { DomQueryService } from "./abstractions/dom-query.service";
import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service";
import {
AutofillVector,
InlineMenuFieldQualificationService,
QualificationResult,
} from "./abstractions/inline-menu-field-qualifications.service";
import { AutoFillConstants } from "./autofill-constants";
import { AutofillDebugService } from "./autofill-debug.service";
@@ -123,6 +127,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
getInlineMenuFormFieldData: ({ message }) =>
this.handleGetInlineMenuFormFieldDataMessage(message),
generatedPasswordModifyLogin: () => this.sendGeneratedPasswordModifyLogin(),
getAutofillDebugExport: () =>
this.debugService?.exportCurrentSession("summary") ?? "Debug mode not enabled",
};
private readonly loginFieldQualifiers: Record<string, CallableFunction> = {
[AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField,
@@ -263,16 +269,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
failure: "field previously qualified",
},
},
// Function should be entirely deconstructed first.
// {
// qualifier: ({ autofillFieldData, pageDetails }: QualificationCriteria) =>
// !this.isIgnoredField(autofillFieldData, pageDetails),
// alias: "isNotIgnoredField",
// message: {
// success: "field is not ignored",
// failure: "field is ignored",
// },
// },
{
qualifier: ({ autofillFieldData }: QualificationCriteria) =>
!this.ignoredFieldTypes.has(autofillFieldData.type),
@@ -297,6 +293,88 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
effect: ({ autofillFieldData }: QualificationCriteria) =>
this.setQualifiedLoginFillType(autofillFieldData),
},
{
qualifier: ({ autofillFieldData, pageDetails }: QualificationCriteria) =>
this.showInlineMenuCards &&
this.inlineMenuFieldQualificationService.isFieldForCreditCardForm(
autofillFieldData,
pageDetails,
),
alias: "fieldIsForCreditCardForm",
message: {
success: "field is for credit card form",
failure: "field is not for credit card form",
},
blocking: false,
effect: ({ autofillFieldData }: QualificationCriteria) => {
if (!autofillFieldData.inlineMenuFillType) {
autofillFieldData.inlineMenuFillType = CipherType.Card;
}
},
},
{
qualifier: ({ autofillFieldData, pageDetails }: QualificationCriteria) =>
this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm(
autofillFieldData,
pageDetails,
),
alias: "fieldIsForAccountCreationForm",
message: {
success: "field is for account creation form",
failure: "field is not for account creation form",
},
blocking: false,
effect: ({ autofillFieldData }: QualificationCriteria) => {
if (!autofillFieldData.inlineMenuFillType) {
this.setQualifiedAccountCreationFillType(autofillFieldData);
}
},
},
{
qualifier: ({ autofillFieldData, pageDetails }: QualificationCriteria) =>
this.showInlineMenuIdentities &&
this.inlineMenuFieldQualificationService.isFieldForIdentityForm(
autofillFieldData,
pageDetails,
),
alias: "fieldIsForIdentityForm",
message: {
success: "field is for identity form",
failure: "field is not for identity form",
},
blocking: false,
effect: ({ autofillFieldData }: QualificationCriteria) => {
if (!autofillFieldData.inlineMenuFillType) {
autofillFieldData.inlineMenuFillType = CipherType.Identity;
}
},
},
{
qualifier: ({ autofillFieldData, pageDetails }: QualificationCriteria) =>
this.inlineMenuFieldQualificationService.isFieldForLoginForm(
autofillFieldData,
pageDetails,
) ||
this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm(
autofillFieldData,
pageDetails,
) ||
(this.showInlineMenuCards &&
this.inlineMenuFieldQualificationService.isFieldForCreditCardForm(
autofillFieldData,
pageDetails,
)) ||
(this.showInlineMenuIdentities &&
this.inlineMenuFieldQualificationService.isFieldForIdentityForm(
autofillFieldData,
pageDetails,
)),
alias: "fieldIsForKnownFormType",
message: {
success: "field is for a recognized form type",
failure: "field is not for any recognized form type",
},
},
{
qualifier: ({ formFieldElement, autofillFieldData }: QualificationCriteria) =>
!this.isHiddenField(formFieldElement, autofillFieldData),
@@ -324,6 +402,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
isQualifiedField(criteria: QualificationCriteria) {
const debugEnabled = this.debugService?.isDebugEnabled() ?? false;
const responses: QualificationResponse[] = [];
const { autofillFieldData } = criteria;
const elementSelector = autofillFieldData.htmlID
? `#${autofillFieldData.htmlID}`
: autofillFieldData.htmlName
? `[name="${autofillFieldData.htmlName}"]`
: autofillFieldData.opid;
for (const definition of this.qualifiers) {
const response = this.qualify(definition, criteria);
@@ -331,8 +416,25 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
if (response.result === false && definition?.blocking !== false) {
if (debugEnabled) {
const qualificationResult: QualificationResult = {
result: false,
conditions: {
pass: responses
.filter((r) => r !== response && r.result)
.map((r) => ({ name: r.alias })),
fail: [{ name: response.alias }],
},
};
this.debugService?.recordQualification(
autofillFieldData.opid,
elementSelector,
"inline-menu" as AutofillVector,
qualificationResult,
"setupOverlayListeners",
);
/* eslint-disable no-console */
console.group(
`%c❌ Field Rejected: ${criteria.autofillFieldData.opid}`,
`%c❌ Field Rejected: ${autofillFieldData.opid}`,
"color: #ef4444; font-weight: bold",
);
console.log("Field:", criteria.formFieldElement);
@@ -340,7 +442,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
console.log("Message:", response.message);
console.log("All responses:", responses);
console.groupEnd();
/* eslint-enable no-console */
} else {
// eslint-disable-next-line no-console
console.log({ element: criteria.formFieldElement, responses });
}
return false;
@@ -352,8 +456,23 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
if (debugEnabled) {
const qualificationResult: QualificationResult = {
result: true,
conditions: {
pass: responses.map((r) => ({ name: r.alias })),
fail: [],
},
};
this.debugService?.recordQualification(
autofillFieldData.opid,
elementSelector,
"inline-menu" as AutofillVector,
qualificationResult,
"setupOverlayListeners",
);
/* eslint-disable no-console */
console.group(
`%c✅ Field Qualified: ${criteria.autofillFieldData.opid}`,
`%c✅ Field Qualified: ${autofillFieldData.opid}`,
"color: #10b981; font-weight: bold",
);
console.log("Field:", criteria.formFieldElement);
@@ -363,7 +482,9 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
);
console.log("All responses:", responses);
console.groupEnd();
/* eslint-enable no-console */
} else {
// eslint-disable-next-line no-console
console.log({ element: criteria.formFieldElement, responses });
}

View File

@@ -16,8 +16,8 @@ import {
SubmitChangePasswordButtonNames,
SubmitLoginButtonNames,
} from "./autofill-constants";
import AutofillService from "./autofill.service";
import { AutofillDebugService } from "./autofill-debug.service";
import AutofillService from "./autofill.service";
export class InlineMenuFieldQualificationService implements InlineMenuFieldQualificationServiceInterface {
private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
@@ -159,6 +159,116 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
return this.debugService?.isDebugEnabled() ?? false;
}
private static readonly conditionDescriptions: Record<
string,
{ description?: string; suggestion?: string }
> = {
isUsernameField: {
description: "Field is recognized as a username or email input",
suggestion: 'Add autocomplete="username" or autocomplete="email" to the field',
},
isCurrentPasswordField: {
description: "Field is recognized as an existing (login) password field",
suggestion: 'Add autocomplete="current-password" to the field',
},
isNewPasswordField: {
description: "Field is recognized as a new password field (account creation)",
suggestion: 'Add autocomplete="new-password" to the field',
},
isPasswordField: {
description: "Field has type='password' or matches password heuristics",
suggestion: "Set type='password' on the field",
},
isFieldForCardholderName: {
description: "Field is recognized as a cardholder name field",
suggestion: 'Add autocomplete="cc-name" to the field',
},
isFieldForCardNumber: {
description: "Field is recognized as a credit card number field",
suggestion: 'Add autocomplete="cc-number" to the field',
},
isFieldForCardExpirationDate: {
description: "Field is recognized as a card expiration date field",
suggestion: 'Add autocomplete="cc-exp" to the field',
},
isFieldForCardExpirationMonth: {
description: "Field is recognized as a card expiration month field",
suggestion: 'Add autocomplete="cc-exp-month" to the field',
},
isFieldForCardExpirationYear: {
description: "Field is recognized as a card expiration year field",
suggestion: 'Add autocomplete="cc-exp-year" to the field',
},
isFieldForCardCvv: {
description: "Field is recognized as a card CVV/security code field",
suggestion: 'Add autocomplete="cc-csc" to the field',
},
isFieldForIdentityTitle: {
description: "Field is recognized as an honorific title field",
suggestion: 'Add autocomplete="honorific-prefix" to the field',
},
isFieldForIdentityFirstName: {
description: "Field is recognized as a first name field",
suggestion: 'Add autocomplete="given-name" to the field',
},
isFieldForIdentityMiddleName: {
description: "Field is recognized as a middle name field",
suggestion: 'Add autocomplete="additional-name" to the field',
},
isFieldForIdentityLastName: {
description: "Field is recognized as a last name field",
suggestion: 'Add autocomplete="family-name" to the field',
},
isFieldForIdentityFullName: {
description: "Field is recognized as a full name field",
suggestion: 'Add autocomplete="name" to the field',
},
isFieldForIdentityAddress1: {
description: "Field is recognized as a primary address line field",
suggestion: 'Add autocomplete="address-line1" to the field',
},
isFieldForIdentityAddress2: {
description: "Field is recognized as a secondary address line field",
suggestion: 'Add autocomplete="address-line2" to the field',
},
isFieldForIdentityAddress3: {
description: "Field is recognized as a third address line field",
suggestion: 'Add autocomplete="address-line3" to the field',
},
isFieldForIdentityCity: {
description: "Field is recognized as a city field",
suggestion: 'Add autocomplete="address-level2" to the field',
},
isFieldForIdentityState: {
description: "Field is recognized as a state or province field",
suggestion: 'Add autocomplete="address-level1" to the field',
},
isFieldForIdentityPostalCode: {
description: "Field is recognized as a postal or ZIP code field",
suggestion: 'Add autocomplete="postal-code" to the field',
},
isFieldForIdentityCountry: {
description: "Field is recognized as a country field",
suggestion: 'Add autocomplete="country" to the field',
},
isFieldForIdentityCompany: {
description: "Field is recognized as a company or organization field",
suggestion: 'Add autocomplete="organization" to the field',
},
isFieldForIdentityPhone: {
description: "Field is recognized as a phone number field",
suggestion: 'Add autocomplete="tel" to the field',
},
isFieldForIdentityEmail: {
description: "Field is recognized as an email address field",
suggestion: 'Add autocomplete="email" to the field',
},
isFieldForIdentityUsername: {
description: "Field is recognized as a username field for identity",
suggestion: 'Add autocomplete="username" to the field',
},
};
setCurrentVector(vector: AutofillVector): void {
this.currentVector = vector;
}
@@ -192,8 +302,11 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
const maxDepth = this.debugService?.getTracingDepth() ?? 1;
const shouldTrace = currentDepth < maxDepth;
const descMeta = InlineMenuFieldQualificationService.conditionDescriptions[name];
const condition = {
name,
description: descMeta?.description,
suggestion: result ? undefined : descMeta?.suggestion,
functionSource: shouldTrace ? fn.toString() : undefined,
};
@@ -640,104 +753,97 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
*
* @param field - The field to validate
*/
isFieldForCardholderName = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.creditCardNameAutocompleteValues)) {
return true;
}
isFieldForCardholderName = (field: AutofillField): boolean =>
this.isFieldForCardholderNameWithResult(field).result;
return this.keywordsFoundInFieldData(
field,
CreditCardAutoFillConstants.CardHolderFieldNames,
false,
);
};
isFieldForCardholderNameWithResult(field: AutofillField): QualificationResult {
const result =
this.fieldContainsAutocompleteValues(field, this.creditCardNameAutocompleteValues) ||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardHolderFieldNames, false);
return this.wrapQualifier("isFieldForCardholderName", () => result, field);
}
/**
* Validates the provided field as a credit card number field.
*
* @param field - The field to validate
*/
isFieldForCardNumber = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.creditCardNumberAutocompleteValue)) {
return true;
}
isFieldForCardNumber = (field: AutofillField): boolean =>
this.isFieldForCardNumberWithResult(field).result;
return this.keywordsFoundInFieldData(
field,
CreditCardAutoFillConstants.CardNumberFieldNames,
false,
);
};
isFieldForCardNumberWithResult(field: AutofillField): QualificationResult {
const result =
this.fieldContainsAutocompleteValues(field, this.creditCardNumberAutocompleteValue) ||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardNumberFieldNames, false);
return this.wrapQualifier("isFieldForCardNumber", () => result, field);
}
/**
* Validates the provided field as a credit card expiration date field.
*
* @param field - The field to validate
*/
isFieldForCardExpirationDate = (field: AutofillField): boolean => {
if (
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationDateAutocompleteValue)
) {
return true;
}
isFieldForCardExpirationDate = (field: AutofillField): boolean =>
this.isFieldForCardExpirationDateWithResult(field).result;
return this.keywordsFoundInFieldData(
field,
CreditCardAutoFillConstants.CardExpiryFieldNames,
false,
);
};
isFieldForCardExpirationDateWithResult(field: AutofillField): QualificationResult {
const result =
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationDateAutocompleteValue) ||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CardExpiryFieldNames, false);
return this.wrapQualifier("isFieldForCardExpirationDate", () => result, field);
}
/**
* Validates the provided field as a credit card expiration month field.
*
* @param field - The field to validate
*/
isFieldForCardExpirationMonth = (field: AutofillField): boolean => {
if (
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationMonthAutocompleteValue)
) {
return true;
}
isFieldForCardExpirationMonth = (field: AutofillField): boolean =>
this.isFieldForCardExpirationMonthWithResult(field).result;
return this.keywordsFoundInFieldData(
field,
CreditCardAutoFillConstants.ExpiryMonthFieldNames,
false,
);
};
isFieldForCardExpirationMonthWithResult(field: AutofillField): QualificationResult {
const result =
this.fieldContainsAutocompleteValues(
field,
this.creditCardExpirationMonthAutocompleteValue,
) ||
this.keywordsFoundInFieldData(
field,
CreditCardAutoFillConstants.ExpiryMonthFieldNames,
false,
);
return this.wrapQualifier("isFieldForCardExpirationMonth", () => result, field);
}
/**
* Validates the provided field as a credit card expiration year field.
*
* @param field - The field to validate
*/
isFieldForCardExpirationYear = (field: AutofillField): boolean => {
if (
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationYearAutocompleteValue)
) {
return true;
}
isFieldForCardExpirationYear = (field: AutofillField): boolean =>
this.isFieldForCardExpirationYearWithResult(field).result;
return this.keywordsFoundInFieldData(
field,
CreditCardAutoFillConstants.ExpiryYearFieldNames,
false,
);
};
isFieldForCardExpirationYearWithResult(field: AutofillField): QualificationResult {
const result =
this.fieldContainsAutocompleteValues(field, this.creditCardExpirationYearAutocompleteValue) ||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.ExpiryYearFieldNames, false);
return this.wrapQualifier("isFieldForCardExpirationYear", () => result, field);
}
/**
* Validates the provided field as a credit card CVV field.
*
* @param field - The field to validate
*/
isFieldForCardCvv = (field: AutofillField): boolean => {
if (this.fieldContainsAutocompleteValues(field, this.creditCardCvvAutocompleteValue)) {
return true;
}
isFieldForCardCvv = (field: AutofillField): boolean =>
this.isFieldForCardCvvWithResult(field).result;
return this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CVVFieldNames, false);
};
isFieldForCardCvvWithResult(field: AutofillField): QualificationResult {
const result =
this.fieldContainsAutocompleteValues(field, this.creditCardCvvAutocompleteValue) ||
this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CVVFieldNames, false);
return this.wrapQualifier("isFieldForCardCvv", () => result, field);
}
/**
* Validates the provided field as an identity title type field.

View File

@@ -1,7 +1,8 @@
export type TabMessage =
| CopyTextTabMessage
| ClearClipboardTabMessage
| GetClickedElementTabMessage;
| GetClickedElementTabMessage
| GetAutofillDebugExportTabMessage;
export type TabMessageBase<T extends string> = {
command: T;
@@ -14,3 +15,5 @@ type CopyTextTabMessage = TabMessageBase<"copyText"> & {
type ClearClipboardTabMessage = TabMessageBase<"clearClipboard">;
type GetClickedElementTabMessage = TabMessageBase<"getClickedElement">;
type GetAutofillDebugExportTabMessage = TabMessageBase<"getAutofillDebugExport">;