Source of truth: https://wicg.github.io/webhid/
| Field | Value |
|---|---|
| Project | FF-WebHID |
| Evaluated at | 2026-09-04 compliance scope audit against the current WebHID draft and implementation |
| Required/Core Compliance | 100% of mandatory WebHID requirements (107/107) |
| Conditional / non-scored items | 3 items (I9, B62, B63), not scored |
| Validation Detail % | 100.00% (29/29 subs) |
| Report Descriptor Shape % | 100.00% |
The required/core percentage covers only mandatory primary requirements. The conditional items remain documented below and are excluded because the WebHID specification permits conditional ServiceWorker exposure and does not expose requestDevice() outside Window.
| Section | Required | ✅ | ❌ | ❓ | Conditional / non-scored |
|---|---|---|---|---|---|
| Interfaces (I1-I9) | 9 | 9 | 0 | 0 | I9 (1) |
| Enum (E1) | 1 | 1 | 0 | 0 | |
| Dictionaries (D1a-D4) | 5 | 5 | 0 | 0 | |
| Report Descriptor Shape (D5-D11) | 9 | 9 | 0 | 0 | |
| HID members (H1-H4) | 4 | 4 | 0 | 0 | |
| HIDDevice members (HD1-HD12) | 12 | 12 | 0 | 0 | |
| Event members (C1-IC4) | 6 | 6 | 0 | 0 | |
| Behaviors (B1-B63) | 61 | 61 | 0 | 0 | B62-B63 (2) |
| Mandatory total | 107 | 107 | 0 | 0 | 3 |
The three conditional items are not unresolved mandatory requirements. They are retained in the item-level evidence with an N/S result.
| Axis | Total | ✅ | ❌ | ❓ |
|---|---|---|---|---|
| Sub items | 29 | 29 | 0 | 0 |
| Recommendations (R1-R4, not scored) | 4 | 4 | 0 | 0 |
- Dedicated Worker WebHID: supported by the worker polyfill and covered by the Dedicated Worker items below.
- Service Worker WebHID: not provided by this WebExtension polyfill. The WebHID specification says a user agent may choose to conditionally expose
hidwhen the relevant global object isServiceWorkerGlobalScope, so this is a conditional, non-scored capability rather than a core compliance failure. requestDevice(): page-only exposure is required by the WebHID WebIDL ([Exposed=Window]); the polyfill rejects it from Dedicated Workers and does not provide it in Service Workers.
Every scored ✅ and ❌ item cites file:line-or-range. Conditional items use N/S and cite the relevant specification and implementation scope. JS paths below are relative to addon/js/ unless prefixed with crates/ or tests/.
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
I1 |
HID : EventTarget exposed on global |
✅ | content/main/index.js:1495-1500,1751-1754 | HID extends EventTarget, exposed on globalThis |
I2 |
HIDDevice : EventTarget exposed on global |
✅ | content/main/index.js:759-767,1751-1754 | HIDDevice extends EventTarget, exposed on globalThis |
I3 |
HIDConnectionEvent : Event exposed on global, constructor works |
✅ | content/main/index.js:1463-1501,1751-1754 | HIDConnectionEvent extends Event with required init conversion, exposed on globalThis |
I4 |
HIDInputReportEvent : Event exposed on global, constructor works |
✅ | content/main/index.js:1401-1455,1751-1754 | HIDInputReportEvent extends Event with required init conversion, exposed on globalThis |
I5 |
Navigator.hid readonly attribute returns a HID instance |
✅ | content/main/index.js (hidInstance, navigator.hid getter) |
Navigator.prototype.hid getter returns the module singleton |
I5b |
Navigator.hid returns the SAME instance on every access ([SameObject]) |
✅ | content/main/index.js (hidInstance, navigator.hid getter) |
hidInstance is created once and returned by every getter call |
I6 |
WorkerNavigator.hid readonly attribute returns HID on DedicatedWorkerGlobalScope |
✅ | content/main/index.js:1669-1696 | installNavigatorHid() defines hid on Object.getPrototypeOf(self.navigator) when running in a worker |
I7 |
WorkerNavigator.hid returns same instance on every access ([SameObject]) on DedicatedWorkerGlobalScope |
✅ | content/main/index.js:1683-1696 | The worker navigator getter returns the module-level hidInstance |
I8 |
HID, HIDDevice, HIDConnectionEvent, HIDInputReportEvent exposed on DedicatedWorkerGlobalScope (self) |
✅ | content/main/index.js:1669-1681 | defineWebhidGlobal() defines all four interfaces on self in worker scope |
I9 |
WorkerNavigator.hid on ServiceWorkerGlobalScope |
N/S | WebHID §5 and §6 note; addon/js/background/webrequest.js:216-243 | Service Worker injection is intentionally unsupported. The specification permits conditional hid exposure in this context, so this capability is excluded from the mandatory denominator rather than treated as unresolved |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
E1 |
HIDUnitSystem produces only the 7 spec values |
✅ | crates/webhid-daemon/src/descriptor/fields.rs:18-29 | unit_system_from_nibble handles all 7 values: 0→none, 1→si-linear, 2→si-rotation, 3→english-linear, 4→english-rotation, 15→vendor-defined, else→reserved |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
D1a |
HIDDeviceRequestOptions.filters (required field) |
✅ | content/main/index.js:1510-1549,1571-1572 | requestDevice requires the filters member before converting the required sequence; missing or non-iterable values throw TypeError |
D1b |
HIDDeviceRequestOptions.exclusionFilters (optional) |
✅ | content/main/index.js:1510-1549 | requestDevice reads the optional exclusionFilters sequence and validates it when present |
D2a |
HIDDeviceFilter (vendorId?, productId?) |
✅ | crates/webhid-daemon/src/enumeration_filter.rs:39-50 | daemon-side matching compares each specified vendorId/productId against the page-visible device |
D2b |
HIDDeviceFilter (usagePage?, usage?) |
✅ | crates/webhid-daemon/src/enumeration_filter.rs:48-70 | daemon-side matching compares each specified usagePage/usage against a page-visible collection |
D-shape-simple |
D3+D4 aggregate: HIDConnectionEventInit.device and HIDInputReportEventInit.{device,reportId,data} |
✅ | content/main/index.js:1401-1423,1463-1473 | constructors require the dictionary members, preserve EventInit, and convert reportId as an [EnforceRange] octet |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
D5 |
HIDCollectionInfo has usagePage, usage, type, children, inputReports, outputReports, featureReports |
✅ | crates/webhid/src/types.rs:35-50 | Collection has type/usagePage/usage/children/inputReports/outputReports/featureReports |
D6 |
HIDReportInfo has reportId (octet) and items |
✅ | crates/webhid/src/types.rs:54-59 | Report has reportId:u8 and items:Vec<Field> |
D7a |
HIDReportItem core 10 fields (isAbsolute, isArray, isConstant, isRange, hasNull, usages, usageMinimum, usageMaximum, reportSize, reportCount) |
✅ | crates/webhid/src/types.rs:63-118 | Field has isAbsolute/isArray/isConstant/isRange/hasNull/usages/usageMinimum/usageMaximum/reportSize/reportCount |
D7b |
HIDReportItem logical/physical scaling (logicalMinimum, logicalMaximum, physicalMinimum, physicalMaximum) |
✅ | crates/webhid/src/types.rs:74-81 | Field has logicalMinimum/logicalMaximum/physicalMinimum/physicalMaximum |
D7c |
HIDReportItem unit-system/metadata 14 fields (isBufferedBytes, isLinear, isVolatile, hasPreferredState, wrap, unitExponent, unitSystem, unitFactor*Exponent ×7, strings) |
✅ | crates/webhid/src/types.rs:117-119; crates/webhid-daemon/src/descriptor/fields.rs:104-127,189-208 | strings: Vec<String> with #[serde(default)]; populated from HidField strings; all 14 fields present |
D8 |
isArray/isAbsolute/isLinear are NEGATIONS of raw bits | ✅ | crates/webhid-daemon/src/descriptor/fields.rs:90-91,104-127,189-208,251-255 | is_absolute/is_linear come straight from hidreport lib; is_array set true for Field::Array, false for Field::Variable |
D9 |
isRange true IFF usageMinimum < usageMaximum (strict) | ✅ | crates/webhid-daemon/src/descriptor/fields.rs:136-160 | detect_contiguous_range: isRange=true implies hi>lo since count>1; array range from hidreport usage_range |
D10 |
unitSystem nibble decoded correctly (0→none, 1→si-linear, 2→si-rotation, 3→english-linear, 4→english-rotation, -1→vendor-defined, else→reserved) | ✅ | crates/webhid-daemon/src/descriptor/fields.rs:18-29 | unit_system_from_nibble: 0→none, 1→si-linear, 2→si-rotation, 3→english-linear, 4→english-rotation, nibble 15→vendor-defined, else→reserved |
D11 |
HIDCollectionInfo.type is OCTET (numeric) with correct value per spec table | ✅ | crates/webhid/src/types.rs:36-37 | collection_type:u8 serde-renamed to type; value from col.collection_type().into() |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
H1 |
onconnect EventHandler IDL attribute |
✅ | content/main/index.js:1631-1648 | onconnect getter/setter via addEventListener('connect') |
H2 |
ondisconnect EventHandler IDL attribute |
✅ | content/main/index.js:1650-1666 | ondisconnect getter/setter via addEventListener('disconnect') |
H3 |
getDevices() returns Promise<sequence<HIDDevice>> |
✅ | content/main/index.js:1565-1589 | getDevices returns array of HIDDevice |
H4 |
requestDevice(options) returns Promise<sequence<HIDDevice>> |
✅ | content/main/index.js:1591-1660 | requestDevice resolves with array of HIDDevice |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
HD1 |
oninputreport EventHandler IDL attribute |
✅ | content/main/index.js:828-846 | oninputreport getter/setter via addEventListener('inputreport') |
HD2 |
opened readonly boolean getter |
✅ | content/main/index.js:785-795 | opened getter returns boolean |
HD3 |
vendorId readonly unsigned short getter |
✅ | content/main/index.js:796-803 | vendorId getter |
HD4 |
productId readonly unsigned short getter |
✅ | content/main/index.js:804-810 | productId getter |
HD5 |
productName readonly DOMString |
✅ | content/main/index.js:812-818 | productName getter |
HD6 |
collections readonly FrozenArray<HIDCollectionInfo> (existence+type only) |
✅ | content/main/index.js:820-826,1333-1343,1351-1371 | collections getter; deepFreeze'd in createHIDDevice |
HD7 |
open() returns Promise<undefined> |
✅ | content/main/index.js:848-880 | open() success path has no explicit return → Promise<undefined> |
HD8 |
close() returns Promise<undefined> |
✅ | content/main/index.js:882-920 | close() async, no explicit return on success → Promise<undefined> |
HD9 |
forget() returns Promise<undefined> |
✅ | content/main/index.js:1021-1055 | forget() async, no explicit return → Promise<undefined> |
HD10 |
sendReport(reportId, data) returns Promise<undefined> |
✅ | content/main/index.js:904-927 | sendReport resolves with undefined in the ack-wait branch |
HD11 |
sendFeatureReport(reportId, data) returns Promise<undefined> |
✅ | content/main/index.js:969-992 | sendFeatureReport resolves with undefined |
HD12 |
receiveFeatureReport(reportId) returns Promise<DataView> |
✅ | content/main/index.js:938-961 | receiveFeatureReport resolves with DataView |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
C1 |
HIDConnectionEvent constructor accepts (type, eventInitDict) |
✅ | content/main/index.js:1463-1473 | HIDConnectionEvent(type, init) requires device and forwards inherited EventInit members |
C2 |
HIDConnectionEvent.device readonly, [SameObject] identity stability |
✅ | content/main/index.js:1463-1501 | device stored in evtState WeakMap, returned by getter; same identity |
IC1 |
HIDInputReportEvent constructor accepts (type, eventInitDict) |
✅ | content/main/index.js:1401-1423 | HIDInputReportEvent(type, init) requires device, reportId, and data |
IC2 |
HIDInputReportEvent.device readonly, [SameObject] |
✅ | content/main/index.js:1401-1455 | device stored in irState Symbol, returned by getter; same identity |
IC3 |
HIDInputReportEvent.reportId readonly octet |
✅ | content/main/index.js:1401-1423 | reportId is converted with [EnforceRange] and returned by getter |
IC4 |
HIDInputReportEvent.data readonly DataView |
✅ | content/main/index.js:1401-1455 | data stored in irState Symbol, returned by getter |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
B1 |
getDevices() refuses when disallowed by Permissions Policy "hid" |
✅ | background/messages.js (policyForRequest), background/webrequest.js, content/isolated/bridge.js (handleGetPolicyRequest), content/main/index.js (getDevices) |
Permissions Policy is tracked in background state and resolved for the requesting frame; getDevices() throws SecurityError when the result is denied |
B2 |
getDevices() returns only previously-allowed, non-forgotten devices |
✅ | content/main/index.js (getDevices, getPairedDevices, getDeviceCache) |
The page-facing enumerate action is rewritten to enumeratePaired; background filters current daemon inventory by the origin's stored grants |
B3 |
requestDevice() refuses when disallowed by Permissions Policy |
✅ | background/messages.js (policyForRequest), content/isolated/bridge.js (handleGetPolicyRequest), content/main/index.js (requestDevice) |
requestDevice() throws SecurityError when policy is denied |
B4 |
requestDevice() refuses without transient activation |
✅ | content/main/index.js:1530-1540 | requestDevice checks navigator.userActivation.isActive unless called from devtools console (isCalledFromConsole, 463-476) |
B5 |
requestDevice() grants access to ALL HID interfaces of chosen device |
✅ | content/isolated/picker/index.js:70-71,290; content/main/index.js:1507-1514 | onDeviceSelected returns all devices in selected group; the bridge persists the grant for each (pairDevice runs bridge-side after the chooser), the polyfill maps them to HIDDevice instances |
B6 |
User declining chooser RESOLVES with empty sequence (never rejects) | ✅ | content/main/index.js:1507-1514 | result.cancelled → resolve([]) |
B7 |
open() refuses unless [[state]] is "closed" |
✅ | content/main/index.js:851-853 | if state.opened (or opening) throw InvalidStateError |
B8 |
close() refuses if [[state]] is "forgotten" or "forgetting" |
✅ | content/main/index.js:885-886 | if state.forgotten throw InvalidStateError |
B9 |
close() settles all pending sendReport() promises |
✅ | content/main/index.js:895-900 | close() calls rejectPendingReports with AbortError |
B10 |
close() settles all pending sendFeatureReport() promises |
✅ | content/main/index.js:895-900 | same rejectPendingReports covers sendFeatureReport pending |
B11 |
close() settles all pending receiveFeatureReport() promises |
✅ | content/main/index.js:895-900 | same rejectPendingReports covers receiveFeatureReport pending |
B12 |
forget() revokes access, sets [[state]] to "forgotten" |
✅ | content/main/index.js:1021-1055,1197-1225 | forget sets state.forgotten=true via teardownForgottenDevice, then un-pairs via the bridge |
B13 |
forget() settles pending sendReport() on related devices |
✅ | content/main/index.js:1197-1225; addon/js/background/messages.js:82-113; addon/js/background/storage.js:178-229 | Devices granted together form a grant group; revoking any member cascades to the group, and each affected page device settles its pendings with AbortError via the revoked event → teardownForgottenDevice |
B14 |
forget() settles pending sendFeatureReport() on related devices |
✅ | content/main/index.js:1197-1225 | same teardownForgottenDevice rejectPendingReports covers sendFeatureReport |
B15 |
forget() settles pending receiveFeatureReport() on related devices |
✅ | content/main/index.js:1197-1225 | same teardownForgottenDevice rejectPendingReports covers receiveFeatureReport |
B16 |
sendReport() validates reportId against whether device uses report IDs |
✅ | content/main/index.js:917-941,1167-1198 | sendReport converts reportId with [EnforceRange] semantics before state checks, then validateReportId checks device report-ID usage |
B17 |
sendFeatureReport() validates reportId the same way |
✅ | content/main/index.js:988-1008,1167-1198 | sendFeatureReport converts reportId with [EnforceRange] semantics before state checks, then validateReportId checks device report-ID usage |
B18 |
receiveFeatureReport() validates reportId and resolves with DataView |
✅ | content/main/index.js:955-973,1167-1198 | receiveFeatureReport converts reportId with [EnforceRange] semantics before state checks, then validateReportId checks device report-ID usage |
B19 |
vendorId is 0 when device has no vendor ID |
✅ | crates/webhid-daemon/src/hid.rs:120 | vendor_id: info.vendor_id(); hidapi returns 0 when no vendor ID |
B20 |
productId is 0 when device has no product ID |
✅ | crates/webhid-daemon/src/hid.rs:121 | product_id: info.product_id(); hidapi returns 0 when no product ID |
B21 |
productName is "" (empty string) when unavailable |
✅ | crates/webhid/src/types.rs:17; crates/webhid-daemon/src/hid.rs:122 | product_name: String with #[serde(default)]; product_name: info.product_string().map(String::from).unwrap_or_default() |
B22 |
HIDReportItem.usages undefined when isRange/no usages; usageMinimum/Maximum undefined when !isRange; strings always sequence<DOMString> |
✅ | crates/webhid/src/types.rs:67-119; crates/webhid/src/types.rs:67-119; crates/webhid-daemon/src/descriptor/fields.rs:136-160,189-208 | usages: Option<Vec<u32>>, None when is_range; strings: Vec<String> |
B23 |
HIDReportItem.reportSize always > 0 |
✅ | crates/webhid-daemon/src/descriptor/fields.rs:108-109,190-191,251-255 | report_size: clamped with .max(1) in Constant and aggregated variable fields |
B24 |
HIDReportItem.reportCount always > 0 |
✅ | crates/webhid-daemon/src/descriptor/fields.rs:108-109,190-191,251-255 | report_count: clamped with .max(1) in array and aggregated variable fields |
B25 |
Blocklist: entry matches device if all vendorId/productId equal | ✅ | crates/webhid-daemon/src/hid.rs:164-217,236-243 | BLOCKED_DEVICES list; is_blocked_pub checks (vid,pid) tuple equality |
B26 |
Blocklist: entry matches collection if all usagePage/usage present | ✅ | crates/webhid-daemon/src/hid.rs:233,236-243 | FIDO_USAGE_PAGE=0xF1D0; blocks any device whose usage_page matches |
B27 |
Blocklist: entry matches report if all reportId/reportType equal | ✅ | crates/webhid-daemon/src/blocklist.rs:14-15,114-152; crates/webhid-daemon/src/report_blocking.rs:90-115,252-330 | is_report_blocked checks all 6 rule fields; compute_blocked_input_ids pre-computes blocked input reports; is_report_blocked used at runtime |
B28 |
inputreport event does NOT fire for blocked reports |
✅ | crates/webhid-daemon/src/device_mgr.rs:1301-1317; crates/webhid-daemon/src/report_blocking.rs:310-331 | the persistent reader drops blocked input IDs and protected interface reports before routing to NM or page sessions |
B29 |
HIDInputReportEvent.data MUST NOT contain report ID byte when device uses report IDs |
✅ | crates/webhid-daemon/src/device_mgr.rs (spawn_reader); content/main/index.js (dispatchDeviceEvent) |
The reader separates the numbered report ID before forwarding the payload to the page |
B30 |
connect event fires ONLY for already-granted devices |
✅ | content/main/index.js:1068-1077,1080-1105 | dispatchDeviceEvent connect path calls resolvePairedDevice which checks paired devices before firing event |
B31 |
disconnect event fires ONLY for previously-granted devices |
✅ | content/main/index.js:1080-1105 | disconnect event only fires if device is in deviceRegistry; i.e., previously granted |
B32 |
inputreport event fires on HIDDevice with correct device, reportId, data |
✅ | content/main/index.js:1080-1120 | inputreport dispatched as HIDInputReportEvent with device/reportId/data |
B33 |
Permissions Policy: feature "hid", default allowlist ["self"] |
✅ | background/webrequest.js (storePermissionsPolicy), background/messages.js (policyForRequest), content/isolated/bridge.js (handleGetPolicyRequest) |
Header state is tracked per tab/frame and the bridge supplies the engine-originated frame context and allow attribute |
B34 |
sendReport() refuses unless [[state]] is "opened" (distinct from B16) |
✅ | content/main/index.js:904-927 | sendReport throws InvalidStateError if !state.opened |
B35 |
sendFeatureReport() refuses unless [[state]] is "opened" |
✅ | content/main/index.js:969-992 | sendFeatureReport throws InvalidStateError if !state.opened |
B36 |
receiveFeatureReport() refuses unless [[state]] is "opened" |
✅ | content/main/index.js:938-961 | receiveFeatureReport throws InvalidStateError if !state.opened |
B37 |
open() rejects when OS-level open fails |
✅ | content/main/index.js:848-880 | open() awaits sendRequest('open'); on non-OK status throws Error wrapped as NetworkError |
B38 |
sendReport() rejects when blocked report |
✅ | crates/webhid-daemon/src/client.rs:292-306; crates/webhid-daemon/src/batching.rs:355-403; content/main/index.js:904-927; content/isolated/worker/index.js:125-158 | NM path: is_report_blocked check returns 403; WS path: returns status 2; blocked → NotAllowedError |
B39 |
sendReport() rejects when OS-level send fails |
✅ | content/main/index.js:904-927 | sendReport reject path wraps error in DOMException(..., 'NetworkError') |
B40 |
sendFeatureReport() rejects when blocked report |
✅ | crates/webhid-daemon/src/client.rs:292-306; crates/webhid-daemon/src/batching.rs:355-403; content/main/index.js:969-992 | NM path: is_report_blocked check returns 403; WS path: same check applies for send and sendFeature; blocked → NotAllowedError |
B41 |
sendFeatureReport() rejects when OS-level send fails |
✅ | content/main/index.js:969-992 | sendFeatureReport reject path wraps in DOMException(..., 'NetworkError') |
B42 |
receiveFeatureReport() rejects when blocked report |
✅ | crates/webhid-daemon/src/client.rs:292-306; crates/webhid-daemon/src/batching.rs:406-427; content/main/index.js:938-961 | NM path: is_report_blocked check returns 403; WS path: returns status 2; blocked → NotAllowedError |
B43 |
receiveFeatureReport() rejects when OS-level read fails |
✅ | content/main/index.js:938-961 | receiveFeatureReport reject path wraps in DOMException(..., 'NetworkError') |
B44 |
receiveFeatureReport() resolved DataView contains raw bytes WITHOUT stripping report ID byte |
✅ | crates/webhid-daemon/src/hid.rs:369-377; content/main/index.js:938-961 | read_feature_report returns buf[..n] including report ID byte at buf[0]; wraps in DataView without stripping |
B45 |
requestDevice() validates each filters entry, refuses if any invalid |
✅ | content/main/index.js:1510-1530 | requestDevice iterates the required filters sequence and calls isValidFilter; throws on invalid |
B46 |
Empty filter object {} is invalid |
✅ | utils/device-filters.js:50-56 | isValidFilter returns false if Object.keys(filter).length===0 |
B47 |
productId without vendorId is invalid |
✅ | utils/device-filters.js:50-56 | isValidFilter returns false if 'productId' in filter && !('vendorId' in filter) |
B48 |
usage without usagePage is invalid |
✅ | utils/device-filters.js:50-56 | isValidFilter returns false if 'usage' in filter && !('usagePage' in filter) |
B49 |
Device included if matches AT LEAST ONE filters entry |
✅ | crates/webhid-daemon/src/enumeration_filter.rs:24-36 | matches_device includes a device when any inclusion filter matches |
B50 |
Empty filters array matches ALL devices |
✅ | crates/webhid-daemon/src/enumeration_filter.rs:24-36 | matches_device treats an empty inclusion sequence as matching all devices |
B51 |
Device ID matching: every specified vendorId/productId must equal device's | ✅ | crates/webhid-daemon/src/enumeration_filter.rs:39-46 | matches_filter_vid_pid requires every specified vendorId/productId to equal the device's |
B52 |
Collection matching: every specified usagePage/usage must equal at least one collection's | ✅ | crates/webhid-daemon/src/enumeration_filter.rs:48-70 | matches_collection_usage requires every specified usagePage/usage to match one collection |
B53 |
requestDevice() refuses when exclusionFilters is present but empty |
✅ | content/main/index.js:1532-1548 | requestDevice throws TypeError if exclusionFilters is present but empty |
B54 |
requestDevice() validates each exclusionFilters entry |
✅ | content/main/index.js:1532-1548 | requestDevice iterates exclusionFilters and validates each with isValidFilter |
B55 |
Empty filter {} invalid for exclusionFilters |
✅ | utils/device-filters.js:50-56 | same isValidFilter used for exclusion filters rejects empty {} |
B56 |
productId without vendorId invalid for exclusionFilters |
✅ | utils/device-filters.js:50-56 | isValidFilter rejects productId without vendorId for exclusion filters |
B57 |
usage without usagePage invalid for exclusionFilters |
✅ | utils/device-filters.js:50-56 | isValidFilter rejects usage without usagePage for exclusion filters |
B58 |
Device matching any exclusionFilters entry is excluded |
✅ | crates/webhid-daemon/src/enumeration_filter.rs:24-36 | matches_device excludes a device when any exclusion filter matches |
B59 |
getDevices() callable from DedicatedWorker (no [Exposed=Window]; spec §6.1 algorithm explicitly handles DW and SW) |
✅ | content/main/index.js:1616-1645,1751-1774; tests/browser/worker.spec.ts:17-80 | Dedicated Worker injection exposes navigator.hid; getDevices() is present and the test exercises the worker API |
B60 |
requestDevice() throws NotSupportedError from DedicatedWorker (spec §6.2: [Exposed=Window] only; checks "relevant global object is not window") |
✅ | content/main/index.js:1571-1579 | the worker guard rejects requestDevice() with NotSupportedError after required options conversion |
B61 |
HIDDevice methods open, close, sendReport, sendFeatureReport, receiveFeatureReport, forget exposed on DedicatedWorker (spec does not restrict these from Worker) |
✅ | content/main/index.js:759-1015,1751-1774 | The shared HIDDevice prototype contains all six methods, and the worker exposure defines the constructor and related interfaces on self; the worker-polyfill feature does not create a functional worker data plane |
B62 |
getDevices() from ServiceWorker: conditional ServiceWorker exposure described by WebHID §6.1 |
N/S | WebHID §6.1; addon/js/background/webrequest.js:216-243 | Shared and Service Worker destinations are left unmodified; this conditional capability is not provided by the polyfill and is not scored |
B63 |
requestDevice() in ServiceWorkerGlobalScope (WebIDL is [Exposed=Window]) |
N/S | WebHID §6.2; background/webrequest.js:216-243; content/main/index.js:1571-1579 | requestDevice() is page-only by the specification; Service Worker exposure is outside the polyfill scope and is not scored |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
B1-sub |
B1 Sub: rejection uses SecurityError |
✅ | content/main/index.js (getDevices) |
getDevices throws SecurityError when policy.hid === 'none' |
B3-sub |
B3 Sub: rejection uses SecurityError |
✅ | content/main/index.js (requestDevice) |
requestDevice throws SecurityError when policy.hid === 'none' |
B4-sub |
B4 Sub: rejection uses SecurityError |
✅ | content/main/index.js (requestDeviceImpl) |
requestDevice throws when user activation is not active |
B7-sub |
B7 Sub: rejection uses InvalidStateError |
✅ | content/main/index.js:851-853 | open throws DOMException('Device is already open', 'InvalidStateError') |
B8-sub |
B8 Sub: rejection uses InvalidStateError |
✅ | content/main/index.js:885-886 | close throws DOMException('Device has been forgotten', 'InvalidStateError') |
B9-sub |
B9 Sub: settlement uses AbortError |
✅ | content/main/index.js:895-900 | rejectPendingReports called with DOMException('Device closed', 'AbortError') |
B10-sub |
B10 Sub: settlement uses AbortError |
✅ | content/main/index.js:895-900 | same AbortError covers sendFeatureReport pending |
B11-sub |
B11 Sub: settlement uses AbortError |
✅ | content/main/index.js:895-900 | same AbortError covers receiveFeatureReport pending |
B13-sub |
B13 Sub: settlement uses AbortError |
✅ | content/main/index.js:1197-1225 | teardownForgottenDevice calls rejectPendingReports with DOMException('Device forgotten', 'AbortError') |
B14-sub |
B14 Sub: settlement uses AbortError |
✅ | content/main/index.js:1197-1225 | same AbortError covers sendFeatureReport pending |
B15-sub |
B15 Sub: settlement uses AbortError |
✅ | content/main/index.js:1197-1225 | same AbortError covers receiveFeatureReport pending |
B16-sub |
B16 Sub: rejection uses TypeError (covers logical mismatch AND out-of-range via [EnforceRange]) |
✅ | content/main/index.js:1167-1198 | convertEnforcedOctet performs ToNumber, truncation, and range checks; validateReportId then checks device report-ID semantics |
B17-sub |
B17 Sub: rejection uses TypeError |
✅ | content/main/index.js:988-1012,1167-1198 | sendFeatureReport uses the converted octet and validateReportId throws TypeError on mismatch |
B18-sub |
B18 Sub: rejection uses TypeError |
✅ | content/main/index.js:955-976,1167-1198 | receiveFeatureReport uses the converted octet and validateReportId throws TypeError on mismatch |
B34-sub |
B34 Sub: rejection uses InvalidStateError |
✅ | content/main/index.js:904-927 | sendReport throws DOMException('Device is not open', 'InvalidStateError') |
B35-sub |
B35 Sub: rejection uses InvalidStateError |
✅ | content/main/index.js:969-992 | sendFeatureReport throws DOMException('Device is not open', 'InvalidStateError') |
B36-sub |
B36 Sub: rejection uses InvalidStateError |
✅ | content/main/index.js:938-961 | receiveFeatureReport throws DOMException('Device is not open', 'InvalidStateError') |
B37-sub |
B37 Sub: rejection uses NetworkError |
✅ | content/main/index.js:848-880 | open() catches failure and throws DOMException(error.message, 'NetworkError') |
B38-sub |
B38 Sub: rejection uses NotAllowedError |
✅ | content/main/index.js:904-927 | sendReport reject with DOMException(..., 'NotAllowedError') when blocked |
B39-sub |
B39 Sub: rejection uses NetworkError |
✅ | content/main/index.js:904-927 | sendReport reject wraps in DOMException(e.message, 'NetworkError') |
B40-sub |
B40 Sub: rejection uses NotAllowedError |
✅ | content/main/index.js:969-992 | sendFeatureReport reject with DOMException(..., 'NotAllowedError') when blocked |
B41-sub |
B41 Sub: rejection uses NetworkError |
✅ | content/main/index.js:969-992 | sendFeatureReport reject wraps in DOMException(e.message, 'NetworkError') |
B42-sub |
B42 Sub: rejection uses NotAllowedError |
✅ | content/main/index.js:938-961 | receiveFeatureReport reject with DOMException(..., 'NotAllowedError') when blocked |
B43-sub |
B43 Sub: rejection uses NetworkError |
✅ | content/main/index.js:938-961 | receiveFeatureReport reject wraps in DOMException(e.message, 'NetworkError') |
B45-sub |
B45 Sub: rejection uses TypeError |
✅ | content/main/index.js:1497-1527 | throws TypeError for an invalid member of the required filters sequence |
B46/B47/B48-sub |
B46/B47/B48 Sub (shared): rejection uses TypeError |
✅ | utils/device-filters.js:50-56 | same TypeError path covers empty {}, productId w/o vendorId, usagePage/usage without usagePage |
B53-sub |
B53 Sub: rejection uses TypeError |
✅ | content/main/index.js:1529-1545 | throws TypeError when exclusionFilters is present but empty |
B54-sub |
B54 Sub: rejection uses TypeError |
✅ | content/main/index.js:1529-1545 | throws TypeError for an invalid exclusionFilter |
B55/B56/B57-sub |
B55/B56/B57 Sub (shared): rejection uses TypeError |
✅ | utils/device-filters.js:50-56 | same TypeError path covers empty {}, productId w/o vendorId, usagePage/usage without usagePage |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
R1 |
Two-step confirmation in chooser (SHOULD/MAY) | ✅ | addon/js/content/isolated/picker/fragment.html + addon/js/content/isolated/picker/index.js; addon/js/internal/pages/picker/index.html | The modal picker uses the isolated fragment; page-action/window modes use the internal picker page. Each requires device selection followed by Connect |
R2 |
productName contains USB iProduct string descriptor (SHOULD) |
✅ | crates/webhid-daemon/src/hid.rs:122 | product_name: info.product_string().map(String::from); hidapi returns USB iProduct string |
R3 |
productName contains Bluetooth Device Name (SHOULD) |
✅ | crates/webhid-daemon/src/hid.rs:122 | hidapi's product_string() returns Bluetooth Device Name for BT devices |
R4 |
productName does NOT contain serial number or BT address (SHOULD NOT) |
✅ | crates/webhid-daemon/src/hid.rs:122-124 | productName sourced only from product_string(); serial_number is a separate DeviceInfo field not merged into productName |
B13: Devices granted together in onerequestDevice()are recorded as a grant group (recordGrantGroup, IndexedDBgrantGroupsstore). Revoking any member cascades to the whole group (revokeDevicesinmessages.js): every member is removed from the allowlist and closed, and each affected page receives arevokeddevice event that runsforceForgetDevice→teardownForgottenDevice, rejecting pendingsendReport/sendFeatureReport/receiveFeatureReportwithAbortError. The cascade is reachable throughrevokeDevice(popup/devices pages); the polyfill'sforget()un-pairs the single device it is called on. One deviceId per top-level Application collection still holds.B33: Implemented via webRequest.onHeadersReceived intercepting the Permissions-Policy header. The default (no header) grants access to same-origin frames, matching the["self"]default allowlist. Cross-origin iframes default to denied unless the parent setsallow="hid".I5b:hidInstanceis a module-level singleton incontent/main/index.js; thenavigator.hidgetter returns it on every access, satisfying[SameObject].B59/B61(worker context): the dual-context polyfill incontent/main/index.jsexposes the interfaces in page-created workers. WorkergetDevices()uses the worker bridge path andrequestDevice()throwsNotSupportedError; no functional worker data plane is created by this worker-polyfill feature. Verified bytests/browser/worker.spec.ts.I9/B62/B63(ServiceWorker context): The WebHID draft explicitly says a user agent may choose to conditionally exposehidwhen the relevant global isServiceWorkerGlobalScope. This polyfill supports Dedicated Workers but does not inject into ServiceWorkerGlobalScope.requestDevice()is[Exposed=Window]and remains page-only. These are conditional, non-scored capabilities, not unresolved mandatory failures.- Worker polyfill wiring: when
workerPolyfillEnabledis on,background/webrequest.jsserves the bundle listed byaddon/js/utils/bundle-files.jsinto page-created worker scripts throughwebRequest.filterResponseData. The bundle contains bootstrap, logger, http, settings, device-filters, webtransport, wire-format, andcontent/main/index.js. The patched Worker constructor relays the worker's bridge port.