@webability/core 1.11.0 → 1.13.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/engine.d.ts CHANGED
@@ -1,4 +1,40 @@
1
- import { c as ScanIssue, m as JudgeOptions, I as InternalBug, q as JudgedEntry, P as PageLike } from './types-jXnoW_NT.js';
1
+ import { c as ScanIssue, m as JudgeOptions, I as InternalBug, q as JudgedEntry, P as PageLike } from './types-gjAJ8j0I.js';
2
+
3
+ /**
4
+ * Visitor HTML (`input: { html, baseURL }`): what the page may fetch while the
5
+ * served document is scanned, and the wait for its styles.
6
+ *
7
+ * The html is answered AS the response for baseURL. Every other request goes
8
+ * through `htmlRequestPolicy`: only stylesheets, fonts and images load (what
9
+ * computed styles and pixel contrast need); iframes, media, fetches, beacons
10
+ * and any other document are aborted, and so is a URL whose query carries a
11
+ * token-like key (a signed or session URL captured from a visitor must not be
12
+ * replayed from our IP). The adapters run the policy in the same interception
13
+ * the caller's guard uses, so that guard still sees, and can abort, every
14
+ * request the policy lets through.
15
+ */
16
+ /** One intercepted request, as the policy reads it. */
17
+ interface HtmlRequest {
18
+ url: string;
19
+ /** Driver resource type ('document', 'stylesheet', 'image', 'xhr'…). */
20
+ resourceType: string;
21
+ /** A navigation (document) request. */
22
+ navigation: boolean;
23
+ /** Issued by the top frame (false for an iframe's document). */
24
+ mainFrame: boolean;
25
+ }
26
+ /** fulfill: answer with the html. continue: let it through (to the caller's guard, then the network). abort: refuse. */
27
+ type HtmlDecision = 'fulfill' | 'continue' | 'abort';
28
+ type HtmlRequestPolicy = (r: HtmlRequest) => HtmlDecision;
29
+ /** Query keys of signed, session or nonce URLs. */
30
+ declare const TOKEN_QUERY_KEY: RegExp;
31
+ /**
32
+ * The request policy for html served at `target`. `scripts: true` (the html's
33
+ * scripts run) also lets script requests through.
34
+ */
35
+ declare function htmlRequestPolicy(target: string, opts?: {
36
+ scripts?: boolean;
37
+ }): HtmlRequestPolicy;
2
38
 
3
39
  /**
4
40
  * The one engine's contract: two injected drivers in, one result out.
@@ -16,10 +52,14 @@ import { c as ScanIssue, m as JudgeOptions, I as InternalBug, q as JudgedEntry,
16
52
  * (`fromPlaywright`: its `evaluate` takes one argument). Optional members widen what the engine can
17
53
  * do; each absent one is named in `stats.warnings`, never guessed around.
18
54
  *
19
- * Isolation is the driver's: for `html` input the engine removes `<script>`
20
- * (HTML and SVG) and meta refresh but keeps inline handler attributes (detectors read them),
21
- * so the driver must give the page no egress it would not give a scanned site
22
- * (the hosted MCP's SSRF route, a sandbox), or turn JavaScript off.
55
+ * Isolation is the driver's: for snapshot input (`{ html, url }`) the engine
56
+ * removes `<script>` (HTML and SVG) and meta refresh but keeps inline handler
57
+ * attributes (detectors read them), so the driver must give the page no egress
58
+ * it would not give a scanned site (the hosted MCP's SSRF route, a sandbox),
59
+ * or turn JavaScript off. Visitor HTML (`{ html, baseURL }`) also loses
60
+ * `<noscript>`, its `on*` handlers are commented out and its `javascript:` URLs
61
+ * become `javascript:void(0)`, and its requests go through
62
+ * `htmlRequestPolicy` (see `loadHtml`).
23
63
  */
24
64
  interface Page {
25
65
  /** A Response-like return (`status()`) is read for the main document's HTTP status. */
@@ -55,8 +95,21 @@ interface Page {
55
95
  * Serve `html` as the document at `url` and navigate there (no network fetch
56
96
  * of the page itself). Absent: a snapshot stays on about:blank with a
57
97
  * `<base href>`, and `location` is about:blank.
98
+ *
99
+ * `policy` decides every request the page makes until the returned release
100
+ * function runs (the engine calls it when the scan is done): 'fulfill' is
101
+ * answered with `html`, 'abort' is refused, 'continue' goes on to the
102
+ * caller's own interception (a private-target guard), which may still abort
103
+ * it. The policy must compose with that guard, never replace it: Puppeteer
104
+ * cooperative interception at the default priority, Playwright
105
+ * `route.fallback()`. Absent policy: only the document is answered.
58
106
  */
59
- loadHtml?(html: string, url: string): Promise<void>;
107
+ loadHtml?(html: string, url: string, policy?: HtmlRequestPolicy): Promise<void | (() => Promise<void>)>;
108
+ /** Set the viewport (CSS px). Absent: an input viewport is not applied, named in `stats.warnings`. */
109
+ setViewport?(size: {
110
+ width: number;
111
+ height: number;
112
+ }): Promise<void>;
60
113
  /** Script injection (Playwright/Puppeteer). Absent: string `evaluate` is used. */
61
114
  addScriptTag?(opts: {
62
115
  content?: string;
@@ -98,20 +151,74 @@ interface Model {
98
151
  }): Promise<ModelResponse>;
99
152
  }
100
153
  /**
101
- * What to scan. `url` alone: the engine loads it on the page. `html`: a DOM
102
- * snapshot (the widget Worker's input) loaded into the page, with `url` as its
103
- * base and the source of the fix records' page path.
154
+ * What to scan. `url` alone: the engine loads it on the page. `url` with
155
+ * `loaded: true`: the caller already navigated the page there (its own
156
+ * interception, waits and status); the engine scans it as it is and reads the
157
+ * HTTP status from `opts.mainStatus`. `html`: a DOM snapshot (the widget
158
+ * Worker's input) loaded into the page, with `url` as its base and the source
159
+ * of the fix records' page path.
104
160
  */
105
161
  type EngineInput = {
106
162
  url: string;
107
163
  html?: undefined;
164
+ loaded?: boolean;
165
+ baseURL?: undefined;
108
166
  } | {
109
167
  html: string;
110
168
  url?: string;
169
+ loaded?: undefined;
170
+ baseURL?: undefined;
171
+ }
172
+ /**
173
+ * Visitor HTML (the widget's capture): served AS the response for `baseURL`
174
+ * through the driver's `loadHtml`, with no fetch of the main document, so
175
+ * relative stylesheets, images and fonts load from the site and computed
176
+ * styles are real. Before serving, `<script>`, `<noscript>` and meta
177
+ * refresh are removed, every `on*` handler keeps its attribute with its text
178
+ * inside a comment (it runs nothing; detectors still read it), every
179
+ * `javascript:` URL becomes `javascript:void(0)`, and a relative
180
+ * `<base href>` is made absolute against `baseURL` (unless `scripts: true`:
181
+ * served as captured). Only stylesheets, fonts and images load
182
+ * (`htmlRequestPolicy`). Stylesheets and fonts are waited for (8 s bound),
183
+ * then CSS animations are put at their end state (carousels excepted) and
184
+ * frozen, before the scan. `viewport` sets the page size first.
185
+ * A driver without `loadHtml` fails closed: nothing is loaded or scanned,
186
+ * `stats.input` is 'html-blank' and `stats.errors` says why.
187
+ */
188
+ | {
189
+ html: string;
190
+ baseURL: string;
191
+ scripts?: boolean;
192
+ viewport?: {
193
+ width: number;
194
+ height: number;
195
+ };
196
+ url?: undefined;
197
+ loaded?: undefined;
111
198
  };
112
199
  interface EngineOptions {
113
- /** Highest WCAG level reported. Default 'AA'. */
200
+ /**
201
+ * Highest WCAG level reported. Default 'AA'. At 'A'/'AA' a fix is emitted
202
+ * only when its write remedies a WCAG 2.2 criterion at that level
203
+ * (guards/aaFixGate); findings are reported either way.
204
+ */
114
205
  level?: 'A' | 'AA' | 'AAA';
206
+ /**
207
+ * Also run core's experimental best-practice checks (default false):
208
+ * adjacent_redundant_link, missing_button_type, decorative_image_with_title,
209
+ * nested_landmark. They fail no WCAG 2.2 A/AA criterion, so the default run
210
+ * reports none of them. Passed to scan() as `experimental`.
211
+ */
212
+ includeBestPractice?: boolean;
213
+ /**
214
+ * Post-fix verification (default true): the structural fixers run, every
215
+ * fix is applied to the page, axe runs again and a fix that introduces a
216
+ * violation is dropped; the page is restored. false: never-harm only, no
217
+ * structural fixes, `verification` absent.
218
+ */
219
+ verify?: boolean;
220
+ /** Axe runs verification may spend isolating regressions. Default 40. */
221
+ verifyMaxAxeRuns?: number;
115
222
  /** HTTP status of the main document when the caller navigated. */
116
223
  mainStatus?: number;
117
224
  /**
@@ -151,30 +258,115 @@ interface EngineFix {
151
258
  /** What the fix rests on: the finding's message and the element markup. */
152
259
  evidence: string;
153
260
  engineVersion: string;
261
+ /**
262
+ * Post-fix verification: 'verified' (applied on the page, axe found no new
263
+ * violation on it, its ancestors or descendants); 'not_verifiable' (a
264
+ * manifest operation that is not an attribute write, or verification could
265
+ * not run: `stats.verify.error`). Rejected fixes are not emitted. Absent
266
+ * with `opts.verify: false`.
267
+ */
268
+ verification?: 'verified' | 'not_verifiable';
154
269
  }
155
270
  /** One reported finding (scan()'s issue row). */
156
271
  type EngineIssue = ScanIssue;
272
+ /**
273
+ * A WebAbility detector's own record of a reported issue: the fields scan()'s
274
+ * issue row drops, for a consumer that writes values itself (the platform's
275
+ * DetectedIssue). One per `source: 'webability'` issue in `issues[]`, same
276
+ * `id`; axe-core rows have none. Only issues that survived scan()'s judging,
277
+ * dedup and level gate get one.
278
+ */
279
+ interface EngineFinding {
280
+ /** The issue row's id (`issues[k].id`). */
281
+ id: string;
282
+ selector: string;
283
+ /** Attribute the fix targets (alt, aria-label, role, style…). */
284
+ attribute: string;
285
+ /** What the element has now ('' when missing). */
286
+ currentValue: string;
287
+ issueType: string;
288
+ wcag: string;
289
+ /** The detector's context: visible text, nearby heading, parent tag, href. */
290
+ context: string;
291
+ /** The value must be written by a model (alt text, a label). */
292
+ needsAI: boolean;
293
+ /** The detector's known value, as the detector produced it (style values keep `!important`). Absent: no known value. */
294
+ fixValue?: string;
295
+ /** Image source, for alt text written from the image. */
296
+ imageSrc?: string;
297
+ /** The element's outer HTML, first 300 characters. */
298
+ outerHtml?: string;
299
+ /** Must not auto-apply: the detector marked the verdict uncertain (`needsReview`) or the fix not mechanical (`manualFix`). */
300
+ needsReview: boolean;
301
+ /** The detector's reason for review. */
302
+ reviewReason?: string;
303
+ /** State pseudo-class the detector's style fix applies in (`:focus-visible`); `selector` names the element at rest. */
304
+ pseudo?: string;
305
+ }
157
306
 
158
307
  interface EngineStats {
159
308
  engineVersion: string;
160
- input: 'url' | 'snapshot';
309
+ /**
310
+ * 'loaded': the caller's page was scanned as it was, with no navigation.
311
+ * 'html': visitor html served at its baseURL; 'html-blank': visitor html
312
+ * refused, unscanned, because the driver had no `loadHtml` to hold the
313
+ * request policy.
314
+ */
315
+ input: 'url' | 'loaded' | 'snapshot' | 'html' | 'html-blank';
161
316
  ms: number;
162
317
  deterministic: {
163
318
  issues: number;
164
319
  bySource: Record<string, number>;
165
320
  engineWarnings: string[];
166
321
  };
322
+ /**
323
+ * Fix bookkeeping. Every candidate is either emitted or in `rejected`:
324
+ * `candidates === emitted + rejected.length === emitted + sum(rejectedBy*)`.
325
+ * `candidates` includes the structural fixes (`structuralCandidates` of them).
326
+ * `rejectedBySelector`: the selector matched no element (or did not parse, or
327
+ * could not be checked) on the page at fix time, why `selector_unmatched...`.
328
+ * `rejectedByDuplicate`: a structural fix whose attribute another fix writes,
329
+ * or a detector candidate whose (selector, attribute) an earlier candidate of
330
+ * another issue type already writes (why `duplicate_candidate`). Only
331
+ * candidates that passed the validator and the A/AA gate take part, so a
332
+ * rejected candidate never blocks a valid one.
333
+ * `rejectedByEmptyValue`: a detector fix with an empty value on an attribute
334
+ * other than alt (a boolean attribute such as controls=""), why `empty_value`.
335
+ */
167
336
  fixes: {
168
337
  candidates: number;
338
+ structuralCandidates: number;
169
339
  emitted: number;
170
340
  rejectedByValidator: number;
341
+ rejectedByGate: number;
171
342
  rejectedByHidingGuard: number;
343
+ rejectedBySelector: number;
344
+ rejectedByDuplicate: number;
345
+ rejectedByEmptyValue: number;
346
+ rejectedByNeverHarm: number;
347
+ rejectedByVerify: number;
172
348
  rejected: Array<{
173
349
  sel: string;
174
350
  attr: string;
175
351
  why: string;
176
352
  }>;
177
353
  };
354
+ /**
355
+ * Post-fix verification. `structural`: structural fixes that joined the
356
+ * batch. `verified` / `rejected` / `notVerifiable`: per-fix outcomes
357
+ * (rejected fixes are not emitted). `axeRuns` includes the pre-fix run.
358
+ */
359
+ verify: {
360
+ ran: boolean;
361
+ skipped?: string;
362
+ error?: string;
363
+ structural: number;
364
+ verified: number;
365
+ rejected: number;
366
+ notVerifiable: number;
367
+ axeRuns: number;
368
+ ms: number;
369
+ };
178
370
  /** No agent stage yet: always `ran: false`, and `skipped` says why. */
179
371
  agent: {
180
372
  ran: false;
@@ -197,6 +389,8 @@ interface EngineResult {
197
389
  issues: EngineIssue[];
198
390
  incomplete: EngineIssue[];
199
391
  fixes: EngineFix[];
392
+ /** The detector record of each WebAbility issue (see EngineFinding). */
393
+ findings: EngineFinding[];
200
394
  internal_bugs: InternalBug[];
201
395
  stats: EngineStats;
202
396
  /** Verdicts of core's in-scan judge (`opts.scan.judge`) on findings the engines could not decide. */
@@ -237,18 +431,39 @@ declare function toPageLike(page: Page, fallbackUrl: string): PageLike;
237
431
  * are removed, so the snapshot cannot run its scripts or navigate the page on
238
432
  * load.
239
433
  *
240
- * Inline event-handler attributes (`onclick`, `onerror`…) are KEPT: detectors
241
- * read their text (missingRoles, linkWarnings, targetSize, timingAndInput). An
242
- * `<img onerror>` can therefore still run when the snapshot loads; isolating
243
- * that (no egress, or JavaScript off) is the driver's job, as stated on
244
- * `Page`.
434
+ * Snapshot input keeps inline event-handler attributes (`onclick`,
435
+ * `onerror`…): detectors read their text (missingRoles, linkWarnings,
436
+ * targetSize, timingAndInput). An `<img onerror>` can therefore still run when
437
+ * the snapshot loads; isolating that (no egress, or JavaScript off) is the
438
+ * driver's job, as stated on `Page`.
439
+ *
440
+ * `visitor` (visitor HTML, `{ html, baseURL }`) also removes `<noscript>`
441
+ * (tag-manager and pixel fallbacks would fire fake pageviews), `srcdoc` and
442
+ * `data:` frames; comments out every `on*` handler (the attribute and its
443
+ * text stay for the detectors that read them); turns every `javascript:` URL
444
+ * into `javascript:void(0)`; and makes a `<base href>` absolute against
445
+ * `baseUrl` (a site's `<base href="/">` resolves against about:blank
446
+ * otherwise).
245
447
  *
246
448
  * With `baseUrl` and a driver `loadHtml`, the cleaned HTML is served at
247
- * `baseUrl`, so `location` is the live page's (detectors compare links with
248
- * `location.origin`/`pathname`). Without `loadHtml` the document stays on
249
- * about:blank with a `<base href>`; the engine names that in `stats.warnings`.
449
+ * `baseUrl` under `policy`, so `location` is the live page's (detectors
450
+ * compare links with `location.origin`/`pathname`); the returned `release`
451
+ * removes the policy and must run once the scan is done. A served document
452
+ * carries a one-off marker (a comment before the doctype, which keeps
453
+ * standards mode and touches no element), checked and removed after the load,
454
+ * on the sanitized and the `scripts` path alike: a caller's interception that
455
+ * answered the navigation itself (the live page from the network) is an
456
+ * error, never a silent scan of something else. Without `loadHtml` the
457
+ * document stays on about:blank with a `<base href>` (`served: false`).
250
458
  */
251
- declare function loadSnapshot(page: Page, html: string, baseUrl?: string): Promise<void>;
459
+ declare function loadSnapshot(page: Page, html: string, baseUrl?: string, opts?: {
460
+ scripts?: boolean;
461
+ visitor?: boolean;
462
+ policy?: HtmlRequestPolicy;
463
+ }): Promise<{
464
+ served: boolean;
465
+ release?: () => Promise<void>;
466
+ }>;
252
467
  /** The Playwright page members the adapter reads (structural: no playwright import). */
253
468
  interface PlaywrightPageLike {
254
469
  goto(url: string, opts?: any): Promise<any>;
@@ -267,6 +482,10 @@ interface PlaywrightPageLike {
267
482
  }): Promise<unknown>;
268
483
  route?(url: (u: URL) => boolean, handler: (route: any) => unknown): Promise<void>;
269
484
  unroute?(url: (u: URL) => boolean, handler?: (route: any) => unknown): Promise<void>;
485
+ setViewportSize?(size: {
486
+ width: number;
487
+ height: number;
488
+ }): Promise<void>;
270
489
  }
271
490
  /**
272
491
  * A Playwright page as the engine's Page. Playwright's evaluate takes one
@@ -274,6 +493,50 @@ interface PlaywrightPageLike {
274
493
  * so `location` is the live page's.
275
494
  */
276
495
  declare function fromPlaywright(p: PlaywrightPageLike): Page;
496
+ /** The puppeteer-core page members the adapter reads (structural: no puppeteer import). */
497
+ interface PuppeteerPageLike {
498
+ goto(url: string, opts?: any): Promise<any>;
499
+ evaluate(fn: any, ...args: any[]): Promise<any>;
500
+ screenshot(opts?: any): Promise<Uint8Array | string>;
501
+ keyboard: {
502
+ press(key: string): Promise<void>;
503
+ };
504
+ url(): string;
505
+ waitForFunction?(fn: any, opts?: {
506
+ timeout?: number;
507
+ }, ...args: any[]): Promise<unknown>;
508
+ addScriptTag?(opts: {
509
+ content?: string;
510
+ url?: string;
511
+ }): Promise<unknown>;
512
+ setRequestInterception?(on: boolean): Promise<void>;
513
+ setViewport?(v: {
514
+ width: number;
515
+ height: number;
516
+ }): Promise<void>;
517
+ on?(event: 'request', handler: (req: any) => void): unknown;
518
+ off?(event: 'request', handler: (req: any) => void): unknown;
519
+ }
520
+ /**
521
+ * A puppeteer-core page as the engine's Page. Puppeteer's evaluate takes every
522
+ * argument; its screenshot has no `animations` option (dropped). It has no
523
+ * waitForLoadState: with `waitForFunction` the adapter waits for
524
+ * `document.readyState === 'complete'`.
525
+ *
526
+ * `loadHtml` only with `interceptDocument`. It joins the page's request
527
+ * interception as one more cooperative handler (default priority), so the
528
+ * caller's own guards (the platform's private-target, own-widget and bandwidth
529
+ * guards) still see every request and their aborts win. A caller handler in
530
+ * legacy mode (no priority) resolves requests before it can: the served-marker
531
+ * check then fails the load instead of scanning the live page. Release turns
532
+ * interception off again unless `page.__keepRequestInterception` is set (the
533
+ * platform's session-long owners set it). Without `interceptDocument` a
534
+ * snapshot stays on about:blank, named in `stats.warnings`.
535
+ * No `playwrightPage`: axe runs in the top document only.
536
+ */
537
+ declare function fromPuppeteer(p: PuppeteerPageLike, opts?: {
538
+ interceptDocument?: boolean;
539
+ }): Page;
277
540
 
278
541
  /**
279
542
  * Fix Manifest Validator — Three-Layer Safety Architecture
@@ -350,11 +613,6 @@ declare function classifyFixRisk(fix: {
350
613
  attribute: string;
351
614
  value: string;
352
615
  }): FixClassification;
353
- /**
354
- * `issueType` + `context` are optional: when the caller has the element's
355
- * stored context (visible text), the WCAG 2.5.3 Label-in-Name rule runs on
356
- * aria-label fixes (see labelInNameRule.ts). Without them the check is a no-op.
357
- */
358
616
  declare function validateFix(fix: {
359
617
  selector: string;
360
618
  attribute: string;
@@ -585,4 +843,4 @@ interface SelectorQualityResult {
585
843
  declare const BARE_TAGS: Set<string>;
586
844
  declare function scoreSelectorQuality(selector: string): SelectorQualityResult;
587
845
 
588
- export { BARE_TAGS, type EngineFix, type EngineInput, type EngineIssue, type EngineOptions, type EngineResult, type EngineStats, FORM_CONTROL_ROLES, type HazardElementView, InternalBug, LABEL_IN_NAME_JS, type Model, type ModelRequest, type ModelResponse, type Page, type PlaywrightPageLike, type ProtectedScope, SERVED_FIX_KEYS, classifyFixRisk, detectPageLanguage, dropHazardousHidingFixes, foldContains, foldLabel, fromPlaywright, isHidingFix, isHtmlLangFix, isInteractiveOrContainsFocusable, isLabelText, labelContainsText, labelInNameCheck, languageFromText, languageFromUrl, loadSnapshot, normalizeVisibleText, parseContext, primaryLanguage, runEngine, scoreSelectorQuality, selectorParses, toPageLike, validateAttribute, validateFix, validateRoleValue, validateSelector, validateSelectorForAttribute, validateTextValue };
846
+ export { BARE_TAGS, type EngineFinding, type EngineFix, type EngineInput, type EngineIssue, type EngineOptions, type EngineResult, type EngineStats, FORM_CONTROL_ROLES, type HazardElementView, type HtmlDecision, type HtmlRequest, type HtmlRequestPolicy, InternalBug, LABEL_IN_NAME_JS, type Model, type ModelRequest, type ModelResponse, type Page, type PlaywrightPageLike, type ProtectedScope, type PuppeteerPageLike, SERVED_FIX_KEYS, TOKEN_QUERY_KEY, classifyFixRisk, detectPageLanguage, dropHazardousHidingFixes, foldContains, foldLabel, fromPlaywright, fromPuppeteer, htmlRequestPolicy, isHidingFix, isHtmlLangFix, isInteractiveOrContainsFocusable, isLabelText, labelContainsText, labelInNameCheck, languageFromText, languageFromUrl, loadSnapshot, normalizeVisibleText, parseContext, primaryLanguage, runEngine, scoreSelectorQuality, selectorParses, toPageLike, validateAttribute, validateFix, validateRoleValue, validateSelector, validateSelectorForAttribute, validateTextValue };