@webability/core 1.1.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -11,6 +11,18 @@ interface PageLike {
11
11
  content: string;
12
12
  }): Promise<unknown>;
13
13
  url(): string;
14
+ /**
15
+ * Real keyboard event dispatch (Playwright & Puppeteer both expose this).
16
+ * Optional so minimal PageLike shims still typecheck. The focusVisible
17
+ * detector uses `keyboard.press('Tab')` to put the browser into keyboard
18
+ * interaction modality before probing focus styles — otherwise
19
+ * `:focus-visible` / keyboard-detection-class patterns (focus-visible
20
+ * polyfill, `.user-is-tabbing` body class) never activate under a bare
21
+ * programmatic `.focus()`, producing mass false positives.
22
+ */
23
+ keyboard?: {
24
+ press(key: string): Promise<void>;
25
+ };
14
26
  }
15
27
  /** All fix issue types the pipeline can detect and fix. */
16
28
  type FixIssueType = 'weak_button_name' | 'weak_link_name' | 'weak_heading' | 'weak_iframe_title' | 'weak_page_title' | 'weak_svg_name' | 'weak_skip_link' | 'missing_button_role' | 'missing_semantic_role' | 'missing_landmark' | 'heading_skip' | 'svg_missing_name' | 'aria_hidden_focusable' | 'adjacent_redundant_link' | 'contrast_insufficient' | 'non_text_contrast_insufficient' | 'target_too_small' | 'focus_not_visible' | 'empty_link' | 'unlabeled_button' | 'missing_label' | 'decorative_icon' | 'skip_link' | 'aria_live' | 'missing_alt' | 'identify_input_purpose' | 'redundant_role' | 'duplicate_id' | 'new_window_link' | 'missing_fieldset_legend' | 'missing_table_header' | 'missing_table_caption' | 'missing_table_scope' | 'external_link_warning' | 'download_link_warning' | 'broken_anchor' | 'placeholder_link' | 'missing_aria_required' | 'missing_pattern_title' | 'missing_autocomplete' | 'missing_media_controls' | 'unsafe_autoplay' | 'invalid_aria_role' | 'zoom_restriction' | 'missing_button_type' | 'link_consistency' | 'semantic_emphasis' | 'keyboard_trap' | 'focus_order_disruption' | 'missing_captions' | 'color_only_link' | 'label_in_name' | 'onfocus_context_change' | 'focus_obscured_by_sticky' | 'text_spacing_blocked' | 'nested_landmark' | 'combobox_name_missing' | 'landmark_duplicate_unlabeled' | 'motion_uncontrollable' | 'auto_refresh' | 'motion_without_reduced_motion' | 'flashing_content' | 'animated_gif_no_pause' | 'missing_required_indicator' | 'background_image_unlabeled' | 'missing_h1' | 'multiple_h1' | 'empty_heading' | 'javascript_link' | 'broken_aria_reference' | 'main_content_hidden' | 'scrollable_not_focusable' | 'meaningful_sequence' | 'sensory_characteristic' | 'orientation_lock' | 'image_of_text' | 'reflow_overflow' | 'missing_audio_description' | 'missing_transcript' | 'character_key_shortcut' | 'on_input_change' | 'pointer_gesture' | 'pointer_cancellation' | 'motion_actuation' | 'error_prevention_missing' | 'insufficient_navigation' | 'inconsistent_navigation';
@@ -116,9 +128,33 @@ interface ScanIssue {
116
128
  needsManualReview: boolean;
117
129
  };
118
130
  }
131
+ /**
132
+ * Signal that the scan never reached the intended page — the target served a
133
+ * bot-challenge / interstitial (Cloudflare "Just a moment…", "Access Denied",
134
+ * a CAPTCHA wall) or the main document returned an HTTP error status. When
135
+ * present, `issues`/`incomplete` are empty and MUST NOT be read as "the page is
136
+ * clean": nothing real was scanned. Lets a caller distinguish "scanned and
137
+ * clean" from "was never actually scanned".
138
+ */
139
+ interface BlockedInfo {
140
+ /** Human-readable explanation of why the page could not be scanned. */
141
+ reason: string;
142
+ /** Challenge vendor when identifiable (e.g. 'Cloudflare', 'PerimeterX/HUMAN'). */
143
+ vendor?: string;
144
+ /** HTTP status of the main document response, when known. */
145
+ status?: number;
146
+ /** The specific signal that matched (page title, marker selector, or status). */
147
+ signal?: string;
148
+ }
119
149
  interface ScanResult {
120
150
  url: string;
121
151
  timestamp: string;
152
+ /**
153
+ * Set when the target served a bot-challenge/interstitial or an HTTP error
154
+ * instead of the real page. When present, treat `issues`/`incomplete` as
155
+ * "not scanned" (they are empty), NOT as "no problems found".
156
+ */
157
+ blocked?: BlockedInfo;
122
158
  /** High-confidence violations. Safe to surface as bugs. */
123
159
  issues: ScanIssue[];
124
160
  /** Detectors that hit their per-page cap this run — their counts are lower
@@ -186,6 +222,16 @@ interface ScanOptions {
186
222
  * wait cap in ms. Default: enabled with a 4000ms cap.
187
223
  */
188
224
  settle?: boolean | number;
225
+ /**
226
+ * HTTP status of the main-document response, for the Page-object call shape
227
+ * where the CALLER navigated (e.g. mcp's flow_scan does its own `page.goto`).
228
+ * The URL-string path captures this itself; on the Page path core cannot see
229
+ * the response, so a plain 4xx/5xx error page with no challenge DOM markers
230
+ * would otherwise be scanned as a clean page. Pass the caller's
231
+ * `response.status()` here so bot-challenge/error-page detection works
232
+ * uniformly across both entrypoints. Ignored on the URL-string path.
233
+ */
234
+ mainStatus?: number;
189
235
  }
190
236
 
191
237
  declare function scan(target: string | PageLike, options?: ScanOptions): Promise<ScanResult>;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,18 @@ interface PageLike {
11
11
  content: string;
12
12
  }): Promise<unknown>;
13
13
  url(): string;
14
+ /**
15
+ * Real keyboard event dispatch (Playwright & Puppeteer both expose this).
16
+ * Optional so minimal PageLike shims still typecheck. The focusVisible
17
+ * detector uses `keyboard.press('Tab')` to put the browser into keyboard
18
+ * interaction modality before probing focus styles — otherwise
19
+ * `:focus-visible` / keyboard-detection-class patterns (focus-visible
20
+ * polyfill, `.user-is-tabbing` body class) never activate under a bare
21
+ * programmatic `.focus()`, producing mass false positives.
22
+ */
23
+ keyboard?: {
24
+ press(key: string): Promise<void>;
25
+ };
14
26
  }
15
27
  /** All fix issue types the pipeline can detect and fix. */
16
28
  type FixIssueType = 'weak_button_name' | 'weak_link_name' | 'weak_heading' | 'weak_iframe_title' | 'weak_page_title' | 'weak_svg_name' | 'weak_skip_link' | 'missing_button_role' | 'missing_semantic_role' | 'missing_landmark' | 'heading_skip' | 'svg_missing_name' | 'aria_hidden_focusable' | 'adjacent_redundant_link' | 'contrast_insufficient' | 'non_text_contrast_insufficient' | 'target_too_small' | 'focus_not_visible' | 'empty_link' | 'unlabeled_button' | 'missing_label' | 'decorative_icon' | 'skip_link' | 'aria_live' | 'missing_alt' | 'identify_input_purpose' | 'redundant_role' | 'duplicate_id' | 'new_window_link' | 'missing_fieldset_legend' | 'missing_table_header' | 'missing_table_caption' | 'missing_table_scope' | 'external_link_warning' | 'download_link_warning' | 'broken_anchor' | 'placeholder_link' | 'missing_aria_required' | 'missing_pattern_title' | 'missing_autocomplete' | 'missing_media_controls' | 'unsafe_autoplay' | 'invalid_aria_role' | 'zoom_restriction' | 'missing_button_type' | 'link_consistency' | 'semantic_emphasis' | 'keyboard_trap' | 'focus_order_disruption' | 'missing_captions' | 'color_only_link' | 'label_in_name' | 'onfocus_context_change' | 'focus_obscured_by_sticky' | 'text_spacing_blocked' | 'nested_landmark' | 'combobox_name_missing' | 'landmark_duplicate_unlabeled' | 'motion_uncontrollable' | 'auto_refresh' | 'motion_without_reduced_motion' | 'flashing_content' | 'animated_gif_no_pause' | 'missing_required_indicator' | 'background_image_unlabeled' | 'missing_h1' | 'multiple_h1' | 'empty_heading' | 'javascript_link' | 'broken_aria_reference' | 'main_content_hidden' | 'scrollable_not_focusable' | 'meaningful_sequence' | 'sensory_characteristic' | 'orientation_lock' | 'image_of_text' | 'reflow_overflow' | 'missing_audio_description' | 'missing_transcript' | 'character_key_shortcut' | 'on_input_change' | 'pointer_gesture' | 'pointer_cancellation' | 'motion_actuation' | 'error_prevention_missing' | 'insufficient_navigation' | 'inconsistent_navigation';
@@ -116,9 +128,33 @@ interface ScanIssue {
116
128
  needsManualReview: boolean;
117
129
  };
118
130
  }
131
+ /**
132
+ * Signal that the scan never reached the intended page — the target served a
133
+ * bot-challenge / interstitial (Cloudflare "Just a moment…", "Access Denied",
134
+ * a CAPTCHA wall) or the main document returned an HTTP error status. When
135
+ * present, `issues`/`incomplete` are empty and MUST NOT be read as "the page is
136
+ * clean": nothing real was scanned. Lets a caller distinguish "scanned and
137
+ * clean" from "was never actually scanned".
138
+ */
139
+ interface BlockedInfo {
140
+ /** Human-readable explanation of why the page could not be scanned. */
141
+ reason: string;
142
+ /** Challenge vendor when identifiable (e.g. 'Cloudflare', 'PerimeterX/HUMAN'). */
143
+ vendor?: string;
144
+ /** HTTP status of the main document response, when known. */
145
+ status?: number;
146
+ /** The specific signal that matched (page title, marker selector, or status). */
147
+ signal?: string;
148
+ }
119
149
  interface ScanResult {
120
150
  url: string;
121
151
  timestamp: string;
152
+ /**
153
+ * Set when the target served a bot-challenge/interstitial or an HTTP error
154
+ * instead of the real page. When present, treat `issues`/`incomplete` as
155
+ * "not scanned" (they are empty), NOT as "no problems found".
156
+ */
157
+ blocked?: BlockedInfo;
122
158
  /** High-confidence violations. Safe to surface as bugs. */
123
159
  issues: ScanIssue[];
124
160
  /** Detectors that hit their per-page cap this run — their counts are lower
@@ -186,6 +222,16 @@ interface ScanOptions {
186
222
  * wait cap in ms. Default: enabled with a 4000ms cap.
187
223
  */
188
224
  settle?: boolean | number;
225
+ /**
226
+ * HTTP status of the main-document response, for the Page-object call shape
227
+ * where the CALLER navigated (e.g. mcp's flow_scan does its own `page.goto`).
228
+ * The URL-string path captures this itself; on the Page path core cannot see
229
+ * the response, so a plain 4xx/5xx error page with no challenge DOM markers
230
+ * would otherwise be scanned as a clean page. Pass the caller's
231
+ * `response.status()` here so bot-challenge/error-page detection works
232
+ * uniformly across both entrypoints. Ignored on the URL-string path.
233
+ */
234
+ mainStatus?: number;
189
235
  }
190
236
 
191
237
  declare function scan(target: string | PageLike, options?: ScanOptions): Promise<ScanResult>;