WebHID: Add support for dedicated workers

This CL exposes the WebHID API to dedicated workers behind the blink
runtime feature WebHIDOnDedicatedWorkers.

Intent to prototype: https://groups.google.com/a/chromium.org/g/blink-dev/c/y__BOYfZWzI
Spec PR: https://github.com/WICG/webhid/pull/121
Spec issue: https://github.com/WICG/webhid/issues/120
Demo: https://webhid-worker.glitch.me/

Change-Id: If145f34faf9211a54c8b4a15fbe7e903411b8d1d
Bug: 365932453
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5841991
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Reviewed-by: Yoav Weiss (@Shopify) <yoavweiss@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Commit-Queue: Fr <beaufort.francois@gmail.com>
Cr-Commit-Position: refs/heads/main@{#1357046}
diff --git a/content/browser/browser_interface_binders.cc b/content/browser/browser_interface_binders.cc
index 1c4444d..a480e76 100644
--- a/content/browser/browser_interface_binders.cc
+++ b/content/browser/browser_interface_binders.cc
@@ -1343,6 +1343,8 @@
 #if !BUILDFLAG(IS_ANDROID)
   map->Add<blink::mojom::SerialService>(base::BindRepeating(
       &DedicatedWorkerHost::BindSerialService, base::Unretained(host)));
+  map->Add<blink::mojom::HidService>(base::BindRepeating(
+      &DedicatedWorkerHost::BindHidService, base::Unretained(host)));
 #endif  // !BUILDFLAG(IS_ANDROID)
   map->Add<blink::mojom::BucketManagerHost>(base::BindRepeating(
       &DedicatedWorkerHost::CreateBucketManagerHost, base::Unretained(host)));
diff --git a/content/browser/worker_host/dedicated_worker_host.cc b/content/browser/worker_host/dedicated_worker_host.cc
index ca9cb1c..0a32fad 100644
--- a/content/browser/worker_host/dedicated_worker_host.cc
+++ b/content/browser/worker_host/dedicated_worker_host.cc
@@ -793,6 +793,20 @@
 
   ancestor_render_frame_host->BindSerialService(std::move(receiver));
 }
+
+void DedicatedWorkerHost::BindHidService(
+    mojo::PendingReceiver<blink::mojom::HidService> receiver) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  RenderFrameHostImpl* ancestor_render_frame_host =
+      RenderFrameHostImpl::FromID(ancestor_render_frame_host_id_);
+  // The ancestor frame may have already been closed. In that case, the worker
+  // will soon be terminated too, so abort the connection.
+  if (!ancestor_render_frame_host) {
+    return;
+  }
+
+  ancestor_render_frame_host->GetHidService(std::move(receiver));
+}
 #endif
 
 void DedicatedWorkerHost::CreateBucketManagerHost(
diff --git a/content/browser/worker_host/dedicated_worker_host.h b/content/browser/worker_host/dedicated_worker_host.h
index 35dccb2..546dd0c1 100644
--- a/content/browser/worker_host/dedicated_worker_host.h
+++ b/content/browser/worker_host/dedicated_worker_host.h
@@ -51,6 +51,7 @@
 
 #if !BUILDFLAG(IS_ANDROID)
 #include "third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom-forward.h"
+#include "third_party/blink/public/mojom/hid/hid.mojom-forward.h"
 #include "third_party/blink/public/mojom/serial/serial.mojom-forward.h"
 #endif
 
@@ -174,6 +175,7 @@
 #if !BUILDFLAG(IS_ANDROID)
   void BindSerialService(
       mojo::PendingReceiver<blink::mojom::SerialService> receiver);
+  void BindHidService(mojo::PendingReceiver<blink::mojom::HidService> receiver);
 #endif
 
   // PlzDedicatedWorker:
diff --git a/third_party/blink/renderer/modules/hid/hid.cc b/third_party/blink/renderer/modules/hid/hid.cc
index 702c3a9..fe7eb4e 100644
--- a/third_party/blink/renderer/modules/hid/hid.cc
+++ b/third_party/blink/renderer/modules/hid/hid.cc
@@ -19,6 +19,7 @@
 #include "third_party/blink/renderer/core/execution_context/navigator_base.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/workers/worker_global_scope.h"
 #include "third_party/blink/renderer/modules/event_target_modules.h"
 #include "third_party/blink/renderer/modules/hid/hid_connection_event.h"
 #include "third_party/blink/renderer/modules/hid/hid_device.h"
@@ -33,6 +34,25 @@
 const char kFeaturePolicyBlocked[] =
     "Access to the feature \"hid\" is disallowed by permissions policy.";
 
+bool IsContextSupported(ExecutionContext* context) {
+  // Since WebHID on Web Workers is in the process of being implemented, we
+  // check here if the runtime flag for the appropriate worker is enabled.
+  // TODO(https://crbug.com/365932453): Remove this check once the feature has
+  // shipped.
+  if (!context) {
+    return false;
+  }
+
+  DCHECK(context->IsWindow() || context->IsDedicatedWorkerGlobalScope() ||
+         context->IsServiceWorkerGlobalScope());
+  DCHECK(!context->IsDedicatedWorkerGlobalScope() ||
+         RuntimeEnabledFeatures::WebHIDOnDedicatedWorkersEnabled());
+  DCHECK(!context->IsServiceWorkerGlobalScope() ||
+         RuntimeEnabledFeatures::WebHIDOnServiceWorkersEnabled());
+
+  return true;
+}
+
 // Carries out basic checks for the web-exposed APIs, to make sure the minimum
 // requirements for them to be served are met. Returns true if any conditions
 // fail to be met, generating an appropriate exception as well. Otherwise,
@@ -40,7 +60,7 @@
 bool ShouldBlockHidServiceCall(LocalDOMWindow* window,
                                ExecutionContext* context,
                                ExceptionState* exception_state) {
-  if (!context) {
+  if (!IsContextSupported(context)) {
     if (exception_state) {
       exception_state->ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                          kContextGone);
@@ -48,14 +68,21 @@
     return true;
   }
 
-  // The security origin must match the one checked by the browser process.
-  // Service Workers do not use delegated permissions so we use their security
-  // origin directly.
-  DCHECK(context->IsWindow() || context->IsServiceWorkerGlobalScope());
-  auto* security_origin =
-      window
-          ? window->GetFrame()->Top()->GetSecurityContext()->GetSecurityOrigin()
-          : context->GetSecurityOrigin();
+  // For window and dedicated workers, reject the request if the top-level frame
+  // has an opaque origin. For Service Workers, we use their security origin
+  // directly as they do not use delegated permissions.
+  const SecurityOrigin* security_origin = nullptr;
+  if (context->IsWindow()) {
+    security_origin =
+        window->GetFrame()->Top()->GetSecurityContext()->GetSecurityOrigin();
+  } else if (context->IsDedicatedWorkerGlobalScope()) {
+    security_origin = static_cast<WorkerGlobalScope*>(context)
+                          ->top_level_frame_security_origin();
+  } else if (context->IsServiceWorkerGlobalScope()) {
+    security_origin = context->GetSecurityOrigin();
+  } else {
+    NOTREACHED();
+  }
   if (security_origin->IsOpaque()) {
     if (exception_state) {
       exception_state->ThrowSecurityError(
@@ -333,6 +360,8 @@
   if (service_.is_bound())
     return;
 
+  DCHECK(IsContextSupported(GetExecutionContext()));
+
   auto task_runner =
       GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI);
   GetExecutionContext()->GetBrowserInterfaceBroker().GetInterface(
diff --git a/third_party/blink/renderer/modules/hid/hid.idl b/third_party/blink/renderer/modules/hid/hid.idl
index 48069a5d..94f1167 100644
--- a/third_party/blink/renderer/modules/hid/hid.idl
+++ b/third_party/blink/renderer/modules/hid/hid.idl
@@ -8,6 +8,7 @@
 
 [
     Exposed(ServiceWorker WebHIDOnServiceWorkers,
+            DedicatedWorker WebHIDOnDedicatedWorkers,
             Window WebHID),
     SecureContext
 ] interface HID : EventTarget {
diff --git a/third_party/blink/renderer/modules/hid/hid_connection_event.idl b/third_party/blink/renderer/modules/hid/hid_connection_event.idl
index 2594e0bb..a55b517 100644
--- a/third_party/blink/renderer/modules/hid/hid_connection_event.idl
+++ b/third_party/blink/renderer/modules/hid/hid_connection_event.idl
@@ -7,6 +7,7 @@
 
 [
     Exposed(ServiceWorker WebHIDOnServiceWorkers,
+            DedicatedWorker WebHIDOnDedicatedWorkers,
             Window WebHID),
     SecureContext
 ] interface HIDConnectionEvent : Event {
diff --git a/third_party/blink/renderer/modules/hid/hid_device.idl b/third_party/blink/renderer/modules/hid/hid_device.idl
index 9047961..fb2b15a 100644
--- a/third_party/blink/renderer/modules/hid/hid_device.idl
+++ b/third_party/blink/renderer/modules/hid/hid_device.idl
@@ -10,6 +10,7 @@
 [
     ActiveScriptWrappable,
     Exposed(ServiceWorker WebHIDOnServiceWorkers,
+            DedicatedWorker WebHIDOnDedicatedWorkers,
             Window WebHID),
     SecureContext
 ] interface HIDDevice : EventTarget {
diff --git a/third_party/blink/renderer/modules/hid/hid_input_report_event.idl b/third_party/blink/renderer/modules/hid/hid_input_report_event.idl
index 3b01b30..bd948e2c 100644
--- a/third_party/blink/renderer/modules/hid/hid_input_report_event.idl
+++ b/third_party/blink/renderer/modules/hid/hid_input_report_event.idl
@@ -7,6 +7,7 @@
 
 [
     Exposed(ServiceWorker WebHIDOnServiceWorkers,
+            DedicatedWorker WebHIDOnDedicatedWorkers,
             Window WebHID),
     SecureContext
 ] interface HIDInputReportEvent : Event {
diff --git a/third_party/blink/renderer/modules/hid/worker_navigator_hid.idl b/third_party/blink/renderer/modules/hid/worker_navigator_hid.idl
index e803a63..21e60ea 100644
--- a/third_party/blink/renderer/modules/hid/worker_navigator_hid.idl
+++ b/third_party/blink/renderer/modules/hid/worker_navigator_hid.idl
@@ -5,7 +5,8 @@
 // https://wicg.github.io/webhid/index.html#enumeration
 
 [
-    Exposed(ServiceWorker WebHIDOnServiceWorkers),
+    Exposed(ServiceWorker WebHIDOnServiceWorkers,
+            DedicatedWorker WebHIDOnDedicatedWorkers),
     ImplementedAs=HID,
     RuntimeEnabled=WebHID,
     SecureContext
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 9a60fa3..fa5ea1b 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -4689,6 +4689,11 @@
       status: {"Android": "", "default": "stable"},
     },
     {
+      name: "WebHIDOnDedicatedWorkers",
+      status: "experimental",
+      depends_on: ["WebHID"],
+    },
+    {
       // It is only enabled in extension environment for now.
       name: "WebHIDOnServiceWorkers",
       depends_on: ["WebHID"],
diff --git a/third_party/blink/web_tests/external/wpt/interfaces/webhid.idl b/third_party/blink/web_tests/external/wpt/interfaces/webhid.idl
index 03e72511..56e3161 100644
--- a/third_party/blink/web_tests/external/wpt/interfaces/webhid.idl
+++ b/third_party/blink/web_tests/external/wpt/interfaces/webhid.idl
@@ -7,12 +7,12 @@
     [SameObject] readonly attribute HID hid;
 };
 
-[Exposed=ServiceWorker, SecureContext]
+[Exposed=(DedicatedWorker,ServiceWorker), SecureContext]
 partial interface WorkerNavigator {
     [SameObject] readonly attribute HID hid;
 };
 
-[Exposed=(Window,ServiceWorker), SecureContext]
+[Exposed=(DedicatedWorker,ServiceWorker,Window), SecureContext]
 interface HID : EventTarget {
     attribute EventHandler onconnect;
     attribute EventHandler ondisconnect;
@@ -33,7 +33,7 @@
     unsigned short usage;
 };
 
-[Exposed=Window, SecureContext]
+[Exposed=(DedicatedWorker,ServiceWorker,Window), SecureContext]
 interface HIDDevice : EventTarget {
     attribute EventHandler oninputreport;
     readonly attribute boolean opened;
@@ -51,7 +51,7 @@
     Promise<DataView> receiveFeatureReport([EnforceRange] octet reportId);
 };
 
-[Exposed=Window, SecureContext]
+[Exposed=(DedicatedWorker,ServiceWorker,Window), SecureContext]
 interface HIDConnectionEvent : Event {
     constructor(DOMString type, HIDConnectionEventInit eventInitDict);
     [SameObject] readonly attribute HIDDevice device;
@@ -61,7 +61,7 @@
     required HIDDevice device;
 };
 
-[Exposed=Window, SecureContext]
+[Exposed=(DedicatedWorker,ServiceWorker,Window), SecureContext]
 interface HIDInputReportEvent : Event {
     constructor(DOMString type, HIDInputReportEventInit eventInitDict);
     [SameObject] readonly attribute HIDDevice device;
diff --git a/third_party/blink/web_tests/external/wpt/permissions-policy/resources/permissions-policy-hid-worker.html b/third_party/blink/web_tests/external/wpt/permissions-policy/resources/permissions-policy-hid-worker.html
new file mode 100644
index 0000000..9a0c1f94f
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/permissions-policy/resources/permissions-policy-hid-worker.html
@@ -0,0 +1,10 @@
+<script>
+'use strict';
+
+let worker = new Worker('permissions-policy-hid-worker.js');
+
+worker.onmessage = event => {
+  window.parent.postMessage(event.data, '*');
+};
+worker.postMessage({ type: 'ready' });
+</script>
\ No newline at end of file
diff --git a/third_party/blink/web_tests/external/wpt/permissions-policy/resources/permissions-policy-hid-worker.js b/third_party/blink/web_tests/external/wpt/permissions-policy/resources/permissions-policy-hid-worker.js
new file mode 100644
index 0000000..d7490b2
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/permissions-policy/resources/permissions-policy-hid-worker.js
@@ -0,0 +1,14 @@
+'use strict';
+
+// Dedicated worker
+if (typeof postMessage === 'function') {
+  onmessage = event => {
+    switch(event.data.type) {
+      case 'ready':
+        navigator.hid.getDevices().then(
+            () => postMessage({ type: 'availability-result', enabled: true }),
+            error => postMessage ({ type: 'availability-result', enabled: false }));
+        break;
+    }
+  };
+}
diff --git a/third_party/blink/web_tests/external/wpt/permissions-policy/resources/permissions-policy-hid.html b/third_party/blink/web_tests/external/wpt/permissions-policy/resources/permissions-policy-hid.html
new file mode 100644
index 0000000..0b54d57
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/permissions-policy/resources/permissions-policy-hid.html
@@ -0,0 +1,9 @@
+<script>
+'use strict';
+
+Promise.resolve().then(() => navigator.hid.getDevices()).then(devices => {
+  window.parent.postMessage({ type: 'availability-result', enabled: true }, '*');
+}, error => {
+  window.parent.postMessage({ type: 'availability-result', enabled: false }, '*');
+});
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html b/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html
new file mode 100644
index 0000000..0a286ac
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+const relative_path = '/permissions-policy/resources/permissions-policy-hid.html';
+const base_src = '/permissions-policy/resources/redirect-on-load.html#';
+const relative_worker_frame_path =
+    '/permissions-policy/resources/permissions-policy-hid-worker.html';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = base_src + relative_path;
+const cross_origin_src = base_src + sub + relative_path;
+const same_origin_worker_frame_src = base_src + relative_worker_frame_path;
+const cross_origin_worker_frame_src = base_src + sub +
+    relative_worker_frame_path;
+const header = 'Permissions-Policy allow="hid"';
+
+async_test(t => {
+  test_feature_availability(
+      'hid.getDevices()', t, same_origin_src,
+      expect_feature_available_default, 'hid');
+}, header + ' allows same-origin relocation.');
+
+async_test(t => {
+  test_feature_availability(
+      'hid.getDevices()', t, same_origin_worker_frame_src,
+      expect_feature_available_default, 'hid');
+}, header + ' allows workers in same-origin relocation.');
+
+async_test(t => {
+  test_feature_availability(
+      'hid.getDevices()', t, cross_origin_src,
+      expect_feature_unavailable_default, 'hid');
+}, header + ' disallows cross-origin relocation.');
+
+async_test(t => {
+  test_feature_availability(
+      'hid.getDevices()', t, cross_origin_worker_frame_src,
+      expect_feature_unavailable_default, 'hid');
+}, header + ' disallows workers in cross-origin relocation.');
+</script>
+</body>
diff --git a/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy-attribute.https.sub.html b/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy-attribute.https.sub.html
new file mode 100644
index 0000000..f0963c4e
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy-attribute.https.sub.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = '/permissions-policy/resources/permissions-policy-hid.html';
+const cross_origin_src = sub + same_origin_src;
+const same_origin_worker_frame_src =
+    '/permissions-policy/resources/permissions-policy-hid-worker.html';
+const cross_origin_worker_frame_src = sub + same_origin_worker_frame_src;
+const feature_name = 'Permissions policy "hid"';
+const header = 'allow="hid" attribute';
+
+async_test(t => {
+  test_feature_availability(
+      'hid.getDevices()', t, same_origin_src,
+      expect_feature_available_default, 'hid');
+}, feature_name + ' can be enabled in same-origin iframe using ' + header);
+
+async_test(t => {
+  test_feature_availability(
+      'hid.getDevices()', t, same_origin_worker_frame_src,
+      expect_feature_available_default, 'hid');
+}, feature_name + ' can be enabled in a worker in same-origin iframe using ' +
+    header);
+
+async_test(t => {
+  test_feature_availability(
+      'hid.getDevices()', t, cross_origin_src,
+      expect_feature_available_default, 'hid');
+}, feature_name + ' can be enabled in cross-origin iframe using ' + header);
+
+async_test(t => {
+  test_feature_availability(
+      'hid.getDevices()', t, cross_origin_worker_frame_src,
+      expect_feature_available_default, 'hid');
+}, feature_name + ' can be enabled in a worker in cross-origin iframe using ' +
+    header);
+
+fetch_tests_from_worker(new Worker(
+    '/webhid/resources/hid-allowed-by-permissions-policy-worker.js'));
+</script>
+</body>
diff --git a/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy.https.sub.html b/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy.https.sub.html
new file mode 100644
index 0000000..645989c
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy.https.sub.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = '/permissions-policy/resources/permissions-policy-hid.html';
+const cross_origin_src = sub + same_origin_src;
+const same_origin_worker_frame_src =
+    '/permissions-policy/resources/permissions-policy-hid-worker.html';
+const cross_origin_worker_frame_src = sub + same_origin_worker_frame_src;
+const header = 'Permissions-Policy header hid=*';
+
+promise_test(
+    () => navigator.hid.getDevices(),
+    header + ' allows the top-level document.');
+
+async_test(t => {
+  test_feature_availability('hid.getDevices()', t, same_origin_src,
+      expect_feature_available_default);
+}, header + ' allows same-origin iframes.');
+
+async_test(t => {
+  test_feature_availability('hid.getDevices()', t, same_origin_worker_frame_src,
+      expect_feature_available_default);
+}, header + ' allows workers in same-origin iframes.');
+
+// Set allow="hid" on iframe element to delegate 'hid' to cross origin subframe.
+async_test(t => {
+  test_feature_availability('hid.getDevices()', t, cross_origin_src,
+      expect_feature_available_default, 'hid');
+}, header + ' allows cross-origin iframes.');
+
+// Set allow="hid" on iframe element to delegate 'hid' to cross origin subframe.
+async_test(t => {
+  test_feature_availability('hid.getDevices()', t,
+      cross_origin_worker_frame_src,
+      expect_feature_available_default, 'hid');
+}, header + ' allows workers in cross-origin iframes.');
+
+fetch_tests_from_worker(new Worker(
+    '/webhid/resources/hid-allowed-by-permissions-policy-worker.js'));
+</script>
+</body>
diff --git a/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy.https.sub.html.headers b/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy.https.sub.html.headers
new file mode 100644
index 0000000..e365007
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/webhid/hid-allowed-by-permissions-policy.https.sub.html.headers
@@ -0,0 +1 @@
+Permissions-Policy: hid=*
diff --git a/third_party/blink/web_tests/external/wpt/webhid/idlharness.https.window-expected.txt b/third_party/blink/web_tests/external/wpt/webhid/idlharness.https.any-expected.txt
similarity index 100%
rename from third_party/blink/web_tests/external/wpt/webhid/idlharness.https.window-expected.txt
rename to third_party/blink/web_tests/external/wpt/webhid/idlharness.https.any-expected.txt
diff --git a/third_party/blink/web_tests/external/wpt/webhid/idlharness.https.window.js b/third_party/blink/web_tests/external/wpt/webhid/idlharness.https.any.js
similarity index 63%
rename from third_party/blink/web_tests/external/wpt/webhid/idlharness.https.window.js
rename to third_party/blink/web_tests/external/wpt/webhid/idlharness.https.any.js
index bdc8419b..c0ed066 100644
--- a/third_party/blink/web_tests/external/wpt/webhid/idlharness.https.window.js
+++ b/third_party/blink/web_tests/external/wpt/webhid/idlharness.https.any.js
@@ -9,9 +9,14 @@
   ['webhid'],
   ['html', 'dom'],
   idl_array => {
+    if (self.GLOBAL.isWindow()) {
+      idl_array.add_objects({ Navigator: ['navigator'] });
+    } else if (self.GLOBAL.isWorker()) {
+      idl_array.add_objects({ WorkerNavigator: ['navigator'] });
+    }
+
     idl_array.add_objects({
       HID: ['navigator.hid'],
-      Navigator: ['navigator'],
       // TODO: HIDConnectionEvent
       // TODO: HIDInputReportEvent
       // TODO: HIDDevice
diff --git a/third_party/blink/web_tests/external/wpt/webhid/idlharness.https.window-expected.txt b/third_party/blink/web_tests/external/wpt/webhid/idlharness.https.any.worker-expected.txt
similarity index 100%
copy from third_party/blink/web_tests/external/wpt/webhid/idlharness.https.window-expected.txt
copy to third_party/blink/web_tests/external/wpt/webhid/idlharness.https.any.worker-expected.txt
diff --git a/third_party/blink/web_tests/external/wpt/webhid/resources/hid-allowed-by-permissions-policy-worker.js b/third_party/blink/web_tests/external/wpt/webhid/resources/hid-allowed-by-permissions-policy-worker.js
new file mode 100644
index 0000000..20cdb5d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/webhid/resources/hid-allowed-by-permissions-policy-worker.js
@@ -0,0 +1,14 @@
+'use strict';
+
+importScripts('/resources/testharness.js');
+
+let workerType;
+
+if (typeof postMessage === 'function') {
+  workerType = 'dedicated';
+}
+
+promise_test(() => navigator.hid.getDevices(),
+    `Inherited header permissions policy allows ${workerType} workers.`);
+
+done();
diff --git a/third_party/blink/web_tests/external/wpt/webhid/resources/hid-disabled-by-permissions-policy-worker.js b/third_party/blink/web_tests/external/wpt/webhid/resources/hid-disabled-by-permissions-policy-worker.js
new file mode 100644
index 0000000..108a09d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/webhid/resources/hid-disabled-by-permissions-policy-worker.js
@@ -0,0 +1,17 @@
+'use strict';
+
+importScripts('/resources/testharness.js');
+
+const header = 'Permissions-Policy header hid=()';
+let workerType;
+
+if (typeof postMessage === 'function') {
+  workerType = 'dedicated';
+}
+
+promise_test(() => navigator.hid.getDevices().then(
+        () => assert_unreached('expected promise to reject with SecurityError'),
+        error => assert_equals(error.name, 'SecurityError')),
+    `Inherited ${header} disallows ${workerType} workers.`);
+
+done();
diff --git a/third_party/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt b/third_party/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt
index 209c674..44bb337 100644
--- a/third_party/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt
+++ b/third_party/blink/web_tests/webexposed/global-interface-listing-dedicated-worker-expected.txt
@@ -1030,6 +1030,40 @@
 [Worker] interface GPUValidationError : GPUError
 [Worker]     attribute @@toStringTag
 [Worker]     method constructor
+[Worker] interface HID : EventTarget
+[Worker]     attribute @@toStringTag
+[Worker]     getter onconnect
+[Worker]     getter ondisconnect
+[Worker]     method constructor
+[Worker]     method getDevices
+[Worker]     setter onconnect
+[Worker]     setter ondisconnect
+[Worker] interface HIDConnectionEvent : Event
+[Worker]     attribute @@toStringTag
+[Worker]     getter device
+[Worker]     method constructor
+[Worker] interface HIDDevice : EventTarget
+[Worker]     attribute @@toStringTag
+[Worker]     getter collections
+[Worker]     getter oninputreport
+[Worker]     getter opened
+[Worker]     getter productId
+[Worker]     getter productName
+[Worker]     getter vendorId
+[Worker]     method close
+[Worker]     method constructor
+[Worker]     method forget
+[Worker]     method open
+[Worker]     method receiveFeatureReport
+[Worker]     method sendFeatureReport
+[Worker]     method sendReport
+[Worker]     setter oninputreport
+[Worker] interface HIDInputReportEvent : Event
+[Worker]     attribute @@toStringTag
+[Worker]     getter data
+[Worker]     getter device
+[Worker]     getter reportId
+[Worker]     method constructor
 [Worker] interface Headers
 [Worker]     attribute @@toStringTag
 [Worker]     method @@iterator
@@ -3918,6 +3952,7 @@
 [Worker]     getter deviceMemory
 [Worker]     getter gpu
 [Worker]     getter hardwareConcurrency
+[Worker]     getter hid
 [Worker]     getter language
 [Worker]     getter languages
 [Worker]     getter locks
diff --git a/third_party/blink/web_tests/wpt_internal/hid/hid_connectionEvents.https.window.js b/third_party/blink/web_tests/wpt_internal/hid/hid_connectionEvents.https.any.js
similarity index 100%
rename from third_party/blink/web_tests/wpt_internal/hid/hid_connectionEvents.https.window.js
rename to third_party/blink/web_tests/wpt_internal/hid/hid_connectionEvents.https.any.js
diff --git a/third_party/blink/web_tests/wpt_internal/hid/hid_getDevices.https.window.js b/third_party/blink/web_tests/wpt_internal/hid/hid_getDevices.https.any.js
similarity index 100%
rename from third_party/blink/web_tests/wpt_internal/hid/hid_getDevices.https.window.js
rename to third_party/blink/web_tests/wpt_internal/hid/hid_getDevices.https.any.js