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

[WIP] First generated pass.

This commit is contained in:
Miles Blackwood
2026-02-09 11:00:20 -05:00
parent 1dc250a341
commit b11f111fd8
11 changed files with 939 additions and 21 deletions

1
.env.development Normal file
View File

@@ -0,0 +1 @@
DEV_FLAGS={"autofillDebugMode": true}

View File

@@ -0,0 +1,311 @@
# Autofill Debug Mode
This document describes how to use the autofill debug mode for troubleshooting field qualification issues.
## 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
```
3. Load the extension in your browser (or reload if already loaded)
## 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
#### `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');
console.log(JSON.parse(data));
// Export as human-readable summary
console.log(__BITWARDEN_AUTOFILL_DEBUG__.exportSession('summary'));
// Output to console with pretty formatting
__BITWARDEN_AUTOFILL_DEBUG__.exportSession('console');
```
#### `exportSummary()`
Returns a human-readable summary of the most recent session.
```javascript
console.log(__BITWARDEN_AUTOFILL_DEBUG__.exportSummary());
```
#### `setTracingDepth(depth: number)`
Configures how deep to trace precondition qualifiers.
```javascript
// Don't trace preconditions (faster, less data)
__BITWARDEN_AUTOFILL_DEBUG__.setTracingDepth(0);
// Trace immediate preconditions only (default)
__BITWARDEN_AUTOFILL_DEBUG__.setTracingDepth(1);
// Trace full precondition chain (more detail)
__BITWARDEN_AUTOFILL_DEBUG__.setTracingDepth(2);
```
#### `getTracingDepth()`
Returns the current tracing depth.
```javascript
const depth = __BITWARDEN_AUTOFILL_DEBUG__.getTracingDepth();
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);
```
## Enhanced Console Logging
When debug mode is enabled, the console automatically displays enhanced qualification messages:
### Field Qualified
```
✅ Field Qualified: opid_12345
Field: <input type="text" ...>
Passed conditions: ["notCurrentlyInSandboxedIframe", "isVisibleFormField", ...]
All responses: [...]
```
### Field Rejected
```
❌ Field Rejected: opid_12345
Field: <input type="text" ...>
Blocking condition failed: isNotDisabledField
Message: field is disabled
All responses: [...]
```
## Understanding Debug Output
### JSON Export Structure
```json
{
"sessionId": "session_1234567890_abc123",
"startTime": 1234567890000,
"endTime": 1234567891000,
"url": "https://example.com/login",
"qualifications": [
{
"fieldId": "opid_12345",
"elementSelector": "input[type='text']#username",
"attempts": [
{
"attemptId": "attempt_1234567890_xyz789",
"timestamp": 1234567890500,
"vector": "inline-menu",
"result": {
"result": true,
"conditions": {
"pass": [
{
"name": "isUsernameField",
"functionSource": "function isUsernameField(field) { ... }"
}
],
"fail": []
},
"meta": {
"timestamp": 1234567890500,
"vector": "inline-menu",
"fieldSnapshot": {
"opid": "opid_12345",
"type": "text",
"value": "[REDACTED]"
},
"tracingDepth": 0
}
},
"triggeredBy": "page-load"
}
],
"finalDecision": { ... }
}
]
}
```
### Summary Export Structure
```
================================================================================
Bitwarden Autofill Debug Summary
================================================================================
Session ID: session_1234567890_abc123
URL: https://example.com/login
Start Time: 2026-02-06T12:00:00.000Z
End Time: 2026-02-06T12:00:01.000Z
Duration: 1.00s
Total Fields Qualified: 2
--------------------------------------------------------------------------------
Field ID: opid_12345
Selector: input[type='text']#username
Attempts: 1
Final Decision: ✅ QUALIFIED
Passed Conditions:
✓ isUsernameField
✓ notCurrentlyInSandboxedIframe
Vector: inline-menu
Timestamp: 2026-02-06T12:00:00.500Z
================================================================================
⚠️ WARNING: This debug data may contain sensitive information.
Do not share this data publicly or with untrusted parties.
================================================================================
```
## Autofill Vectors
Debug data tracks which autofill vector triggered the qualification:
- **inline-menu**: Inline menu (autofill button) shown on field focus
- **popup-autofill**: Autofill triggered from popup UI
- **context-menu**: Autofill triggered from browser context menu
- **keyboard-shortcut**: Autofill triggered by keyboard shortcut (Ctrl+Shift+L)
- **page-load**: Autofill triggered automatically on page load
## 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)
## Performance Impact
- **Debug Disabled**: Zero performance overhead
- **Debug Enabled (depth 0)**: ~1-2ms per field qualification
- **Debug Enabled (depth 1)**: ~2-3ms per field qualification
- **Debug Enabled (depth 2+)**: ~3-5ms per field qualification
## Data Retention
- Sessions are stored **in-memory only** (never persisted to disk)
- Sessions automatically expire after **5 minutes**
- Maximum **100 qualifications per session** to prevent memory issues
- Field values are **always redacted** (`[REDACTED]`) to prevent PII leakage
## Security Considerations
⚠️ **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
## Troubleshooting Common Issues
### Debug API Not Available
```javascript
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 []
```
**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);
```
## Example Workflow
1. Enable debug mode and rebuild extension
2. Navigate to the problematic login page
3. Open browser DevTools console
4. Focus the form field in question
5. Check console for qualification messages
6. Export detailed data:
```javascript
const summary = __BITWARDEN_AUTOFILL_DEBUG__.exportSummary();
console.log(summary);
```
7. If needed, increase tracing depth for more detail:
```javascript
__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');
// 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

@@ -1,4 +1,5 @@
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
import { AutofillDebugService } from "../services/autofill-debug.service";
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
import DomElementVisibilityService from "../services/dom-element-visibility.service";
import { DomQueryService } from "../services/dom-query.service";
@@ -16,12 +17,21 @@ import AutofillInit from "./autofill-init";
const domQueryService = new DomQueryService();
const domElementVisibilityService = new DomElementVisibilityService(inlineMenuContentService);
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
const debugService = new AutofillDebugService();
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(
debugService.isDebugEnabled() ? debugService : undefined,
);
if (debugService.isDebugEnabled()) {
inlineMenuFieldQualificationService.setDebugService(debugService);
}
const autofillOverlayContentService = new AutofillOverlayContentService(
domQueryService,
domElementVisibilityService,
inlineMenuFieldQualificationService,
inlineMenuContentService,
debugService.isDebugEnabled() ? debugService : undefined,
);
windowContext.bitwardenAutofillInit = new AutofillInit(

View File

@@ -1,4 +1,5 @@
import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service";
import { AutofillDebugService } from "../services/autofill-debug.service";
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
import DomElementVisibilityService from "../services/dom-element-visibility.service";
import { DomQueryService } from "../services/dom-query.service";
@@ -11,11 +12,21 @@ import AutofillInit from "./autofill-init";
if (!windowContext.bitwardenAutofillInit) {
const domQueryService = new DomQueryService();
const domElementVisibilityService = new DomElementVisibilityService();
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
const debugService = new AutofillDebugService();
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(
debugService.isDebugEnabled() ? debugService : undefined,
);
if (debugService.isDebugEnabled()) {
inlineMenuFieldQualificationService.setDebugService(debugService);
}
const autofillOverlayContentService = new AutofillOverlayContentService(
domQueryService,
domElementVisibilityService,
inlineMenuFieldQualificationService,
undefined,
debugService.isDebugEnabled() ? debugService : undefined,
);
let overlayNotificationsContentService: undefined | OverlayNotificationsContentService;

View File

@@ -1,10 +1,13 @@
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";
import { AutofillDebugService } from "../services/autofill-debug.service";
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
import DomElementVisibilityService from "../services/dom-element-visibility.service";
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";
@@ -19,12 +22,21 @@ import AutofillInit from "./autofill-init";
const domQueryService = new DomQueryService();
const domElementVisibilityService = new DomElementVisibilityService(inlineMenuContentService);
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
const debugService = new AutofillDebugService();
const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(
debugService.isDebugEnabled() ? debugService : undefined,
);
if (debugService.isDebugEnabled()) {
inlineMenuFieldQualificationService.setDebugService(debugService);
}
const autofillOverlayContentService = new AutofillOverlayContentService(
domQueryService,
domElementVisibilityService,
inlineMenuFieldQualificationService,
inlineMenuContentService,
debugService.isDebugEnabled() ? debugService : undefined,
);
windowContext.bitwardenAutofillInit = new AutofillInit(
@@ -37,5 +49,39 @@ import AutofillInit from "./autofill-init";
setupAutofillInitDisconnectAction(windowContext);
windowContext.bitwardenAutofillInit.init();
if (devFlagEnabled("autofillDebugMode")) {
(windowContext as any).__BITWARDEN_AUTOFILL_DEBUG__ = {
exportSession: (format: DebugExportFormat = "json") => {
return debugService.exportCurrentSession(format);
},
exportSummary: () => {
return debugService.generateSummary(
Array.from(debugService.sessionStore.keys())[0] || "",
);
},
setTracingDepth: (depth: number) => {
debugService.setTracingDepth(depth);
console.log(`[Bitwarden Debug] Precondition tracing depth set to ${depth}`);
},
getTracingDepth: () => {
return debugService.getTracingDepth();
},
getSessions: () => {
return Array.from(debugService.sessionStore.keys());
},
};
console.log(
"%c[Bitwarden Debug] Autofill debug mode enabled. Use window.__BITWARDEN_AUTOFILL_DEBUG__",
"color: #175DDC; font-weight: bold; font-size: 12px",
);
console.log("Available methods:");
console.log(" - exportSession(format?: 'json' | 'summary' | 'console')");
console.log(" - exportSummary()");
console.log(" - setTracingDepth(depth: number)");
console.log(" - getTracingDepth()");
console.log(" - getSessions()");
}
}
})(window);

View File

@@ -0,0 +1,26 @@
import { AutofillVector, QualificationResult } from "../services/abstractions/inline-menu-field-qualifications.service";
export type AutofillDebugSession = {
sessionId: string;
startTime: number;
endTime?: number;
url: string;
qualifications: FieldQualificationRecord[];
};
export type FieldQualificationRecord = {
fieldId: string;
elementSelector: string;
attempts: QualificationAttempt[];
finalDecision?: QualificationResult;
};
export type QualificationAttempt = {
attemptId: string;
timestamp: number;
vector: AutofillVector;
result: QualificationResult;
triggeredBy?: string;
};
export type DebugExportFormat = "json" | "summary" | "console";

View File

@@ -11,7 +11,38 @@ export type AutofillKeywordsMap = WeakMap<
export type SubmitButtonKeywordsMap = WeakMap<HTMLElement, string>;
export type AutofillVector =
| "inline-menu"
| "popup-autofill"
| "context-menu"
| "keyboard-shortcut"
| "page-load";
export type QualificationCondition = {
name: string;
functionSource?: string;
};
export type QualificationMeta = {
timestamp: number;
vector: AutofillVector;
fieldSnapshot: AutofillField;
pageSnapshot?: Partial<AutofillPageDetails>;
preconditions?: QualificationResult[];
tracingDepth?: number;
};
export type QualificationResult = {
result: boolean;
conditions: {
pass: QualificationCondition[];
fail: QualificationCondition[];
};
meta?: QualificationMeta;
};
export interface InlineMenuFieldQualificationService {
setCurrentVector(vector: AutofillVector): void;
isUsernameField(field: AutofillField): boolean;
isCurrentPasswordField(field: AutofillField): boolean;
isUpdateCurrentPasswordField(field: AutofillField): boolean;

View File

@@ -0,0 +1,295 @@
import AutofillField from "../models/autofill-field";
import AutofillPageDetails from "../models/autofill-page-details";
import {
AutofillDebugSession,
DebugExportFormat,
FieldQualificationRecord,
QualificationAttempt,
} from "../models/autofill-debug-data";
import { devFlagEnabled } from "../../platform/flags";
import {
AutofillVector,
QualificationResult,
} from "./abstractions/inline-menu-field-qualifications.service";
export class AutofillDebugService {
private tracingDepth = 1;
private currentSession: AutofillDebugSession | null = null;
readonly sessionStore: Map<string, AutofillDebugSession> = new Map();
private readonly maxQualificationsPerSession = 100;
private readonly sessionTimeoutMs = 5 * 60 * 1000; // 5 minutes
isDebugEnabled(): boolean {
return devFlagEnabled("autofillDebugMode");
}
hasCurrentSession(): boolean {
return this.currentSession !== null;
}
getTracingDepth(): number {
return this.tracingDepth;
}
setTracingDepth(depth: number): void {
if (depth < 0) {
console.warn("[Bitwarden Debug] Tracing depth must be >= 0. Setting to 0.");
this.tracingDepth = 0;
return;
}
this.tracingDepth = depth;
}
startSession(url: string): string {
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
this.currentSession = {
sessionId,
startTime: Date.now(),
url,
qualifications: [],
};
this.sessionStore.set(sessionId, this.currentSession);
setTimeout(() => this.cleanupSession(sessionId), this.sessionTimeoutMs);
return sessionId;
}
endSession(): void {
if (this.currentSession) {
this.currentSession.endTime = Date.now();
this.currentSession = null;
}
}
recordQualification(
fieldId: string,
elementSelector: string,
vector: AutofillVector,
result: QualificationResult,
triggeredBy?: string,
): void {
if (!this.currentSession) {
return;
}
if (this.currentSession.qualifications.length >= this.maxQualificationsPerSession) {
console.warn(
`[Bitwarden Debug] Max qualifications (${this.maxQualificationsPerSession}) reached for session`,
);
return;
}
let fieldRecord = this.currentSession.qualifications.find((q) => q.fieldId === fieldId);
if (!fieldRecord) {
fieldRecord = {
fieldId,
elementSelector,
attempts: [],
};
this.currentSession.qualifications.push(fieldRecord);
}
const attempt: QualificationAttempt = {
attemptId: `attempt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
timestamp: Date.now(),
vector,
result,
triggeredBy,
};
fieldRecord.attempts.push(attempt);
fieldRecord.finalDecision = result;
}
exportCurrentSession(format: DebugExportFormat = "json"): string {
if (!this.currentSession) {
return format === "json" ? JSON.stringify({ error: "No active session" }) : "No active session";
}
return this.exportSession(this.currentSession.sessionId, format);
}
exportSession(sessionId: string, format: DebugExportFormat = "json"): string {
const session = this.sessionStore.get(sessionId);
if (!session) {
return format === "json" ? JSON.stringify({ error: "Session not found" }) : "Session not found";
}
switch (format) {
case "json":
return JSON.stringify(session, null, 2);
case "summary":
return this.generateSummary(sessionId);
case "console":
this.generateConsoleOutput(sessionId);
return "Output logged to console";
default:
return JSON.stringify(session, null, 2);
}
}
generateSummary(sessionId: string): string {
const session = this.sessionStore.get(sessionId);
if (!session) {
return "Session not found";
}
const lines: string[] = [];
lines.push("=".repeat(80));
lines.push("Bitwarden Autofill Debug Summary");
lines.push("=".repeat(80));
lines.push(`Session ID: ${session.sessionId}`);
lines.push(`URL: ${session.url}`);
lines.push(`Start Time: ${new Date(session.startTime).toISOString()}`);
if (session.endTime) {
lines.push(`End Time: ${new Date(session.endTime).toISOString()}`);
lines.push(`Duration: ${((session.endTime - session.startTime) / 1000).toFixed(2)}s`);
}
lines.push("");
lines.push(`Total Fields Qualified: ${session.qualifications.length}`);
lines.push("");
for (const fieldRecord of session.qualifications) {
lines.push("-".repeat(80));
lines.push(`Field ID: ${fieldRecord.fieldId}`);
lines.push(`Selector: ${fieldRecord.elementSelector}`);
lines.push(`Attempts: ${fieldRecord.attempts.length}`);
if (fieldRecord.finalDecision) {
lines.push(
`Final Decision: ${fieldRecord.finalDecision.result ? "✅ QUALIFIED" : "❌ REJECTED"}`,
);
if (fieldRecord.finalDecision.conditions.pass.length > 0) {
lines.push("");
lines.push("Passed Conditions:");
for (const condition of fieldRecord.finalDecision.conditions.pass) {
lines.push(`${condition.name}`);
}
}
if (fieldRecord.finalDecision.conditions.fail.length > 0) {
lines.push("");
lines.push("Failed Conditions:");
for (const condition of fieldRecord.finalDecision.conditions.fail) {
lines.push(`${condition.name}`);
}
}
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("");
}
lines.push("=".repeat(80));
lines.push("⚠️ WARNING: This debug data may contain sensitive information.");
lines.push("Do not share this data publicly or with untrusted parties.");
lines.push("=".repeat(80));
return lines.join("\n");
}
generateConsoleOutput(sessionId: string): void {
const session = this.sessionStore.get(sessionId);
if (!session) {
console.warn("[Bitwarden Debug] Session not found");
return;
}
console.group(
`%c[Bitwarden Debug] Session: ${session.sessionId}`,
"color: #175DDC; font-weight: bold; font-size: 14px",
);
console.log(`URL: ${session.url}`);
console.log(`Start Time: ${new Date(session.startTime).toISOString()}`);
if (session.endTime) {
console.log(`End Time: ${new Date(session.endTime).toISOString()}`);
console.log(`Duration: ${((session.endTime - session.startTime) / 1000).toFixed(2)}s`);
}
console.log(`Total Fields: ${session.qualifications.length}`);
for (const fieldRecord of session.qualifications) {
const icon = fieldRecord.finalDecision?.result ? "✅" : "❌";
const status = fieldRecord.finalDecision?.result ? "QUALIFIED" : "REJECTED";
console.group(`${icon} Field: ${fieldRecord.fieldId} (${status})`);
console.log(`Selector: ${fieldRecord.elementSelector}`);
console.log(`Attempts: ${fieldRecord.attempts.length}`);
if (fieldRecord.finalDecision) {
if (fieldRecord.finalDecision.conditions.pass.length > 0) {
console.group("✓ Passed Conditions");
for (const condition of fieldRecord.finalDecision.conditions.pass) {
console.log(`${condition.name}`);
if (condition.functionSource) {
console.log(`Function:`, condition.functionSource);
}
}
console.groupEnd();
}
if (fieldRecord.finalDecision.conditions.fail.length > 0) {
console.group("✗ Failed Conditions");
for (const condition of fieldRecord.finalDecision.conditions.fail) {
console.log(`${condition.name}`);
if (condition.functionSource) {
console.log(`Function:`, condition.functionSource);
}
}
console.groupEnd();
}
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(`Field Snapshot:`, fieldRecord.finalDecision.meta.fieldSnapshot);
if (fieldRecord.finalDecision.meta.pageSnapshot) {
console.log(`Page Snapshot:`, fieldRecord.finalDecision.meta.pageSnapshot);
}
if (fieldRecord.finalDecision.meta.preconditions) {
console.log(`Preconditions:`, fieldRecord.finalDecision.meta.preconditions);
}
console.groupEnd();
}
}
console.groupEnd();
}
console.groupEnd();
}
captureFieldSnapshot(field: AutofillField): AutofillField {
// Return a shallow copy to avoid capturing field values
return { ...field, value: "[REDACTED]" };
}
capturePageSnapshot(pageDetails: AutofillPageDetails): Partial<AutofillPageDetails> {
// Return only non-sensitive page information
return {
title: pageDetails.title,
url: pageDetails.url,
documentUrl: pageDetails.documentUrl,
forms: pageDetails.forms,
// Exclude fields array to avoid capturing field values
};
}
private cleanupSession(sessionId: string): void {
if (this.currentSession?.sessionId === sessionId) {
this.endSession();
}
this.sessionStore.delete(sessionId);
}
}

View File

@@ -56,6 +56,7 @@ import { DomElementVisibilityService } from "./abstractions/dom-element-visibili
import { DomQueryService } from "./abstractions/dom-query.service";
import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service";
import { AutoFillConstants } from "./autofill-constants";
import { AutofillDebugService } from "./autofill-debug.service";
export type QualificationCriteria = {
formFieldElement: ElementWithOpId<FormFieldElement>;
@@ -186,6 +187,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
private domElementVisibilityService: DomElementVisibilityService,
private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService,
private inlineMenuContentService?: AutofillInlineMenuContentService,
private debugService?: AutofillDebugService,
) {}
/**
@@ -225,11 +227,17 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
autofillFieldData: AutofillField,
pageDetails: AutofillPageDetails,
) {
if (this.debugService?.isDebugEnabled() && !this.debugService.hasCurrentSession()) {
this.debugService.startSession(globalThis.location.href);
this.inlineMenuFieldQualificationService.setCurrentVector("inline-menu");
}
const qualification = this.isQualifiedField({
formFieldElement,
autofillFieldData,
pageDetails,
});
if (!qualification) {
return;
}
@@ -314,19 +322,51 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
isQualifiedField(criteria: QualificationCriteria) {
const debugEnabled = this.debugService?.isDebugEnabled() ?? false;
const responses: QualificationResponse[] = [];
for (const definition of this.qualifiers) {
const response = this.qualify(definition, criteria);
responses.push(response);
if (response.result === false && definition?.blocking !== false) {
console.log({ element: criteria.formFieldElement, responses });
if (debugEnabled) {
console.group(
`%c❌ Field Rejected: ${criteria.autofillFieldData.opid}`,
"color: #ef4444; font-weight: bold",
);
console.log("Field:", criteria.formFieldElement);
console.log("Blocking condition failed:", response.alias);
console.log("Message:", response.message);
console.log("All responses:", responses);
console.groupEnd();
} else {
console.log({ element: criteria.formFieldElement, responses });
}
return false;
}
if (response.result === true && definition.effect) {
void definition.effect(criteria);
}
}
console.log({ element: criteria.formFieldElement, responses });
if (debugEnabled) {
console.group(
`%c✅ Field Qualified: ${criteria.autofillFieldData.opid}`,
"color: #10b981; font-weight: bold",
);
console.log("Field:", criteria.formFieldElement);
console.log(
"Passed conditions:",
responses.filter((r) => r.result).map((r) => r.alias),
);
console.log("All responses:", responses);
console.groupEnd();
} else {
console.log({ element: criteria.formFieldElement, responses });
}
return true;
}

View File

@@ -4,7 +4,9 @@ import { getSubmitButtonKeywordsSet, sendExtensionMessage } from "../utils";
import {
AutofillKeywordsMap,
AutofillVector,
InlineMenuFieldQualificationService as InlineMenuFieldQualificationServiceInterface,
QualificationResult,
SubmitButtonKeywordsMap,
} from "./abstractions/inline-menu-field-qualifications.service";
import {
@@ -15,6 +17,7 @@ import {
SubmitLoginButtonNames,
} from "./autofill-constants";
import AutofillService from "./autofill.service";
import { AutofillDebugService } from "./autofill-debug.service";
export class InlineMenuFieldQualificationService implements InlineMenuFieldQualificationServiceInterface {
private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
@@ -149,6 +152,66 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
]);
private totpFieldAutocompleteValue = "one-time-code";
private premiumEnabled = false;
private currentVector: AutofillVector = "inline-menu";
private debugService?: AutofillDebugService;
private get debugEnabled(): boolean {
return this.debugService?.isDebugEnabled() ?? false;
}
setCurrentVector(vector: AutofillVector): void {
this.currentVector = vector;
}
setDebugService(debugService: AutofillDebugService): void {
this.debugService = debugService;
}
/**
* Wraps a qualification function to return QualificationResult with debug metadata.
*
* @param name - Human-readable name for the condition
* @param fn - The qualification function to wrap
* @param field - The field being qualified
* @param pageDetails - Optional page details
* @param currentDepth - Current depth for precondition tracing
*/
private wrapQualifier(
name: string,
fn: (field: AutofillField, pageDetails?: AutofillPageDetails) => boolean,
field: AutofillField,
pageDetails?: AutofillPageDetails,
currentDepth = 0,
): QualificationResult {
const result = fn.call(this, field, pageDetails);
if (!this.debugEnabled) {
return { result, conditions: { pass: [], fail: [] } };
}
const maxDepth = this.debugService?.getTracingDepth() ?? 1;
const shouldTrace = currentDepth < maxDepth;
const condition = {
name,
functionSource: shouldTrace ? fn.toString() : undefined,
};
return {
result,
conditions: {
pass: result ? [condition] : [],
fail: result ? [] : [condition],
},
meta: {
timestamp: Date.now(),
vector: this.currentVector,
fieldSnapshot: this.debugService?.captureFieldSnapshot(field) ?? field,
pageSnapshot: pageDetails ? this.debugService?.capturePageSnapshot(pageDetails) : undefined,
tracingDepth: currentDepth,
},
};
}
/**
* Validates the provided field to indicate if the field is a new email field used for account creation/registration.
@@ -207,7 +270,8 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
return false;
}
constructor() {
constructor(debugService?: AutofillDebugService) {
this.debugService = debugService;
void sendExtensionMessage("getUserPremiumStatus").then((premiumStatus) => {
this.premiumEnabled = !!premiumStatus?.result;
});
@@ -938,6 +1002,16 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
* @param field - The field to validate
*/
isUsernameField = (field: AutofillField): boolean => {
return this.isUsernameFieldWithResult(field).result;
};
/**
* Validates the provided field as a username field with debug metadata.
*
* @param field - The field to validate
* @param depth - Current depth for precondition tracing
*/
isUsernameFieldWithResult(field: AutofillField, depth = 0): QualificationResult {
const fieldType = field.type;
if (
!fieldType ||
@@ -946,11 +1020,12 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
this.fieldHasDisqualifyingAttributeValue(field) ||
this.isTotpField(field)
) {
return false;
return this.wrapQualifier("isUsernameField", () => false, field, undefined, depth);
}
return this.keywordsFoundInFieldData(field, AutoFillConstants.UsernameFieldNames);
};
const result = this.keywordsFoundInFieldData(field, AutoFillConstants.UsernameFieldNames);
return this.wrapQualifier("isUsernameField", () => result, field, undefined, depth);
}
/**
* Validates the provided field as an email field.
@@ -974,15 +1049,44 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
* @param field - The field to validate
*/
isCurrentPasswordField = (field: AutofillField): boolean => {
return this.isCurrentPasswordFieldWithResult(field).result;
};
/**
* Validates the provided field as a current password field with debug metadata.
*
* @param field - The field to validate
* @param depth - Current depth for precondition tracing
*/
isCurrentPasswordFieldWithResult(field: AutofillField, depth = 0): QualificationResult {
const maxDepth = this.debugService?.getTracingDepth() ?? 1;
if (
this.fieldContainsAutocompleteValues(field, this.newPasswordAutoCompleteValue) ||
this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords)
) {
return false;
return this.wrapQualifier("isCurrentPasswordField", () => false, field, undefined, depth);
}
return this.isPasswordField(field);
};
const passwordCheck =
depth < maxDepth
? this.isPasswordFieldWithResult(field, depth + 1)
: this.wrapQualifier("isPasswordField", this.isPasswordField, field, undefined, depth);
if (!passwordCheck.result) {
return {
result: false,
conditions: passwordCheck.conditions,
meta: {
...passwordCheck.meta,
preconditions: [passwordCheck],
tracingDepth: depth,
},
};
}
return this.wrapQualifier("isCurrentPasswordField", () => true, field, undefined, depth);
}
/**
* Validates the provided field as a current password field for an update password form.
@@ -1006,15 +1110,46 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
* @param field - The field to validate
*/
isNewPasswordField = (field: AutofillField): boolean => {
return this.isNewPasswordFieldWithResult(field).result;
};
/**
* Validates the provided field as a new password field with debug metadata.
*
* @param field - The field to validate
* @param depth - Current depth for precondition tracing
*/
isNewPasswordFieldWithResult(field: AutofillField, depth = 0): QualificationResult {
const maxDepth = this.debugService?.getTracingDepth() ?? 1;
if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) {
return false;
return this.wrapQualifier("isNewPasswordField", () => false, field, undefined, depth);
}
return (
this.isPasswordField(field) &&
this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords)
);
};
const passwordCheck =
depth < maxDepth
? this.isPasswordFieldWithResult(field, depth + 1)
: this.wrapQualifier("isPasswordField", this.isPasswordField, field, undefined, depth);
if (!passwordCheck.result) {
return {
result: false,
conditions: passwordCheck.conditions,
meta: {
...passwordCheck.meta,
preconditions: [passwordCheck],
tracingDepth: depth,
},
};
}
const keywordCheck = this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords);
if (!keywordCheck) {
return this.wrapQualifier("isNewPasswordField", () => false, field, undefined, depth);
}
return this.wrapQualifier("isNewPasswordField", () => true, field, undefined, depth);
}
/**
* Validates the provided field as a password field.
@@ -1022,6 +1157,16 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
* @param field - The field to validate
*/
private isPasswordField = (field: AutofillField): boolean => {
return this.isPasswordFieldWithResult(field).result;
};
/**
* Validates the provided field as a password field with debug metadata.
*
* @param field - The field to validate
* @param depth - Current depth for precondition tracing
*/
private isPasswordFieldWithResult(field: AutofillField, depth = 0): QualificationResult {
const isInputPasswordType = field.type === "password";
if (
(!isInputPasswordType &&
@@ -1029,11 +1174,12 @@ export class InlineMenuFieldQualificationService implements InlineMenuFieldQuali
this.fieldHasDisqualifyingAttributeValue(field) ||
this.isTotpField(field)
) {
return false;
return this.wrapQualifier("isPasswordField", () => false, field, undefined, depth);
}
return isInputPasswordType || this.isLikePasswordField(field);
};
const result = isInputPasswordType || this.isLikePasswordField(field);
return this.wrapQualifier("isPasswordField", () => result, field, undefined, depth);
}
/**
* Validates the provided field as a field to indicate if the

View File

@@ -14,6 +14,7 @@ export type Flags = SharedFlags;
// required to avoid linting errors when there are no flags
export type DevFlags = {
managedEnvironment?: GroupPolicyEnvironment;
autofillDebugMode?: boolean;
} & SharedDevFlags;
export function flagEnabled(flag: keyof Flags): boolean {