Source of truth: https://wicg.github.io/webhid/
| Field | Value |
|---|---|
| Project | FF-WebHID |
| Evaluated at | 2026-10-22 |
| Required/Core Compliance | 100% of mandatory WebHID requirements (113/113) |
| 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 mandatory requirements only. Conditional items are excluded from the denominator.
| 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-B64) | 62 | 62 | 0 | 0 | B62-B63 (2) |
| WebIDL runtime (W1-W5) | 5 | 5 | 0 | 0 | |
| Mandatory total | 113 | 113 | 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 |
Every scored ✅ and ❌ item cites implementation files and stable behavior/symbol anchors where practical. Conditional items use N/S. 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 (HID, defineWebhidGlobal) |
HID.prototype inherits from EventTarget.prototype and the constructor is installed on the realm global |
I2 |
HIDDevice : EventTarget exposed on global |
✅ | content/main/index.js (HIDDevice, createHIDDevice, defineWebhidGlobal) |
HIDDevice has an EventTarget prototype and the exposed objects use its branded state |
I3 |
HIDConnectionEvent : Event exposed on global, constructor works |
✅ | content/main/index.js (HIDConnectionEvent, defineWebhidGlobal); tests/conformance/e2e/e2e.spec.ts |
The constructor uses captured native Event construction, validates its device dictionary and requires new |
I4 |
HIDInputReportEvent : Event exposed on global, constructor works |
✅ | content/main/index.js (HIDInputReportEvent, defineWebhidGlobal); tests/conformance/e2e/e2e.spec.ts |
The constructor validates device, reportId and DataView, performs octet conversion and requires new |
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 | 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 | The worker navigator getter returns the module-level hidInstance |
I8 |
HID, HIDDevice, HIDConnectionEvent, HIDInputReportEvent exposed on DedicatedWorkerGlobalScope (self) |
✅ | content/main/index.js | 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 | 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 | 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 (normalizeRequestOptions, requestDevice); tests/conformance/e2e/e2e.spec.ts |
Required filters are converted as a WebIDL sequence synchronously before the async implementation runs |
D1b |
HIDDeviceRequestOptions.exclusionFilters (optional) |
✅ | content/main/index.js (normalizeRequestOptions, convertSequence) |
When present, exclusionFilters is converted as an optional WebIDL sequence before returning a Promise |
D2a |
HIDDeviceFilter (vendorId?, productId?) |
✅ | content/main/index.js (convertFilterDictionary, convertUnsignedInteger); crates/webhid-daemon/src/enumeration_filter.rs |
Unknown members are ignored; vendorId uses unsigned long conversion and productId uses unsigned short conversion |
D2b |
HIDDeviceFilter (usagePage?, usage?) |
✅ | content/main/index.js (convertFilterDictionary, convertUnsignedInteger); crates/webhid-daemon/src/enumeration_filter.rs |
usagePage and usage use WebIDL unsigned short conversion before matching |
D-shape-simple |
D3+D4 aggregate: HIDConnectionEventInit.device and HIDInputReportEventInit.{device,reportId,data} |
✅ | content/main/index.js | constructors require dictionary members, preserve EventInit, and convert reportId with ordinary WebIDL octet modulo semantics |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
D5 |
HIDCollectionInfo has usagePage, usage, type, children, inputReports, outputReports, featureReports |
✅ | crates/webhid/src/types.rs | Collection has type/usagePage/usage/children/inputReports/outputReports/featureReports |
D6 |
HIDReportInfo has reportId (octet) and items |
✅ | crates/webhid/src/types.rs | 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 | 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 | 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; crates/webhid-daemon/src/hid.rs (parse_report_descriptor_with_strings); crates/webhid-daemon/src/descriptor/fields.rs |
strings are resolved from descriptor string indices through the parser callback; unit metadata is serialized by WebHidField |
D8 |
isArray/isAbsolute/isLinear are NEGATIONS of raw bits | ✅ | crates/webhid-daemon/src/descriptor/fields.rs | 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 (detect_contiguous_range, array_usage_range) |
A range is emitted only when its maximum exceeds its minimum; a singleton is an explicit usage list |
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 | 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 | 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 | onconnect getter/setter via addEventListener('connect') |
H2 |
ondisconnect EventHandler IDL attribute |
✅ | content/main/index.js | ondisconnect getter/setter via addEventListener('disconnect') |
H3 |
getDevices() returns Promise<sequence<HIDDevice>> |
✅ | content/main/index.js | getDevices returns array of HIDDevice |
H4 |
requestDevice(options) returns Promise<sequence<HIDDevice>> |
✅ | content/main/index.js | requestDevice resolves with array of HIDDevice |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
HD1 |
oninputreport EventHandler IDL attribute |
✅ | content/main/index.js | oninputreport getter/setter via addEventListener('inputreport') |
HD2 |
opened readonly boolean getter |
✅ | content/main/index.js | opened getter returns boolean |
HD3 |
vendorId readonly unsigned short getter |
✅ | content/main/index.js | vendorId getter |
HD4 |
productId readonly unsigned short getter |
✅ | content/main/index.js | productId getter |
HD5 |
productName readonly DOMString |
✅ | content/main/index.js | productName getter |
HD6 |
collections readonly FrozenArray<HIDCollectionInfo> (existence+type only) |
✅ | content/main/index.js (createHIDDevice, collections getter) |
The collections value is recursively frozen and returned through a readonly getter |
HD7 |
open() returns Promise<undefined> |
✅ | content/main/index.js (openDevice, performOpenDevice) |
open() returns a Promise that resolves with undefined after data-plane readiness |
HD8 |
close() returns Promise<undefined> |
✅ | content/main/index.js (closeDevice, performCloseDevice) |
close() returns a Promise that resolves with undefined after the close operation |
HD9 |
forget() returns Promise<undefined> |
✅ | content/main/index.js (forgetDevice) |
forget() returns the shared group operation Promise, resolving with undefined |
HD10 |
sendReport(reportId, data) returns Promise<undefined> |
✅ | content/main/index.js | sendReport resolves with undefined in the ack-wait branch |
HD11 |
sendFeatureReport(reportId, data) returns Promise<undefined> |
✅ | content/main/index.js | sendFeatureReport resolves with undefined |
HD12 |
receiveFeatureReport(reportId) returns Promise<DataView> |
✅ | content/main/index.js | receiveFeatureReport resolves with DataView |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
C1 |
HIDConnectionEvent constructor accepts (type, eventInitDict) |
✅ | content/main/index.js | HIDConnectionEvent(type, init) requires device and forwards inherited EventInit members |
C2 |
HIDConnectionEvent.device readonly, [SameObject] identity stability |
✅ | content/main/index.js | device stored in evtState WeakMap, returned by getter; same identity |
IC1 |
HIDInputReportEvent constructor accepts (type, eventInitDict) |
✅ | content/main/index.js | HIDInputReportEvent(type, init) requires device, reportId, and data |
IC2 |
HIDInputReportEvent.device readonly, [SameObject] |
✅ | content/main/index.js (inputReportEventState, HIDInputReportEvent.prototype.device) |
The branded getter returns the same HIDDevice reference stored in the event WeakMap |
IC3 |
HIDInputReportEvent.reportId readonly octet |
✅ | content/main/index.js | reportId uses ordinary WebIDL octet conversion and is returned by getter |
IC4 |
HIDInputReportEvent.data readonly DataView |
✅ | content/main/index.js (inputReportEventState, HIDInputReportEvent.prototype.data) |
The branded getter returns the validated DataView captured in the event WeakMap |
| # | 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 (requestDevice, hasTransientActivation, isCalledFromConsole); tests/conformance/browser/activation.spec.ts |
Ordinary page requests require transient activation; DevTools console invocation follows the compatibility path |
B5 |
requestDevice() grants access to ALL HID interfaces of chosen device |
✅ | content/isolated/picker/index.js; content/main/index.js | 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 | result.cancelled → resolve([]) |
B7 |
open() refuses unless [[state]] is "closed" |
✅ | content/main/index.js (openDevice, connectionState); tests/conformance/e2e/e2e.spec.ts |
Only closed enters opening; other states return an InvalidStateError rejection |
B8 |
close() refuses if [[state]] is "forgotten" or "forgetting" |
✅ | content/main/index.js (closeDevice, connectionState); tests/conformance/e2e/e2e.spec.ts |
forgetting and forgotten return an InvalidStateError rejection |
B9 |
close() settles all pending sendReport() promises |
✅ | content/main/index.js (closeDevice, abortDeviceReports); tests/conformance/e2e/e2e.spec.ts |
Pending output reports are rejected with AbortError at the start of closing, before awaiting daemon close |
B10 |
close() settles all pending sendFeatureReport() promises |
✅ | content/main/index.js (closeDevice, abortDeviceReports); tests/conformance/e2e/e2e.spec.ts |
The same immediate AbortError settlement applies to pending feature writes |
B11 |
close() settles all pending receiveFeatureReport() promises |
✅ | content/main/index.js (closeDevice, abortDeviceReports); tests/conformance/e2e/e2e.spec.ts |
The same immediate AbortError settlement applies to pending feature reads |
B12 |
forget() revokes access, sets [[state]] to "forgotten" |
✅ | content/main/index.js (forgetDevice, reconcileAuthoritativeLifetime); tests/conformance/e2e/e2e.spec.ts |
forgetDevice enters forgetting, unpairs via the bridge, then reconciles successful completion to forgotten |
B13 |
forget() settles pending sendReport() on related devices |
✅ | content/main/index.js (permissionGroupStates, forgetDevice, abortDeviceReports); background/messages.js; tests/conformance/e2e/e2e.spec.ts |
The permission group's device states enter forgetting and abort pending report operations before unpair completes |
B14 |
forget() settles pending sendFeatureReport() on related devices |
✅ | content/main/index.js (permissionGroupStates, forgetDevice, abortDeviceReports) |
Pending feature writes are aborted across the permission group |
B15 |
forget() settles pending receiveFeatureReport() on related devices |
✅ | content/main/index.js (permissionGroupStates, forgetDevice, abortDeviceReports) |
Pending feature reads are aborted across the permission group |
B16 |
sendReport() validates reportId against whether device uses report IDs |
✅ | content/main/index.js (convertEnforcedOctet, validateReportId, HIDDevice.prototype.sendReport) |
Public method conversion is synchronous, followed by report-ID compatibility validation |
B17 |
sendFeatureReport() validates reportId the same way |
✅ | content/main/index.js (convertEnforcedOctet, validateReportId, HIDDevice.prototype.sendFeatureReport) |
Public method conversion is synchronous, followed by report-ID compatibility validation |
B18 |
receiveFeatureReport() validates reportId and resolves with DataView |
✅ | content/main/index.js (convertEnforcedOctet, validateReportId, HIDDevice.prototype.receiveFeatureReport) |
Public method conversion is synchronous; successful feature reads resolve with a DataView |
B19 |
vendorId is 0 when device has no vendor ID |
✅ | crates/webhid-daemon/src/hid.rs | 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 | product_id: info.product_id(); hidapi returns 0 when no product ID |
B21 |
productName is "" (empty string) when unavailable |
✅ | crates/webhid/src/types.rs; crates/webhid-daemon/src/hid.rs | 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; crates/webhid-daemon/src/descriptor/fields.rs | usages and range endpoints use optional serialized fields; strings is always a sequence |
B23 |
HIDReportItem.reportSize always > 0 |
✅ | crates/webhid-daemon/src/descriptor/fields.rs | report_size: clamped with .max(1) in Constant and aggregated variable fields |
B24 |
HIDReportItem.reportCount always > 0 |
✅ | crates/webhid-daemon/src/descriptor/fields.rs | 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 | 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 | 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; crates/webhid-daemon/src/report_blocking.rs | 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; crates/webhid-daemon/src/report_blocking.rs | 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 (dispatchDeviceEvent, resolvePairedDevice); tests/conformance/e2e/e2e.spec.ts |
Connection dispatch resolves the device against the current permission grant before firing |
B31 |
disconnect event fires ONLY for previously-granted devices |
✅ | content/main/index.js | 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 | 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 (sendReportDevice, connectionState); tests/conformance/e2e/e2e.spec.ts |
Output reports reject with InvalidStateError in every state other than opened, including closing |
B35 |
sendFeatureReport() refuses unless [[state]] is "opened" |
✅ | content/main/index.js (sendFeatureReportDevice, connectionState); tests/conformance/e2e/e2e.spec.ts |
Feature writes reject with InvalidStateError outside opened |
B36 |
receiveFeatureReport() refuses unless [[state]] is "opened" |
✅ | content/main/index.js (receiveFeatureReportDevice, connectionState); tests/conformance/e2e/e2e.spec.ts |
Feature reads reject with InvalidStateError outside opened |
B37 |
open() rejects when OS-level open fails |
✅ | content/main/index.js | 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; crates/webhid-daemon/src/batching.rs; content/main/index.js; content/isolated/worker/index.js | 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 | sendReport reject path wraps error in DOMException(..., 'NetworkError') |
B40 |
sendFeatureReport() rejects when blocked report |
✅ | crates/webhid-daemon/src/client.rs; crates/webhid-daemon/src/batching.rs; content/main/index.js | 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 | sendFeatureReport reject path wraps in DOMException(..., 'NetworkError') |
B42 |
receiveFeatureReport() rejects when blocked report |
✅ | crates/webhid-daemon/src/client.rs; crates/webhid-daemon/src/batching.rs; content/main/index.js | 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 | 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; content/main/index.js | 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 (convertFilterDictionary, validateRequestOptions); utils/device-filters.js (isValidFilter) |
Unknown dictionary members are discarded before filter validation, so an unknown-only filter is invalid |
B46 |
Empty filter object {} is invalid |
✅ | utils/device-filters.js | isValidFilter returns false if Object.keys(filter).length===0 |
B47 |
productId without vendorId is invalid |
✅ | utils/device-filters.js | isValidFilter returns false if 'productId' in filter && !('vendorId' in filter) |
B48 |
usage without usagePage is invalid |
✅ | utils/device-filters.js | 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 | matches_device includes a device when any inclusion filter matches |
B50 |
Empty filters array matches ALL devices |
✅ | crates/webhid-daemon/src/enumeration_filter.rs | 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 | 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 | 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 | requestDevice throws TypeError if exclusionFilters is present but empty |
B54 |
requestDevice() validates each exclusionFilters entry |
✅ | content/main/index.js (convertFilterDictionary, validateRequestOptions); utils/device-filters.js (isValidFilter) |
The same dictionary conversion and validation rules apply to exclusion filters |
B55 |
Empty filter {} invalid for exclusionFilters |
✅ | utils/device-filters.js | same isValidFilter used for exclusion filters rejects empty {} |
B56 |
productId without vendorId invalid for exclusionFilters |
✅ | utils/device-filters.js | isValidFilter rejects productId without vendorId for exclusion filters |
B57 |
usage without usagePage invalid for exclusionFilters |
✅ | utils/device-filters.js | isValidFilter rejects usage without usagePage for exclusion filters |
B58 |
Device matching any exclusionFilters entry is excluded |
✅ | crates/webhid-daemon/src/enumeration_filter.rs | 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 (installNavigatorHid, getDevicesImpl); tests/conformance/browser/worker.spec.ts |
With worker polyfill enabled, getDevices() is exposed and exercised in the Dedicated Worker scenario |
B60 |
requestDevice() is Window-only; non-Window invocation is unsupported |
✅ | WebHID IDL ([Exposed=Window]), §6.2; content/main/index.js (requestDevice member); tests/conformance/browser/worker.spec.ts |
Accept either omission under [Exposed=Window] or NotSupportedError rejection per §6.2; this build omits the worker method |
B61 |
HIDDevice methods open, close, sendReport, sendFeatureReport, receiveFeatureReport, forget exposed on DedicatedWorker (spec does not restrict these from Worker) |
✅ | content/main/index.js (HIDDevice.prototype, defineWebhidGlobal); tests/conformance/browser/worker.spec.ts |
The worker realm installs HIDDevice and its six methods; worker data-plane functionality is separately scoped |
B62 |
getDevices() from ServiceWorker: conditional ServiceWorker exposure described by WebHID §6.1 |
N/S | WebHID §6.1; addon/js/background/webrequest.js | 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; content/main/index.js | requestDevice() is page-only by the specification; Service Worker exposure is outside the polyfill scope and is not scored |
B64 |
close() during an in-progress open() transitions toward closing and closes the opening session |
✅ | content/main/index.js (closeDevice, performCloseDevice); tests/conformance/e2e/e2e.spec.ts |
closeDevice enters closing, settles pending reports and awaits openPromise before issuing the daemon close; the close-during-open test covers this ordering |
| # | Item | Result | Evidence | Notes |
|---|---|---|---|---|
W1 |
Navigator.hid and WorkerNavigator.hid getters enforce the receiver's interface brand |
✅ | content/main/index.js (installNavigatorHid); tests/conformance/browser/hid-class.spec.ts |
The getter calls the captured native navigator userAgent getter on this, which rejects incompatible receivers |
W2 |
HID and HIDDevice methods reject incompatible receivers with TypeError |
✅ | content/main/index.js (requireState, hidState, devState); tests/conformance/browser/hid-class.spec.ts; tests/conformance/browser/hid-device-class.spec.ts |
Public methods validate their receiver before returning a Promise or performing an operation |
W3 |
WebHID event constructors require new |
✅ | content/main/index.js (HIDConnectionEvent, HIDInputReportEvent); tests/conformance/e2e/e2e.spec.ts |
Both constructors check new.target and throw TypeError for direct calls |
W4 |
[EnforceRange] octet argument conversion errors are synchronous |
✅ | content/main/index.js (convertEnforcedOctet, HIDDevice method wrappers); tests/conformance/e2e/e2e.spec.ts |
Report ID conversion occurs in ordinary public functions before the async report operation starts |
W5 |
Report data arguments undergo branded WebIDL BufferSource conversion |
✅ | content/main/index.js (copyBufferSource); tests/conformance/e2e/e2e.spec.ts |
Captured native ArrayBuffer and ArrayBufferView accessors reject structural impostors and unsupported shared buffers before sending |
| # | 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) |
A normal page request without activation and without the opt-in override rejects with SecurityError |
B7-sub |
B7 Sub: rejection uses InvalidStateError |
✅ | content/main/index.js (openDevice) |
A state other than closed produces an InvalidStateError rejection |
B8-sub |
B8 Sub: rejection uses InvalidStateError |
✅ | content/main/index.js (closeDevice) |
forgetting or forgotten produces an InvalidStateError rejection |
B9-sub |
B9 Sub: settlement uses AbortError |
✅ | content/main/index.js (abortDeviceReports, closeDevice) |
Closing settles pending output reports with AbortError before daemon close |
B10-sub |
B10 Sub: settlement uses AbortError |
✅ | content/main/index.js (abortDeviceReports, closeDevice) |
The same AbortError settles pending feature writes |
B11-sub |
B11 Sub: settlement uses AbortError |
✅ | content/main/index.js (abortDeviceReports, closeDevice) |
The same AbortError settles pending feature reads |
B13-sub |
B13 Sub: settlement uses AbortError |
✅ | content/main/index.js (permissionGroupStates, forgetDevice, abortDeviceReports) |
All permission-group report promises receive an AbortError while entering forgetting |
B14-sub |
B14 Sub: settlement uses AbortError |
✅ | content/main/index.js (permissionGroupStates, forgetDevice, abortDeviceReports) |
The group-wide abort covers pending feature writes |
B15-sub |
B15 Sub: settlement uses AbortError |
✅ | content/main/index.js (permissionGroupStates, forgetDevice, abortDeviceReports) |
The group-wide abort covers pending feature reads |
B16-sub |
B16 Sub: rejection uses TypeError (covers logical mismatch AND out-of-range via [EnforceRange]) |
✅ | content/main/index.js (convertEnforcedOctet, validateReportId) |
The report method wrapper converts reportId synchronously and validates report-ID compatibility |
B17-sub |
B17 Sub: rejection uses TypeError |
✅ | content/main/index.js (convertEnforcedOctet, validateReportId) |
The feature-write wrapper applies the same synchronous TypeError checks |
B18-sub |
B18 Sub: rejection uses TypeError |
✅ | content/main/index.js (convertEnforcedOctet, validateReportId) |
The feature-read wrapper applies the same synchronous TypeError checks |
B34-sub |
B34 Sub: rejection uses InvalidStateError |
✅ | content/main/index.js (sendReportDevice) |
All states other than opened reject with InvalidStateError |
B35-sub |
B35 Sub: rejection uses InvalidStateError |
✅ | content/main/index.js (sendFeatureReportDevice) |
All states other than opened reject with InvalidStateError |
B36-sub |
B36 Sub: rejection uses InvalidStateError |
✅ | content/main/index.js (receiveFeatureReportDevice) |
All states other than opened reject with InvalidStateError |
B37-sub |
B37 Sub: rejection uses NetworkError |
✅ | content/main/index.js | open() catches failure and throws DOMException(error.message, 'NetworkError') |
B38-sub |
B38 Sub: rejection uses NotAllowedError |
✅ | content/main/index.js | sendReport reject with DOMException(..., 'NotAllowedError') when blocked |
B39-sub |
B39 Sub: rejection uses NetworkError |
✅ | content/main/index.js | sendReport reject wraps in DOMException(e.message, 'NetworkError') |
B40-sub |
B40 Sub: rejection uses NotAllowedError |
✅ | content/main/index.js | sendFeatureReport reject with DOMException(..., 'NotAllowedError') when blocked |
B41-sub |
B41 Sub: rejection uses NetworkError |
✅ | content/main/index.js | sendFeatureReport reject wraps in DOMException(e.message, 'NetworkError') |
B42-sub |
B42 Sub: rejection uses NotAllowedError |
✅ | content/main/index.js | receiveFeatureReport reject with DOMException(..., 'NotAllowedError') when blocked |
B43-sub |
B43 Sub: rejection uses NetworkError |
✅ | content/main/index.js | receiveFeatureReport reject wraps in DOMException(e.message, 'NetworkError') |
B45-sub |
B45 Sub: rejection uses TypeError |
✅ | content/main/index.js (convertFilterDictionary, validateRequestOptions) |
Unknown WebIDL dictionary members are dropped, then an invalid filter is rejected with TypeError |
B46/B47/B48-sub |
B46/B47/B48 Sub (shared): rejection uses TypeError |
✅ | utils/device-filters.js | same TypeError path covers empty {}, productId w/o vendorId, usagePage/usage without usagePage |
B53-sub |
B53 Sub: rejection uses TypeError |
✅ | content/main/index.js | throws TypeError when exclusionFilters is present but empty |
B54-sub |
B54 Sub: rejection uses TypeError |
✅ | content/main/index.js (convertFilterDictionary, validateRequestOptions) |
Unknown exclusion-filter members are dropped, then an invalid filter is rejected with TypeError |
B55/B56/B57-sub |
B55/B56/B57 Sub (shared): rejection uses TypeError |
✅ | utils/device-filters.js | 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 | 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 | 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 | productName sourced only from product_string(); serial_number is a separate DeviceInfo field not merged into productName |
B4(activation): Ordinary page calls torequestDevice()require transient activation. DevTools console invocation follows Chromium-compatible behavior; the chooser still requires user selection.B13: Devices granted together are tracked as a permission group.permissionGroupStates()resolves the related page-device states andforgetDevice()immediately marks the group asforgetting, aborts its pending report operations, then requests revocation throughunpairDevice. The background grant-group revocation path closes and invalidates the corresponding daemon authority. See the group lifecycle assertions intests/conformance/e2e/e2e.spec.ts.B33: Header policy is captured inbackground/webrequest.jsand keyed to browser-owned frame/document identity. The exact child bridge resolves delegation against its real parent endpoint; the parent checks the matching iframe/frame element using browser runtime frame/document IDs and evaluates thehidallow directive. The default remains same-origin (self); cross-origin children fail closed without valid delegation.I5b:hidInstanceis a module-level singleton incontent/main/index.js; thenavigator.hidgetter returns it on every access, satisfying[SameObject].B59/B60/B61(worker context):workerPolyfillEnabledis off by default. When enabled, the polyfill exposesnavigator.hid, WebHID interfaces andgetDevices()inside page-created Dedicated Workers.requestDevice()remains Window-only; non-Window behavior is compliant when the method is absent under[Exposed=Window]or rejects withNotSupportedErrorunder §6.2. The current implementation omits the method in Workers. No functional worker data plane is created by this feature. Browser-test evidence is intests/conformance/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.