Skip to content

Latest commit

 

History

History
253 lines (217 loc) · 73.3 KB

File metadata and controls

253 lines (217 loc) · 73.3 KB

WebHID Spec Compliance Report

Source of truth: https://wicg.github.io/webhid/

Result table

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-level breakdown

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.

Sub-item (Validation Detail) totals

Axis Total
Sub items 29 29 0 0
Recommendations (R1-R4, not scored) 4 4 0 0

Supported exposure scope

  • 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 hid when the relevant global object is ServiceWorkerGlobalScope, 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.

Item-level evidence

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/.

Interfaces (I1-I9)

# 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

Enum (E1)

# 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

Dictionaries (D1a-D4)

# 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

Report Descriptor Shape (D5-D11)

# 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()

HID members (H1-H4)

# 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

HIDDevice members (HD1-HD12)

# 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

Event members (C1-IC4)

# 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

Behaviors (B1-B63)

# 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

Sub-item (Validation Detail) evidence

Behaviors: Sub items

# 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

Recommendations (R1-R4, reported but not scored)

# 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

Notes

  • B13: Devices granted together in one requestDevice() are recorded as a grant group (recordGrantGroup, IndexedDB grantGroups store). Revoking any member cascades to the whole group (revokeDevices in messages.js): every member is removed from the allowlist and closed, and each affected page receives a revoked device event that runs forceForgetDeviceteardownForgottenDevice, rejecting pending sendReport/sendFeatureReport/receiveFeatureReport with AbortError. The cascade is reachable through revokeDevice (popup/devices pages); the polyfill's forget() 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 sets allow="hid".
  • I5b: hidInstance is a module-level singleton in content/main/index.js; the navigator.hid getter returns it on every access, satisfying [SameObject].
  • B59 / B61 (worker context): the dual-context polyfill in content/main/index.js exposes the interfaces in page-created workers. Worker getDevices() uses the worker bridge path and requestDevice() throws NotSupportedError; no functional worker data plane is created by this worker-polyfill feature. Verified by tests/browser/worker.spec.ts.
  • I9 / B62 / B63 (ServiceWorker context): The WebHID draft explicitly says a user agent may choose to conditionally expose hid when the relevant global is ServiceWorkerGlobalScope. 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 workerPolyfillEnabled is on, background/webrequest.js serves the bundle listed by addon/js/utils/bundle-files.js into page-created worker scripts through webRequest.filterResponseData. The bundle contains bootstrap, logger, http, settings, device-filters, webtransport, wire-format, and content/main/index.js. The patched Worker constructor relays the worker's bridge port.