Skip to content

Latest commit

 

History

History
260 lines (224 loc) · 77.1 KB

SPECIFICATION.md

File metadata and controls

260 lines (224 loc) · 77.1 KB

WebHID Spec Compliance Report

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

Result table

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

Sub-item (Validation Detail) totals

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

Item-level evidence

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

Interfaces (I1-I9)

# 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

Enum (E1)

# 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

Dictionaries (D1a-D4)

# 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

Report Descriptor Shape (D5-D11)

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

HID members (H1-H4)

# 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

HIDDevice members (HD1-HD12)

# 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

Event members (C1-IC4)

# 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

Behaviors (B1-B64)

# 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

WebIDL runtime (W1-W5)

# 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

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

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

Notes

  • B4 (activation): Ordinary page calls to requestDevice() 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 and forgetDevice() immediately marks the group as forgetting, aborts its pending report operations, then requests revocation through unpairDevice. The background grant-group revocation path closes and invalidates the corresponding daemon authority. See the group lifecycle assertions in tests/conformance/e2e/e2e.spec.ts.
  • B33: Header policy is captured in background/webrequest.js and 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 the hid allow directive. The default remains same-origin (self); cross-origin children fail closed without valid delegation.
  • I5b: hidInstance is a module-level singleton in content/main/index.js; the navigator.hid getter returns it on every access, satisfying [SameObject].
  • B59 / B60 / B61 (worker context): workerPolyfillEnabled is off by default. When enabled, the polyfill exposes navigator.hid, WebHID interfaces and getDevices() inside page-created Dedicated Workers. requestDevice() remains Window-only; non-Window behavior is compliant when the method is absent under [Exposed=Window] or rejects with NotSupportedError under §6.2. The current implementation omits the method in Workers. No functional worker data plane is created by this feature. Browser-test evidence is in tests/conformance/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.